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.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();

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('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();

View File

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

View File

@ -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) => {

View File

@ -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 () => {

View File

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

View File

@ -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 (
<SchemaSettings.Item
title={schema.title || title}
title={title}
{...others}
onClick={async () => {
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 (
<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 (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{
ArrayCollapse,
FormLayout,
VariableInput: (props) => {
return (
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable} isInSetDefaultValueDialog>
<VariableInput {...props} />
</FlagProvider>
);
},
}}
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 (
<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,
});
}}
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 (
<DatePickerProvider value={{ utc: false }}>
<VariableInput
{...props}
form={form}
record={record}
shouldChange={getShouldChange({
collectionField: props.collectionField,
variables,
localVariables,
getAllCollectionsInheritChain,
})}
/>
</DatePickerProvider>
);
const dynamicComponent = useCallback(
(props: DynamicComponentProps) => {
return (
<DatePickerProvider value={{ utc: false }}>
<VariableInput
{...props}
form={form}
record={record}
shouldChange={getShouldChange({
collectionField: props.collectionField,
variables,
localVariables,
getAllCollectionsInheritChain,
})}
/>
</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 (
<SchemaSettings.ModalItem
title={t('Set the data scope')}
initialValues={{ filter: props.defaultFilter }}
schema={
{
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
}
schema={getSchema as () => ISchema}
onSubmit={props.onSubmit}
/>
);