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 | ||||
|         ? { | ||||
|       if (children) { | ||||
|         return { | ||||
|           value: '', | ||||
|           label: '{{t("Constant")}}', | ||||
|         }; | ||||
|       } | ||||
|         : useTypedConstant | ||||
|         ? getTypedConstantOption(type, useTypedConstant) | ||||
|         : { | ||||
|       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")}}`, | ||||
|   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