diff --git a/packages/core/client/src/schema-component/antd/variable/Input.tsx b/packages/core/client/src/schema-component/antd/variable/Input.tsx index 1debb19da..2ab9e183e 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -14,6 +14,23 @@ import { Option } from '../../../schema-settings/VariableInput/type'; import { XButton } from './XButton'; const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/; +const groupClass = css` + width: auto; + display: flex !important; + .ant-input-disabled { + .ant-tag { + color: #bfbfbf; + border-color: #d9d9d9; + } + } + .ant-input.null-value { + width: 4em; + min-width: 4em; + } +`; +const userSelectNone = css` + user-select: 'none'; +`; function parseValue(value: any): string | string[] { if (value == null) { @@ -133,34 +150,40 @@ export function Input(props) { const [variableText, setVariableText] = React.useState(''); - const loadData = (selectedOptions: Option[]) => { - const option = selectedOptions[selectedOptions.length - 1]; - if (option.loadChildren) { - // 需要保证 selectedOptions 是一个响应式对象,这样才能触发重新渲染 - option.loadChildren(option); - } - }; - const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC } = useMemo(() => { - return children - ? { - value: '', - label: '{{t("Constant")}}', - } - : useTypedConstant - ? getTypedConstantOption(type, useTypedConstant) - : { - value: '', - label: '{{t("Null")}}', - component: ConstantTypes.null.component, - }; + if (children) { + return { + value: '', + label: '{{t("Constant")}}', + }; + } + if (useTypedConstant) { + return getTypedConstantOption(type, useTypedConstant); + } + return { + value: '', + label: '{{t("Null")}}', + component: ConstantTypes.null.component, + }; }, [type, useTypedConstant]); - const options: VariableOptions[] = useMemo( - () => compile([constantOption, ...variableOptions]), - [constantOption, variableOptions], - ); + const [options, setOptions] = React.useState(() => { + return compile([constantOption, ...variableOptions]); + }); + + useEffect(() => { + const newOptions: Option[] = [constantOption, ...variableOptions]; + setOptions(deepCompileLabel(newOptions, compile)); + }, [variableOptions]); + + const loadData = async (selectedOptions: Option[]) => { + const option = selectedOptions[selectedOptions.length - 1]; + if (option.loadChildren) { + await option.loadChildren(option); + setOptions((prev) => [...prev]); + } + }; const onSwitch = useCallback( (next) => { @@ -183,7 +206,7 @@ export function Input(props) { useEffect(() => { const run = async () => { - if (!variable) { + if (!variable || options.length <= 1) { return; } let prevOption: Option = null; @@ -195,47 +218,28 @@ export function Input(props) { if (i === 0) { prevOption = options.find((item) => item.value === key); } else { - if (prevOption.children?.length === 0 && prevOption.loadChildren) { + if (prevOption.loadChildren && !prevOption.children?.length) { await prevOption.loadChildren(prevOption); } prevOption = prevOption.children.find((item) => item.value === key); } labels.push(prevOption.label); - setVariableText(labels.join(' / ')); } catch (err) { error(err); } } + setOptions([...options]); + setVariableText(labels.join(' / ')); }; // 如果没有这个延迟,会导致选择父节点时不展开子节点 setTimeout(run); - }, [variable]); + }, [variable, options.length]); const disabled = props.disabled || form.disabled; return ( - + {variable ? (
{!disabled ? ( ); } + +function deepCompileLabel(list: Option[], compile) { + return list.map((item) => { + const children = item.children ? deepCompileLabel(item.children, compile) : null; + return { + ...item, + label: compile(item.label), + children, + }; + }); +} diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts index a80d44039..2168e28b5 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useDateVariable.ts @@ -1,152 +1,156 @@ -import { useCompile } from '../../../schema-component'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; export const useDateVariable = ({ operator, schema }) => { - const compile = useCompile(); + const { t } = useTranslation(); const operatorValue = operator?.value || ''; - - if (!operator || !schema) return null; - - const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema['x-component']); + const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema?.['x-component']); const dateOptions = [ { key: 'now', value: 'now', - label: `{{t("Now")}}`, - disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', + label: t('Now'), + disabled: schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', }, { key: 'yesterday', value: 'yesterday', - label: `{{t("Yesterday")}}`, + label: t('Yesterday'), disabled, }, { key: 'today', value: 'today', - label: `{{t("Today")}}`, + label: t('Today'), disabled, }, { key: 'tomorrow', value: 'tomorrow', - label: `{{t("Tomorrow")}}`, + label: t('Tomorrow'), disabled, }, { key: 'lastIsoWeek', value: 'lastIsoWeek', - label: `{{t("Last week")}}`, + label: t('Last week'), disabled, }, { key: 'thisIsoWeek', value: 'thisIsoWeek', - label: `{{t("This week")}}`, + label: t('This week'), disabled, }, { key: 'nextIsoWeek', value: 'nextIsoWeek', - label: `{{t("Next week")}}`, + label: t('Next week'), disabled, }, { key: 'lastMonth', value: 'lastMonth', - label: `{{t("Last month")}}`, + label: t('Last month'), disabled, }, { key: 'thisMonth', value: 'thisMonth', - label: `{{t("This month")}}`, + label: t('This month'), disabled, }, { key: 'nextMonth', value: 'nextMonth', - label: `{{t("Next month")}}`, + label: t('Next month'), disabled, }, { key: 'lastQuarter', value: 'lastQuarter', - label: `{{t("Last quarter")}}`, + label: t('Last quarter'), disabled, }, { key: 'thisQuarter', value: 'thisQuarter', - label: `{{t("This quarter")}}`, + label: t('This quarter'), disabled, }, { key: 'nextQuarter', value: 'nextQuarter', - label: `{{t("Next quarter")}}`, + label: t('Next quarter'), disabled, }, { key: 'lastYear', value: 'lastYear', - label: `{{t("Last year")}}`, + label: t('Last year'), disabled, }, { key: 'thisYear', value: 'thisYear', - label: `{{t("This year")}}`, + label: t('This year'), disabled, }, { key: 'nextYear', value: 'nextYear', - label: `{{t("Next year")}}`, + label: t('Next year'), disabled, }, { key: 'last7Days', value: 'last7Days', - label: `{{t("Last 7 days")}}`, + label: t('Last 7 days'), disabled, }, { key: 'next7Days', value: 'next7Days', - label: `{{t("Next 7 days")}}`, + label: t('Next 7 days'), disabled, }, { key: 'last30Days', value: 'last30Days', - label: `{{t("Last 30 days")}}`, + label: t('Last 30 days'), disabled, }, { key: 'next30Days', value: 'next30Days', - label: `{{t("Next 30 days")}}`, + label: t('Next 30 days'), disabled, }, { key: 'last90Days', value: 'last90Days', - label: `{{t("Last 90 days")}}`, + label: t('Last 90 days'), disabled, }, { key: 'next90Days', value: 'next90Days', - label: `{{t("Next 90 days")}}`, + label: t('Next 90 days'), disabled, }, ]; - return compile({ - label: `{{t("Date variables")}}`, - value: '$date', - key: '$date', - disabled: dateOptions.every((option) => option.disabled), - children: dateOptions, - }); + const result = useMemo(() => { + return { + label: t('Date variables'), + value: '$date', + key: '$date', + disabled: dateOptions.every((option) => option.disabled), + children: dateOptions, + }; + }, [schema?.['x-component']]); + + if (!operator || !schema) return null; + + return result; }; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts index b46559d8a..c27a2decf 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useUserVariable.ts @@ -1,6 +1,6 @@ -import { observable } from '@formily/reactive'; import { error } from '@nocobase/utils/client'; -import { useMemo, useRef } from 'react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useCompile, useGetFilterOptions } from '../../../schema-component'; import { FieldOption, Option } from '../type'; @@ -9,16 +9,20 @@ interface GetOptionsParams { depth: number; maxDepth?: number; loadChildren?: (option: Option) => Promise; + compile: (value: string) => any; } -const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChildren }: GetOptionsParams): Option[] => { +const getChildren = ( + options: FieldOption[], + { schema, depth, maxDepth, loadChildren, compile }: GetOptionsParams, +): Option[] => { const result = options .map((option): Option => { if (!option.target) { return { key: option.name, value: option.name, - label: option.title, + label: compile(option.title), // TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化 disabled: schema?.['x-component'] !== option.schema?.['x-component'], isLeaf: true, @@ -33,7 +37,7 @@ const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChil return { key: option.name, value: option.name, - label: option.title, + label: compile(option.title), children: [], isLeaf: false, field: option, @@ -47,10 +51,9 @@ const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChil }; export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDepth?: number }) => { + const { t } = useTranslation(); const compile = useCompile(); const getFilterOptions = useGetFilterOptions(); - const schemaRef = useRef(schema); - schemaRef.current = schema; const loadChildren = (option: Option): Promise => { if (!option.field?.target) { @@ -65,14 +68,20 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept setTimeout(() => { const children = getChildren(getFilterOptions(collectionName), { - schema: schemaRef.current, + schema, depth: option.depth + 1, maxDepth, loadChildren, + compile, }) || []; - option.children = compile(children); - resolve(void 0); + if (children.length === 0) { + option.disabled = true; + resolve(); + return; + } + option.children = children; + resolve(); // 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿 }, 5); @@ -80,8 +89,8 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept }; const result = useMemo(() => { - return compile({ - label: `{{t("Current user")}}`, + return { + label: t('Current user'), value: '$user', key: '$user', children: [], @@ -91,9 +100,8 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept }, depth: 0, loadChildren, - } as Option); - }, []); + } as Option; + }, [schema?.['x-component']]); - // 必须使用 observable 包一下,使其变成响应式对象,不然 children 加载后不会更新 UI - return observable(result); + return result; }; diff --git a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts index 9bfcf9fe0..024bb3b65 100644 --- a/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts +++ b/packages/core/client/src/schema-settings/VariableInput/hooks/useVariableOptions.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useDateVariable } from './useDateVariable'; import { useUserVariable } from './useUserVariable'; @@ -7,7 +8,9 @@ export const useVariableOptions = () => { const userVariable = useUserVariable({ maxDepth: 3, schema }); const dateVariable = useDateVariable({ operator, schema }); + const result = useMemo(() => [userVariable, dateVariable], [dateVariable, userVariable]); + if (!operator || !schema) return []; - return [userVariable, dateVariable]; + return result; };