From dd71cdaaa40f796f4a7f8ff367841d2fc95ce692 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Mon, 21 Oct 2024 08:46:04 +0800 Subject: [PATCH] feat(variable): add a new variable named Parent object (#5449) * feat(variable): add a new variable named Parent object * test: add tests and fix bug * fix(linkageRules): fix issue with variable options in subtables * test: add e2e test --- packages/core/client/src/locale/en_US.json | 3 +- packages/core/client/src/locale/es_ES.json | 3 +- packages/core/client/src/locale/fr_FR.json | 3 +- packages/core/client/src/locale/ja_JP.json | 3 +- packages/core/client/src/locale/ko_KR.json | 3 +- packages/core/client/src/locale/pt_BR.json | 3 +- packages/core/client/src/locale/ru_RU.json | 3 +- packages/core/client/src/locale/tr_TR.json | 3 +- packages/core/client/src/locale/uk_UA.json | 3 +- packages/core/client/src/locale/zh-CN.json | 3 +- packages/core/client/src/locale/zh-TW.json | 3 +- .../variable/__e2e__/parentObject.test.ts | 135 ++++++ .../src/modules/variable/__e2e__/templates.ts | 403 ++++++++++++++++++ .../antd/association-field/SubTable.tsx | 4 +- .../__tests__/hooks.test.tsx | 118 +++++ .../antd/association-field/hooks.tsx | 22 +- .../schema-settings/LinkageRules/index.tsx | 20 +- .../src/schema-settings/SchemaSettings.tsx | 4 +- .../hooks/useParentIterationVariable.ts | 69 +++ .../VariableInput/hooks/useVariableOptions.ts | 10 + .../src/variables/hooks/useLocalVariables.tsx | 14 + 21 files changed, 799 insertions(+), 33 deletions(-) create mode 100644 packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts create mode 100644 packages/core/client/src/schema-component/antd/association-field/__tests__/hooks.test.tsx create mode 100644 packages/core/client/src/schema-settings/VariableInput/hooks/useParentIterationVariable.ts diff --git a/packages/core/client/src/locale/en_US.json b/packages/core/client/src/locale/en_US.json index 9e443fc302..5204c019ae 100644 --- a/packages/core/client/src/locale/en_US.json +++ b/packages/core/client/src/locale/en_US.json @@ -841,5 +841,6 @@ "is any of": "is any of", "Plugin dependency version mismatch": "Plugin dependency version mismatch", "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?", - "Allow multiple selection": "Allow multiple selection" + "Allow multiple selection": "Allow multiple selection", + "Parent object": "Parent object" } diff --git a/packages/core/client/src/locale/es_ES.json b/packages/core/client/src/locale/es_ES.json index baf301ca54..3e9baf07f5 100644 --- a/packages/core/client/src/locale/es_ES.json +++ b/packages/core/client/src/locale/es_ES.json @@ -766,5 +766,6 @@ "Clear default value": "Borrar valor por defecto", "Open in new window": "Abrir en una nueva ventana", "Sorry, the page you visited does not exist.": "Lo siento, la página que visitaste no existe.", - "Allow multiple selection": "Permitir selección múltiple" + "Allow multiple selection": "Permitir selección múltiple", + "Parent object": "Objeto padre" } diff --git a/packages/core/client/src/locale/fr_FR.json b/packages/core/client/src/locale/fr_FR.json index f4de482328..bfb4c7a425 100644 --- a/packages/core/client/src/locale/fr_FR.json +++ b/packages/core/client/src/locale/fr_FR.json @@ -786,5 +786,6 @@ "Clear default value": "Effacer la valeur par défaut", "Open in new window": "Ouvrir dans une nouvelle fenêtre", "Sorry, the page you visited does not exist.": "Désolé, la page que vous avez visitée n'existe pas.", - "Allow multiple selection": "Permettre la sélection multiple" + "Allow multiple selection": "Permettre la sélection multiple", + "Parent object": "Objet parent" } diff --git a/packages/core/client/src/locale/ja_JP.json b/packages/core/client/src/locale/ja_JP.json index 9a5e7d9ce3..951f325f56 100644 --- a/packages/core/client/src/locale/ja_JP.json +++ b/packages/core/client/src/locale/ja_JP.json @@ -1007,5 +1007,6 @@ "NaN": "なし", "true": "真", "false": "偽", - "Allow multiple selection": "複数選択を許可" + "Allow multiple selection": "複数選択を許可", + "Parent object": "親オブジェクト" } diff --git a/packages/core/client/src/locale/ko_KR.json b/packages/core/client/src/locale/ko_KR.json index 57a49d1b67..8bb862ae81 100644 --- a/packages/core/client/src/locale/ko_KR.json +++ b/packages/core/client/src/locale/ko_KR.json @@ -877,5 +877,6 @@ "Clear default value": "기본값 지우기", "Open in new window": "새 창에서 열기", "Sorry, the page you visited does not exist.": "죄송합니다. 방문한 페이지가 존재하지 않습니다.", - "Allow multiple selection": "다중 선택 허용" + "Allow multiple selection": "다중 선택 허용", + "Parent object": "부모 객체" } diff --git a/packages/core/client/src/locale/pt_BR.json b/packages/core/client/src/locale/pt_BR.json index 8ffa0bfb53..cfc5ba7db5 100644 --- a/packages/core/client/src/locale/pt_BR.json +++ b/packages/core/client/src/locale/pt_BR.json @@ -743,5 +743,6 @@ "Clear default value": "Limpar valor padrão", "Open in new window": "Abrir em nova janela", "Sorry, the page you visited does not exist.": "Desculpe, a página que você visitou não existe.", - "Allow multiple selection": "Permitir seleção múltipla" + "Allow multiple selection": "Permitir seleção múltipla", + "Parent object": "Objeto pai" } diff --git a/packages/core/client/src/locale/ru_RU.json b/packages/core/client/src/locale/ru_RU.json index 04191f2e06..698177a1f5 100644 --- a/packages/core/client/src/locale/ru_RU.json +++ b/packages/core/client/src/locale/ru_RU.json @@ -580,5 +580,6 @@ "Clear default value": "Очистить значение по умолчанию", "Open in new window": "Открыть в новом окне", "Sorry, the page you visited does not exist.": "Извините, посещенной вами страницы не существует.", - "Allow multiple selection": "Разрешить множественный выбор" + "Allow multiple selection": "Разрешить множественный выбор", + "Parent object": "Родительский объект" } diff --git a/packages/core/client/src/locale/tr_TR.json b/packages/core/client/src/locale/tr_TR.json index df90bc6327..b000898d32 100644 --- a/packages/core/client/src/locale/tr_TR.json +++ b/packages/core/client/src/locale/tr_TR.json @@ -578,5 +578,6 @@ "Clear default value": "Varsayılan değeri temizle", "Open in new window": "Yeni pencerede aç", "Sorry, the page you visited does not exist.": "Üzgünüz, ziyaret ettiğiniz sayfa mevcut değil.", - "Allow multiple selection": "Çoklu seçim izni" + "Allow multiple selection": "Çoklu seçim izni", + "Parent object": "Üst nesne" } diff --git a/packages/core/client/src/locale/uk_UA.json b/packages/core/client/src/locale/uk_UA.json index 0f18dec1b9..bdcfae81b5 100644 --- a/packages/core/client/src/locale/uk_UA.json +++ b/packages/core/client/src/locale/uk_UA.json @@ -786,5 +786,6 @@ "Clear default value": "Очистити значення за замовчуванням", "Open in new window": "Відкрити в новому вікні", "Sorry, the page you visited does not exist.": "Вибачте, сторінка, яку ви відвідали, не існує.", - "Allow multiple selection": "Дозволити множинний вибір" + "Allow multiple selection": "Дозволити множинний вибір", + "Parent object": "Батьківський об'єкт" } diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index ad10d76d18..ee30950fe2 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -975,5 +975,6 @@ "The current user only has the UI configuration permission, but don't have view permission for collection \"{{name}}\"": "当前用户只有 UI 配置权限,但没有数据表 \"{{name}}\" 查看权限。", "Plugin dependency version mismatch": "插件依赖版本不一致", "The current dependency version of the plugin does not match the version of the application and may not work properly. Are you sure you want to continue enabling the plugin?": "当前插件的依赖版本与应用的版本不一致,可能无法正常工作。您确定要继续激活插件吗?", - "Allow multiple selection": "允许多选" + "Allow multiple selection": "允许多选", + "Parent object": "上级对象" } diff --git a/packages/core/client/src/locale/zh-TW.json b/packages/core/client/src/locale/zh-TW.json index 5c6f16d0ab..9faa0805ba 100644 --- a/packages/core/client/src/locale/zh-TW.json +++ b/packages/core/client/src/locale/zh-TW.json @@ -875,5 +875,6 @@ "Clear default value": "清除預設值", "Open in new window": "新窗口打開", "Sorry, the page you visited does not exist.": "抱歉,你訪問的頁面不存在。", - "Allow multiple selection": "允許多選" + "Allow multiple selection": "允許多選", + "Parent object": "上級物件" } diff --git a/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts new file mode 100644 index 0000000000..42151faefb --- /dev/null +++ b/packages/core/client/src/modules/variable/__e2e__/parentObject.test.ts @@ -0,0 +1,135 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { expect, test } from '@nocobase/test/e2e'; +import { inDefaultValue } from './templates'; + +test.describe('variable: parent object', () => { + test('in default value', async ({ page, mockPage }) => { + await mockPage(inDefaultValue).goto(); + + // 1. 在当前表单中的子表单中,使用 “当前表单” 变量为 text2 字段设置默认值 + await page.getByLabel('block-item-CollectionField-collection2-form-collection2.text2-text2').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection2-collection2.text2', { + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'text1' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // 2. 在子表单中的子表格中,使用 “上级对象” 变量为 text3 字段设置默认值 + await page.getByRole('button', { name: 'text3' }).click(); + await page.getByLabel('designer-schema-settings-TableV2.Column-fieldSettings:TableColumn-collection3').click(); + await page.getByRole('menuitem', { name: 'Set default value' }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent object right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'text2' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // 3. 当更改当前表单中的 text1 字段后,text2 和 text3 字段应该也会被自动更改 + await page.getByRole('button', { name: 'Add new' }).click(); + await page + .getByLabel('block-item-CollectionField-collection1-form-collection1.text1-text1') + .getByRole('textbox') + .fill('123456abcdefg'); + await expect( + page.getByLabel('block-item-CollectionField-collection2-form-collection2.text2-text2').getByRole('textbox'), + ).toHaveValue('123456abcdefg'); + await expect( + page.getByLabel('block-item-CollectionField-collection2-form-collection2.m2m2-m2m2').getByRole('textbox'), + ).toHaveValue('123456abcdefg'); + }); + + test('in linkage rules', async ({ page, mockPage }) => { + await mockPage(inDefaultValue).goto(); + + // 1. Use "Current form" and "Parent object" variables in nested subforms and subtables + await page.getByLabel('block-item-CollectionField-collection1-form-collection1.m2m1-m2m1').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection1-collection1.m2m1', { + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByTitle('text2').click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByTestId('select-linkage-value-type').click(); + await page.getByTitle('Expression').click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current form right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'text1' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + await page.getByLabel('block-item-CollectionField-collection2-form-collection2.m2m2-m2m2').hover(); + await page + .getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-collection2-collection2.m2m2', { + exact: true, + }) + .hover(); + await page.getByRole('menuitem', { name: 'Linkage rules' }).click(); + await page.getByRole('button', { name: 'plus Add linkage rule' }).click(); + await page.getByText('Add property').click(); + await page.getByTestId('select-linkage-property-field').click(); + await page.getByTitle('text3').click(); + await page.getByTestId('select-linkage-action-field').click(); + await page.getByRole('option', { name: 'Value', exact: true }).click(); + await page.getByTestId('select-linkage-value-type').click(); + await page.getByTitle('Expression').click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Parent object right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'text2' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + + // 2. Assert: When the text1 field in the current form is changed, the text2 and text3 fields should also be automatically changed + await page.getByRole('button', { name: 'Add new' }).click(); + await page + .getByLabel('block-item-CollectionField-collection1-form-collection1.text1-text1') + .getByRole('textbox') + .fill('123456abcdefg'); + + await expect( + page.getByLabel('block-item-CollectionField-collection2-form-collection2.text2-text2').getByRole('textbox'), + ).toHaveValue('123456abcdefg'); + await expect( + page.getByLabel('block-item-CollectionField-collection2-form-collection2.m2m2-m2m2').getByRole('textbox'), + ).toHaveValue('123456abcdefg'); + + // 3. Test if the "Current object" variable can be used normally in the subform + await page.getByLabel('schema-initializer-Grid-form:configureFields-collection2').hover(); + await page.getByRole('menuitem', { name: 'form Add text' }).click(); + await page.getByLabel('block-item-Markdown.Void-').hover(); + await page.getByLabel('designer-schema-settings-Markdown.Void-blockSettings:markdown-collection2').hover(); + await page.getByRole('menuitem', { name: 'Edit markdown' }).click(); + await page.getByText('This is a demo text, **').click(); + await page.getByText('This is a demo text, **').clear(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'Current object right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'text2' }).click(); + await page.getByRole('button', { name: 'Save' }).click(); + + await page + .getByLabel('block-item-CollectionField-collection2-form-collection2.text2-text2') + .getByRole('textbox') + .fill('987654321'); + + // 4. Assert: The subtable and Markdown should be updated in real-time + await expect( + page.getByLabel('block-item-CollectionField-collection2-form-collection2.m2m2-m2m2').getByRole('textbox'), + ).toHaveValue('987654321'); + // await expect(page.getByLabel('block-item-Markdown.Void-')).toHaveText('987654321'); + }); +}); diff --git a/packages/core/client/src/modules/variable/__e2e__/templates.ts b/packages/core/client/src/modules/variable/__e2e__/templates.ts index c502a0955a..f5c59fee6d 100644 --- a/packages/core/client/src/modules/variable/__e2e__/templates.ts +++ b/packages/core/client/src/modules/variable/__e2e__/templates.ts @@ -1297,3 +1297,406 @@ export const tableSelectedRecords = { 'x-index': 1, }, }; +export const inDefaultValue = { + collections: [ + { + name: 'collection1', + fields: [ + { + name: 'text1', + interface: 'input', + }, + { + name: 'm2m1', + interface: 'm2m', + target: 'collection2', + }, + ], + }, + { + name: 'collection2', + fields: [ + { + name: 'text2', + interface: 'input', + }, + { + name: 'm2m2', + interface: 'm2m', + target: 'collection3', + }, + ], + }, + { + name: 'collection3', + fields: [ + { + name: 'text3', + interface: 'input', + }, + ], + }, + ], + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + properties: { + qiis77b2b96: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + properties: { + xn73tu52o4l: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.33-beta', + properties: { + ovrxf0qi4oh: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.33-beta', + properties: { + clq66owv5vt: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'collection1:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'collection1', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.3.33-beta', + properties: { + jgyr5k5rhl5: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.3.33-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.33-beta', + properties: { + '704zd4gqwia': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.33-beta', + properties: { + bng2scwwp21: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.33-beta', + properties: { + text1: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'collection1.text1', + 'x-component-props': {}, + 'x-app-version': '1.3.33-beta', + 'x-uid': '8yrtgs4fij4', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '5mpw6xv5t53', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'umhyk321or1', + 'x-async': false, + 'x-index': 1, + }, + '7jmmz0am2mp': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.33-beta', + properties: { + kuwqh6jsb0z: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.33-beta', + properties: { + m2m1: { + 'x-uid': '4cojuep3jug', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'collection1.m2m1', + 'x-component-props': { + fieldNames: { + label: 'id', + value: 'id', + }, + mode: 'Nester', + }, + 'x-app-version': '1.3.33-beta', + default: null, + properties: { + sqommd77rxp: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'AssociationField.Nester', + 'x-index': 1, + 'x-app-version': '1.3.33-beta', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.3.33-beta', + properties: { + '7pnogkc6aso': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.33-beta', + properties: { + xsbs3warqhf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.33-beta', + properties: { + text2: { + 'x-uid': 's0lsw2l9gxo', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'collection2.text2', + 'x-component-props': {}, + 'x-app-version': '1.3.33-beta', + default: null, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fh40b1ec8xe', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'twc2iso6ij8', + 'x-async': false, + 'x-index': 1, + }, + '758esk132v5': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.3.33-beta', + properties: { + t9ijtjbpryx: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.3.33-beta', + properties: { + m2m2: { + 'x-uid': 'dgul9qn182o', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'collection2.m2m2', + 'x-component-props': { + fieldNames: { + label: 'id', + value: 'id', + }, + mode: 'SubTable', + }, + 'x-app-version': '1.3.33-beta', + default: null, + properties: { + wzyleesvy5a: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'AssociationField.SubTable', + 'x-initializer': 'table:configureColumns', + 'x-initializer-props': { + action: false, + }, + 'x-index': 1, + 'x-app-version': '1.3.33-beta', + properties: { + '1rhnqhhrxtl': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-decorator': 'TableV2.Column.Decorator', + 'x-toolbar': 'TableColumnSchemaToolbar', + 'x-settings': 'fieldSettings:TableColumn', + 'x-component': 'TableV2.Column', + 'x-app-version': '1.3.33-beta', + properties: { + text3: { + 'x-uid': 'qr2z1604tdt', + _isJSONSchemaObject: true, + version: '2.0', + 'x-collection-field': 'collection3.text3', + 'x-component': 'CollectionField', + 'x-component-props': { + ellipsis: true, + }, + 'x-decorator': 'FormItem', + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + 'x-app-version': '1.3.33-beta', + default: null, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'veibwzrxmwt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '12y4qwbwh9v', + 'x-async': false, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'fjqt5n7vnp1', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'p5xwk5asiyx', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'rv2r7oq9i5v', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'mk2emm6340d', + 'x-async': false, + }, + }, + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'iibkaselueb', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cgkroe30mh2', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'ccpvbj4s1fn', + 'x-async': false, + 'x-index': 1, + }, + u9ryrklw5oj: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + }, + 'x-app-version': '1.3.33-beta', + 'x-uid': '0ib597ro9p7', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'gx13vgubf5i', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '01nwdjwsedu', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cxse6wcqnm3', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pp209qnmn8v', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'i11l6wkz6b2', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'a9u1cjps5th', + 'x-async': true, + 'x-index': 1, + }, +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx index 6af7248181..1496505d23 100644 --- a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx @@ -168,8 +168,8 @@ export const SubTable: any = observer( - {/* 在这里加,是为了让 “当前对象” 的配置显示正确 */} - + {/* 在这里加,是为了让子表格中默认值的 “当前对象” 的配置显示正确 */} + { + it('should return the correct values from SubFormContext', () => { + const mockValue = { id: 1, name: 'Test' }; + const mockCollection = { name: 'users' }; + const mockFieldSchema = { type: 'object', properties: {} }; + const mockParent = { value: { parentId: 2 }, collection: { name: 'parents' } }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useSubFormValue(), { wrapper }); + + expect(result.current).toEqual({ + formValue: mockValue, + collection: mockCollection, + fieldSchema: mockFieldSchema, + parent: mockParent, + }); + }); + + it('should return undefined values when SubFormContext is not provided', () => { + const { result } = renderHook(() => useSubFormValue()); + + expect(result.current).toEqual({ + formValue: undefined, + collection: undefined, + fieldSchema: undefined, + parent: undefined, + }); + }); + + it('should update values when SubFormContext changes', () => { + const initialValue = { id: 1, name: 'Initial' }; + const updatedValue = { id: 1, name: 'Updated' }; + const mockCollection = { name: 'users' }; + const mockFieldSchema = { type: 'object', properties: {} }; + + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result, rerender } = renderHook(() => useSubFormValue(), { wrapper }); + + expect(result.current.formValue).toEqual(initialValue); + + // Update the context value + Object.assign(initialValue, updatedValue); + + rerender(); + + expect(result.current.formValue).toEqual(updatedValue); + }); + + it('should use provided parent when available', () => { + const mockParent = { value: { id: 1 } }; + const wrapper = ({ children }) => ( + {children} + ); + const { result } = renderHook(() => useSubFormValue(), { wrapper }); + expect(result.current.parent).toBe(mockParent); + }); + + it('should use _parent when no parent is provided and skip is false', () => { + const mockParent = { value: { id: 1 }, skip: false, parent: null }; + const wrapper = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(() => useSubFormValue(), { wrapper }); + expect(result.current.parent).toEqual(mockParent); + }); + + it('should use _parent.parent when _parent.skip is true', () => { + const mockGrandParent = { value: { id: 1 }, parent: null }; + const wrapper = ({ children }) => ( + + + {children} + + + ); + const { result } = renderHook(() => useSubFormValue(), { wrapper }); + expect(result.current.parent).toEqual(mockGrandParent); + }); +}); diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.tsx b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx index eb7b109ac1..b9d26fcee2 100644 --- a/packages/core/client/src/schema-component/antd/association-field/hooks.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/hooks.tsx @@ -9,7 +9,7 @@ import { GeneralField } from '@formily/core'; import { Schema, useField, useFieldSchema } from '@formily/react'; -import { isString } from 'lodash'; +import _, { isString } from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; import React, { createContext, FC, useCallback, useContext, useMemo } from 'react'; import { useParsedFilter } from '../../../block-provider/hooks/useParsedFilter'; @@ -149,14 +149,27 @@ interface SubFormProviderProps { * the schema of the current sub-table or sub-form */ fieldSchema?: Schema; + parent?: SubFormProviderProps; + /** + * Ignore the current value in the upper and lower levels + */ + skip?: boolean; } const SubFormContext = createContext(null); SubFormContext.displayName = 'SubFormContext'; export const SubFormProvider: FC<{ value: SubFormProviderProps }> = (props) => { - const { value, collection, fieldSchema } = props.value; - const memoValue = useMemo(() => ({ value, collection, fieldSchema }), [value, collection, fieldSchema]); + const _parent = useContext(SubFormContext); + const { value, collection, fieldSchema, parent, skip } = props.value; + const memoValue = useMemo( + () => + _.omitBy( + { value, collection, fieldSchema, skip, parent: parent || (_parent?.skip ? _parent.parent : _parent) }, + _.isUndefined, + ), + [value, collection, fieldSchema, skip, parent, _parent], + ) as SubFormProviderProps; return {props.children}; }; @@ -170,10 +183,11 @@ export const SubFormProvider: FC<{ value: SubFormProviderProps }> = (props) => { * @returns */ export const useSubFormValue = () => { - const { value, collection, fieldSchema } = useContext(SubFormContext) || {}; + const { value, collection, fieldSchema, parent } = useContext(SubFormContext) || {}; return { formValue: value, collection, fieldSchema, + parent, }; }; diff --git a/packages/core/client/src/schema-settings/LinkageRules/index.tsx b/packages/core/client/src/schema-settings/LinkageRules/index.tsx index ce8cbc0935..c5b97bfb27 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/index.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/index.tsx @@ -10,13 +10,13 @@ import { css } from '@emotion/css'; import { observer, useFieldSchema } from '@formily/react'; import React, { useMemo } from 'react'; -import { FormBlockContext } from '../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; import { CollectionProvider } from '../../data-source/collection/CollectionProvider'; import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps'; import { RecordProvider } from '../../record-provider'; import { SchemaComponent, useProps } from '../../schema-component'; +import { SubFormProvider } from '../../schema-component/antd/association-field/hooks'; import { DynamicComponentProps } from '../../schema-component/antd/filter/DynamicComponent'; import { FilterContext } from '../../schema-component/antd/filter/context'; import { VariableInput, getShouldChange } from '../VariableInput/VariableInput'; @@ -31,17 +31,8 @@ export interface Props { export const FormLinkageRules = withDynamicSchemaProps( observer((props: Props) => { const fieldSchema = useFieldSchema(); - const { - options, - defaultValues, - collectionName, - form, - formBlockType, - variables, - localVariables, - record, - dynamicComponent, - } = useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema + const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } = + useProps(props); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const parentRecordData = useCollectionParentRecordData(); @@ -176,7 +167,8 @@ export const FormLinkageRules = withDynamicSchemaProps( ); return ( - + // 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确 + @@ -184,7 +176,7 @@ export const FormLinkageRules = withDynamicSchemaProps( - + ); }), { displayName: 'FormLinkageRules' }, diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 117d2eda1f..10d8d8c99a 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -780,7 +780,7 @@ export const SchemaSettingsModalItem: FC = (props) const locationSearch = useLocationSearch(); // 解决变量`当前对象`值在弹窗中丢失的问题 - const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue(); + const { formValue: subFormValue, collection: subFormCollection, parent } = useSubFormValue(); // 解决弹窗变量丢失的问题 const popupRecordVariable = useCurrentPopupRecord(); @@ -812,7 +812,7 @@ export const SchemaSettingsModalItem: FC = (props) > - + { + // const { getActiveFieldsName } = useFormActiveFields() || {}; + const { parent } = useSubFormValue(); + const { value: parentObjectCtx, collection: collectionOfParentObject } = parent || {}; + const { isInSubForm, isInSubTable } = useFlag() || {}; + const { t } = useTranslation(); + const parentObjectSettings = useBaseVariable({ + collectionField, + uiSchema: schema, + targetFieldSchema, + maxDepth: 4, + name: '$nParentIteration', + title: t('Parent object'), + collectionName: collectionOfParentObject?.name, + noDisabled, + returnFields: (fields, option) => { + return fields; + // const activeFieldsName = getActiveFieldsName?.('nester') || []; + + // return option.depth === 0 + // ? fields.filter((field) => { + // return activeFieldsName?.includes(field.name); + // }) + // : fields; + }, + }); + + return { + /** 是否显示变量 */ + shouldDisplayParentObject: (isInSubForm || isInSubTable) && !!collectionOfParentObject, + /** 变量的值 */ + parentObjectCtx, + /** 变量的配置项 */ + parentObjectSettings, + collectionName: collectionOfParentObject?.name, + }; +}; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts index 28344a8da8..4bce40f562 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts @@ -15,6 +15,7 @@ import { useAPITokenVariable } from './useAPITokenVariable'; import { useDatetimeVariable } from './useDateVariable'; import { useCurrentFormVariable } from './useFormVariable'; import { useCurrentObjectVariable } from './useIterationVariable'; +import { useParentObjectVariable } from './useParentIterationVariable'; import { useParentPopupVariable } from './useParentPopupVariable'; import { useCurrentParentRecordVariable } from './useParentRecordVariable'; import { usePopupVariable } from './usePopupVariable'; @@ -87,6 +88,12 @@ export const useVariableOptions = ({ noDisabled, targetFieldSchema, }); + const { parentObjectSettings, shouldDisplayParentObject } = useParentObjectVariable({ + collectionField, + schema: uiSchema, + noDisabled, + targetFieldSchema, + }); const { currentRecordSettings, shouldDisplayCurrentRecord } = useCurrentRecordVariable({ schema: uiSchema, collectionField, @@ -122,6 +129,7 @@ export const useVariableOptions = ({ datetimeSettings, shouldDisplayCurrentForm && currentFormSettings, shouldDisplayCurrentObject && currentObjectSettings, + shouldDisplayParentObject && parentObjectSettings, shouldDisplayCurrentRecord && currentRecordSettings, shouldDisplayCurrentParentRecord && currentParentRecordSettings, shouldDisplayPopupRecord && popupRecordSettings, @@ -137,6 +145,8 @@ export const useVariableOptions = ({ currentFormSettings, shouldDisplayCurrentObject, currentObjectSettings, + shouldDisplayParentObject, + parentObjectSettings, shouldDisplayCurrentRecord, currentRecordSettings, shouldDisplayCurrentParentRecord, diff --git a/packages/core/client/src/variables/hooks/useLocalVariables.tsx b/packages/core/client/src/variables/hooks/useLocalVariables.tsx index 686518f834..f8291202b0 100644 --- a/packages/core/client/src/variables/hooks/useLocalVariables.tsx +++ b/packages/core/client/src/variables/hooks/useLocalVariables.tsx @@ -14,6 +14,7 @@ import { useBlockCollection } from '../../schema-settings/VariableInput/hooks/us import { useDatetimeVariable } from '../../schema-settings/VariableInput/hooks/useDateVariable'; import { useCurrentFormVariable } from '../../schema-settings/VariableInput/hooks/useFormVariable'; import { useCurrentObjectVariable } from '../../schema-settings/VariableInput/hooks/useIterationVariable'; +import { useParentObjectVariable } from '../../schema-settings/VariableInput/hooks/useParentIterationVariable'; import { useParentPopupVariable } from '../../schema-settings/VariableInput/hooks/useParentPopupVariable'; import { useCurrentParentRecordVariable } from '../../schema-settings/VariableInput/hooks/useParentRecordVariable'; import { usePopupVariable } from '../../schema-settings/VariableInput/hooks/usePopupVariable'; @@ -26,6 +27,11 @@ interface Props { } const useLocalVariables = (props?: Props) => { + const { + parentObjectCtx, + shouldDisplayParentObject, + collectionName: collectionNameOfParentObject, + } = useParentObjectVariable(); const { currentObjectCtx, shouldDisplayCurrentObject } = useCurrentObjectVariable(); const { currentRecordCtx, collectionName: collectionNameOfRecord } = useCurrentRecordVariable(); const { @@ -131,6 +137,11 @@ const useLocalVariables = (props?: Props) => { ctx: currentObjectCtx, collectionName: currentCollectionName, }, + shouldDisplayParentObject && { + name: '$nParentIteration', + ctx: parentObjectCtx, + collectionName: collectionNameOfParentObject, + }, ] as VariableOption[] ).filter(Boolean); }, [ @@ -151,6 +162,9 @@ const useLocalVariables = (props?: Props) => { currentCollectionName, defaultValueOfPopupRecord, defaultValueOfParentPopupRecord, + shouldDisplayParentObject, + parentObjectCtx, + collectionNameOfParentObject, ]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中 };