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
This commit is contained in:
Zeke Zhang 2024-10-21 08:46:04 +08:00 committed by GitHub
parent 361558a0d0
commit dd71cdaaa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 799 additions and 33 deletions

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -1007,5 +1007,6 @@
"NaN": "なし",
"true": "真",
"false": "偽",
"Allow multiple selection": "複数選択を許可"
"Allow multiple selection": "複数選択を許可",
"Parent object": "親オブジェクト"
}

View File

@ -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": "부모 객체"
}

View File

@ -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"
}

View File

@ -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": "Родительский объект"
}

View File

@ -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"
}

View File

@ -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": "Батьківський об'єкт"
}

View File

@ -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": "上级对象"
}

View File

@ -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": "上級物件"
}

View File

@ -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');
});
});

View File

@ -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,
},
};

View File

@ -168,8 +168,8 @@ export const SubTable: any = observer(
<FlagProvider isInSubTable>
<CollectionRecordProvider record={null} parentRecord={recordV2}>
<FormActiveFieldsProvider name="nester">
{/* 在这里加,是为了让 “当前对象” 的配置显示正确 */}
<SubFormProvider value={{ value: null, collection, fieldSchema: fieldSchema.parent }}>
{/* 在这里加,是为了让子表格中默认值的 “当前对象” 的配置显示正确 */}
<SubFormProvider value={{ value: null, collection, fieldSchema: fieldSchema.parent, skip: true }}>
<Table
className={tableClassName}
bordered

View File

@ -0,0 +1,118 @@
/**
* 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 { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { describe, expect, it } from 'vitest';
import { SubFormProvider, useSubFormValue } from '../hooks';
describe('useSubFormValue', () => {
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 }) => (
<SubFormProvider
value={{
value: mockValue,
collection: mockCollection as any,
fieldSchema: mockFieldSchema as any,
parent: mockParent as any,
}}
>
{children}
</SubFormProvider>
);
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 }) => (
<SubFormProvider
value={{
value: initialValue,
collection: mockCollection as any,
fieldSchema: mockFieldSchema as any,
}}
>
{children}
</SubFormProvider>
);
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 }) => (
<SubFormProvider value={{ parent: mockParent as any } as any}>{children}</SubFormProvider>
);
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 }) => (
<SubFormProvider value={mockParent as any}>
<SubFormProvider value={{ skip: false } as any}>{children}</SubFormProvider>
</SubFormProvider>
);
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 }) => (
<SubFormProvider value={mockGrandParent as any}>
<SubFormProvider value={{ skip: true } as any}>
<SubFormProvider value={{} as any}>{children}</SubFormProvider>
</SubFormProvider>
</SubFormProvider>
);
const { result } = renderHook(() => useSubFormValue(), { wrapper });
expect(result.current.parent).toEqual(mockGrandParent);
});
});

View File

@ -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<SubFormProviderProps>(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 <SubFormContext.Provider value={memoValue}>{props.children}</SubFormContext.Provider>;
};
@ -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,
};
};

View File

@ -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); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } =
useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const parentRecordData = useCollectionParentRecordData();
@ -176,7 +167,8 @@ export const FormLinkageRules = withDynamicSchemaProps(
);
return (
<FormBlockContext.Provider value={{ form, type: formBlockType, collectionName }}>
// 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
<SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}>
<RecordProvider record={record} parent={parentRecordData}>
<FilterContext.Provider value={value}>
<CollectionProvider name={collectionName}>
@ -184,7 +176,7 @@ export const FormLinkageRules = withDynamicSchemaProps(
</CollectionProvider>
</FilterContext.Provider>
</RecordProvider>
</FormBlockContext.Provider>
</SubFormProvider>
);
}),
{ displayName: 'FormLinkageRules' },

View File

@ -780,7 +780,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (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<SchemaSettingsModalItemProps> = (props)
>
<CollectionRecordProvider record={noRecord ? null : record}>
<FormBlockContext.Provider value={formCtx}>
<SubFormProvider value={{ value: subFormValue, collection: subFormCollection }}>
<SubFormProvider value={{ value: subFormValue, collection: subFormCollection, parent }}>
<FormActiveFieldsProvider
name="form"
getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}

View File

@ -0,0 +1,69 @@
/**
* 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 { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions } from '../../../data-source/collection/Collection';
import { useFlag } from '../../../flag-provider';
import { useSubFormValue } from '../../../schema-component/antd/association-field/hooks';
import { useBaseVariable } from './useBaseVariable';
/**
* `上级对象`
* @param param0
* @returns
*/
export const useParentObjectVariable = ({
collectionField,
schema,
noDisabled,
targetFieldSchema,
}: {
collectionField?: CollectionFieldOptions;
schema?: any;
noDisabled?: boolean;
/** 消费变量值的字段 */
targetFieldSchema?: Schema;
} = {}) => {
// 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,
};
};

View File

@ -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,

View File

@ -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 中
};