From 723cc45d0f364e543599740f8383d3b37536419a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=AB=E9=9B=A8=E6=B0=B4=E8=BF=87=E6=BB=A4=E7=9A=84?= =?UTF-8?q?=E7=A9=BA=E6=B0=94-Rain?= <958414905@qq.com> Date: Mon, 27 Nov 2023 19:27:35 +0800 Subject: [PATCH] fix(linkageRules): avoid infinite loop (#3095) * Revert "refactor: avoid errors (#3091)" This reverts commit 85be616f352a98e615b0c2c6f48624260e1bcb26. * fix(linkageRules): avoid infinite loop * chore: remove console.log * chore: remove useless code --- .../schema-component/antd/form-v2/Form.tsx | 81 ++++++++++++---- .../schema-component/antd/form-v2/utils.tsx | 92 ++++++++++--------- 2 files changed, 114 insertions(+), 59 deletions(-) diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index 14ffcecab..c0444c3a8 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -1,16 +1,17 @@ import { css } from '@emotion/css'; import { FormLayout } from '@formily/antd-v5'; -import { createForm, Field, Form as FormilyForm, onFieldChange, onFieldInit, onFormInputChange } from '@formily/core'; +import { createForm, Field, Form as FormilyForm, onFieldInit, onFormInputChange } from '@formily/core'; import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; -import { autorun, raw } from '@formily/reactive'; +import { autorun, toJS } from '@formily/reactive'; import { uid } from '@formily/shared'; import { ConfigProvider, Spin } from 'antd'; import React, { useEffect, useMemo } from 'react'; import { useActionContext } from '..'; import { useAttach, useComponent } from '../..'; +import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { useLocalVariables, useVariables } from '../../../variables'; import { useProps } from '../../hooks/useProps'; -import { linkageMergeAction } from './utils'; +import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils'; export interface FormProps { [key: string]: any; @@ -105,38 +106,64 @@ const WithForm = (props: WithFormProps) => { const disposes = []; form.addEffects(id, () => { - linkageRules.forEach((v, index) => { + linkageRules.forEach((v) => { v.actions?.forEach((h) => { if (h.targetFields?.length) { const fields = h.targetFields.join(','); onFieldInit(`*(${fields})`, (field: any, form) => { field['initProperty'] = field?.['initProperty'] ?? { - display: field.display, - required: field.required, - pattern: field.pattern, - value: field.value || field.initialValue, - }; - }); - onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => { - field.linkageProperty = { - display: field.linkageProperty?.display, + display: getTempFieldState(true, field.display), + required: getTempFieldState(true, field.required), + pattern: getTempFieldState(true, field.pattern), + value: getTempFieldState(true, field.value || field.initialValue), }; }); // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替 onFieldInit(`*(${fields})`, (field: any, form) => { - field.linkageProperty = {}; disposes.push( - autorun(async () => { - await linkageMergeAction({ + autorun(() => { + // 当条件改变触发 autorun 时,会同步收集字段状态,并保存到 field.linkageProperty 中 + collectFieldStateOfLinkageRules({ operator: h.operator, value: h.value, field, condition: v.condition, - values: raw(form?.values), + values: toJS(form?.values), variables, localVariables, }); + + // 当条件改变时,有可能会触发多个 autorun,所以这里需要延迟一下,确保所有的 autorun 都执行完毕后, + // 再从 field.linkageProperty 中取值,因为此时 field.linkageProperty 中的值才是全的。 + setTimeout(async () => { + const fieldName = getFieldNameByOperator(h.operator); + + // 防止重复赋值 + if (!field.linkageProperty[fieldName]) { + return; + } + + let stateList = field.linkageProperty[fieldName]; + + stateList = await Promise.all(stateList); + stateList = stateList.filter((v) => v.condition); + + const lastState = stateList[stateList.length - 1]; + + if (fieldName === 'value') { + // value 比较特殊,它只有在匹配条件时才需要赋值,当条件不匹配时,维持现在的值; + // stateList 中肯定会有一个初始值,所以当 stateList.length > 1 时,就说明有匹配条件的情况; + if (stateList.length > 1) { + field.value = lastState.value; + } + } else { + field[fieldName] = lastState?.value; + } + + // 在这里清空 field.linkageProperty,就可以保证:当条件再次改变时,如果该字段没有和任何条件匹配,则需要把对应的值恢复到初始值; + field.linkageProperty[fieldName] = null; + }); }), ); }); @@ -210,3 +237,23 @@ export const Form: React.FC & { }, { displayName: 'Form' }, ); + +function getFieldNameByOperator(operator: ActionType) { + switch (operator) { + case ActionType.Required: + case ActionType.InRequired: + return 'required'; + case ActionType.Visible: + case ActionType.None: + case ActionType.Hidden: + return 'display'; + case ActionType.Editable: + case ActionType.ReadOnly: + case ActionType.ReadPretty: + return 'pattern'; + case ActionType.Value: + return 'value'; + default: + return null; + } +} diff --git a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx index 7b14385c7..1438fac41 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/utils.tsx @@ -1,7 +1,7 @@ import { Field } from '@formily/core'; import { evaluators } from '@nocobase/evaluators/client'; import { uid } from '@nocobase/utils/client'; -import _, { last } from 'lodash'; +import _ from 'lodash'; import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { VariableOption, VariablesContextType } from '../../../variables/types'; import { REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable'; @@ -19,7 +19,19 @@ interface Props { localVariables: VariableOption[]; } -export const linkageMergeAction = async ({ +/** + * 获取字段临时状态对象 + */ +export async function getTempFieldState(condition: boolean | Promise, value: any) { + [condition, value] = await Promise.all([condition, value]); + + return { + condition, + value, + }; +} + +export const collectFieldStateOfLinkageRules = ({ operator, value, field, @@ -36,83 +48,79 @@ export const linkageMergeAction = async ({ switch (operator) { case ActionType.Required: - if (await conditionAnalyses({ rules: condition, variables, localVariables })) { - requiredResult.push(true); - } + requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), true)); field.linkageProperty = { ...field.linkageProperty, required: requiredResult, }; - field.required = last(field.linkageProperty.required); break; case ActionType.InRequired: - if (await conditionAnalyses({ rules: condition, variables, localVariables })) { - requiredResult.push(false); - } + requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), false)); field.linkageProperty = { ...field.linkageProperty, required: requiredResult, }; - field.required = last(field.linkageProperty.required); break; case ActionType.Visible: case ActionType.None: case ActionType.Hidden: - if (await conditionAnalyses({ rules: condition, variables, localVariables })) { - displayResult.push(operator); - } + displayResult.push( + getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator), + ); field.linkageProperty = { ...field.linkageProperty, display: displayResult, }; - field.display = last(displayResult); break; case ActionType.Editable: case ActionType.ReadOnly: case ActionType.ReadPretty: - if (await conditionAnalyses({ rules: condition, variables, localVariables })) { - patternResult.push(operator); - } + patternResult.push( + getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator), + ); field.linkageProperty = { ...field.linkageProperty, pattern: patternResult, }; - field.pattern = last(patternResult); break; case ActionType.Value: - if (isConditionEmpty(condition) || (await conditionAnalyses({ rules: condition, variables, localVariables }))) { - if (value?.mode === 'express') { - if ((value.value || value.result) == null) { - return; - } + { + const getValue = async () => { + if (value?.mode === 'express') { + if ((value.value || value.result) == null) { + return; + } - // 1. 解析如 `{{$user.name}}` 之类的变量 - const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { - variables, - localVariables, - }); + // 1. 解析如 `{{$user.name}}` 之类的变量 + const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { + variables, + localVariables, + }); - try { - // 2. TODO: 需要把里面解析变量的逻辑删除,因为在上一步已经解析过了 - const result = evaluate(exp, { ...values, now: () => new Date().toString(), ...expScope }); - valueResult.push(result); - } catch (error) { - console.error(error); + try { + // 2. TODO: 需要把里面解析变量的逻辑删除,因为在上一步已经解析过了 + const result = evaluate(exp, { ...values, now: () => new Date().toString(), ...expScope }); + return result; + } catch (error) { + console.error(error); + } + } else if (value?.mode === 'constant') { + return value?.value || value; + } else { + return null; } - } else if (value?.mode === 'constant') { - valueResult.push(value?.value || value); + }; + if (isConditionEmpty(condition)) { + valueResult.push(getTempFieldState(true, getValue())); } else { - valueResult.push(null); + valueResult.push( + getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), getValue()), + ); } - field.linkageProperty = { ...field.linkageProperty, value: valueResult, }; - - if (last(valueResult) !== undefined) { - field.value = last(valueResult); - } } break; default: