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:
parent
50865d76bc
commit
e506c1781e
@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user