From 72367a9f852890058cceddf53d92abab493dab9b Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Wed, 11 Oct 2023 19:28:17 +0800 Subject: [PATCH] fix: record picker cannot select data when editing (#2798) * fix: record picker cannot select data when editing * refactor: rename file name * fix: just run the lazy load once * test: add test * test: fix test * chore: better comment --------- Co-authored-by: Rain <958414905@qq.com> --- .../src/__tests__/e2e/bugs/T-2200.test.ts | 367 ++++++++++++++++++ .../antd/form-item/FormItem.tsx | 4 +- .../useLazyLoadAssociationFieldOfForm.ts | 49 ++- .../useLazyLoadAssociationFieldOfSubForm.ts | 66 ---- ...eLazyLoadDisplayAssociationFieldsOfForm.ts | 53 +++ 5 files changed, 453 insertions(+), 86 deletions(-) create mode 100644 packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts delete mode 100644 packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfSubForm.ts create mode 100644 packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts diff --git a/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts b/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts new file mode 100644 index 000000000..68eeb5956 --- /dev/null +++ b/packages/core/client/src/__tests__/e2e/bugs/T-2200.test.ts @@ -0,0 +1,367 @@ +import { expect, test } from '@nocobase/test/client'; + +const config = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + n7k4be0tpgt: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'BlockInitializers', + properties: { + ai6ojrzwkiv: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + amsy67ggjr3: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + ajob701dt18: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableBlockProvider', + 'x-acl-action': 'users:list', + 'x-decorator-props': { + collection: 'users', + resource: 'users', + action: 'list', + params: { + pageSize: 20, + }, + rowKey: 'id', + showIndex: true, + dragSort: false, + disableTemplate: false, + }, + 'x-designer': 'TableBlockDesigner', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'TableActionInitializers', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--nb-spacing)', + }, + }, + 'x-uid': 'farfrcvsydq', + 'x-async': false, + 'x-index': 1, + }, + t1o3f4r1j4q: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'array', + 'x-initializer': 'TableColumnInitializers', + 'x-component': 'TableV2', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + useProps: '{{ useTableBlockProps }}', + }, + 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-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'TableActionColumnInitializers', + properties: { + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: { + ezur2k7gz8k: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + title: '{{ t("Edit") }}', + 'x-action': 'update', + 'x-designer': 'Action.Designer', + 'x-component': 'Action.Link', + 'x-component-props': { + openMode: 'drawer', + icon: 'EditOutlined', + }, + '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': 'TabPaneInitializers', + 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': 'RecordBlockInitializers', + properties: { + dv88ljcl0qw: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + '548w29v01eb': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + q83tzsynghl: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: false, + }, + 'x-acl-action': 'users:update', + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + useSourceId: '{{ useSourceIdFromParentRecord }}', + useParams: '{{ useParamsFromRecord }}', + action: 'get', + resource: 'users', + collection: 'users', + }, + 'x-designer': 'FormV2.Designer', + 'x-component': 'CardItem', + 'x-component-props': {}, + properties: { + '8kzqujhwsyl': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-component-props': { + useProps: '{{ useFormBlockProps }}', + }, + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'FormItemInitializers', + properties: { + '3i87cpmv87x': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + properties: { + am7wls8qu12: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + properties: { + roles: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-designer': 'FormItem.Designer', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'users.roles', + 'x-component-props': {}, + 'x-uid': 'e196r19yspx', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ato3s57xtbt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'dx3bqu24t50', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'rjbjrnn88sq', + 'x-async': false, + 'x-index': 1, + }, + actions: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'UpdateFormActionInitializers', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 24, + }, + }, + 'x-uid': 'zh8o2drtd9h', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ytafkst3w0s', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ioqb8wteiwr', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'iavf0r5sl2n', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '994dlhgh1iz', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'k390f3n1rz7', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xm264vyzmkf', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'asl2rgfrz7p', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'xfre469n0yi', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'gdjgqvxv6b1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fnctj4uc8xy', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '2dc4d3mlw3k', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '51i2scjmis1', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '75owqo6visj', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'r4bvdbzinup', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'h066j2xouwn', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'qrkik5iy790', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3vzzy7tipgq', + 'x-async': true, + 'x-index': 1, + }, +}; + +// fix https://nocobase.height.app/T-2200 +test('BUG: should be possible to change the value of the association field normally', async ({ page, mockPage }) => { + await mockPage(config).goto(); + + await page.getByTestId('table-block').getByText('Edit').click(); + await expect(page.getByTestId('antd-select').getByText('AdminMemberRoot')).toBeVisible(); + + await page.getByTestId('antd-select').getByText('AdminMemberRoot').click(); + await page.getByRole('option', { name: 'Member' }).click(); + + // 点击空白处,关闭下拉框。如不关闭,会挡住按钮,导致鼠标 hover 无效 + await page + .getByTestId('action-drawer') + .locator('div') + .filter({ hasText: 'EditAdd tabRoles:AdminRoot Configure fieldsConfigure actionsAdd block' }) + .nth(2) + .click(); + + await expect(page.getByTestId('antd-select').getByText('AdminRoot')).toBeVisible(); + + await page.getByTestId('configure-fields-button-of-form-item-users').hover(); + await page.getByRole('menuitem', { name: 'Nickname' }).click(); + await page.waitForTimeout(200); + await expect(page.getByTestId('antd-select').getByText('AdminRoot')).toBeVisible(); +}); diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index b7d03bbb9..0bc11af75 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -27,8 +27,8 @@ import { HTMLEncode } from '../input/shared'; import { useColorFields } from '../table-v2/Table.Column.Designer'; import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; import { useEnsureOperatorsValid } from './SchemaSettingOptions'; -import useLazyLoadDisplayAssociationFieldsOfForm from './hooks/useLazyLoadAssociationFieldOfForm'; -import useLazyLoadAssociationFieldOfForm from './hooks/useLazyLoadAssociationFieldOfSubForm'; +import useLazyLoadAssociationFieldOfForm from './hooks/useLazyLoadAssociationFieldOfForm'; +import useLazyLoadDisplayAssociationFieldsOfForm from './hooks/useLazyLoadDisplayAssociationFieldsOfForm'; import useParseDefaultValue from './hooks/useParseDefaultValue'; export const FormItem: any = observer( diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfForm.ts index bbc9e5556..96d63d1dd 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfForm.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfForm.ts @@ -1,53 +1,66 @@ import { Field } from '@formily/core'; -import { useField, useFieldSchema, useForm } from '@formily/react'; +import { useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; import { useEffect } from 'react'; +import { useFormBlockType, useRecord } from '../../../..'; import { useCollection, useCollectionManager } from '../../../../collection-manager'; +import { useFlag } from '../../../../flag-provider'; import { useVariables } from '../../../../variables'; import { transformVariableValue } from '../../../../variables/utils/transformVariableValue'; /** - * 用于懒加载 Form 区块中只用于展示的关联字段的值 + * 用于懒加载 Form 区块中正常的关系字段 * - * - 在表单区块中,有一个 Display association fields 的选项,这里面的字段,只是为了显示出相应的值,不可更改 - * - 这里就是用于加载这些字段的值 + * - 当 record 中缺少对应的字段值时,通过解析变量的方法去获取关系字段的值 + * - 注意:应该只有在编辑模式下运行,创建模式下直接返回 */ -const useLazyLoadDisplayAssociationFieldsOfForm = () => { +const useLazyLoadAssociationFieldOfForm = () => { const { name } = useCollection(); const { getCollectionJoinField } = useCollectionManager(); - const form = useForm(); + const record = useRecord(); const fieldSchema = useFieldSchema(); const variables = useVariables(); const field = useField(); + const { isInAssignFieldValues } = useFlag() || {}; + const { type: formBlockType } = useFormBlockType(); const schemaName = fieldSchema.name.toString(); useEffect(() => { - // 在数据表管理页面,也存在 `a.b` 之类的 schema name,其 collectionName 为 fields,所以在这里排除一下 `name === 'fields'` 的情况 - if (!schemaName.includes('.') || !variables || name === 'fields') { + if (isInAssignFieldValues || formBlockType === 'create') { return; } - const formVariable = { - name: '$nForm', - ctx: form.values, - collectionName: name, - }; - const variableString = `{{ $nForm.${schemaName} }}`; const collectionField = getCollectionJoinField(`${name}.${schemaName}`); if (!collectionField) { - return console.error(new Error(`useLazyLoadAssociationField: ${schemaName} not found in collection ${name}`)); + return; } + const cloneRecord = { ...record }; + delete cloneRecord['__parent']; + + if (_.isEmpty(cloneRecord) || !variables || record[schemaName] != null) { + return; + } + + // 通过模拟一个 record 变量,用解析变量的方法去获取关系字段的值 + const recordVariable = { + name: '$nRecord', + ctx: cloneRecord, + collectionName: name, + }; + const variableString = `{{ $nRecord.${schemaName} }}`; + variables - .parseVariable(variableString, formVariable) + .parseVariable(variableString, recordVariable) .then((value) => { field.value = transformVariableValue(value, { targetCollectionField: collectionField }); }) .catch((err) => { console.error(err); }); - }, [schemaName.includes('.') ? form.values[schemaName.split('.')[0]] : null]); + }, []); }; -export default useLazyLoadDisplayAssociationFieldsOfForm; +export default useLazyLoadAssociationFieldOfForm; diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfSubForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfSubForm.ts deleted file mode 100644 index 8cb7fa1de..000000000 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadAssociationFieldOfSubForm.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Field } from '@formily/core'; -import { useField, useFieldSchema } from '@formily/react'; -import _ from 'lodash'; -import { useEffect } from 'react'; -import { useFormBlockType, useRecord } from '../../../..'; -import { useCollection, useCollectionManager } from '../../../../collection-manager'; -import { useFlag } from '../../../../flag-provider'; -import { useVariables } from '../../../../variables'; -import { transformVariableValue } from '../../../../variables/utils/transformVariableValue'; - -/** - * 用于懒加载 Form 区块中正常的关系字段 - * - * - 当 record 中缺少对应的字段值时,通过解析变量的方法去获取关系字段的值 - * - 注意:应该只有在编辑模式下运行,创建模式下直接返回 - */ -const useLazyLoadAssociationFieldOfForm = () => { - const { name } = useCollection(); - const { getCollectionJoinField } = useCollectionManager(); - const record = useRecord(); - const fieldSchema = useFieldSchema(); - const variables = useVariables(); - const field = useField(); - const { isInAssignFieldValues } = useFlag() || {}; - const { type: formBlockType } = useFormBlockType(); - - const schemaName = fieldSchema.name.toString(); - - useEffect(() => { - if (isInAssignFieldValues || formBlockType === 'create') { - return; - } - - const collectionField = getCollectionJoinField(`${name}.${schemaName}`); - - if (!collectionField) { - return; - } - - const cloneRecord = { ...record }; - delete cloneRecord['__parent']; - - if (_.isEmpty(cloneRecord) || !variables || record[schemaName] != null) { - return; - } - - // 通过模拟一个 record 变量,用解析变量的方法去获取关系字段的值 - const recordVariable = { - name: '$nRecord', - ctx: cloneRecord, - collectionName: name, - }; - const variableString = `{{ $nRecord.${schemaName} }}`; - - variables - .parseVariable(variableString, recordVariable) - .then((value) => { - field.value = transformVariableValue(value, { targetCollectionField: collectionField }); - }) - .catch((err) => { - console.error(err); - }); - }, [field, name, record, schemaName, isInAssignFieldValues]); -}; - -export default useLazyLoadAssociationFieldOfForm; diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts new file mode 100644 index 000000000..bbc9e5556 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLazyLoadDisplayAssociationFieldsOfForm.ts @@ -0,0 +1,53 @@ +import { Field } from '@formily/core'; +import { useField, useFieldSchema, useForm } from '@formily/react'; +import { useEffect } from 'react'; +import { useCollection, useCollectionManager } from '../../../../collection-manager'; +import { useVariables } from '../../../../variables'; +import { transformVariableValue } from '../../../../variables/utils/transformVariableValue'; + +/** + * 用于懒加载 Form 区块中只用于展示的关联字段的值 + * + * - 在表单区块中,有一个 Display association fields 的选项,这里面的字段,只是为了显示出相应的值,不可更改 + * - 这里就是用于加载这些字段的值 + */ +const useLazyLoadDisplayAssociationFieldsOfForm = () => { + const { name } = useCollection(); + const { getCollectionJoinField } = useCollectionManager(); + const form = useForm(); + const fieldSchema = useFieldSchema(); + const variables = useVariables(); + const field = useField(); + + const schemaName = fieldSchema.name.toString(); + + useEffect(() => { + // 在数据表管理页面,也存在 `a.b` 之类的 schema name,其 collectionName 为 fields,所以在这里排除一下 `name === 'fields'` 的情况 + if (!schemaName.includes('.') || !variables || name === 'fields') { + return; + } + + const formVariable = { + name: '$nForm', + ctx: form.values, + collectionName: name, + }; + const variableString = `{{ $nForm.${schemaName} }}`; + const collectionField = getCollectionJoinField(`${name}.${schemaName}`); + + if (!collectionField) { + return console.error(new Error(`useLazyLoadAssociationField: ${schemaName} not found in collection ${name}`)); + } + + variables + .parseVariable(variableString, formVariable) + .then((value) => { + field.value = transformVariableValue(value, { targetCollectionField: collectionField }); + }) + .catch((err) => { + console.error(err); + }); + }, [schemaName.includes('.') ? form.values[schemaName.split('.')[0]] : null]); +}; + +export default useLazyLoadDisplayAssociationFieldsOfForm;