From 97d2ad6f52202c4e8a24d699db1a5c598ad3e240 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Tue, 10 Sep 2024 11:13:37 +0800 Subject: [PATCH] feat: support for displaying deeper level association fields in data blocks (#5243) * feat(table): add support for selecting child fields of association fields * style: fix style * feat(details): add support for selecting child fields of association fields * fix: correct sourceId retrieval * test: add tests * chore: fix build error * chore: remove ConfigProvider * test: update e2e tests --- .../components/SchemaInitializerSubMenu.tsx | 22 +- .../schema-initializer/components/style.ts | 5 - .../__e2e__/schemaInitializer.test.ts | 4 +- .../form-create/schemaInitializer.test.ts | 2 +- .../form-edit/schemaInitializer.test.ts | 29 +- .../form/__e2e__/form-edit/templatesOfBug.ts | 367 ++++++++++++++++++ .../__e2e__/schemaInitializer.test.ts | 2 +- .../list/__e2e__/schemaInitializer.test.ts | 2 +- .../table/__e2e__/schemaInitializer.test.ts | 136 +++++++ .../antd/association-field/InternalViewer.tsx | 27 +- .../client/src/schema-initializer/utils.ts | 349 ++++++++++++----- .../SchemaSettingsNumberFormat.tsx | 5 +- 12 files changed, 834 insertions(+), 116 deletions(-) diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx index 76faaf2db..fbd32324b 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerSubMenu.tsx @@ -48,7 +48,11 @@ export const SchemaInitializerMenu: FC = (props) => { const { items, ...others } = props; const { token } = theme.useToken(); const itemsWithPopupClass = useMemo( - () => items.map((item) => ({ ...item, popupClassName: `${hashId} ${componentCls}-menu-sub` })), + () => + items.map((item) => ({ + ...item, + popupClassName: `${hashId} ${componentCls}-menu-sub`, + })), [componentCls, hashId, items], ); // selectedKeys 为了不让有选中效果 @@ -62,9 +66,23 @@ export const SchemaInitializerMenu: FC = (props) => { border-inline-end: 0 !important; .ant-menu-sub { max-height: 50vh !important; + padding: ${token.paddingXXS}px !important; } .ant-menu-item { - margin-block: 0; + margin-inline: ${token.marginXXS}px !important; + margin-block: 0 !important; + width: auto !important; + padding: 0 ${token.paddingSM}px 0 ${token.padding}px !important; + } + .ant-menu-item-group-title { + padding: 0 ${token.padding}px; + margin-inline: 0; + line-height: 32px; + } + .ant-menu-submenu-title { + margin: 0 ${token.marginXXS}px !important; + padding-left: ${token.padding}px !important; + width: auto !important; } .ant-menu-root { margin: 0 -${token.margin}px; diff --git a/packages/core/client/src/application/schema-initializer/components/style.ts b/packages/core/client/src/application/schema-initializer/components/style.ts index 9d8e20811..deb6ade3d 100644 --- a/packages/core/client/src/application/schema-initializer/components/style.ts +++ b/packages/core/client/src/application/schema-initializer/components/style.ts @@ -51,11 +51,6 @@ export const useSchemaInitializerStyles = genStyleHook('nb-schema-initializer', }, }, }, - [`${componentCls}-menu-sub`]: { - ul: { - maxHeight: '50vh !important', - }, - }, [`${componentCls}-item-content`]: { marginLeft: token.marginXS, }, diff --git a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts index 93a59a41e..286fc2024 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/details-multi/__e2e__/schemaInitializer.test.ts @@ -76,9 +76,7 @@ test.describe('configure fields', () => { await expect( page.getByLabel('block-item-CollectionField-general-details-general.singleSelect-Single select'), ).toBeVisible(); - await expect( - page.getByLabel('block-item-CollectionField-general-details-general.manyToOne.nickname'), - ).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-details-users.nickname-Nickname')).toBeVisible(); // delete fields await formItemInitializer.hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts index 619e4f45b..4ad63921c 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaInitializer.test.ts @@ -68,7 +68,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-form-general.id-ID')).toBeVisible(); - await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne.nickname')).toBeVisible(); + await expect(page.getByLabel('block-item-CollectionField-general-form-users.nickname-Nickname')).toBeVisible(); // delete fields await page.getByLabel('schema-initializer-Grid-form:configureFields-general').hover(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index d824e98d9..40ea937b1 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -9,6 +9,7 @@ import { expect, oneEmptyTableBlockWithActions, test } from '@nocobase/test/e2e'; import { T3848 } from '../../../details-single/__e2e__/templatesOfBug'; +import { addAssociationFields } from './templatesOfBug'; test.describe('where edit form block can be added', () => { test('popup', async ({ page, mockPage, mockRecord }) => { @@ -69,4 +70,30 @@ test.describe('where edit form block can be added', () => { test.describe('configure actions', () => {}); -test.describe('configure fields', () => {}); +test.describe('configure fields', () => { + test('add association fields', async ({ page, mockPage, mockRecord }) => { + const nocoPage = await mockPage(addAssociationFields).waitForInit(); + const record = await mockRecord('general', 3); + await nocoPage.goto(); + + // Create association fields for the first, second, and third levels respectively, and assert whether the values are correct + await page.getByLabel('action-Action.Link-Edit-').first().click(); + await page.getByLabel('schema-initializer-Grid-form:').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( + `manyToOne1:${record.manyToOne1.id}`, + ); + await expect( + page.getByLabel('block-item-CollectionField-general-form-targetCollection1.manyToOne2-manyToOne2'), + ).toHaveText(`manyToOne2:${record.manyToOne1.manyToOne2.id}`); + await expect( + page.getByLabel('block-item-CollectionField-general-form-targetCollection2.manyToOne3-manyToOne3'), + ).toHaveText(`manyToOne3:${record.manyToOne1.manyToOne2.manyToOne3.id}`); + }); +}); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts index b41e4fe94..3709035b6 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/templatesOfBug.ts @@ -1111,3 +1111,370 @@ export const T3924: PageConfig = { 'x-index': 1, }, }; +export const addAssociationFields = { + collections: [ + { + name: 'general', + fields: [ + { + interface: 'm2o', + name: 'manyToOne1', + target: 'targetCollection1', + }, + { + interface: 'input', + name: 'generalText', + }, + ], + }, + { + name: 'targetCollection1', + fields: [ + { + interface: 'm2o', + name: 'manyToOne2', + target: 'targetCollection2', + }, + { + interface: 'input', + name: 'targetCollection1Text', + }, + ], + }, + { + name: 'targetCollection2', + fields: [ + { + interface: 'm2o', + name: 'manyToOne3', + target: 'emptyCollection', + }, + { + interface: 'input', + name: 'targetCollection2Text', + }, + ], + }, + { + name: 'emptyCollection', + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + '6d3i9i6e7vg': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + krwfqgbq4fy: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.19-beta', + properties: { + js8p7716v8y: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.19-beta', + properties: { + nwk3otm0wbu: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'general:list', + 'x-use-decorator-props': 'useTableBlockDecoratorProps', + 'x-decorator-props': { + collection: 'general', + dataSource: 'main', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + 'x-app-version': '1.3.19-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.3.19-beta', + 'x-uid': 'wkkuk2ca9oz', + 'x-async': false, + 'x-index': 1, + }, + s00slxnrdog: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + 'x-app-version': '1.3.19-beta', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-initializer': 'table:configureItemActions', + 'x-settings': 'fieldSettings:TableColumn', + 'x-toolbar-props': { + initializer: 'table:configureItemActions', + }, + 'x-app-version': '1.3.19-beta', + properties: { + mpthybxwhi6: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + 'x-app-version': '1.3.19-beta', + properties: { + yp1u36754xm: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:edit', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + 'x-action-context': { + dataSource: 'main', + collection: 'general', + }, + 'x-decorator': 'ACLActionProvider', + 'x-designer-props': { + linkageAction: true, + }, + properties: { + drawer: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit record") }}', + 'x-component': 'Action.Container', + 'x-component-props': { + className: 'nb-action-popup', + }, + properties: { + tabs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Tabs', + 'x-component-props': {}, + 'x-initializer': 'popup:addTab', + properties: { + tab1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{t("Edit")}}', + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': {}, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:common:addBlock', + properties: { + dhu7la398bc: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.19-beta', + properties: { + ex9i4u2sm9o: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.19-beta', + properties: { + gdna46r5948: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': 'general:update', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': + 'useEditFormBlockDecoratorProps', + 'x-decorator-props': { + action: 'get', + dataSource: 'main', + collection: 'general', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:editForm', + 'x-component': 'CardItem', + 'x-app-version': '1.3.19-beta', + properties: { + q7vswewc9bk: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useEditFormBlockProps', + 'x-app-version': '1.3.19-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.19-beta', + 'x-uid': 'fu2wc0gvxg7', + 'x-async': false, + 'x-index': 1, + }, + jvi33jag3zs: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'editForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.3.19-beta', + 'x-uid': 'zox6jjontst', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'e91gqbinrdp', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'd2lh71p96uu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'e5pfc9sit0l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '50qpb5g4zcw', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3u9vcm0gftv', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '59ihnv5o5u5', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '7i9nnjy10pf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kov4btsbs2c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '4fixo409z6d', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'kacm537dm0m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'agw5b9adl6m', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '85dhumz9qp7', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '300ltjt2u3c', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ogjd21lszel', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fn1knfs54ow', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'im83gpz4p8l', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'tir06tp0oyx', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts index 8790f8e36..1cc78d4ee 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/grid-card/__e2e__/schemaInitializer.test.ts @@ -159,7 +159,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-grid-card-general.id-ID').first()).toBeVisible(); await expect( - page.getByLabel('block-item-CollectionField-general-grid-card-general.manyToOne.nickname').first(), + page.getByLabel('block-item-CollectionField-general-grid-card-users.nickname-Nickname').first(), ).toBeVisible(); // delete fields diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts index 07858f5e6..d36800be1 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts @@ -157,7 +157,7 @@ test.describe('configure fields', () => { await page.mouse.move(300, 0); await expect(page.getByLabel('block-item-CollectionField-general-list-general.id-ID').first()).toBeVisible(); await expect( - page.getByLabel('block-item-CollectionField-general-list-general.manyToOne.nickname').first(), + page.getByLabel('block-item-CollectionField-general-list-users.nickname-Nickname').first(), ).toBeVisible(); // delete fields diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index a1be4dbb8..1c281c1b0 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -8,6 +8,7 @@ */ import { Page, expect, oneEmptyTable, test } from '@nocobase/test/e2e'; +import { addAssociationFields } from '../../form/__e2e__/form-edit/templatesOfBug'; import { oneTableWithInheritFields } from './templatesOfBug'; const deleteButton = async (page: Page, name: string) => { @@ -113,6 +114,141 @@ test.describe('configure columns', () => { await expect(page.getByRole('button', { name: 'Nickname', exact: true })).not.toBeVisible(); }); + test('multiple depths of association fields', async ({ page, mockPage, mockRecord, mockRecords }) => { + const nocoPage = await mockPage(addAssociationFields).waitForInit(); + + // The purpose here is to make the IDs in the same row different + await mockRecords('targetCollection1', 2, 0); + await mockRecords('targetCollection2', 1, 0); + + const record = await mockRecord('general', 3); + await nocoPage.goto(); + + // 1. Create association fields for the first, second, and third levels + await page.getByLabel('schema-initializer-TableV2-').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + // 2. Click on the association field, create a details block in the popup, display the ID field, and assert if it's correct + await page + .getByRole('button', { name: `${record.manyToOne1.id}`, exact: true }) + .getByText(record.manyToOne1.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection1').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(`ID:${record.manyToOne1.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection1-View record-mask').click(); + + await page + .getByRole('button', { name: `${record.manyToOne1.manyToOne2.id}`, exact: true }) + .getByText(record.manyToOne1.manyToOne2.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection2').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText(`ID:${record.manyToOne1.manyToOne2.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection2-View record-mask').click(); + + await page + .getByRole('button', { name: `${record.manyToOne1.manyToOne2.manyToOne3.id}`, exact: true }) + .getByText(record.manyToOne1.manyToOne2.manyToOne3.id) + .click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText( + `ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`, + ); + await page.getByLabel('drawer-AssociationField.Viewer-emptyCollection-View record-mask').click(); + + // 测试详情区块的关系字段 + // 1. 点击行操作按钮打开弹窗,创建一个详情区块,并配置第一、二、三级关系字段 + await page.getByLabel('action-Action.Link-Edit-').first().click(); + await page.getByLabel('schema-initializer-Grid-popup').hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-general').hover(); + await page.getByRole('menuitem', { name: 'manyToOne1', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne1 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne2', exact: true }).click(); + await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); + await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); + await page.mouse.move(600, 0); + + // 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确 + await page + .getByLabel('block-item-CollectionField-general-details-general.manyToOne1-manyToOne1') + .getByText(String(record.manyToOne1.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-targetCollection1-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection1').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-targetCollection1-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection1-View record-mask').click(); + + // another field + await page + .getByLabel('block-item-CollectionField-general-details-targetCollection1.manyToOne2-') + .getByText(String(record.manyToOne1.manyToOne2.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-targetCollection2-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-targetCollection2').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-targetCollection2-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.manyToOne2.id}`); + await page.getByLabel('drawer-AssociationField.Viewer-targetCollection2-View record-mask').click(); + + // another field + await page + .getByLabel('block-item-CollectionField-general-details-targetCollection2.manyToOne3-') + .getByText(String(record.manyToOne1.manyToOne2.manyToOne3.id), { exact: true }) + .click(); + await page + .getByTestId('drawer-AssociationField.Viewer-emptyCollection-View record') + .getByLabel('schema-initializer-Grid-popup') + .hover(); + await page.getByRole('menuitem', { name: 'table Details right' }).hover(); + await page.getByRole('menuitem', { name: 'Current record' }).click(); + await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover(); + await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); + await expect( + page + .getByTestId('drawer-AssociationField.Viewer-emptyCollection-View record') + .getByLabel('block-item-CollectionField-'), + ).toHaveText(`ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`); + }); + test.pgOnly('display inherit fields', async ({ page, mockPage, mockRecord }) => { const nocoPage = await mockPage(oneTableWithInheritFields).waitForInit(); const record = await mockRecord('child'); diff --git a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx index 87427f366..a1742a6aa 100644 --- a/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/InternalViewer.tsx @@ -9,10 +9,11 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { toArr } from '@formily/shared'; +import _ from 'lodash'; import React, { FC, Fragment, useRef, useState } from 'react'; import { useDesignable } from '../../'; import { WithoutTableFieldResource } from '../../../block-provider'; -import { useCollectionManager, useCollectionRecordData } from '../../../data-source'; +import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source'; import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider'; import { useCompile } from '../../hooks'; @@ -133,6 +134,25 @@ interface ReadPrettyInternalViewerProps { }; } +/** + * the sourceData is used to get the sourceId + * @param recordData + * @param fieldSchema + * @returns + */ +const getSourceData = (recordData, fieldSchema) => { + const sourceRecordKey = (fieldSchema.name as string) + .split('.') + .filter((o, i, arr) => i < arr.length - 1) + .join('.'); + + if (!sourceRecordKey) { + return recordData; + } + + return _.get(recordData, sourceRecordKey); +}; + export const ReadPrettyInternalViewer: React.FC = observer( (props: ReadPrettyInternalViewerProps) => { const { value, ButtonList = ButtonLinkList } = props; @@ -146,10 +166,13 @@ export const ReadPrettyInternalViewer: React.FC = observer( const { visibleWithURL, setVisibleWithURL } = usePopupUtils(); const [btnHover, setBtnHover] = useState(!!visibleWithURL); const { defaultOpenMode } = useOpenModeContext(); + const recordData = useCollectionRecordData(); const btnElement = ( - + + + ); diff --git a/packages/core/client/src/schema-initializer/utils.ts b/packages/core/client/src/schema-initializer/utils.ts index cba8c5d01..7311de1cb 100644 --- a/packages/core/client/src/schema-initializer/utils.ts +++ b/packages/core/client/src/schema-initializer/utils.ts @@ -24,8 +24,13 @@ import { } from '../'; import { useFormBlockContext } from '../block-provider/FormBlockProvider'; import { useFormActiveFields } from '../block-provider/hooks/useFormActiveFields'; -import { FieldOptions, useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager'; -import { Collection, CollectionFieldOptions } from '../data-source/collection/Collection'; +import { + CollectionFieldOptions_deprecated, + FieldOptions, + useCollectionManager_deprecated, + useCollection_deprecated, +} from '../collection-manager'; +import { Collection, CollectionFieldOptions, CollectionOptions } from '../data-source/collection/Collection'; import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; import { isAssocField } from '../filter-provider/utils'; import { useActionContext, useCompile, useDesignable } from '../schema-component'; @@ -172,60 +177,132 @@ export function useTableColumnInitializerFields() { export function useAssociatedTableColumnInitializerFields() { const { name, fields } = useCollection_deprecated(); + const { t } = useTranslation(); const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); const groups = fields ?.filter((field) => { return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface); }) ?.map((field) => { - const subFields = getCollectionFields(field.target); - const items = subFields - // ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface)) - ?.filter( - (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren, - ) - ?.map((subField) => { - const interfaceConfig = getInterface(subField.interface); - const schema = { - // type: 'string', - name: `${field.name}.${subField.name}`, - // title: subField?.uiSchema?.title || subField.name, - 'x-component': 'CollectionField', - 'x-read-pretty': true, - 'x-collection-field': `${name}.${field.name}.${subField.name}`, - 'x-component-props': {}, - }; - - return { - type: 'item', - name: subField.name, - title: subField?.uiSchema?.title || subField.name, - Component: 'TableCollectionFieldInitializer', - find: findTableColumn, - remove: removeTableColumn, - schemaInitialize: (s) => { - interfaceConfig?.schemaInitialize?.(s, { - field: subField, - readPretty: true, - block: 'Table', - targetCollection: getCollection(field.target), - }); - }, - field: subField, - schema, - } as SchemaInitializerItemType; - }); - return { - type: 'subMenu', - name: field.uiSchema?.title, - title: field.uiSchema?.title, - children: items, - } as SchemaInitializerItemType; + return getGroupItemForTable({ + getCollectionFields, + field, + getInterface, + getCollection, + schemaName: field.name, + maxDepth: 2, + depth: 1, + t, + }); }); return groups; } +function getGroupItemForTable({ + getCollectionFields, + field, + getInterface, + getCollection, + schemaName, + maxDepth, + depth, + t, +}: { + getCollectionFields: (name: any, customDataSource?: string) => CollectionFieldOptions_deprecated[]; + field: CollectionFieldOptions; + getInterface: (name: string) => any; + getCollection: (name: any, customDataSource?: string) => CollectionOptions; + schemaName: string; + maxDepth: number; + depth: number; + t: any; +}) { + const subFields = getCollectionFields(field.target); + const items = subFields + ?.filter( + (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren, + ) + ?.map((subField) => { + const interfaceConfig = getInterface(subField.interface); + const newSchemaName = `${schemaName}.${subField.name}`; + const schema = { + // type: 'string', + name: newSchemaName, + // title: subField?.uiSchema?.title || subField.name, + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-collection-field': `${field.target}.${subField.name}`, + 'x-component-props': {}, + }; + + return { + type: 'item', + name: newSchemaName, + title: subField?.uiSchema?.title || subField.name, + Component: 'TableCollectionFieldInitializer', + find: findTableColumn, + remove: removeTableColumn, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field: subField, + readPretty: true, + block: 'Table', + targetCollection: getCollection(field.target), + }); + }, + field: subField, + schema, + } as SchemaInitializerItemType; + }); + + const displayCollectionFields = { + type: 'itemGroup', + name: `${schemaName}-displayCollectionFields`, + title: t('Display fields'), + children: items, + }; + + const children = [displayCollectionFields]; + + if (depth < maxDepth) { + const subChildren = subFields + ?.filter((subField) => { + return ['o2o', 'oho', 'obo', 'm2o'].includes(subField.interface); + }) + .map((subField) => { + return getGroupItemForTable({ + getCollectionFields, + field: subField, + getInterface, + getCollection, + schemaName: `${schemaName}.${subField.name}`, + maxDepth, + depth: depth + 1, + t, + }); + }); + + if (subChildren.length) { + const group: any = { + type: 'itemGroup', + name: `${schemaName}-associationFields`, + title: t('Display association fields'), + children: subChildren, + }; + + children.push(group); + } + } + + return { + type: 'subMenu', + name: `${schemaName}`, + title: field.uiSchema?.title, + children, + } as SchemaInitializerItemType; +} + export function useInheritsTableColumnInitializerFields() { const { name } = useCollection_deprecated(); const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = @@ -429,6 +506,7 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { const { name, fields } = useCollection_deprecated(); const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); const form = useForm(); + const { t } = useTranslation(); const { readPretty = form.readPretty, block = 'Form' } = options || {}; const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o']; const groups = fields @@ -436,58 +514,18 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => { return interfaces.includes(field.interface); }) ?.map((field) => { - const subFields = getCollectionFields(field.target); - const items = subFields - ?.filter( - (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren, - ) - ?.map((subField) => { - const interfaceConfig = getInterface(subField.interface); - const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; - const schema = { - type: 'string', - name: `${field.name}.${subField.name}`, - // 'x-designer': 'FormItem.Designer', - 'x-toolbar': 'FormItemSchemaToolbar', - 'x-settings': 'fieldSettings:FormItem', - 'x-component': 'CollectionField', - 'x-read-pretty': readPretty, - 'x-component-props': { - 'pattern-disable': block === 'Form' && readPretty, - fieldNames: isFileCollection - ? { - label: 'preview', - value: 'id', - } - : undefined, - }, - 'x-decorator': 'FormItem', - 'x-collection-field': `${name}.${field.name}.${subField.name}`, - }; - return { - name: subField?.uiSchema?.title || subField.name, - type: 'item', - title: subField?.uiSchema?.title || subField.name, - Component: 'CollectionFieldInitializer', - remove: removeGridFormItem, - schemaInitialize: (s) => { - interfaceConfig?.schemaInitialize?.(s, { - field: subField, - block, - readPretty, - targetCollection: getCollection(field.target), - }); - }, - schema, - } as SchemaInitializerItemType; - }); - - return { - type: 'subMenu', - name: field.uiSchema?.title, - title: field.uiSchema?.title, - children: items, - } as SchemaInitializerItemType; + return getGroupItemForForm({ + getCollectionFields, + field, + getInterface, + getCollection, + readPretty, + block, + schemaName: field.name, + maxDepth: 2, + depth: 1, + t, + }); }); return groups; }; @@ -1540,6 +1578,123 @@ const getChildren = ({ }); }; +function getGroupItemForForm({ + getCollectionFields, + field, + getInterface, + getCollection, + readPretty, + block, + maxDepth, + depth, + schemaName, + t, +}: { + getCollectionFields: (name: any, customDataSource?: string) => CollectionFieldOptions_deprecated[]; + field: CollectionFieldOptions; + getInterface: (name: string) => any; + getCollection: (name: any, customDataSource?: string) => CollectionOptions; + readPretty: any; + block: any; + maxDepth: number; + depth: number; + schemaName: string; + t: any; +}) { + const subFields = getCollectionFields(field.target); + const items = subFields + ?.filter((subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren) + ?.map((subField) => { + const interfaceConfig = getInterface(subField.interface); + const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; + const newSchemaName = `${schemaName}.${subField.name}`; + const schema = { + type: 'string', + name: newSchemaName, + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-read-pretty': readPretty, + 'x-component-props': { + 'pattern-disable': block === 'Form' && readPretty, + fieldNames: isFileCollection + ? { + label: 'preview', + value: 'id', + } + : undefined, + }, + 'x-decorator': 'FormItem', + 'x-collection-field': `${field.target}.${subField.name}`, + }; + return { + name: newSchemaName, + type: 'item', + title: subField?.uiSchema?.title || subField.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field: subField, + block, + readPretty, + targetCollection: getCollection(field.target), + }); + }, + schema, + } as SchemaInitializerItemType; + }); + + const displayCollectionFields = { + type: 'itemGroup', + name: `${schemaName}-displayCollectionFields`, + title: t('Display fields'), + children: items, + }; + + const children = [displayCollectionFields]; + + if (depth < maxDepth) { + const subChildren = subFields + ?.filter((subField) => { + return ['o2o', 'oho', 'obo', 'm2o'].includes(subField.interface); + }) + .map((subField) => { + return getGroupItemForForm({ + getCollectionFields, + field: subField, + getInterface, + getCollection, + schemaName: `${schemaName}.${subField.name}`, + readPretty, + block, + maxDepth, + depth: depth + 1, + t, + }); + }); + + if (subChildren.length) { + const group: any = { + type: 'itemGroup', + name: `${schemaName}-associationFields`, + title: t('Display association fields'), + children: subChildren, + }; + + children.push(group); + } + } + + return { + type: 'subMenu', + name: `${schemaName}.${field.name}`, + title: field.uiSchema?.title, + children, + } as SchemaInitializerItemType; +} + function useAssociationFields({ componentName, filterCollections, diff --git a/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx b/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx index f6c6549c6..592fd4467 100644 --- a/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettingsNumberFormat.tsx @@ -7,11 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { css } from '@emotion/css'; import { ISchema, Schema, useField, useForm } from '@formily/react'; +import { Select } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Select } from 'antd'; import { useCollectionManager_deprecated, useDesignable } from '..'; import { SchemaSettingsModalItem } from './SchemaSettings'; @@ -41,7 +40,7 @@ export const SchemaSettingsNumberFormat = function NumberFormatConfig(props: { f const collectionField = getCollectionJoinField(fieldSchema?.['x-collection-field']) || {}; const { formatStyle, unitConversion, unitConversionType, separator, step, addonBefore, addonAfter } = fieldSchema['x-component-props'] || {}; - const { step: prescition } = collectionField?.uiSchema['x-component-props'] || {}; + const { step: prescition } = collectionField?.uiSchema?.['x-component-props'] || {}; return (