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';
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<any> } =
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<Option[]>(() => {
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 (
<AntInput.Group
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;
}
`,
)}
>
<AntInput.Group compact style={style} className={classNames(className, groupClass)}>
{variable ? (
<div
className={css`
@ -282,12 +286,7 @@ export function Input(props) {
</div>
{!disabled ? (
<span
className={cx(
'ant-select-clear',
css`
user-select: 'none';
`,
)}
className={cx('ant-select-clear', userSelectNone)}
// eslint-disable-next-line react/no-unknown-property
unselectable="on"
aria-hidden
@ -314,3 +313,14 @@ export function Input(props) {
</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 }) => {
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;
};

View File

@ -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<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
.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<void> => {
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;
};

View File

@ -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;
};