From bce02ad1d7e5478a404d66b01f9959324600b535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rain?= <958414905@qq.com> Date: Sun, 5 Nov 2023 17:59:22 +0800 Subject: [PATCH] perf: avoid page lag or stuttering (#2964) * perf: avoid page lag or stuttering * perf: improve performance of data scope * chore: remove console.log * perf: avoid crashing * fix: make tests passing * fix: fix title of dialog --- .../client/src/__tests__/e2e/menu.test.ts | 4 +- .../client/src/__tests__/e2e/page.test.ts | 4 +- .../src/block-provider/BlockProvider.tsx | 1 - .../src/collection-manager/action-hooks.ts | 63 +++- .../schema-component/antd/form-v2/Form.tsx | 12 +- .../schema-component/antd/form-v2/utils.tsx | 7 +- .../src/schema-settings/SchemaSettings.tsx | 346 ++++++++++-------- 7 files changed, 262 insertions(+), 175 deletions(-) diff --git a/packages/core/client/src/__tests__/e2e/menu.test.ts b/packages/core/client/src/__tests__/e2e/menu.test.ts index c0f3d12d1..1487b7586 100644 --- a/packages/core/client/src/__tests__/e2e/menu.test.ts +++ b/packages/core/client/src/__tests__/e2e/menu.test.ts @@ -94,7 +94,7 @@ test.describe('menu page', () => { await page.getByLabel('Insert before').hover(); - await page.getByRole('button', { name: 'Add page' }).click(); + await page.getByRole('button', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').fill(pageTitle4); await page.getByRole('button', { name: 'OK' }).click(); @@ -124,7 +124,7 @@ test.describe('menu page', () => { await page.getByLabel('Insert after').hover(); - await page.getByRole('button', { name: 'Add page' }).click(); + await page.getByRole('button', { name: 'Page', exact: true }).click(); await page.getByRole('textbox').fill(pageTitle6); await page.getByRole('button', { name: 'OK' }).click(); diff --git a/packages/core/client/src/__tests__/e2e/page.test.ts b/packages/core/client/src/__tests__/e2e/page.test.ts index 954d8b5c7..ae2ff4845 100644 --- a/packages/core/client/src/__tests__/e2e/page.test.ts +++ b/packages/core/client/src/__tests__/e2e/page.test.ts @@ -1,4 +1,4 @@ -import { enableToConfig, expect, test } from '@nocobase/test/client'; +import { expect, test } from '@nocobase/test/client'; test.describe('page header', () => { test('disabled & enabled page header', async ({ page, mockPage }) => { @@ -99,7 +99,7 @@ test.describe('page tabs', () => { //修改tab名称 await page.getByText('Unnamed').click(); await page.getByRole('button', { name: 'designer-schema-settings-Page-tab' }).click(); - await page.getByLabel('Edit tab').click(); + await page.getByLabel('Edit', { exact: true }).click(); await page.getByRole('textbox').fill('page tab'); await page.getByRole('button', { name: 'OK' }).click(); diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index c908f16d7..e3a92671d 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -64,7 +64,6 @@ const useResource = (props: UseResourceProps) => { const { fieldSchema } = useActionContext(); const isCreateAction = fieldSchema?.['x-action'] === 'create'; const association = useAssociation(props); - console.log(association); const sourceId = useSourceId?.(); const field = useField(); const withoutTableFieldResource = useContext(WithoutTableFieldResource); diff --git a/packages/core/client/src/collection-manager/action-hooks.ts b/packages/core/client/src/collection-manager/action-hooks.ts index 14de39366..0f6c80a60 100644 --- a/packages/core/client/src/collection-manager/action-hooks.ts +++ b/packages/core/client/src/collection-manager/action-hooks.ts @@ -1,8 +1,9 @@ import { useField, useForm } from '@formily/react'; import { message } from 'antd'; +import _ from 'lodash'; import cloneDeep from 'lodash/cloneDeep'; import omit from 'lodash/omit'; -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useCollection, useCollectionManager } from '.'; import { useRequest } from '../api-client'; @@ -183,7 +184,65 @@ export const useCollectionFilterOptions = (collection: any) => { }; const options = getOptions(fields, 1); return options; - }, [collection]); + }, [_.isString(collection) ? collection : collection?.name]); +}; + +export const useCollectionFilterOptionsV2 = (collection: any) => { + const { getCollectionFields, getInterface } = useCollectionManager(); + + const getFields = useCallback(() => { + const fields = getCollectionFields(collection); + const field2option = (field, depth) => { + if (!field.interface) { + return; + } + const fieldInterface = getInterface(field.interface); + if (!fieldInterface?.filterable) { + return; + } + const { nested, children, operators } = fieldInterface.filterable; + const option = { + name: field.name, + title: field?.uiSchema?.title || field.name, + schema: field?.uiSchema, + operators: + operators?.filter?.((operator) => { + return !operator?.visible || operator.visible(field); + }) || [], + interface: field.interface, + }; + if (field.target && depth > 2) { + return; + } + if (depth > 2) { + return option; + } + if (children?.length) { + option['children'] = children; + } + if (nested) { + const targetFields = getCollectionFields(field.target); + const options = getOptions(targetFields, depth + 1).filter(Boolean); + option['children'] = option['children'] || []; + option['children'].push(...options); + } + return option; + }; + const getOptions = (fields, depth) => { + const options = []; + fields.forEach((field) => { + const option = field2option(field, depth); + if (option) { + options.push(option); + } + }); + return options; + }; + const options = getOptions(fields, 1); + return options; + }, [_.isString(collection) ? collection : collection?.name]); + + return { getFields }; }; export const useLinkageCollectionFilterOptions = (collectionName: string) => { diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index b040bc051..d0d02d1cf 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -1,14 +1,6 @@ import { css } from '@emotion/css'; import { FormLayout } from '@formily/antd-v5'; -import { - createForm, - Field, - Form as FormilyForm, - onFieldChange, - onFieldInit, - onFieldReact, - onFormInputChange, -} from '@formily/core'; +import { createForm, Field, Form as FormilyForm, onFieldChange, onFieldInit, onFormInputChange } from '@formily/core'; import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { autorun } from '@formily/reactive'; import { uid } from '@formily/shared'; @@ -132,7 +124,7 @@ const WithForm = (props: WithFormProps) => { }; }); - // `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 代替 + // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替 onFieldInit(`*(${fields})`, (field: any, form) => { disposes.push( autorun(async () => { diff --git a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx index fe6c2d695..b41c84ee4 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx @@ -31,7 +31,7 @@ export const linkageMergeAction = async ({ const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false]; const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display]; const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern]; - const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value]; + const valueResult = field?.linkageProperty?.value || [field?.initProperty?.value]; const { evaluate } = evaluators.get('formula.js'); switch (operator) { @@ -130,7 +130,10 @@ export const linkageMergeAction = async ({ ...field.linkageProperty, value: valueResult, }; - field.value = last(valueResult) === undefined ? field.value : last(valueResult); + + if (last(valueResult) !== undefined) { + field.value = last(valueResult); + } } break; default: diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index 31c4dbdb6..1aee6e5a8 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -55,7 +55,6 @@ import { useActionContext, useBlockRequestContext, useCollection, - useCollectionFilterOptions, useCollectionManager, useCompile, useDesignable, @@ -72,6 +71,7 @@ import { updateFilterTargets, useFormActiveFields, } from '../block-provider/hooks'; +import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks'; import { FilterBlockType, getSupportFieldsByAssociation, @@ -133,7 +133,7 @@ interface ModalItemProps { title: string; onSubmit: (values: any) => void; initialValues?: any; - schema?: ISchema; + schema?: ISchema | (() => ISchema); modalTip?: string; components?: any; hidden?: boolean; @@ -937,7 +937,6 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) { components, scope, effects, - schema, onSubmit, asyncGetInitialValues, initialValues, @@ -957,10 +956,11 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) { } return ( { const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues; + const schema = _.isFunction(props.schema) ? props.schema() : props.schema; FormDialog( { title: schema.title || title, width }, () => { @@ -1585,6 +1585,7 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem const { t } = useTranslation(); const actionCtx = useActionContext(); let targetField; + const { getField } = useCollection(); const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager(); const variables = useVariables(); @@ -1592,17 +1593,21 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem const collection = useCollection(); const record = useRecord(); const { form } = useFormBlockContext(); - const currentFormFields = useCollectionFilterOptions(collection); + const { getFields } = useCollectionFilterOptionsV2(collection); const { isInSubForm, isInSubTable } = useFlag() || {}; const { name } = collection; - const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']); + const collectionField = useMemo( + () => getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']), + [fieldSchema, getCollectionJoinField, getField], + ); const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required'); if (collectionField?.target) { targetField = getCollectionJoinField( `${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`, ); } + const parentFieldSchema = collectionField?.interface === 'm2o' && findParentFieldSchema(fieldSchema); const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']); const tableCtx = useTableBlockContext(); @@ -1619,123 +1624,147 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem // fix https://nocobase.height.app/T-1355 // 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 if (!fields?.length && currentForm) { - currentForm.children = formatVariableScop(currentFormFields); + currentForm.children = formatVariableScop(getFields()); } return scope; }, - [currentFormFields, name], + [getFields, name], + ); + + const DefaultValueComponent: any = useMemo(() => { + return { + ArrayCollapse, + FormLayout, + VariableInput: (props) => { + return ( + + + + ); + }, + }; + }, [isInSubForm, isInSubTable]); + + const schema = useMemo(() => { + return { + type: 'object', + title: t('Set default value'), + properties: { + default: { + 'x-decorator': 'FormItem', + 'x-component': 'VariableInput', + 'x-component-props': { + ...(fieldSchema?.['x-component-props'] || {}), + collectionField, + contextCollectionName: isAllowContextVariable && tableCtx.collection, + schema: collectionField?.uiSchema, + targetFieldSchema: fieldSchema, + className: defaultInputStyle, + form, + record, + returnScope, + shouldChange: getShouldChange({ + collectionField, + variables, + localVariables, + getAllCollectionsInheritChain, + }), + renderSchemaComponent: function Com(props) { + const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema); + s.title = ''; + s.name = 'default'; + s['x-read-pretty'] = false; + s['x-disabled'] = false; + + const defaultValue = getFieldDefaultValue(s, collectionField); + + if (collectionField.target && s['x-component-props']) { + s['x-component-props'].mode = 'Select'; + } + + if (collectionField?.uiSchema.type) { + s.type = collectionField.uiSchema.type; + } + + if (collectionField?.uiSchema['x-component'] === 'Checkbox') { + s['x-component-props'].defaultChecked = defaultValue; + + // 在这里如果不设置 type 为 void,会导致设置的默认值不生效 + // 但是我不知道为什么必须要设置为 void ? + s.type = 'void'; + } + + const schema = { + ...(s || {}), + 'x-decorator': 'FormItem', + 'x-component-props': { + ...s['x-component-props'], + collectionName: collectionField?.collectionName, + targetField, + onChange: props.onChange, + defaultValue: isVariable(defaultValue) ? '' : defaultValue, + style: { + width: '100%', + verticalAlign: 'top', + minWidth: '200px', + }, + }, + default: isVariable(defaultValue) ? '' : defaultValue, + } as ISchema; + + return ( + + + + ); + }, + }, + title: t('Default value'), + default: getFieldDefaultValue(fieldSchema, collectionField), + }, + }, + } as ISchema; + }, [ + collectionField, + fieldSchema, + fieldSchemaWithoutRequired, + form, + getAllCollectionsInheritChain, + isAllowContextVariable, + localVariables, + record, + returnScope, + t, + tableCtx.collection, + targetField, + variables, + ]); + const handleSubmit: (values: any) => void = useCallback( + (v) => { + const schema: ISchema = { + ['x-uid']: fieldSchema['x-uid'], + }; + fieldSchema.default = v.default; + if (!v.default && v.default !== 0) { + field.value = null; + } + schema.default = v.default; + dn.emit('patch', { + schema, + currentSchema, + }); + }, + [currentSchema, dn, field, fieldSchema], ); return ( { - return ( - - - - ); - }, - }} + components={DefaultValueComponent} width={800} - schema={ - { - type: 'object', - title: t('Set default value'), - properties: { - default: { - 'x-decorator': 'FormItem', - 'x-component': 'VariableInput', - 'x-component-props': { - ...(fieldSchema?.['x-component-props'] || {}), - collectionField, - contextCollectionName: isAllowContextVariable && tableCtx.collection, - schema: collectionField?.uiSchema, - targetFieldSchema: fieldSchema, - className: defaultInputStyle, - form, - record, - returnScope, - shouldChange: getShouldChange({ - collectionField, - variables, - localVariables, - getAllCollectionsInheritChain, - }), - renderSchemaComponent: function Com(props) { - const s = _.cloneDeep(fieldSchemaWithoutRequired) || ({} as Schema); - s.title = ''; - s.name = 'default'; - s['x-read-pretty'] = false; - s['x-disabled'] = false; - - const defaultValue = getFieldDefaultValue(s, collectionField); - - if (collectionField.target && s['x-component-props']) { - s['x-component-props'].mode = 'Select'; - } - - if (collectionField?.uiSchema.type) { - s.type = collectionField.uiSchema.type; - } - - if (collectionField?.uiSchema['x-component'] === 'Checkbox') { - s['x-component-props'].defaultChecked = defaultValue; - - // 在这里如果不设置 type 为 void,会导致设置的默认值不生效 - // 但是我不知道为什么必须要设置为 void ? - s.type = 'void'; - } - - const schema = { - ...(s || {}), - 'x-decorator': 'FormItem', - 'x-component-props': { - ...s['x-component-props'], - collectionName: collectionField?.collectionName, - targetField, - onChange: props.onChange, - defaultValue: isVariable(defaultValue) ? '' : defaultValue, - style: { - width: '100%', - verticalAlign: 'top', - minWidth: '200px', - }, - }, - default: isVariable(defaultValue) ? '' : defaultValue, - } as ISchema; - - return ( - - - - ); - }, - }, - title: t('Default value'), - default: getFieldDefaultValue(fieldSchema, collectionField), - }, - }, - } as ISchema - } - onSubmit={(v) => { - const schema: ISchema = { - ['x-uid']: fieldSchema['x-uid'], - }; - fieldSchema.default = v.default; - if (!v.default && v.default !== 0) { - field.value = null; - } - schema.default = v.default; - dn.emit('patch', { - schema, - currentSchema, - }); - }} + schema={schema} + onSubmit={handleSubmit} /> ); }; @@ -1858,7 +1887,7 @@ SchemaSettings.SortingRule = function SortRuleConfigure(props) { SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) { const { t } = useTranslation(); - const options = useCollectionFilterOptions(props.collectionName); + const { getFields } = useCollectionFilterOptionsV2(props.collectionName); const record = useRecord(); const { form } = useFormBlockContext(); const variables = useVariables(); @@ -1866,54 +1895,59 @@ SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) { const { getAllCollectionsInheritChain } = useCollectionManager(); const { isInSubForm, isInSubTable } = useFlag() || {}; - const dynamicComponent = (props: DynamicComponentProps) => { - return ( - - - - ); + const dynamicComponent = useCallback( + (props: DynamicComponentProps) => { + return ( + + + + ); + }, + [form, getAllCollectionsInheritChain, localVariables, record, variables], + ); + + const getSchema = () => { + return { + type: 'object', + title: t('Set the data scope'), + properties: { + filter: { + enum: props.collectionFilterOption || getFields(), + 'x-decorator': (props) => ( + + + {props.children} + + + ), + 'x-decorator-props': { + isDisabled, + }, + 'x-component': 'Filter', + 'x-component-props': { + collectionName: props.collectionName, + dynamicComponent: props.dynamicComponent || dynamicComponent, + }, + }, + }, + }; }; return ( ( - - - {props.children} - - - ), - 'x-decorator-props': { - isDisabled, - }, - 'x-component': 'Filter', - 'x-component-props': { - collectionName: props.collectionName, - dynamicComponent: props.dynamicComponent || dynamicComponent, - }, - }, - }, - } as ISchema - } + schema={getSchema as () => ISchema} onSubmit={props.onSubmit} /> );