diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index f8edeb1ea..77e745edd 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -75,7 +75,7 @@ export const Action: ComposedAction = observer( ); useEffect(() => { - field.linkageProperty = {}; + field.stateOfLinkageRules = {}; linkageRules .filter((k) => !k.disabled) .forEach((v) => { diff --git a/packages/core/client/src/schema-component/antd/action/utils.ts b/packages/core/client/src/schema-component/antd/action/utils.ts index 19067bd16..7de204e56 100644 --- a/packages/core/client/src/schema-component/antd/action/utils.ts +++ b/packages/core/client/src/schema-component/antd/action/utils.ts @@ -84,8 +84,8 @@ export const linkageAction = async ({ variables: VariablesContextType; localVariables: VariableOption[]; }) => { - const disableResult = field?.linkageProperty?.disabled || [false]; - const displayResult = field?.linkageProperty?.display || ['visible']; + const disableResult = field?.stateOfLinkageRules?.disabled || [false]; + const displayResult = field?.stateOfLinkageRules?.display || ['visible']; switch (operator) { case ActionType.Visible: @@ -94,8 +94,8 @@ export const linkageAction = async ({ field.data = field.data || {}; field.data.hidden = false; } - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, display: displayResult, }; field.display = last(displayResult); @@ -113,8 +113,8 @@ export const linkageAction = async ({ if (await conditionAnalyses({ rules: condition, variables, localVariables })) { disableResult.push(true); } - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, disabled: disableResult, }; field.disabled = last(disableResult); @@ -124,8 +124,8 @@ export const linkageAction = async ({ if (await conditionAnalyses({ rules: condition, variables, localVariables })) { disableResult.push(false); } - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, disabled: disableResult, }; field.disabled = last(disableResult); diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx index ff27a8ee1..7eba88ad3 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.tsx @@ -13,6 +13,7 @@ import { Collection, useCollection, useCollectionManager } from '../../../collec import { useRecord } from '../../../record-provider'; import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems'; import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings'; +import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput'; import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch'; @@ -23,6 +24,7 @@ import { isSubMode } from '../association-field/util'; import { BlockItem } from '../block-item'; import { removeNullCondition } from '../filter'; import { DynamicComponentProps } from '../filter/DynamicComponent'; +import { getTempFieldState } from '../form-v2/utils'; import { HTMLEncode } from '../input/shared'; import { useColorFields } from '../table-v2/Table.Column.Designer'; import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; @@ -128,12 +130,6 @@ FormItem.Designer = function Designer() { const isAssociationField = ['obo', 'oho', 'o2o', 'o2m', 'm2m', 'm2o'].includes(collectionField?.interface); const isTableField = fieldSchema['x-component'] === 'TableField'; const isFileField = isFileCollection(targetCollection as any); - const initialValue = { - title: field.title === originalTitle ? undefined : field.title, - }; - if (!field.readPretty) { - initialValue['required'] = field.required; - } const options = targetFields .filter((field) => { return isTitleField(field); @@ -607,6 +603,7 @@ FormItem.Designer = function Designer() { schema['x-disabled'] = true; field.readPretty = false; field.disabled = true; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadOnly)); break; } case 'read-pretty': { @@ -615,6 +612,7 @@ FormItem.Designer = function Designer() { schema['x-read-pretty'] = true; schema['x-disabled'] = false; field.readPretty = true; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.ReadPretty)); break; } default: { @@ -624,6 +622,7 @@ FormItem.Designer = function Designer() { schema['x-disabled'] = false; field.readPretty = false; field.disabled = false; + _.set(field, 'initStateOfLinkageRules.pattern', getTempFieldState(true, ActionType.Editable)); break; } } 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 c0444c3a8..dfec1f542 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 @@ -2,14 +2,21 @@ import { css } from '@emotion/css'; import { FormLayout } from '@formily/antd-v5'; import { createForm, Field, Form as FormilyForm, onFieldInit, onFormInputChange } from '@formily/core'; import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react'; -import { autorun, toJS } from '@formily/reactive'; +import { reaction } from '@formily/reactive'; import { uid } from '@formily/shared'; +import { getValuesByPath } from '@nocobase/utils/client'; import { ConfigProvider, Spin } from 'antd'; +import _ from 'lodash'; 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 { VariableOption, VariablesContextType } from '../../../variables/types'; +import { getPath } from '../../../variables/utils/getPath'; +import { getVariableName } from '../../../variables/utils/getVariableName'; +import { isVariable, REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable'; +import { getInnermostKeyAndValue, getTargetField } from '../../common/utils/uitls'; import { useProps } from '../../hooks/useProps'; import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils'; @@ -106,65 +113,46 @@ const WithForm = (props: WithFormProps) => { const disposes = []; form.addEffects(id, () => { - linkageRules.forEach((v) => { - v.actions?.forEach((h) => { - if (h.targetFields?.length) { - const fields = h.targetFields.join(','); + linkageRules.forEach((rule) => { + rule.actions?.forEach((action) => { + if (action.targetFields?.length) { + const fields = action.targetFields.join(','); + + // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 onFieldInit(`*(${fields})`, (field: any, form) => { - field['initProperty'] = field?.['initProperty'] ?? { - display: getTempFieldState(true, field.display), - required: getTempFieldState(true, field.required), - pattern: getTempFieldState(true, field.pattern), - value: getTempFieldState(true, field.value || field.initialValue), + field['initStateOfLinkageRules'] = { + display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display), + required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false), + pattern: field.initStateOfLinkageRules?.pattern || getTempFieldState(true, field.pattern), + value: + field.initStateOfLinkageRules?.value || getTempFieldState(true, field.value || field.initialValue), }; - }); - // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `autorun` 代替 - onFieldInit(`*(${fields})`, (field: any, form) => { disposes.push( - autorun(() => { - // 当条件改变触发 autorun 时,会同步收集字段状态,并保存到 field.linkageProperty 中 - collectFieldStateOfLinkageRules({ - operator: h.operator, - value: h.value, - field, - condition: v.condition, - values: toJS(form?.values), - variables, - localVariables, - }); + reaction( + // 这里共依赖 3 部分,当这 3 部分中的任意一部分发生变更后,需要触发联动规则: + // 1. 条件中的字段值; + // 2. 条件中的变量值; + // 3. value 表达式中的变量值; + () => { + // 获取条件中的字段值 + const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues: form.values }); - // 当条件改变时,有可能会触发多个 autorun,所以这里需要延迟一下,确保所有的 autorun 都执行完毕后, - // 再从 field.linkageProperty 中取值,因为此时 field.linkageProperty 中的值才是全的。 - setTimeout(async () => { - const fieldName = getFieldNameByOperator(h.operator); + // 获取条件中的变量值 + const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables }); - // 防止重复赋值 - if (!field.linkageProperty[fieldName]) { - return; - } + // 获取 value 表达式中的变量值 + const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables }); - let stateList = field.linkageProperty[fieldName]; + const result = [fieldValuesInCondition, variableValuesInCondition, variableValuesInExpression] + .map((item) => JSON.stringify(item)) + .join(','); - 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; - }); - }), + return result; + }, + getSubscriber(action, field, rule, form, variables, localVariables), + { fireImmediately: true }, + ), ); }); } @@ -238,6 +226,59 @@ export const Form: React.FC & { { displayName: 'Form' }, ); +function getSubscriber( + action: any, + field: any, + rule: any, + form: FormilyForm, + variables: VariablesContextType, + localVariables: VariableOption[], +): (value: string, oldValue: string) => void { + return _.throttle(() => { + // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 + collectFieldStateOfLinkageRules({ + operator: action.operator, + value: action.value, + field, + condition: rule.condition, + values: form.values, + variables, + localVariables, + }); + + // 当条件改变时,有可能会触发多个 reaction,所以这里需要延迟一下,确保所有的 reaction 都执行完毕后, + // 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。 + setTimeout(async () => { + const fieldName = getFieldNameByOperator(action.operator); + + // 防止重复赋值 + if (!field.stateOfLinkageRules[fieldName]) { + return; + } + + let stateList = field.stateOfLinkageRules[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.stateOfLinkageRules,就可以保证:当条件再次改变时,如果该字段没有和任何条件匹配,则需要把对应的值恢复到初始值; + field.stateOfLinkageRules[fieldName] = null; + }); + }, 500); +} + function getFieldNameByOperator(operator: ActionType) { switch (operator) { case ActionType.Required: @@ -257,3 +298,78 @@ function getFieldNameByOperator(operator: ActionType) { return null; } } + +function getFieldValuesInCondition({ linkageRules, formValues }) { + return linkageRules.map((rule) => { + const type = Object.keys(rule.condition)[0] || '$and'; + const conditions = rule.condition[type]; + + return conditions + .map((condition) => { + const path = getTargetField(condition).join('.'); + return getValuesByPath(formValues, path); + }) + .filter(Boolean); + }); +} + +function getVariableValuesInCondition({ + linkageRules, + localVariables, +}: { + linkageRules: any[]; + localVariables: VariableOption[]; +}) { + return linkageRules.map((rule) => { + const type = Object.keys(rule.condition)[0] || '$and'; + const conditions = rule.condition[type]; + + return conditions + .map((condition) => { + const jsonlogic = getInnermostKeyAndValue(condition); + if (!jsonlogic) { + return null; + } + if (isVariable(jsonlogic.value)) { + return getVariableValue(jsonlogic.value, localVariables); + } + + return jsonlogic.value; + }) + .filter(Boolean); + }); +} + +function getVariableValuesInExpression({ action, localVariables }) { + const actionValue = action.value; + const mode = actionValue?.mode; + const value = actionValue?.value || actionValue?.result; + + if (mode !== 'express') { + return; + } + + if (value == null) { + return; + } + + return value + .match(REGEX_OF_VARIABLE) + ?.map((variableString: string) => { + return getVariableValue(variableString, localVariables); + }) + .filter(Boolean); +} + +function getVariableValue(variableString: string, localVariables: VariableOption[]) { + if (!isVariable(variableString)) { + return; + } + + const variableName = getVariableName(variableString); + const ctx = { + [variableName]: localVariables.find((item) => item.name === variableName)?.ctx, + }; + + return getValuesByPath(ctx, getPath(variableString)); +} 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 1438fac41..ea59dcb68 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 @@ -40,24 +40,24 @@ export const collectFieldStateOfLinkageRules = ({ variables, localVariables, }: Props) => { - const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false]; - const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display]; - const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern]; - const valueResult = field?.linkageProperty?.value || [field?.initProperty?.value]; + const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required]; + const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display]; + const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern]; + const valueResult = field?.stateOfLinkageRules?.value || [field?.initStateOfLinkageRules?.value]; const { evaluate } = evaluators.get('formula.js'); switch (operator) { case ActionType.Required: requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), true)); - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, required: requiredResult, }; break; case ActionType.InRequired: requiredResult.push(getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), false)); - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, required: requiredResult, }; break; @@ -67,8 +67,8 @@ export const collectFieldStateOfLinkageRules = ({ displayResult.push( getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator), ); - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, display: displayResult, }; break; @@ -78,8 +78,8 @@ export const collectFieldStateOfLinkageRules = ({ patternResult.push( getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), operator), ); - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, pattern: patternResult, }; break; @@ -117,8 +117,8 @@ export const collectFieldStateOfLinkageRules = ({ getTempFieldState(conditionAnalyses({ rules: condition, variables, localVariables }), getValue()), ); } - field.linkageProperty = { - ...field.linkageProperty, + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, value: valueResult, }; } diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index 6607eb6dc..7ce4a268a 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -38,7 +38,7 @@ export function getInnermostKeyAndValue(obj) { return null; } -const getTargetField = (obj) => { +export const getTargetField = (obj) => { const keys = getAllKeys(obj); const index = findIndex(keys, (key, index, keys) => { if (key.includes('$') && index > 0) { @@ -83,21 +83,20 @@ export const conditionAnalyses = async ({ const type = Object.keys(rules)[0] || '$and'; const conditions = rules[type]; - let results = conditions.map(async (c) => { - const jsonlogic = getInnermostKeyAndValue(c); + let results = conditions.map(async (condition) => { + const jsonlogic = getInnermostKeyAndValue(condition); const operator = jsonlogic?.key; if (!operator) { return true; } - const targetVariableName = targetFieldToVariableString(getTargetField(c)); + const targetVariableName = targetFieldToVariableString(getTargetField(condition)); + const targetValue = variables.parseVariable(targetVariableName, localVariables); + const parsingResult = isVariable(jsonlogic?.value) - ? [ - variables.parseVariable(jsonlogic?.value, localVariables), - variables.parseVariable(targetVariableName, localVariables), - ] - : [jsonlogic?.value, variables.parseVariable(targetVariableName, localVariables)]; + ? [variables.parseVariable(jsonlogic?.value, localVariables), targetValue] + : [jsonlogic?.value, targetValue]; try { const jsonLogic = getJsonLogic(); @@ -127,7 +126,7 @@ export const conditionAnalyses = async ({ * @param targetField * @returns */ -function targetFieldToVariableString(targetField: string[]) { +export function targetFieldToVariableString(targetField: string[]) { // Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单 return `{{ $nForm.${targetField.join('.')} }}`; } diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 5ec9d36cf..68ec67327 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -97,7 +97,7 @@ export const CreateRecordAction = observer( const localVariables = useLocalVariables({ currentForm: { values } as any }); useEffect(() => { - field.linkageProperty = {}; + field.stateOfLinkageRules = {}; linkageRules .filter((k) => !k.disabled) .forEach((v) => { @@ -201,7 +201,7 @@ export const CreateAction = observer( }, [menuItems]); useEffect(() => { - field.linkageProperty = {}; + field.stateOfLinkageRules = {}; linkageRules .filter((k) => !k.disabled) .forEach((v) => { diff --git a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx index 62c8d9706..b8c90cd89 100644 --- a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx +++ b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx @@ -1,9 +1,11 @@ import { Field } from '@formily/core'; import { ISchema, observer, useField, useFieldSchema } from '@formily/react'; +import _ from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { useCollection, useCollectionManager } from '../collection-manager'; import { useDesignable } from '../schema-component'; +import { getTempFieldState } from '../schema-component/antd/form-v2/utils'; import { SchemaSettings } from '../schema-settings'; export const GeneralSchemaItems: React.FC<{ @@ -149,6 +151,7 @@ export const GeneralSchemaItems: React.FC<{ ['x-uid']: fieldSchema['x-uid'], }; field.required = required; + _.set(field, 'initStateOfLinkageRules.required', getTempFieldState(true, required)); fieldSchema['required'] = required; schema['required'] = required; dn.emit('patch', { diff --git a/packages/core/client/src/variables/VariablesProvider.tsx b/packages/core/client/src/variables/VariablesProvider.tsx index 9acc043fa..e3fee87ba 100644 --- a/packages/core/client/src/variables/VariablesProvider.tsx +++ b/packages/core/client/src/variables/VariablesProvider.tsx @@ -1,3 +1,4 @@ +import { untracked } from '@formily/reactive'; import { getValuesByPath } from '@nocobase/utils/client'; import _ from 'lodash'; import React, { createContext, useCallback, useEffect, useMemo, useRef } from 'react'; @@ -278,11 +279,19 @@ VariablesProvider.displayName = 'VariablesProvider'; export default VariablesProvider; function shouldToRequest(value) { - // fix https://nocobase.height.app/T-2502 - // 兼容 `对多` 和 `对一` 子表单子表格字段的情况 - if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') { - return true; - } + let result = false; - return _.isEmpty(value); + // value 有可能是一个响应式对象,使用 untracked 可以避免意外触发 autorun + untracked(() => { + // fix https://nocobase.height.app/T-2502 + // 兼容 `对多` 和 `对一` 子表单子表格字段的情况 + if (JSON.stringify(value) === '[{}]' || JSON.stringify(value) === '{}') { + result = true; + return; + } + + result = _.isEmpty(value); + }); + + return result; }