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
This commit is contained in:
被雨水过滤的空气-Rain 2023-11-05 17:59:22 +08:00 committed by GitHub
parent a42ee95e03
commit bce02ad1d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 175 deletions

View File

@ -94,7 +94,7 @@ test.describe('menu page', () => {
await page.getByLabel('Insert before').hover(); 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('textbox').fill(pageTitle4);
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();
@ -124,7 +124,7 @@ test.describe('menu page', () => {
await page.getByLabel('Insert after').hover(); 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('textbox').fill(pageTitle6);
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();

View File

@ -1,4 +1,4 @@
import { enableToConfig, expect, test } from '@nocobase/test/client'; import { expect, test } from '@nocobase/test/client';
test.describe('page header', () => { test.describe('page header', () => {
test('disabled & enabled page header', async ({ page, mockPage }) => { test('disabled & enabled page header', async ({ page, mockPage }) => {
@ -99,7 +99,7 @@ test.describe('page tabs', () => {
//修改tab名称 //修改tab名称
await page.getByText('Unnamed').click(); await page.getByText('Unnamed').click();
await page.getByRole('button', { name: 'designer-schema-settings-Page-tab' }).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('textbox').fill('page tab');
await page.getByRole('button', { name: 'OK' }).click(); await page.getByRole('button', { name: 'OK' }).click();

View File

@ -64,7 +64,6 @@ const useResource = (props: UseResourceProps) => {
const { fieldSchema } = useActionContext(); const { fieldSchema } = useActionContext();
const isCreateAction = fieldSchema?.['x-action'] === 'create'; const isCreateAction = fieldSchema?.['x-action'] === 'create';
const association = useAssociation(props); const association = useAssociation(props);
console.log(association);
const sourceId = useSourceId?.(); const sourceId = useSourceId?.();
const field = useField(); const field = useField();
const withoutTableFieldResource = useContext(WithoutTableFieldResource); const withoutTableFieldResource = useContext(WithoutTableFieldResource);

View File

@ -1,8 +1,9 @@
import { useField, useForm } from '@formily/react'; import { useField, useForm } from '@formily/react';
import { message } from 'antd'; import { message } from 'antd';
import _ from 'lodash';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionManager } from '.'; import { useCollection, useCollectionManager } from '.';
import { useRequest } from '../api-client'; import { useRequest } from '../api-client';
@ -183,7 +184,65 @@ export const useCollectionFilterOptions = (collection: any) => {
}; };
const options = getOptions(fields, 1); const options = getOptions(fields, 1);
return options; 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) => { export const useLinkageCollectionFilterOptions = (collectionName: string) => {

View File

@ -1,14 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; import { FormLayout } from '@formily/antd-v5';
import { import { createForm, Field, Form as FormilyForm, onFieldChange, onFieldInit, onFormInputChange } from '@formily/core';
createForm,
Field,
Form as FormilyForm,
onFieldChange,
onFieldInit,
onFieldReact,
onFormInputChange,
} from '@formily/core';
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { autorun } from '@formily/reactive'; import { autorun } from '@formily/reactive';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
@ -132,7 +124,7 @@ const WithForm = (props: WithFormProps) => {
}; };
}); });
// `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 代替 // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替
onFieldInit(`*(${fields})`, (field: any, form) => { onFieldInit(`*(${fields})`, (field: any, form) => {
disposes.push( disposes.push(
autorun(async () => { autorun(async () => {

View File

@ -31,7 +31,7 @@ export const linkageMergeAction = async ({
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false]; const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display]; const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern]; 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'); const { evaluate } = evaluators.get('formula.js');
switch (operator) { switch (operator) {
@ -130,7 +130,10 @@ export const linkageMergeAction = async ({
...field.linkageProperty, ...field.linkageProperty,
value: valueResult, value: valueResult,
}; };
field.value = last(valueResult) === undefined ? field.value : last(valueResult);
if (last(valueResult) !== undefined) {
field.value = last(valueResult);
}
} }
break; break;
default: default:

View File

@ -55,7 +55,6 @@ import {
useActionContext, useActionContext,
useBlockRequestContext, useBlockRequestContext,
useCollection, useCollection,
useCollectionFilterOptions,
useCollectionManager, useCollectionManager,
useCompile, useCompile,
useDesignable, useDesignable,
@ -72,6 +71,7 @@ import {
updateFilterTargets, updateFilterTargets,
useFormActiveFields, useFormActiveFields,
} from '../block-provider/hooks'; } from '../block-provider/hooks';
import { useCollectionFilterOptionsV2 } from '../collection-manager/action-hooks';
import { import {
FilterBlockType, FilterBlockType,
getSupportFieldsByAssociation, getSupportFieldsByAssociation,
@ -133,7 +133,7 @@ interface ModalItemProps {
title: string; title: string;
onSubmit: (values: any) => void; onSubmit: (values: any) => void;
initialValues?: any; initialValues?: any;
schema?: ISchema; schema?: ISchema | (() => ISchema);
modalTip?: string; modalTip?: string;
components?: any; components?: any;
hidden?: boolean; hidden?: boolean;
@ -937,7 +937,6 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) {
components, components,
scope, scope,
effects, effects,
schema,
onSubmit, onSubmit,
asyncGetInitialValues, asyncGetInitialValues,
initialValues, initialValues,
@ -957,10 +956,11 @@ SchemaSettings.ModalItem = function ModalItem(props: ModalItemProps) {
} }
return ( return (
<SchemaSettings.Item <SchemaSettings.Item
title={schema.title || title} title={title}
{...others} {...others}
onClick={async () => { onClick={async () => {
const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues; const values = asyncGetInitialValues ? await asyncGetInitialValues() : initialValues;
const schema = _.isFunction(props.schema) ? props.schema() : props.schema;
FormDialog( FormDialog(
{ title: schema.title || title, width }, { title: schema.title || title, width },
() => { () => {
@ -1585,6 +1585,7 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
const { t } = useTranslation(); const { t } = useTranslation();
const actionCtx = useActionContext(); const actionCtx = useActionContext();
let targetField; let targetField;
const { getField } = useCollection(); const { getField } = useCollection();
const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager(); const { getCollectionJoinField, getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager();
const variables = useVariables(); const variables = useVariables();
@ -1592,17 +1593,21 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
const collection = useCollection(); const collection = useCollection();
const record = useRecord(); const record = useRecord();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const currentFormFields = useCollectionFilterOptions(collection); const { getFields } = useCollectionFilterOptionsV2(collection);
const { isInSubForm, isInSubTable } = useFlag() || {}; const { isInSubForm, isInSubTable } = useFlag() || {};
const { name } = collection; 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'); const fieldSchemaWithoutRequired = _.omit(fieldSchema, 'required');
if (collectionField?.target) { if (collectionField?.target) {
targetField = getCollectionJoinField( targetField = getCollectionJoinField(
`${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`, `${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`,
); );
} }
const parentFieldSchema = collectionField?.interface === 'm2o' && findParentFieldSchema(fieldSchema); const parentFieldSchema = collectionField?.interface === 'm2o' && findParentFieldSchema(fieldSchema);
const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']); const parentCollectionField = parentFieldSchema && getCollectionJoinField(parentFieldSchema?.['x-collection-field']);
const tableCtx = useTableBlockContext(); const tableCtx = useTableBlockContext();
@ -1619,123 +1624,147 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
// fix https://nocobase.height.app/T-1355 // fix https://nocobase.height.app/T-1355
// 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 // 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下
if (!fields?.length && currentForm) { if (!fields?.length && currentForm) {
currentForm.children = formatVariableScop(currentFormFields); currentForm.children = formatVariableScop(getFields());
} }
return scope; return scope;
}, },
[currentFormFields, name], [getFields, name],
);
const DefaultValueComponent: any = useMemo(() => {
return {
ArrayCollapse,
FormLayout,
VariableInput: (props) => {
return (
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable} isInSetDefaultValueDialog>
<VariableInput {...props} />
</FlagProvider>
);
},
};
}, [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 (
<FormProvider>
<SchemaComponent schema={schema} />
</FormProvider>
);
},
},
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 (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set default value')} title={t('Set default value')}
components={{ components={DefaultValueComponent}
ArrayCollapse,
FormLayout,
VariableInput: (props) => {
return (
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable} isInSetDefaultValueDialog>
<VariableInput {...props} />
</FlagProvider>
);
},
}}
width={800} width={800}
schema={ schema={schema}
{ onSubmit={handleSubmit}
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 (
<FormProvider>
<SchemaComponent schema={schema} />
</FormProvider>
);
},
},
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,
});
}}
/> />
); );
}; };
@ -1858,7 +1887,7 @@ SchemaSettings.SortingRule = function SortRuleConfigure(props) {
SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) { SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const options = useCollectionFilterOptions(props.collectionName); const { getFields } = useCollectionFilterOptionsV2(props.collectionName);
const record = useRecord(); const record = useRecord();
const { form } = useFormBlockContext(); const { form } = useFormBlockContext();
const variables = useVariables(); const variables = useVariables();
@ -1866,54 +1895,59 @@ SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) {
const { getAllCollectionsInheritChain } = useCollectionManager(); const { getAllCollectionsInheritChain } = useCollectionManager();
const { isInSubForm, isInSubTable } = useFlag() || {}; const { isInSubForm, isInSubTable } = useFlag() || {};
const dynamicComponent = (props: DynamicComponentProps) => { const dynamicComponent = useCallback(
return ( (props: DynamicComponentProps) => {
<DatePickerProvider value={{ utc: false }}> return (
<VariableInput <DatePickerProvider value={{ utc: false }}>
{...props} <VariableInput
form={form} {...props}
record={record} form={form}
shouldChange={getShouldChange({ record={record}
collectionField: props.collectionField, shouldChange={getShouldChange({
variables, collectionField: props.collectionField,
localVariables, variables,
getAllCollectionsInheritChain, localVariables,
})} getAllCollectionsInheritChain,
/> })}
</DatePickerProvider> />
); </DatePickerProvider>
);
},
[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) => (
<BaseVariableProvider {...props}>
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable}>
{props.children}
</FlagProvider>
</BaseVariableProvider>
),
'x-decorator-props': {
isDisabled,
},
'x-component': 'Filter',
'x-component-props': {
collectionName: props.collectionName,
dynamicComponent: props.dynamicComponent || dynamicComponent,
},
},
},
};
}; };
return ( return (
<SchemaSettings.ModalItem <SchemaSettings.ModalItem
title={t('Set the data scope')} title={t('Set the data scope')}
initialValues={{ filter: props.defaultFilter }} initialValues={{ filter: props.defaultFilter }}
schema={ schema={getSchema as () => ISchema}
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
enum: props.collectionFilterOption || options,
'x-decorator': (props) => (
<BaseVariableProvider {...props}>
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable}>
{props.children}
</FlagProvider>
</BaseVariableProvider>
),
'x-decorator-props': {
isDisabled,
},
'x-component': 'Filter',
'x-component-props': {
collectionName: props.collectionName,
dynamicComponent: props.dynamicComponent || dynamicComponent,
},
},
},
} as ISchema
}
onSubmit={props.onSubmit} onSubmit={props.onSubmit}
/> />
); );