fix(Varaible): fix option is disabled (#2043)

* perf: use useMemo

* refactor: extract className

* refactor: fix lint warning

* fix: fix can not be selected

* refactor: reduces count of compile runs

* fix: fix T-494

* fix: should disable options that no children

* fix: fix T-496
This commit is contained in:
被雨水过滤的空气-Rairn 2023-06-18 14:31:53 +08:00 committed by GitHub
parent 50865d76bc
commit e506c1781e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 108 deletions

View File

@ -14,6 +14,23 @@ 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*$/;
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[] { function parseValue(value: any): string | string[] {
if (value == null) { if (value == null) {
@ -133,34 +150,40 @@ export function Input(props) {
const [variableText, setVariableText] = React.useState(''); 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<any> } = const { component: ConstantComponent, ...constantOption }: VariableOptions & { component?: React.FC<any> } =
useMemo(() => { useMemo(() => {
return children if (children) {
? { return {
value: '', value: '',
label: '{{t("Constant")}}', label: '{{t("Constant")}}',
};
} }
: useTypedConstant if (useTypedConstant) {
? getTypedConstantOption(type, useTypedConstant) return getTypedConstantOption(type, useTypedConstant);
: { }
return {
value: '', value: '',
label: '{{t("Null")}}', label: '{{t("Null")}}',
component: ConstantTypes.null.component, component: ConstantTypes.null.component,
}; };
}, [type, useTypedConstant]); }, [type, useTypedConstant]);
const options: VariableOptions[] = useMemo( const [options, setOptions] = React.useState<Option[]>(() => {
() => compile([constantOption, ...variableOptions]), return compile([constantOption, ...variableOptions]);
[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( const onSwitch = useCallback(
(next) => { (next) => {
@ -183,7 +206,7 @@ export function Input(props) {
useEffect(() => { useEffect(() => {
const run = async () => { const run = async () => {
if (!variable) { if (!variable || options.length <= 1) {
return; return;
} }
let prevOption: Option = null; let prevOption: Option = null;
@ -195,47 +218,28 @@ export function Input(props) {
if (i === 0) { if (i === 0) {
prevOption = options.find((item) => item.value === key); prevOption = options.find((item) => item.value === key);
} else { } else {
if (prevOption.children?.length === 0 && prevOption.loadChildren) { if (prevOption.loadChildren && !prevOption.children?.length) {
await prevOption.loadChildren(prevOption); await prevOption.loadChildren(prevOption);
} }
prevOption = prevOption.children.find((item) => item.value === key); prevOption = prevOption.children.find((item) => item.value === key);
} }
labels.push(prevOption.label); labels.push(prevOption.label);
setVariableText(labels.join(' / '));
} catch (err) { } catch (err) {
error(err); error(err);
} }
} }
setOptions([...options]);
setVariableText(labels.join(' / '));
}; };
// 如果没有这个延迟,会导致选择父节点时不展开子节点 // 如果没有这个延迟,会导致选择父节点时不展开子节点
setTimeout(run); setTimeout(run);
}, [variable]); }, [variable, options.length]);
const disabled = props.disabled || form.disabled; const disabled = props.disabled || form.disabled;
return ( return (
<AntInput.Group <AntInput.Group compact style={style} className={classNames(className, groupClass)}>
compact
style={style}
className={classNames(
className,
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;
}
`,
)}
>
{variable ? ( {variable ? (
<div <div
className={css` className={css`
@ -282,12 +286,7 @@ export function Input(props) {
</div> </div>
{!disabled ? ( {!disabled ? (
<span <span
className={cx( className={cx('ant-select-clear', userSelectNone)}
'ant-select-clear',
css`
user-select: 'none';
`,
)}
// eslint-disable-next-line react/no-unknown-property // eslint-disable-next-line react/no-unknown-property
unselectable="on" unselectable="on"
aria-hidden aria-hidden
@ -314,3 +313,14 @@ export function Input(props) {
</AntInput.Group> </AntInput.Group>
); );
} }
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,
};
});
}

View File

@ -1,152 +1,156 @@
import { useCompile } from '../../../schema-component'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useDateVariable = ({ operator, schema }) => { export const useDateVariable = ({ operator, schema }) => {
const compile = useCompile(); const { t } = useTranslation();
const operatorValue = operator?.value || ''; const operatorValue = operator?.value || '';
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema?.['x-component']);
if (!operator || !schema) return null;
const disabled = !['DatePicker', 'DatePicker.RangePicker'].includes(schema['x-component']);
const dateOptions = [ const dateOptions = [
{ {
key: 'now', key: 'now',
value: 'now', value: 'now',
label: `{{t("Now")}}`, label: t('Now'),
disabled: schema['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween', disabled: schema?.['x-component'] !== 'DatePicker' || operatorValue === '$dateBetween',
}, },
{ {
key: 'yesterday', key: 'yesterday',
value: 'yesterday', value: 'yesterday',
label: `{{t("Yesterday")}}`, label: t('Yesterday'),
disabled, disabled,
}, },
{ {
key: 'today', key: 'today',
value: 'today', value: 'today',
label: `{{t("Today")}}`, label: t('Today'),
disabled, disabled,
}, },
{ {
key: 'tomorrow', key: 'tomorrow',
value: 'tomorrow', value: 'tomorrow',
label: `{{t("Tomorrow")}}`, label: t('Tomorrow'),
disabled, disabled,
}, },
{ {
key: 'lastIsoWeek', key: 'lastIsoWeek',
value: 'lastIsoWeek', value: 'lastIsoWeek',
label: `{{t("Last week")}}`, label: t('Last week'),
disabled, disabled,
}, },
{ {
key: 'thisIsoWeek', key: 'thisIsoWeek',
value: 'thisIsoWeek', value: 'thisIsoWeek',
label: `{{t("This week")}}`, label: t('This week'),
disabled, disabled,
}, },
{ {
key: 'nextIsoWeek', key: 'nextIsoWeek',
value: 'nextIsoWeek', value: 'nextIsoWeek',
label: `{{t("Next week")}}`, label: t('Next week'),
disabled, disabled,
}, },
{ {
key: 'lastMonth', key: 'lastMonth',
value: 'lastMonth', value: 'lastMonth',
label: `{{t("Last month")}}`, label: t('Last month'),
disabled, disabled,
}, },
{ {
key: 'thisMonth', key: 'thisMonth',
value: 'thisMonth', value: 'thisMonth',
label: `{{t("This month")}}`, label: t('This month'),
disabled, disabled,
}, },
{ {
key: 'nextMonth', key: 'nextMonth',
value: 'nextMonth', value: 'nextMonth',
label: `{{t("Next month")}}`, label: t('Next month'),
disabled, disabled,
}, },
{ {
key: 'lastQuarter', key: 'lastQuarter',
value: 'lastQuarter', value: 'lastQuarter',
label: `{{t("Last quarter")}}`, label: t('Last quarter'),
disabled, disabled,
}, },
{ {
key: 'thisQuarter', key: 'thisQuarter',
value: 'thisQuarter', value: 'thisQuarter',
label: `{{t("This quarter")}}`, label: t('This quarter'),
disabled, disabled,
}, },
{ {
key: 'nextQuarter', key: 'nextQuarter',
value: 'nextQuarter', value: 'nextQuarter',
label: `{{t("Next quarter")}}`, label: t('Next quarter'),
disabled, disabled,
}, },
{ {
key: 'lastYear', key: 'lastYear',
value: 'lastYear', value: 'lastYear',
label: `{{t("Last year")}}`, label: t('Last year'),
disabled, disabled,
}, },
{ {
key: 'thisYear', key: 'thisYear',
value: 'thisYear', value: 'thisYear',
label: `{{t("This year")}}`, label: t('This year'),
disabled, disabled,
}, },
{ {
key: 'nextYear', key: 'nextYear',
value: 'nextYear', value: 'nextYear',
label: `{{t("Next year")}}`, label: t('Next year'),
disabled, disabled,
}, },
{ {
key: 'last7Days', key: 'last7Days',
value: 'last7Days', value: 'last7Days',
label: `{{t("Last 7 days")}}`, label: t('Last 7 days'),
disabled, disabled,
}, },
{ {
key: 'next7Days', key: 'next7Days',
value: 'next7Days', value: 'next7Days',
label: `{{t("Next 7 days")}}`, label: t('Next 7 days'),
disabled, disabled,
}, },
{ {
key: 'last30Days', key: 'last30Days',
value: 'last30Days', value: 'last30Days',
label: `{{t("Last 30 days")}}`, label: t('Last 30 days'),
disabled, disabled,
}, },
{ {
key: 'next30Days', key: 'next30Days',
value: 'next30Days', value: 'next30Days',
label: `{{t("Next 30 days")}}`, label: t('Next 30 days'),
disabled, disabled,
}, },
{ {
key: 'last90Days', key: 'last90Days',
value: 'last90Days', value: 'last90Days',
label: `{{t("Last 90 days")}}`, label: t('Last 90 days'),
disabled, disabled,
}, },
{ {
key: 'next90Days', key: 'next90Days',
value: 'next90Days', value: 'next90Days',
label: `{{t("Next 90 days")}}`, label: t('Next 90 days'),
disabled, disabled,
}, },
]; ];
return compile({ const result = useMemo(() => {
label: `{{t("Date variables")}}`, return {
label: t('Date variables'),
value: '$date', value: '$date',
key: '$date', key: '$date',
disabled: dateOptions.every((option) => option.disabled), disabled: dateOptions.every((option) => option.disabled),
children: dateOptions, children: dateOptions,
}); };
}, [schema?.['x-component']]);
if (!operator || !schema) return null;
return result;
}; };

View File

@ -1,6 +1,6 @@
import { observable } from '@formily/reactive';
import { error } from '@nocobase/utils/client'; 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 { useCompile, useGetFilterOptions } from '../../../schema-component';
import { FieldOption, Option } from '../type'; import { FieldOption, Option } from '../type';
@ -9,16 +9,20 @@ interface GetOptionsParams {
depth: number; depth: number;
maxDepth?: number; maxDepth?: number;
loadChildren?: (option: Option) => Promise<void>; loadChildren?: (option: Option) => Promise<void>;
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 const result = options
.map((option): Option => { .map((option): Option => {
if (!option.target) { if (!option.target) {
return { return {
key: option.name, key: option.name,
value: option.name, value: option.name,
label: option.title, label: compile(option.title),
// TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化 // TODO: 现在是通过组件的名称来过滤能够被选择的选项,这样的坏处是不够精确,后续可以优化
disabled: schema?.['x-component'] !== option.schema?.['x-component'], disabled: schema?.['x-component'] !== option.schema?.['x-component'],
isLeaf: true, isLeaf: true,
@ -33,7 +37,7 @@ const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChil
return { return {
key: option.name, key: option.name,
value: option.name, value: option.name,
label: option.title, label: compile(option.title),
children: [], children: [],
isLeaf: false, isLeaf: false,
field: option, field: option,
@ -47,10 +51,9 @@ const getChildren = (options: FieldOption[], { schema, depth, maxDepth, loadChil
}; };
export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDepth?: number }) => { export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDepth?: number }) => {
const { t } = useTranslation();
const compile = useCompile(); const compile = useCompile();
const getFilterOptions = useGetFilterOptions(); const getFilterOptions = useGetFilterOptions();
const schemaRef = useRef(schema);
schemaRef.current = schema;
const loadChildren = (option: Option): Promise<void> => { const loadChildren = (option: Option): Promise<void> => {
if (!option.field?.target) { if (!option.field?.target) {
@ -65,14 +68,20 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept
setTimeout(() => { setTimeout(() => {
const children = const children =
getChildren(getFilterOptions(collectionName), { getChildren(getFilterOptions(collectionName), {
schema: schemaRef.current, schema,
depth: option.depth + 1, depth: option.depth + 1,
maxDepth, maxDepth,
loadChildren, loadChildren,
compile,
}) || []; }) || [];
option.children = compile(children); if (children.length === 0) {
resolve(void 0); option.disabled = true;
resolve();
return;
}
option.children = children;
resolve();
// 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿 // 延迟 5 毫秒,防止阻塞主线程,导致 UI 卡顿
}, 5); }, 5);
@ -80,8 +89,8 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept
}; };
const result = useMemo(() => { const result = useMemo(() => {
return compile({ return {
label: `{{t("Current user")}}`, label: t('Current user'),
value: '$user', value: '$user',
key: '$user', key: '$user',
children: [], children: [],
@ -91,9 +100,8 @@ export const useUserVariable = ({ schema, maxDepth = 3 }: { schema: any; maxDept
}, },
depth: 0, depth: 0,
loadChildren, loadChildren,
} as Option); } as Option;
}, []); }, [schema?.['x-component']]);
// 必须使用 observable 包一下,使其变成响应式对象,不然 children 加载后不会更新 UI return result;
return observable(result);
}; };

View File

@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useValues } from '../../../schema-component/antd/filter/useValues'; import { useValues } from '../../../schema-component/antd/filter/useValues';
import { useDateVariable } from './useDateVariable'; import { useDateVariable } from './useDateVariable';
import { useUserVariable } from './useUserVariable'; import { useUserVariable } from './useUserVariable';
@ -7,7 +8,9 @@ export const useVariableOptions = () => {
const userVariable = useUserVariable({ maxDepth: 3, schema }); const userVariable = useUserVariable({ maxDepth: 3, schema });
const dateVariable = useDateVariable({ operator, schema }); const dateVariable = useDateVariable({ operator, schema });
const result = useMemo(() => [userVariable, dateVariable], [dateVariable, userVariable]);
if (!operator || !schema) return []; if (!operator || !schema) return [];
return [userVariable, dateVariable]; return result;
}; };