From 837f4f41589b7343541c3336e631ac5dc24e7662 Mon Sep 17 00:00:00 2001 From: Zeke Zhang <958414905@qq.com> Date: Fri, 7 Jun 2024 10:23:28 +0800 Subject: [PATCH] refactor(variable): support default value setting (#4583) * refactor(variable): support default value setting * chore: add e2e test for URL search params variable * fix: resolve field linkage errors * chore: fix unit test * test: association select data scope linkage --------- Co-authored-by: katherinehhh --- .../actions/__e2e__/link/basic.test.ts | 20 + .../AssociationSelect/dataScope.test.ts | 45 ++ .../AssociationSelect/templatesOfBug.ts | 458 ++++++++++++++++++ .../antd/association-field/hooks.ts | 4 +- .../hooks/useURLSearchParamsVariable.ts | 6 + .../src/variables/VariablesProvider.tsx | 79 ++- .../variables/__tests__/useVariables.test.tsx | 61 ++- .../variables/hooks/useBuiltinVariables.ts | 5 +- packages/core/client/src/variables/types.ts | 7 + .../src/__tests__/getValuesByPath.test.ts | 8 + packages/core/utils/src/getValuesByPath.ts | 6 +- 11 files changed, 642 insertions(+), 57 deletions(-) create mode 100644 packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts create mode 100644 packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/templatesOfBug.ts diff --git a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts index 65ec97b6f8..78fdb62e8e 100644 --- a/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts +++ b/packages/core/client/src/modules/actions/__e2e__/link/basic.test.ts @@ -63,5 +63,25 @@ test.describe('Link', () => { await expect(page.getByRole('button', { name: users[0].username, exact: true })).not.toBeVisible(); await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible(); + + // 5. Change the operator of the data scope from "is not" to "is" + await page.getByLabel('block-item-CardItem-users-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover(); + await page.getByRole('menuitem', { name: 'Set the data scope' }).click(); + await page.getByTestId('select-filter-operator').click(); + await page.getByRole('option', { name: 'is', exact: true }).click(); + await page.getByLabel('variable-button').click(); + await page.getByRole('menuitemcheckbox', { name: 'URL search params right' }).click(); + await page.getByRole('menuitemcheckbox', { name: 'id', exact: true }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect(page.getByRole('button', { name: users[0].username, exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'nocobase', exact: true })).not.toBeVisible(); + await expect(page.getByRole('button', { name: users[1].username, exact: true })).not.toBeVisible(); + + // 6. Re-enter the page (to eliminate the query string in the URL), at this time the value of the variable is undefined, and all data should be displayed + await nocoPage.goto(); + await expect(page.getByRole('button', { name: users[0].username, exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible(); }); }); diff --git a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts new file mode 100644 index 0000000000..35b9035a35 --- /dev/null +++ b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/dataScope.test.ts @@ -0,0 +1,45 @@ +/** + * 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 { associationSelectDataScope } from './templatesOfBug'; + +test.describe('AssociationSelect ', () => { + test('data scope linkage with other association select field', async ({ page, mockPage, mockRecord }) => { + await mockPage(associationSelectDataScope).goto(); + await mockRecord('school', { id: 1 }); + await mockRecord('class', { + school: { id: 1 }, + }); + const [request] = await Promise.all([ + page.waitForRequest((request) => request.url().includes('api/class:list')), + page.getByLabel('block-item-CollectionField-student-form-student.class-class').click(), + ]); + const requestUrl = request.url(); + const queryParams = new URLSearchParams(new URL(requestUrl).search); + const filter = queryParams.get('filter'); + //请求参数符合预期 + expect(JSON.parse(filter)).toEqual({ $and: [{ school: { id: { $eq: null } } }] }); + // 选择数据后联动 + await page.getByLabel('block-item-CollectionField-student-form-student.school-school').click(); + + await page.getByLabel('block-item-CollectionField-student-form-student.school-school').click(); + await page.getByRole('option', { name: '1' }).click(); + + const [request1] = await Promise.all([ + page.waitForRequest((request) => request.url().includes('api/class:list')), + page.getByLabel('block-item-CollectionField-student-form-student.class-class').click(), + ]); + const requestUrl1 = request1.url(); + const queryParams1 = new URLSearchParams(new URL(requestUrl1).search); + const filter1 = queryParams1.get('filter'); + //请求参数符合预期 + await expect(JSON.parse(filter1)).toEqual({ $and: [{ school: { id: { $eq: 1 } } }] }); + }); +}); diff --git a/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/templatesOfBug.ts b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/templatesOfBug.ts new file mode 100644 index 0000000000..503aae7ee3 --- /dev/null +++ b/packages/core/client/src/modules/fields/__e2e__/component/AssociationSelect/templatesOfBug.ts @@ -0,0 +1,458 @@ +/** + * 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. + */ + +export const associationSelectDataScope = { + pageSchema: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Page', + 'x-app-version': '1.0.0-alpha.17', + properties: { + i9pflzjyioi: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'page:addBlock', + 'x-app-version': '1.0.0-alpha.17', + properties: { + '4ai4zgcz8ee': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.0-alpha.17', + properties: { + vpldrh6hgmf: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.0-alpha.17', + properties: { + '5ieufmio6lw': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-acl-action': 'student:create', + 'x-decorator': 'FormBlockProvider', + 'x-use-decorator-props': 'useCreateFormBlockDecoratorProps', + 'x-decorator-props': { + dataSource: 'main', + collection: 'student', + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:createForm', + 'x-component': 'CardItem', + 'x-app-version': '1.0.0-alpha.17', + properties: { + '5husg9t3vv9': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useCreateFormBlockProps', + 'x-app-version': '1.0.0-alpha.17', + properties: { + grid: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'form:configureFields', + 'x-app-version': '1.0.0-alpha.17', + properties: { + tl52qwh5qb2: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.0-alpha.17', + properties: { + k0bea3ifdvt: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.0-alpha.17', + properties: { + school: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'student.school', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'id', + }, + }, + 'x-app-version': '1.0.0-alpha.17', + 'x-uid': '4aqvcj92dmg', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '3menjy0e1ej', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'oowsi97eacq', + 'x-async': false, + 'x-index': 1, + }, + whxgd4trw89: { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Row', + 'x-app-version': '1.0.0-alpha.17', + properties: { + '2lbxox3r15o': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-component': 'Grid.Col', + 'x-app-version': '1.0.0-alpha.17', + properties: { + class: { + 'x-uid': '1gwunmk7b3c', + _isJSONSchemaObject: true, + version: '2.0', + type: 'string', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': 'student.class', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'id', + }, + service: { + params: { + filter: { + $and: [ + { + school: { + id: { + $eq: '{{$nForm.school.id}}', + }, + }, + }, + ], + }, + }, + }, + }, + 'x-app-version': '1.0.0-alpha.17', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'ntz289r21df', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'cavrlbod03e', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': 'k7qwqqhdx60', + 'x-async': false, + 'x-index': 1, + }, + '1kqjiuyj895': { + _isJSONSchemaObject: true, + version: '2.0', + type: 'void', + 'x-initializer': 'createForm:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + layout: 'one-column', + style: { + marginTop: 'var(--nb-spacing)', + }, + }, + 'x-app-version': '1.0.0-alpha.17', + 'x-uid': 'cwurx7plt95', + 'x-async': false, + 'x-index': 2, + }, + }, + 'x-uid': '306cgan6w47', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'wy8gkqcnr6b', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'd30aarrzirt', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'pqdwwjn7y28', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': 'u3r6l5ed5c8', + 'x-async': false, + 'x-index': 1, + }, + }, + 'x-uid': '42t4voitjmc', + 'x-async': true, + 'x-index': 1, + }, + collections: [ + { + name: 'school', + fields: [ + { + key: 'ze26tawdlux', + name: 'id', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'school', + parentKey: null, + reverseKey: null, + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { + type: 'number', + title: '{{t("ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + ], + filterTargetKey: 'id', + }, + { + name: 'class', + fields: [ + { + key: 'rkb2b156str', + name: 'id', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'class', + parentKey: null, + reverseKey: null, + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { + type: 'number', + title: '{{t("ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: 's804a0a6hb4', + name: 'f_nwk9mip9y1k', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'class', + parentKey: null, + reverseKey: null, + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_nwk9mip9y1k', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: 'btl687vmwyo', + name: 'f_ih087vrmag4', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'class', + parentKey: null, + reverseKey: null, + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_ih087vrmag4', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: 'a35wf3880bb', + name: 'f_uwcl0rf78mn', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'class', + parentKey: null, + reverseKey: null, + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_uwcl0rf78mn', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + + { + key: 'nt7i4vmih24', + name: 'school', + type: 'belongsTo', + interface: 'm2o', + description: null, + collectionName: 'class', + parentKey: null, + reverseKey: null, + foreignKey: 'f_uwcl0rf78mn', + onDelete: 'SET NULL', + uiSchema: { + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: false, + }, + title: 'school', + }, + target: 'school', + targetKey: 'id', + }, + ], + filterTargetKey: 'id', + }, + { + name: 'student', + + fields: [ + { + key: 'pjz61nq67ym', + name: 'id', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'student', + parentKey: null, + reverseKey: null, + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { + type: 'number', + title: '{{t("ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: 'jrt23ehbhwn', + name: 'f_firb6d8f8jq', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'student', + parentKey: null, + reverseKey: null, + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_firb6d8f8jq', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: '9hydd4b1out', + name: 'f_7kxab0celw3', + type: 'bigInt', + interface: 'integer', + description: null, + collectionName: 'student', + parentKey: null, + reverseKey: null, + isForeignKey: true, + uiSchema: { + type: 'number', + title: 'f_7kxab0celw3', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + }, + { + key: 'xwpw9a9f7y6', + name: 'class', + type: 'belongsTo', + interface: 'm2o', + description: null, + collectionName: 'student', + parentKey: null, + reverseKey: null, + foreignKey: 'f_firb6d8f8jq', + onDelete: 'SET NULL', + uiSchema: { + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: false, + }, + title: 'class', + }, + target: 'class', + targetKey: 'id', + }, + { + key: 'by6lqjkl50g', + name: 'school', + type: 'belongsTo', + interface: 'm2o', + description: null, + collectionName: 'student', + parentKey: null, + reverseKey: null, + foreignKey: 'f_7kxab0celw3', + onDelete: 'SET NULL', + uiSchema: { + 'x-component': 'AssociationField', + 'x-component-props': { + multiple: false, + }, + title: 'school', + }, + target: 'school', + targetKey: 'id', + }, + ], + + filterTargetKey: 'id', + }, + ], +}; diff --git a/packages/core/client/src/schema-component/antd/association-field/hooks.ts b/packages/core/client/src/schema-component/antd/association-field/hooks.ts index 46d3a22152..9d4422561c 100644 --- a/packages/core/client/src/schema-component/antd/association-field/hooks.ts +++ b/packages/core/client/src/schema-component/antd/association-field/hooks.ts @@ -14,7 +14,8 @@ import { flatten, getValuesByPath } from '@nocobase/utils/client'; import _, { isString } from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; +import { Collection } from '../../../data-source'; import { isInFilterFormBlock } from '../../../filter-provider'; import { mergeFilter } from '../../../filter-provider/utils'; import { useRecord } from '../../../record-provider'; @@ -25,7 +26,6 @@ import { getVariableName } from '../../../variables/utils/getVariableName'; import { isVariable } from '../../../variables/utils/isVariable'; import { useDesignable } from '../../hooks'; import { AssociationFieldContext } from './context'; -import { Collection } from '../../../data-source'; export const useInsertSchema = (component) => { const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts index 2616b8d4d9..7cc687fa66 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useURLSearchParamsVariable.ts @@ -99,6 +99,12 @@ export const useURLSearchParamsVariable = (props: any = {}) => { urlSearchParamsSettings, /** 变量值 */ urlSearchParamsCtx, + /** + * 这里是用于当通过该变量解析出来的值是一个 undefined 时,最终应该返回的值。 + * 默认返回的是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件, + * 所以这里把 defaultValue 设置为 undefined,这样在解析出来的值是 undefined 时,会返回 undefined,从而清除掉 filter 条件。 + */ + defaultValue: undefined, shouldDisplay: !isVariableParsedInOtherContext, }; }; diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index e890868cce..8274052e60 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -28,27 +28,24 @@ import { uniq } from './utils/uniq'; export const VariablesContext = createContext(null); VariablesContext.displayName = 'VariablesContext'; -const variableToCollectionName: Record< - string, - { - collectionName?: string; - dataSource?: string; - } -> = {}; +const variablesStore: Record = {}; -const getFieldPath = (variablePath: string, variableToCollectionName: Record) => { +const getFieldPath = (variablePath: string, variablesStore: Record) => { let dataSource; + let variableOption: VariableOption; const list = variablePath.split('.'); const result = list.map((item) => { - if (variableToCollectionName[item]) { - dataSource = variableToCollectionName[item].dataSource; - return variableToCollectionName[item].collectionName; + if (variablesStore[item]) { + dataSource = variablesStore[item].dataSource; + variableOption = variablesStore[item]; + return variablesStore[item].collectionName; } return item; }); return { fieldPath: result.join('.'), dataSource, + variableOption, }; }; @@ -83,12 +80,9 @@ const VariablesProvider = ({ children }) => { ) => { const list = variablePath.split('.'); const variableName = list[0]; - const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables( - variableToCollectionName, - localVariables, - ); + const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables(variablesStore, localVariables); let current = mergeCtxWithLocalVariables(ctxRef.current, localVariables); - const { fieldPath, dataSource } = getFieldPath(variableName, _variableToCollectionName); + const { fieldPath, dataSource, variableOption } = getFieldPath(variableName, _variableToCollectionName); let collectionName = fieldPath; if (!(variableName in current)) { @@ -97,7 +91,7 @@ const VariablesProvider = ({ children }) => { for (let index = 0; index < list.length; index++) { if (current == null) { - return current; + return current === undefined ? variableOption.defaultValue : current; } const key = list[index]; @@ -165,7 +159,8 @@ const VariablesProvider = ({ children }) => { } } - return compile(_.isFunction(current) ? current() : current); + const result = compile(_.isFunction(current) ? current() : current); + return result === undefined ? variableOption.defaultValue : result; }, [getCollectionJoinField], ); @@ -185,12 +180,10 @@ const VariablesProvider = ({ children }) => { [variableOption.name]: variableOption.ctx, }; }); - if (variableOption.collectionName) { - variableToCollectionName[variableOption.name] = { - collectionName: variableOption.collectionName, - dataSource: variableOption.dataSource, - }; - } + variablesStore[variableOption.name] = { + ...variableOption, + defaultValue: _.has(variableOption, 'defaultValue') ? variableOption.defaultValue : null, + }; }, [setCtx], ); @@ -200,13 +193,8 @@ const VariablesProvider = ({ children }) => { return null; } - const { collectionName, dataSource } = variableToCollectionName[variableName] || {}; - return { - name: variableName, - ctx: ctxRef.current[variableName], - collectionName, - dataSource, + ...variablesStore[variableName], }; }, []); @@ -217,7 +205,7 @@ const VariablesProvider = ({ children }) => { delete next[variableName]; return next; }); - delete variableToCollectionName[variableName]; + delete variablesStore[variableName]; }, [setCtx], ); @@ -264,7 +252,7 @@ const VariablesProvider = ({ children }) => { } const _variableToCollectionName = mergeVariableToCollectionNameWithLocalVariables( - variableToCollectionName, + variablesStore, localVariables as VariableOption[], ); const path = getPath(variableString); @@ -285,7 +273,10 @@ const VariablesProvider = ({ children }) => { useEffect(() => { builtinVariables.forEach((variableOption) => { - registerVariable(variableOption); + registerVariable({ + ...variableOption, + defaultValue: _.has(variableOption, 'defaultValue') ? variableOption.defaultValue : null, + }); }); }, [builtinVariables, registerVariable]); @@ -339,25 +330,17 @@ function mergeCtxWithLocalVariables(ctx: Record, localVariables?: V } function mergeVariableToCollectionNameWithLocalVariables( - variableToCollectionName: Record< - string, - { - collectionName?: string; - dataSource?: string; - } - >, + variablesStore: Record, localVariables?: VariableOption[], ) { - variableToCollectionName = { ...variableToCollectionName }; + variablesStore = { ...variablesStore }; localVariables?.forEach((item) => { - if (item.collectionName) { - variableToCollectionName[item.name] = { - collectionName: item.collectionName, - dataSource: item.dataSource, - }; - } + variablesStore[item.name] = { + ...item, + defaultValue: _.has(item, 'defaultValue') ? item.defaultValue : null, + }; }); - return variableToCollectionName; + return variablesStore; } diff --git a/packages/core/client/src/variables/__tests__/useVariables.test.tsx b/packages/core/client/src/variables/__tests__/useVariables.test.tsx index abe96972eb..1eb77e3a1e 100644 --- a/packages/core/client/src/variables/__tests__/useVariables.test.tsx +++ b/packages/core/client/src/variables/__tests__/useVariables.test.tsx @@ -545,6 +545,7 @@ describe('useVariables', () => { ctx: { name: 'new variable', }, + defaultValue: null, }); }); @@ -567,6 +568,46 @@ describe('useVariables', () => { }); }); + await waitFor(async () => { + expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe(null); + }); + }); + + it('$new.noExist with default value', async () => { + const { result } = renderHook(() => useVariables(), { + wrapper: Providers, + }); + + await waitFor(async () => { + result.current.registerVariable({ + name: '$new', + ctx: { + name: 'new variable', + }, + defaultValue: 'default value', + }); + }); + + await waitFor(async () => { + expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe('default value'); + }); + }); + + it('$new.noExist with undefined default value', async () => { + const { result } = renderHook(() => useVariables(), { + wrapper: Providers, + }); + + await waitFor(async () => { + result.current.registerVariable({ + name: '$new', + ctx: { + name: 'new variable', + }, + defaultValue: undefined, + }); + }); + await waitFor(async () => { expect(await result.current.parseVariable('{{ $new.noExist }}')).toBe(undefined); }); @@ -679,11 +720,27 @@ describe('useVariables', () => { belongsToField: null, }, collectionName: 'some', + defaultValue: 'default value', }); await waitFor(async () => { - // 因为 $some 的 ctx 没有 id 所以无法获取关系字段的数据 - expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toBe(undefined); + // 只有解析后的值是 undefined 才会使用默认值 + expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toBe(null); + }); + + // 会覆盖之前的 $some + result.current.registerVariable({ + name: '$some', + ctx: { + name: 'new variable', + }, + collectionName: 'some', + defaultValue: 'default value', + }); + + await waitFor(async () => { + // 解析后的值是 undefined 所以会返回上面设置的默认值 + expect(await result.current.parseVariable('{{ $some.belongsToField.belongsToField }}')).toBe('default value'); }); }); diff --git a/packages/core/client/src/variables/hooks/useBuiltinVariables.ts b/packages/core/client/src/variables/hooks/useBuiltinVariables.ts index d42265a568..8af69176e4 100644 --- a/packages/core/client/src/variables/hooks/useBuiltinVariables.ts +++ b/packages/core/client/src/variables/hooks/useBuiltinVariables.ts @@ -23,7 +23,7 @@ const useBuiltInVariables = () => { const { currentUserCtx } = useCurrentUserVariable(); const { currentRoleCtx } = useCurrentRoleVariable(); const { datetimeCtx } = useDatetimeVariable(); - const { urlSearchParamsCtx, name: urlSearchParamsName } = useURLSearchParamsVariable(); + const { urlSearchParamsCtx, name: urlSearchParamsName, defaultValue } = useURLSearchParamsVariable(); const builtinVariables: VariableOption[] = useMemo(() => { return [ { @@ -80,9 +80,10 @@ const useBuiltInVariables = () => { { name: urlSearchParamsName, ctx: urlSearchParamsCtx, + defaultValue, }, ]; - }, [currentRoleCtx, currentUserCtx, datetimeCtx, urlSearchParamsCtx, urlSearchParamsName]); + }, [currentRoleCtx, currentUserCtx, datetimeCtx, defaultValue, urlSearchParamsCtx, urlSearchParamsName]); return { builtinVariables }; }; diff --git a/packages/core/client/src/variables/types.ts b/packages/core/client/src/variables/types.ts index b3ddf23ad2..49b8708896 100644 --- a/packages/core/client/src/variables/types.ts +++ b/packages/core/client/src/variables/types.ts @@ -88,4 +88,11 @@ export interface VariableOption { collectionName?: string; /** 数据表所对应的数据源 */ dataSource?: string; + /** + * @default null + * 表示当变量解析出来的值是一个 undefined 时,最终应该返回的值。 + * 默认是 null,这样可以保证数据范围中的 filter 条件不会被清除掉, + * 如果想让数据范围中的 filter 条件被清除掉,可以设置 defaultValue 为 undefined。 + */ + defaultValue?: any; } diff --git a/packages/core/utils/src/__tests__/getValuesByPath.test.ts b/packages/core/utils/src/__tests__/getValuesByPath.test.ts index 7e10f9b4a0..139923a117 100644 --- a/packages/core/utils/src/__tests__/getValuesByPath.test.ts +++ b/packages/core/utils/src/__tests__/getValuesByPath.test.ts @@ -90,6 +90,14 @@ describe('getValuesByPath', () => { expect(result).toEqual(null); }); + it('when return is null', () => { + const obj = { + a: { b: null }, + }; + const result = getValuesByPath(obj, 'a.b'); + expect(result).toEqual(null); + }); + it('should return empty array when obj key value is undefined', () => { const obj = { a: undefined, diff --git a/packages/core/utils/src/getValuesByPath.ts b/packages/core/utils/src/getValuesByPath.ts index 10cc607295..fd622587a8 100644 --- a/packages/core/utils/src/getValuesByPath.ts +++ b/packages/core/utils/src/getValuesByPath.ts @@ -38,15 +38,15 @@ export const getValuesByPath = (obj: object, path: string, defaultValue?: any) = } } - result = result.filter((item) => item != null); + result = result.filter((item) => item !== undefined); if (result.length === 0) { return defaultValue; } if (shouldReturnArray) { - return result; + return result.filter((item) => item !== null); } - return result.length === 1 ? result[0] : result; + return result[0]; };