mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 08:36:44 +00:00
perf(data-scope): async loading of variable data (#1932)
* refactor: remove useless code * perf: async loading of user variable data * perf: async loading children * perf: add maxDepth * refactor: use useMemo * fix: avoid old data
This commit is contained in:
parent
76ddbf2104
commit
f286534752
@ -1,7 +1,7 @@
|
||||
import { useField, useForm } from '@formily/react';
|
||||
import { message } from 'antd';
|
||||
import omit from 'lodash/omit';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useCollectionManager } from '.';
|
||||
import { useCompile } from '..';
|
||||
@ -109,96 +109,98 @@ export const useChildrenCollections = (collectionName: string) => {
|
||||
};
|
||||
|
||||
export const useCollectionFilterOptions = (collectionName: string) => {
|
||||
const { getCollectionFields, getInterface } = useCollectionManager();
|
||||
const fields = getCollectionFields(collectionName);
|
||||
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);
|
||||
const { getCollectionFields, getInterface, getChildrenCollections, getCollection } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const { getChildrenCollections, getCollection } = useCollectionManager();
|
||||
const collection = getCollection(collectionName);
|
||||
const childrenCollections = getChildrenCollections(collectionName);
|
||||
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
|
||||
options.push({
|
||||
name: 'tableoid',
|
||||
type: 'string',
|
||||
title: '{{t("Table OID(Inheritance)")}}',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
enum: [{ value: collectionName, label: compile(collection.title) }].concat(
|
||||
childrenCollections.map((v) => {
|
||||
return {
|
||||
value: v.name,
|
||||
label: compile(v.title),
|
||||
};
|
||||
}),
|
||||
),
|
||||
},
|
||||
operators: [
|
||||
{
|
||||
label: '{{t("contains")}}',
|
||||
value: '$childIn',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
'x-component-props': { mode: 'tags' },
|
||||
},
|
||||
|
||||
return useMemo(() => {
|
||||
const fields = getCollectionFields(collectionName);
|
||||
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);
|
||||
const collection = getCollection(collectionName);
|
||||
const childrenCollections = getChildrenCollections(collectionName);
|
||||
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
|
||||
options.push({
|
||||
name: 'tableoid',
|
||||
type: 'string',
|
||||
title: '{{t("Table OID(Inheritance)")}}',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
enum: [{ value: collectionName, label: compile(collection.title) }].concat(
|
||||
childrenCollections.map((v) => {
|
||||
return {
|
||||
value: v.name,
|
||||
label: compile(v.title),
|
||||
};
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
label: '{{t("does not contain")}}',
|
||||
value: '$childNotIn',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
'x-component-props': { mode: 'tags' },
|
||||
operators: [
|
||||
{
|
||||
label: '{{t("contains")}}',
|
||||
value: '$childIn',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
'x-component-props': { mode: 'tags' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return options;
|
||||
{
|
||||
label: '{{t("does not contain")}}',
|
||||
value: '$childNotIn',
|
||||
schema: {
|
||||
'x-component': 'Select',
|
||||
'x-component-props': { mode: 'tags' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}, [collectionName]);
|
||||
};
|
||||
|
||||
export const useLinkageCollectionFilterOptions = (collectionName: string) => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ArrayItems } from '@formily/antd';
|
||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTableBlockContext } from '../../../block-provider';
|
||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||
@ -9,7 +9,7 @@ import { useCollectionFilterOptions, useSortFields } from '../../../collection-m
|
||||
import { FilterBlockType } from '../../../filter-provider/utils';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
import { useCompile, useDesignable } from '../../hooks';
|
||||
import { removeNullCondition } from '../filter';
|
||||
import { FixedBlockDesignerItem } from '../page';
|
||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||
@ -24,6 +24,7 @@ export const TableBlockDesigner = () => {
|
||||
const { service } = useTableBlockContext();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
@ -43,6 +44,45 @@ export const TableBlockDesigner = () => {
|
||||
const collection = useCollection();
|
||||
const { dragSort, resource } = field.decoratorProps;
|
||||
const treeChildren = resource?.includes('.') ? getCollectionField(resource)?.treeChildren : !!collection?.tree;
|
||||
const dataScopeSchema = useMemo(() => {
|
||||
return {
|
||||
type: 'object',
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: compile(dataSource),
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema;
|
||||
}, [dataSource, defaultFilter]);
|
||||
const onDataScopeSubmit = useCallback(
|
||||
({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
const filters = service.params?.[1]?.filters || {};
|
||||
service.run(
|
||||
{ ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 },
|
||||
{ filters },
|
||||
);
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
},
|
||||
[field],
|
||||
);
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.BlockTitleItem />
|
||||
@ -84,44 +124,7 @@ export const TableBlockDesigner = () => {
|
||||
/>
|
||||
)}
|
||||
<FixedBlockDesignerItem />
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set the data scope')}
|
||||
schema={
|
||||
{
|
||||
type: 'object',
|
||||
title: t('Set the data scope'),
|
||||
properties: {
|
||||
filter: {
|
||||
default: defaultFilter,
|
||||
// title: '数据范围',
|
||||
enum: dataSource,
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ filter }) => {
|
||||
filter = removeNullCondition(filter);
|
||||
const params = field.decoratorProps.params || {};
|
||||
params.filter = filter;
|
||||
field.decoratorProps.params = params;
|
||||
fieldSchema['x-decorator-props']['params'] = params;
|
||||
const filters = service.params?.[1]?.filters || {};
|
||||
service.run(
|
||||
{ ...service.params?.[0], filter: mergeFilter([...Object.values(filters), filter]), page: 1 },
|
||||
{ filters },
|
||||
);
|
||||
dn.emit('patch', {
|
||||
schema: {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-decorator-props': fieldSchema['x-decorator-props'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.ModalItem title={t('Set the data scope')} schema={dataScopeSchema} onSubmit={onDataScopeSubmit} />
|
||||
{!dragSort && (
|
||||
<SchemaSettings.ModalItem
|
||||
title={t('Set default sorting rules')}
|
||||
@ -151,7 +154,7 @@ export const TableBlockDesigner = () => {
|
||||
field: {
|
||||
type: 'string',
|
||||
enum: sortFields,
|
||||
required:true,
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Select',
|
||||
'x-component-props': {
|
||||
|
@ -1,74 +0,0 @@
|
||||
import { CollectionFieldOptions } from '../../../../collection-manager';
|
||||
import { useFilterOptions } from '../../filter';
|
||||
|
||||
interface Operator {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface GetOptionsParams {
|
||||
schema: any;
|
||||
operator: Operator;
|
||||
maxDepth: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const isOperatorSupportMultiRelation = (operator: Operator) => {
|
||||
if (!operator) return false;
|
||||
return ['$eq', '$ne'].includes(operator.value);
|
||||
};
|
||||
|
||||
const isSingleRelationField = (field: CollectionFieldOptions) => {
|
||||
if (!field) return false;
|
||||
return field.type === 'belongsTo' || field.type === 'hasOne';
|
||||
};
|
||||
|
||||
export const useOptions = (collectionName: string, { schema, operator, maxDepth, count = 1 }: GetOptionsParams) => {
|
||||
if (count > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = useFilterOptions(collectionName).map((option) => {
|
||||
if (!option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
||||
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
|
||||
};
|
||||
}
|
||||
|
||||
const children =
|
||||
useOptions(option.target, {
|
||||
schema,
|
||||
operator,
|
||||
maxDepth,
|
||||
count: count + 1,
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
children,
|
||||
disabled:
|
||||
(!isSingleRelationField(option) && !isOperatorSupportMultiRelation(operator)) ||
|
||||
children.every((child) => child.disabled),
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useUserVariable = ({ schema, operator }) => {
|
||||
const options = useOptions('users', { schema, operator, maxDepth: 3 }) || [];
|
||||
|
||||
return {
|
||||
label: `{{t("Current user")}}`,
|
||||
value: '$user',
|
||||
key: '$user',
|
||||
disabled: options.every((option) => option.disabled),
|
||||
children: options,
|
||||
};
|
||||
};
|
@ -3,11 +3,14 @@ import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import { useCompile } from '../..';
|
||||
import { Option } from '../../../schema-settings/VariableInput/type';
|
||||
import { XButton } from './XButton';
|
||||
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
@ -120,49 +123,91 @@ export function Input(props) {
|
||||
const form = useForm();
|
||||
|
||||
const { value = '', scope, onChange, children, button, useTypedConstant, style, className } = props;
|
||||
const parsed = parseValue(value);
|
||||
const parsed = useMemo(() => parseValue(value), [value]);
|
||||
const isConstant = typeof parsed === 'string';
|
||||
const type = isConstant ? parsed : '';
|
||||
const variable = isConstant ? null : parsed;
|
||||
const variableOptions = typeof scope === 'function' ? scope() : scope ?? [];
|
||||
const variableOptions = useMemo(() => (typeof scope === 'function' ? scope() : scope ?? []), [scope]);
|
||||
const [variableText, setVariableText] = React.useState('');
|
||||
|
||||
const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } = children
|
||||
? {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
}
|
||||
: useTypedConstant
|
||||
? getTypedConstantOption(type, useTypedConstant)
|
||||
: {
|
||||
value: '',
|
||||
label: '{{t("Null")}}',
|
||||
component: ConstantTypes.null.component,
|
||||
};
|
||||
const options: VariableOptions[] = compile([constantOption, ...variableOptions]);
|
||||
|
||||
function onSwitch(next) {
|
||||
if (next[0] === '') {
|
||||
if (next[1]) {
|
||||
if (next[1] !== type) {
|
||||
onChange(ConstantTypes[next[1]]?.default ?? null);
|
||||
}
|
||||
} else {
|
||||
if (variable) {
|
||||
onChange(null);
|
||||
}
|
||||
}
|
||||
return;
|
||||
const loadData = async (selectedOptions: Option[]) => {
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (option.loadChildren) {
|
||||
// 需要保证 selectedOptions 是一个响应式对象,这样才能触发重新渲染
|
||||
await option.loadChildren(option);
|
||||
}
|
||||
onChange(`{{${next.join('.')}}}`);
|
||||
}
|
||||
};
|
||||
|
||||
const variableText = variable
|
||||
?.reduce((opts, key, i) => {
|
||||
const option = (i ? (opts[i - 1] as VariableOptions)?.children : options)?.find((item) => item.value === key);
|
||||
return option ? opts.concat(option) : opts;
|
||||
}, [] as VariableOptions[])
|
||||
.map((item) => item.label)
|
||||
.join(' / ');
|
||||
const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } =
|
||||
useMemo(() => {
|
||||
return children
|
||||
? {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
}
|
||||
: useTypedConstant
|
||||
? getTypedConstantOption(type, useTypedConstant)
|
||||
: {
|
||||
value: '',
|
||||
label: '{{t("Null")}}',
|
||||
component: ConstantTypes.null.component,
|
||||
};
|
||||
}, [type, useTypedConstant]);
|
||||
|
||||
const options: VariableOptions[] = useMemo(
|
||||
() => compile([constantOption, ...variableOptions]),
|
||||
[constantOption, variableOptions],
|
||||
);
|
||||
|
||||
const onSwitch = useCallback(
|
||||
(next) => {
|
||||
if (next[0] === '') {
|
||||
if (next[1]) {
|
||||
if (next[1] !== type) {
|
||||
onChange(ConstantTypes[next[1]]?.default ?? null);
|
||||
}
|
||||
} else {
|
||||
if (variable) {
|
||||
onChange(null);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
onChange(`{{${next.join('.')}}}`);
|
||||
},
|
||||
[type, variable],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
let prevOption: Option = null;
|
||||
const labels = [];
|
||||
|
||||
for (let i = 0; i < variable.length; i++) {
|
||||
const key = variable[i];
|
||||
try {
|
||||
if (i === 0) {
|
||||
prevOption = options.find((item) => item.value === key);
|
||||
} else {
|
||||
if (prevOption.children?.length === 0 && prevOption.loadChildren) {
|
||||
await prevOption.loadChildren(prevOption);
|
||||
}
|
||||
prevOption = prevOption.children.find((item) => item.value === key);
|
||||
}
|
||||
labels.push(prevOption.label);
|
||||
setVariableText(labels.join(' / '));
|
||||
} catch (err) {
|
||||
error(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 弹窗动画的延迟时间是 300 毫秒,动画结束之后再执行,防止动画卡顿
|
||||
setTimeout(run, 300);
|
||||
}, [variable]);
|
||||
|
||||
const disabled = props.disabled || form.disabled;
|
||||
|
||||
@ -257,6 +302,7 @@ export function Input(props) {
|
||||
options={options}
|
||||
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
||||
onChange={onSwitch}
|
||||
loadData={loadData as any}
|
||||
changeOnSelect
|
||||
>
|
||||
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
||||
|
@ -17,7 +17,7 @@ type Props = {
|
||||
export const VariableInput = (props: Props) => {
|
||||
const { value, onChange, renderSchemaComponent: RenderSchemaComponent, style, schema, className } = props;
|
||||
const compile = useCompile();
|
||||
const userVariable = useUserVariable({ schema, level: 1 });
|
||||
const userVariable = useUserVariable({ schema, maxDepth: 1 });
|
||||
const scope = useMemo(() => {
|
||||
return [
|
||||
userVariable,
|
||||
|
@ -1,67 +1,97 @@
|
||||
import { observable } from '@formily/reactive';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||
import { FieldOption, Option } from '../type';
|
||||
|
||||
interface GetOptionsParams {
|
||||
schema: any;
|
||||
operator?: string;
|
||||
maxDepth: number;
|
||||
count?: number;
|
||||
getFilterOptions: (collectionName: string) => any[];
|
||||
depth: number;
|
||||
maxDepth?: number;
|
||||
loadChildren?: (option: Option) => Promise<void>;
|
||||
}
|
||||
|
||||
const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, getFilterOptions }: GetOptionsParams) => {
|
||||
if (count > maxDepth) {
|
||||
return [];
|
||||
}
|
||||
const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChildren }: GetOptionsParams): Option[] => {
|
||||
const result = options
|
||||
.map((option): Option => {
|
||||
if (!option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
||||
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
|
||||
isLeaf: true,
|
||||
depth,
|
||||
};
|
||||
}
|
||||
|
||||
if (depth >= maxDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = options.map((option) => {
|
||||
if (!option.target) {
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
||||
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: option,
|
||||
depth,
|
||||
loadChildren,
|
||||
};
|
||||
}
|
||||
|
||||
const children =
|
||||
getChildren(getFilterOptions(option.target), {
|
||||
schema,
|
||||
operator,
|
||||
maxDepth,
|
||||
count: count + 1,
|
||||
getFilterOptions,
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
key: option.name,
|
||||
value: option.name,
|
||||
label: option.title,
|
||||
children,
|
||||
disabled: children.every((child) => child.disabled),
|
||||
};
|
||||
});
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useUserVariable = ({ operator, schema, level }: { operator?: any; schema: any; level?: number }) => {
|
||||
export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDepth?: number }) => {
|
||||
const compile = useCompile();
|
||||
const getFilterOptions = useGetFilterOptions();
|
||||
|
||||
const children = useMemo(
|
||||
() => getChildren(getFilterOptions('users'), { schema, operator, maxDepth: level || 3, getFilterOptions }) || [],
|
||||
[operator, schema],
|
||||
);
|
||||
const loadChildren = (option: Option): Promise<void> => {
|
||||
if (!option.field?.target) {
|
||||
return new Promise((resolve) => {
|
||||
error('Must be set field target');
|
||||
resolve(void 0);
|
||||
});
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
const collectionName = option.field.target;
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const children =
|
||||
getChildren(getFilterOptions(collectionName), {
|
||||
schema,
|
||||
depth: option.depth + 1,
|
||||
maxDepth,
|
||||
loadChildren,
|
||||
}) || [];
|
||||
|
||||
option.children = compile(children);
|
||||
resolve(void 0);
|
||||
|
||||
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
|
||||
}, 5);
|
||||
});
|
||||
};
|
||||
|
||||
const result = useMemo(() => {
|
||||
return compile({
|
||||
label: `{{t("Current user")}}`,
|
||||
value: '$user',
|
||||
key: '$user',
|
||||
disabled: children.every((option) => option.disabled),
|
||||
children: children,
|
||||
});
|
||||
}, [children]);
|
||||
children: [],
|
||||
isLeaf: false,
|
||||
field: {
|
||||
target: 'users',
|
||||
},
|
||||
depth: 0,
|
||||
loadChildren,
|
||||
} as Option);
|
||||
}, [schema]);
|
||||
|
||||
// 必须使用 observable 包一下,使其变成响应式对象,不然 children 加载后不会更新 UI
|
||||
return observable(result);
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { useUserVariable } from './useUserVariable';
|
||||
|
||||
export const useVariableOptions = () => {
|
||||
const { operator, schema } = useValues();
|
||||
const userVariable = useUserVariable({ operator, schema });
|
||||
const userVariable = useUserVariable({ maxDepth: 3, schema });
|
||||
const dateVariable = useDateVariable({ operator, schema });
|
||||
|
||||
if (!operator || !schema) return [];
|
||||
|
@ -0,0 +1,31 @@
|
||||
import { Schema } from '@formily/react';
|
||||
|
||||
export interface Option {
|
||||
key?: string | number;
|
||||
value?: string | number;
|
||||
label?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
children?: Option[];
|
||||
// 标记是否为叶子节点,设置了 `loadData` 时有效
|
||||
// 设为 `false` 时会强制标记为父节点,即使当前节点没有 children,也会显示展开图标
|
||||
isLeaf?: boolean;
|
||||
/** 当开启异步加载时有效,用于加载当前 node 的 children */
|
||||
loadChildren?(option: Option): Promise<void>;
|
||||
field?: FieldOption;
|
||||
depth?: number;
|
||||
}
|
||||
|
||||
export interface FieldOption {
|
||||
name?: string;
|
||||
type?: string;
|
||||
target?: string;
|
||||
title?: string;
|
||||
schema?: Schema;
|
||||
operators?: Operator[];
|
||||
children?: FieldOption[];
|
||||
}
|
||||
|
||||
interface Operator {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user