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,18 +1624,16 @@ 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],
); );
return ( const DefaultValueComponent: any = useMemo(() => {
<SchemaSettings.ModalItem return {
title={t('Set default value')}
components={{
ArrayCollapse, ArrayCollapse,
FormLayout, FormLayout,
VariableInput: (props) => { VariableInput: (props) => {
@ -1640,10 +1643,11 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
</FlagProvider> </FlagProvider>
); );
}, },
}} };
width={800} }, [isInSubForm, isInSubTable]);
schema={
{ const schema = useMemo(() => {
return {
type: 'object', type: 'object',
title: t('Set default value'), title: t('Set default value'),
properties: { properties: {
@ -1720,9 +1724,24 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
default: getFieldDefaultValue(fieldSchema, collectionField), default: getFieldDefaultValue(fieldSchema, collectionField),
}, },
}, },
} as ISchema } as ISchema;
} }, [
onSubmit={(v) => { collectionField,
fieldSchema,
fieldSchemaWithoutRequired,
form,
getAllCollectionsInheritChain,
isAllowContextVariable,
localVariables,
record,
returnScope,
t,
tableCtx.collection,
targetField,
variables,
]);
const handleSubmit: (values: any) => void = useCallback(
(v) => {
const schema: ISchema = { const schema: ISchema = {
['x-uid']: fieldSchema['x-uid'], ['x-uid']: fieldSchema['x-uid'],
}; };
@ -1735,7 +1754,17 @@ SchemaSettings.DefaultValue = function DefaultValueConfigure(props: { fieldSchem
schema, schema,
currentSchema, currentSchema,
}); });
}} },
[currentSchema, dn, field, fieldSchema],
);
return (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={DefaultValueComponent}
width={800}
schema={schema}
onSubmit={handleSubmit}
/> />
); );
}; };
@ -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,7 +1895,8 @@ 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(
(props: DynamicComponentProps) => {
return ( return (
<DatePickerProvider value={{ utc: false }}> <DatePickerProvider value={{ utc: false }}>
<VariableInput <VariableInput
@ -1882,19 +1912,17 @@ SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) {
/> />
</DatePickerProvider> </DatePickerProvider>
); );
}; },
[form, getAllCollectionsInheritChain, localVariables, record, variables],
);
return ( const getSchema = () => {
<SchemaSettings.ModalItem return {
title={t('Set the data scope')}
initialValues={{ filter: props.defaultFilter }}
schema={
{
type: 'object', type: 'object',
title: t('Set the data scope'), title: t('Set the data scope'),
properties: { properties: {
filter: { filter: {
enum: props.collectionFilterOption || options, enum: props.collectionFilterOption || getFields(),
'x-decorator': (props) => ( 'x-decorator': (props) => (
<BaseVariableProvider {...props}> <BaseVariableProvider {...props}>
<FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable}> <FlagProvider isInSubForm={isInSubForm} isInSubTable={isInSubTable}>
@ -1912,8 +1940,14 @@ SchemaSettings.DataScope = function DataScopeConfigure(props: DataScopeProps) {
}, },
}, },
}, },
} as ISchema };
} };
return (
<SchemaSettings.ModalItem
title={t('Set the data scope')}
initialValues={{ filter: props.defaultFilter }}
schema={getSchema as () => ISchema}
onSubmit={props.onSubmit} onSubmit={props.onSubmit}
/> />
); );