fix(linkageRules): avoid infinite loop (#3095)

* Revert "refactor: avoid errors (#3091)"

This reverts commit 85be616f35.

* fix(linkageRules): avoid infinite loop

* chore: remove console.log

* chore: remove useless code
This commit is contained in:
被雨水过滤的空气-Rain 2023-11-27 19:27:35 +08:00 committed by GitHub
parent 6819366049
commit 723cc45d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 114 additions and 59 deletions

View File

@ -1,16 +1,17 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5'; 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 { 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 { uid } from '@formily/shared';
import { ConfigProvider, Spin } from 'antd'; import { ConfigProvider, Spin } from 'antd';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useActionContext } from '..'; import { useActionContext } from '..';
import { useAttach, useComponent } from '../..'; import { useAttach, useComponent } from '../..';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
import { useLocalVariables, useVariables } from '../../../variables'; import { useLocalVariables, useVariables } from '../../../variables';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { linkageMergeAction } from './utils'; import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils';
export interface FormProps { export interface FormProps {
[key: string]: any; [key: string]: any;
@ -105,38 +106,64 @@ const WithForm = (props: WithFormProps) => {
const disposes = []; const disposes = [];
form.addEffects(id, () => { form.addEffects(id, () => {
linkageRules.forEach((v, index) => { linkageRules.forEach((v) => {
v.actions?.forEach((h) => { v.actions?.forEach((h) => {
if (h.targetFields?.length) { if (h.targetFields?.length) {
const fields = h.targetFields.join(','); const fields = h.targetFields.join(',');
onFieldInit(`*(${fields})`, (field: any, form) => { onFieldInit(`*(${fields})`, (field: any, form) => {
field['initProperty'] = field?.['initProperty'] ?? { field['initProperty'] = field?.['initProperty'] ?? {
display: field.display, display: getTempFieldState(true, field.display),
required: field.required, required: getTempFieldState(true, field.required),
pattern: field.pattern, pattern: getTempFieldState(true, field.pattern),
value: field.value || field.initialValue, value: getTempFieldState(true, field.value || field.initialValue),
};
});
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => {
field.linkageProperty = {
display: field.linkageProperty?.display,
}; };
}); });
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替 // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替
onFieldInit(`*(${fields})`, (field: any, form) => { onFieldInit(`*(${fields})`, (field: any, form) => {
field.linkageProperty = {};
disposes.push( disposes.push(
autorun(async () => { autorun(() => {
await linkageMergeAction({ // 当条件改变触发 autorun 时,会同步收集字段状态,并保存到 field.linkageProperty 中
collectFieldStateOfLinkageRules({
operator: h.operator, operator: h.operator,
value: h.value, value: h.value,
field, field,
condition: v.condition, condition: v.condition,
values: raw(form?.values), values: toJS(form?.values),
variables, variables,
localVariables, 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<FormProps> & {
}, },
{ displayName: 'Form' }, { 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;
}
}

View File

@ -1,7 +1,7 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { evaluators } from '@nocobase/evaluators/client'; import { evaluators } from '@nocobase/evaluators/client';
import { uid } from '@nocobase/utils/client'; import { uid } from '@nocobase/utils/client';
import _, { last } from 'lodash'; import _ from 'lodash';
import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { ActionType } from '../../../schema-settings/LinkageRules/type';
import { VariableOption, VariablesContextType } from '../../../variables/types'; import { VariableOption, VariablesContextType } from '../../../variables/types';
import { REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable'; import { REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable';
@ -19,7 +19,19 @@ interface Props {
localVariables: VariableOption[]; localVariables: VariableOption[];
} }
export const linkageMergeAction = async ({ /**
*
*/
export async function getTempFieldState(condition: boolean | Promise<boolean>, value: any) {
[condition, value] = await Promise.all([condition, value]);
return {
condition,
value,
};
}
export const collectFieldStateOfLinkageRules = ({
operator, operator,
value, value,
field, field,
@ -36,83 +48,79 @@ export const linkageMergeAction = async ({
switch (operator) { switch (operator) {
case ActionType.Required: case ActionType.Required:
if (await conditionAnalyses({ rules: condition, variables, localVariables })) { requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), true));
requiredResult.push(true);
}
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
required: requiredResult, required: requiredResult,
}; };
field.required = last(field.linkageProperty.required);
break; break;
case ActionType.InRequired: case ActionType.InRequired:
if (await conditionAnalyses({ rules: condition, variables, localVariables })) { requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), false));
requiredResult.push(false);
}
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
required: requiredResult, required: requiredResult,
}; };
field.required = last(field.linkageProperty.required);
break; break;
case ActionType.Visible: case ActionType.Visible:
case ActionType.None: case ActionType.None:
case ActionType.Hidden: case ActionType.Hidden:
if (await conditionAnalyses({ rules: condition, variables, localVariables })) { displayResult.push(
displayResult.push(operator); getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator),
} );
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
display: displayResult, display: displayResult,
}; };
field.display = last(displayResult);
break; break;
case ActionType.Editable: case ActionType.Editable:
case ActionType.ReadOnly: case ActionType.ReadOnly:
case ActionType.ReadPretty: case ActionType.ReadPretty:
if (await conditionAnalyses({ rules: condition, variables, localVariables })) { patternResult.push(
patternResult.push(operator); getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator),
} );
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
pattern: patternResult, pattern: patternResult,
}; };
field.pattern = last(patternResult);
break; break;
case ActionType.Value: case ActionType.Value:
if (isConditionEmpty(condition) || (await conditionAnalyses({ rules: condition, variables, localVariables }))) { {
if (value?.mode === 'express') { const getValue = async () => {
if ((value.value || value.result) == null) { if (value?.mode === 'express') {
return; if ((value.value || value.result) == null) {
} return;
}
// 1. 解析如 `{{$user.name}}` 之类的变量 // 1. 解析如 `{{$user.name}}` 之类的变量
const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { const { exp, scope: expScope } = await replaceVariables(value.value || value.result, {
variables, variables,
localVariables, localVariables,
}); });
try { try {
// 2. TODO: 需要把里面解析变量的逻辑删除,因为在上一步已经解析过了 // 2. TODO: 需要把里面解析变量的逻辑删除,因为在上一步已经解析过了
const result = evaluate(exp, { ...values, now: () => new Date().toString(), ...expScope }); const result = evaluate(exp, { ...values, now: () => new Date().toString(), ...expScope });
valueResult.push(result); return result;
} catch (error) { } catch (error) {
console.error(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 { } else {
valueResult.push(null); valueResult.push(
getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), getValue()),
);
} }
field.linkageProperty = { field.linkageProperty = {
...field.linkageProperty, ...field.linkageProperty,
value: valueResult, value: valueResult,
}; };
if (last(valueResult) !== undefined) {
field.value = last(valueResult);
}
} }
break; break;
default: default: