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 { useField, useForm } from '@formily/react';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCollection, useCollectionManager } from '.';
|
import { useCollection, useCollectionManager } from '.';
|
||||||
import { useCompile } from '..';
|
import { useCompile } from '..';
|
||||||
@ -109,96 +109,98 @@ export const useChildrenCollections = (collectionName: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useCollectionFilterOptions = (collectionName: string) => {
|
export const useCollectionFilterOptions = (collectionName: string) => {
|
||||||
const { getCollectionFields, getInterface } = useCollectionManager();
|
const { getCollectionFields, getInterface, getChildrenCollections, getCollection } = 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 compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getChildrenCollections, getCollection } = useCollectionManager();
|
|
||||||
const collection = getCollection(collectionName);
|
return useMemo(() => {
|
||||||
const childrenCollections = getChildrenCollections(collectionName);
|
const fields = getCollectionFields(collectionName);
|
||||||
if (childrenCollections.length > 0 && !options.find((v) => v.name == 'tableoid')) {
|
const field2option = (field, depth) => {
|
||||||
options.push({
|
if (!field.interface) {
|
||||||
name: 'tableoid',
|
return;
|
||||||
type: 'string',
|
}
|
||||||
title: '{{t("Table OID(Inheritance)")}}',
|
const fieldInterface = getInterface(field.interface);
|
||||||
schema: {
|
if (!fieldInterface?.filterable) {
|
||||||
'x-component': 'Select',
|
return;
|
||||||
enum: [{ value: collectionName, label: compile(collection.title) }].concat(
|
}
|
||||||
childrenCollections.map((v) => {
|
const { nested, children, operators } = fieldInterface.filterable;
|
||||||
return {
|
const option = {
|
||||||
value: v.name,
|
name: field.name,
|
||||||
label: compile(v.title),
|
title: field?.uiSchema?.title || field.name,
|
||||||
};
|
schema: field?.uiSchema,
|
||||||
}),
|
operators:
|
||||||
),
|
operators?.filter?.((operator) => {
|
||||||
},
|
return !operator?.visible || operator.visible(field);
|
||||||
operators: [
|
}) || [],
|
||||||
{
|
interface: field.interface,
|
||||||
label: '{{t("contains")}}',
|
};
|
||||||
value: '$childIn',
|
if (field.target && depth > 2) {
|
||||||
schema: {
|
return;
|
||||||
'x-component': 'Select',
|
}
|
||||||
'x-component-props': { mode: 'tags' },
|
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),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
operators: [
|
||||||
label: '{{t("does not contain")}}',
|
{
|
||||||
value: '$childNotIn',
|
label: '{{t("contains")}}',
|
||||||
schema: {
|
value: '$childIn',
|
||||||
'x-component': 'Select',
|
schema: {
|
||||||
'x-component-props': { mode: 'tags' },
|
'x-component': 'Select',
|
||||||
|
'x-component-props': { mode: 'tags' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
label: '{{t("does not contain")}}',
|
||||||
});
|
value: '$childNotIn',
|
||||||
}
|
schema: {
|
||||||
return options;
|
'x-component': 'Select',
|
||||||
|
'x-component-props': { mode: 'tags' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}, [collectionName]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useLinkageCollectionFilterOptions = (collectionName: string) => {
|
export const useLinkageCollectionFilterOptions = (collectionName: string) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ArrayItems } from '@formily/antd';
|
import { ArrayItems } from '@formily/antd';
|
||||||
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useTableBlockContext } from '../../../block-provider';
|
import { useTableBlockContext } from '../../../block-provider';
|
||||||
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
|
||||||
@ -9,7 +9,7 @@ import { useCollectionFilterOptions, useSortFields } from '../../../collection-m
|
|||||||
import { FilterBlockType } from '../../../filter-provider/utils';
|
import { FilterBlockType } from '../../../filter-provider/utils';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
import { useSchemaTemplate } from '../../../schema-templates';
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useCompile, useDesignable } from '../../hooks';
|
||||||
import { removeNullCondition } from '../filter';
|
import { removeNullCondition } from '../filter';
|
||||||
import { FixedBlockDesignerItem } from '../page';
|
import { FixedBlockDesignerItem } from '../page';
|
||||||
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
import { FilterDynamicComponent } from './FilterDynamicComponent';
|
||||||
@ -24,6 +24,7 @@ export const TableBlockDesigner = () => {
|
|||||||
const { service } = useTableBlockContext();
|
const { service } = useTableBlockContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
|
const compile = useCompile();
|
||||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
|
||||||
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
|
||||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||||
@ -43,6 +44,45 @@ export const TableBlockDesigner = () => {
|
|||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const { dragSort, resource } = field.decoratorProps;
|
const { dragSort, resource } = field.decoratorProps;
|
||||||
const treeChildren = resource?.includes('.') ? getCollectionField(resource)?.treeChildren : !!collection?.tree;
|
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 (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||||
<SchemaSettings.BlockTitleItem />
|
<SchemaSettings.BlockTitleItem />
|
||||||
@ -84,44 +124,7 @@ export const TableBlockDesigner = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FixedBlockDesignerItem />
|
<FixedBlockDesignerItem />
|
||||||
<SchemaSettings.ModalItem
|
<SchemaSettings.ModalItem title={t('Set the data scope')} schema={dataScopeSchema} onSubmit={onDataScopeSubmit} />
|
||||||
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'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{!dragSort && (
|
{!dragSort && (
|
||||||
<SchemaSettings.ModalItem
|
<SchemaSettings.ModalItem
|
||||||
title={t('Set default sorting rules')}
|
title={t('Set default sorting rules')}
|
||||||
@ -151,7 +154,7 @@ export const TableBlockDesigner = () => {
|
|||||||
field: {
|
field: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: sortFields,
|
enum: sortFields,
|
||||||
required:true,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
'x-component-props': {
|
'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 { useForm } from '@formily/react';
|
||||||
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
|
import { Input as AntInput, Cascader, DatePicker, InputNumber, Select, Tag } from 'antd';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { error } from '@nocobase/utils/client';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useCompile } from '../..';
|
import { useCompile } from '../..';
|
||||||
|
import { Option } from '../../../schema-settings/VariableInput/type';
|
||||||
import { XButton } from './XButton';
|
import { XButton } from './XButton';
|
||||||
|
|
||||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||||
@ -120,49 +123,91 @@ export function Input(props) {
|
|||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
|
||||||
const { value = '', scope, onChange, children, button, useTypedConstant, style, className } = props;
|
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 isConstant = typeof parsed === 'string';
|
||||||
const type = isConstant ? parsed : '';
|
const type = isConstant ? parsed : '';
|
||||||
const variable = isConstant ? null : 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
|
const loadData = async (selectedOptions: Option[]) => {
|
||||||
? {
|
const option = selectedOptions[selectedOptions.length - 1];
|
||||||
value: '',
|
if (option.loadChildren) {
|
||||||
label: '{{t("Constant")}}',
|
// 需要保证 selectedOptions 是一个响应式对象,这样才能触发重新渲染
|
||||||
}
|
await option.loadChildren(option);
|
||||||
: 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;
|
|
||||||
}
|
}
|
||||||
onChange(`{{${next.join('.')}}}`);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const variableText = variable
|
const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } =
|
||||||
?.reduce((opts, key, i) => {
|
useMemo(() => {
|
||||||
const option = (i ? (opts[i - 1] as VariableOptions)?.children : options)?.find((item) => item.value === key);
|
return children
|
||||||
return option ? opts.concat(option) : opts;
|
? {
|
||||||
}, [] as VariableOptions[])
|
value: '',
|
||||||
.map((item) => item.label)
|
label: '{{t("Constant")}}',
|
||||||
.join(' / ');
|
}
|
||||||
|
: 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;
|
const disabled = props.disabled || form.disabled;
|
||||||
|
|
||||||
@ -257,6 +302,7 @@ export function Input(props) {
|
|||||||
options={options}
|
options={options}
|
||||||
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
||||||
onChange={onSwitch}
|
onChange={onSwitch}
|
||||||
|
loadData={loadData as any}
|
||||||
changeOnSelect
|
changeOnSelect
|
||||||
>
|
>
|
||||||
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
{button ?? <XButton type={variable ? 'primary' : 'default'} />}
|
||||||
|
@ -17,7 +17,7 @@ type Props = {
|
|||||||
export const VariableInput = (props: Props) => {
|
export const VariableInput = (props: Props) => {
|
||||||
const { value, onChange, renderSchemaComponent: RenderSchemaComponent, style, schema, className } = props;
|
const { value, onChange, renderSchemaComponent: RenderSchemaComponent, style, schema, className } = props;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const userVariable = useUserVariable({ schema, level: 1 });
|
const userVariable = useUserVariable({ schema, maxDepth: 1 });
|
||||||
const scope = useMemo(() => {
|
const scope = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
userVariable,
|
userVariable,
|
||||||
|
@ -1,67 +1,97 @@
|
|||||||
|
import { observable } from '@formily/reactive';
|
||||||
|
import { error } from '@nocobase/utils/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
import { useCompile, useGetFilterOptions } from '../../../schema-component';
|
||||||
|
import { FieldOption, Option } from '../type';
|
||||||
|
|
||||||
interface GetOptionsParams {
|
interface GetOptionsParams {
|
||||||
schema: any;
|
schema: any;
|
||||||
operator?: string;
|
depth: number;
|
||||||
maxDepth: number;
|
maxDepth?: number;
|
||||||
count?: number;
|
loadChildren?: (option: Option) => Promise<void>;
|
||||||
getFilterOptions: (collectionName: string) => any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getChildren = (options: any[], { schema, operator, maxDepth, count = 1, getFilterOptions }: GetOptionsParams) => {
|
const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChildren }: GetOptionsParams): Option[] => {
|
||||||
if (count > maxDepth) {
|
const result = options
|
||||||
return [];
|
.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 {
|
return {
|
||||||
key: option.name,
|
key: option.name,
|
||||||
value: option.name,
|
value: option.name,
|
||||||
label: option.title,
|
label: option.title,
|
||||||
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
|
children: [],
|
||||||
disabled: schema?.['x-component'] !== option.schema?.['x-component'],
|
isLeaf: false,
|
||||||
|
field: option,
|
||||||
|
depth,
|
||||||
|
loadChildren,
|
||||||
};
|
};
|
||||||
}
|
})
|
||||||
|
.filter(Boolean);
|
||||||
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),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
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 compile = useCompile();
|
||||||
const getFilterOptions = useGetFilterOptions();
|
const getFilterOptions = useGetFilterOptions();
|
||||||
|
|
||||||
const children = useMemo(
|
const loadChildren = (option: Option): Promise<void> => {
|
||||||
() => getChildren(getFilterOptions('users'), { schema, operator, maxDepth: level || 3, getFilterOptions }) || [],
|
if (!option.field?.target) {
|
||||||
[operator, schema],
|
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({
|
return compile({
|
||||||
label: `{{t("Current user")}}`,
|
label: `{{t("Current user")}}`,
|
||||||
value: '$user',
|
value: '$user',
|
||||||
key: '$user',
|
key: '$user',
|
||||||
disabled: children.every((option) => option.disabled),
|
children: [],
|
||||||
children: children,
|
isLeaf: false,
|
||||||
});
|
field: {
|
||||||
}, [children]);
|
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 = () => {
|
export const useVariableOptions = () => {
|
||||||
const { operator, schema } = useValues();
|
const { operator, schema } = useValues();
|
||||||
const userVariable = useUserVariable({ operator, schema });
|
const userVariable = useUserVariable({ maxDepth: 3, schema });
|
||||||
const dateVariable = useDateVariable({ operator, schema });
|
const dateVariable = useDateVariable({ operator, schema });
|
||||||
|
|
||||||
if (!operator || !schema) return [];
|
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