diff --git a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx index 908bcd9e65..cb050435cb 100644 --- a/packages/core/client/src/block-provider/DetailsBlockProvider.tsx +++ b/packages/core/client/src/block-provider/DetailsBlockProvider.tsx @@ -142,7 +142,7 @@ export const useDetailsBlockProps = () => { .reset() .then(() => { ctx.form.setInitialValues(data || {}); - ctx.form.setValues(data || {}); + // Using `ctx.form.setValues(data || {});` here may cause an internal infinite loop in Formily }) .catch(console.error); } diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 97f4d57337..25f1d9457a 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -42,7 +42,7 @@ import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/Tree import { useRecord } from '../../record-provider'; import { removeNullCondition, useActionContext, useCompile } from '../../schema-component'; import { isSubMode } from '../../schema-component/antd/association-field/util'; -import { replaceVariables } from '../../schema-component/antd/form-v2/utils'; +import { replaceVariables } from '../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { useCurrentUserContext } from '../../user'; import { useLocalVariables, useVariables } from '../../variables'; import { VariableOption, VariablesContextType } from '../../variables/types'; diff --git a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx index 71964c2058..43ab9737a1 100644 --- a/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx +++ b/packages/core/client/src/data-source/data-block/DataBlockProvider.tsx @@ -121,6 +121,27 @@ export interface DataBlockContextValue { export const DataBlockContext = createContext>({} as any); DataBlockContext.displayName = 'DataBlockContext'; +const DataBlockResourceContext = createContext<{ rerenderDataBlock: () => void }>(null); +const RerenderDataBlockProvider: FC = ({ children }) => { + const [hidden, setHidden] = React.useState(false); + const value = useMemo(() => { + return { + rerenderDataBlock: () => { + setHidden(true); + setTimeout(() => { + setHidden(false); + }); + }, + }; + }, []); + + if (hidden) { + return null; + } + + return {children}; +}; + /** * @internal */ @@ -170,7 +191,9 @@ export const DataBlockProvider: FC> = withDynamicSche - {children} + + {children} + @@ -195,3 +218,11 @@ export const useDataBlockProps = (): DataBlockContextValue['pro const context = useDataBlock(); return context.props; }; + +export const useRerenderDataBlock = () => { + const context = useContext(DataBlockResourceContext); + if (!context) { + throw new Error('useRerenderDataBlock() must be used within a DataBlockProvider'); + } + return context; +}; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx index 3c8e65bfe2..522a0fcaf2 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx +++ b/packages/core/client/src/modules/blocks/data-blocks/form/fieldSettingsFormItem.tsx @@ -6,29 +6,30 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React from 'react'; import { ArrayCollapse, FormLayout } from '@formily/antd-v5'; import { Field } from '@formily/core'; import { ISchema, useField, useFieldSchema } from '@formily/react'; import _ from 'lodash'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useApp, useSchemaToolbar } from '../../../../application'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFormBlockContext } from '../../../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager'; -import { useCollection } from '../../../../data-source'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollection } from '../../../../data-source'; +import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; import { useDesignable, useValidateSchema } from '../../../../schema-component'; -import { useIsFormReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { getTempFieldState } from '../../../../schema-component/antd/form-v2/utils'; -import { isPatternDisabled } from '../../../../schema-settings'; +import { + useIsFieldReadPretty, + useIsFormReadPretty, +} from '../../../../schema-component/antd/form-item/FormItem.Settings'; +import { SchemaSettingsLinkageRules, isPatternDisabled } from '../../../../schema-settings'; +import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; +import { getTempFieldState } from '../../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { ActionType } from '../../../../schema-settings/LinkageRules/type'; import { SchemaSettingsDefaultValue } from '../../../../schema-settings/SchemaSettingsDefaultValue'; -import { useIsAllowToSetDefaultValue } from '../../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; -import { fieldComponentSettingsItem } from '../../../../data-source/commonsSettingsItem'; -import { SchemaSettingsLinkageRules } from '../../../../schema-settings'; -import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; export const fieldSettingsFormItem = new SchemaSettings({ name: 'fieldSettings:FormItem', items: [ diff --git a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx index 35de555712..8540f64156 100644 --- a/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/Nester/subformComponentFieldSettings.tsx @@ -20,7 +20,7 @@ import { useIsFieldReadPretty, useIsFormReadPretty, } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings'; +import { linkageRules, setDefaultSortingRules } from '../SubTable/subTablePopoverComponentFieldSettings'; const allowMultiple: any = { name: 'allowMultiple', @@ -142,5 +142,6 @@ export const subformComponentFieldSettings = new SchemaSettings({ }, }, setDefaultSortingRules, + linkageRules, ], }); diff --git a/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx index 0149ddcede..d7c7e616ef 100644 --- a/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/PopoverNester/subformPopoverComponentFieldSettings.tsx @@ -12,12 +12,13 @@ import { useField, useFieldSchema } from '@formily/react'; import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollectionField } from '../../../../data-source'; import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component'; import { isSubMode } from '../../../../schema-component/antd/association-field/util'; import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings'; -import { useCollectionField } from '../../../../data-source'; import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator'; import { titleField } from '../Picker/recordPickerComponentFieldSettings'; +import { linkageRules } from '../SubTable/subTablePopoverComponentFieldSettings'; const allowMultiple: any = { name: 'allowMultiple', @@ -103,5 +104,5 @@ const fieldComponent: any = { export const subformPopoverComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:PopoverNester', - items: [fieldComponent, allowMultiple, titleField], + items: [fieldComponent, allowMultiple, titleField, linkageRules], }); diff --git a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx index a0d9d1aada..06339a4bd5 100644 --- a/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx +++ b/packages/core/client/src/modules/fields/component/SubTable/subTablePopoverComponentFieldSettings.tsx @@ -7,12 +7,17 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { Field } from '@formily/core'; -import { useField, useFieldSchema, ISchema } from '@formily/react'; -import { useTranslation } from 'react-i18next'; import { ArrayItems } from '@formily/antd-v5'; +import { Field } from '@formily/core'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings'; +import { useCollectionManager_deprecated, useSortFields } from '../../../../collection-manager'; import { useFieldComponentName } from '../../../../common/useFieldComponentName'; +import { useCollectionManager, useRerenderDataBlock } from '../../../../data-source'; +import { FlagProvider } from '../../../../flag-provider/FlagProvider'; +import { withDynamicSchemaProps } from '../../../../hoc/withDynamicSchemaProps'; import { useDesignable, useFieldModeOptions, @@ -20,8 +25,9 @@ import { useIsFieldReadPretty, } from '../../../../schema-component'; import { isSubMode } from '../../../../schema-component/antd/association-field/util'; -import { useCollectionManager_deprecated, useSortFields } from '../../../../collection-manager'; import { useIsAssociationField } from '../../../../schema-component/antd/form-item'; +import { FormLinkageRules } from '../../../../schema-settings/LinkageRules'; +import { SchemaSettingsLinkageRules } from '../../../../schema-settings/SchemaSettings'; const fieldComponent: any = { name: 'fieldComponent', @@ -253,7 +259,39 @@ export const allowAddNewData = { }; }, }; + +const LinkageRulesComponent = withDynamicSchemaProps( + (props) => { + return ( + // the purpose is to display the `Current object` variable in the linkage rule configuration dialog + + + + ); + }, + { displayName: 'LinkageRulesComponent' }, +); + +export const linkageRules = { + name: 'linkageRules', + Component: SchemaSettingsLinkageRules, + useComponentProps() { + const field = useField(); + const fieldSchema = useFieldSchema(); + const cm = useCollectionManager(); + const collectionField = cm.getCollectionField(fieldSchema['x-collection-field']); + const { rerenderDataBlock } = useRerenderDataBlock(); + + return { + collectionName: collectionField?.target, + Component: LinkageRulesComponent, + readPretty: field.readPretty, + afterSubmit: rerenderDataBlock, + }; + }, +}; + export const subTablePopoverComponentFieldSettings = new SchemaSettings({ name: 'fieldSettings:component:SubTable', - items: [fieldComponent, allowAddNewData, allowSelectExistingRecord, setDefaultSortingRules], + items: [fieldComponent, allowAddNewData, allowSelectExistingRecord, setDefaultSortingRules, linkageRules], }); diff --git a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx index e05880a826..3d219697e9 100644 --- a/packages/core/client/src/schema-component/antd/association-field/Nester.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/Nester.tsx @@ -10,12 +10,12 @@ import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; import { css } from '@emotion/css'; import { ArrayField } from '@formily/core'; +import { spliceArrayState } from '@formily/core/esm/shared/internals'; import { RecursionField, observer, useFieldSchema } from '@formily/react'; import { action } from '@formily/reactive'; import { each } from '@formily/shared'; import { Button, Card, Divider, Tooltip } from 'antd'; import React, { useCallback, useContext } from 'react'; -import { spliceArrayState } from '@formily/core/esm/shared/internals'; import { useTranslation } from 'react-i18next'; import { FormActiveFieldsProvider } from '../../../block-provider/hooks/useFormActiveFields'; import { useCollection } from '../../../data-source'; @@ -58,6 +58,7 @@ const ToOneNester = (props) => { const { field } = useAssociationFieldContext(); const recordV2 = useCollectionRecord(); const collection = useCollection(); + const fieldSchema = useFieldSchema(); const isAllowToSetDefaultValue = useCallback( ({ form, fieldSchema, collectionField, getInterface, formBlockType }: IsAllowToSetDefaultValueParams) => { @@ -94,7 +95,7 @@ const ToOneNester = (props) => { return ( - + {props.children} @@ -200,7 +201,7 @@ const ToManyNester = observer( )} - + diff --git a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx index 7648c6da63..6af7248181 100644 --- a/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx +++ b/packages/core/client/src/schema-component/antd/association-field/SubTable.tsx @@ -44,9 +44,6 @@ const subTableContainer = css` .ant-formily-item-error-help { display: none; } - .ant-description-textarea { - line-height: 34px; - } .ant-table-cell .ant-formily-item-error-help { display: block; position: absolute; @@ -172,7 +169,7 @@ export const SubTable: any = observer( {/* 在这里加,是为了让 “当前对象” 的配置显示正确 */} - + (null); + /** + * the schema of the current sub-table or sub-form + */ + fieldSchema?: Schema; +} + +const SubFormContext = createContext(null); SubFormContext.displayName = 'SubFormContext'; -export const SubFormProvider = SubFormContext.Provider; + +export const SubFormProvider: FC<{ value: SubFormProviderProps }> = (props) => { + const { value, collection, fieldSchema } = props.value; + const memoValue = useMemo(() => ({ value, collection, fieldSchema }), [value, collection, fieldSchema]); + return {props.children}; +}; /** * 用于获取子表单所对应的 form 对象,其应该保持响应性,即一个 Proxy 对象; @@ -159,9 +170,10 @@ export const SubFormProvider = SubFormContext.Provider; * @returns */ export const useSubFormValue = () => { - const { value, collection } = useContext(SubFormContext) || {}; + const { value, collection, fieldSchema } = useContext(SubFormContext) || {}; return { formValue: value, collection, + fieldSchema, }; }; diff --git a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx index 57782fff10..788741a5c2 100644 --- a/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx +++ b/packages/core/client/src/schema-component/antd/form-item/FormItem.Settings.tsx @@ -27,6 +27,7 @@ import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; import { useRecord } from '../../../record-provider'; import { useColumnSchema } from '../../../schema-component/antd/table-v2/Table.Column.Decorator'; import { generalSettingsItems } from '../../../schema-items/GeneralSettings'; +import { getTempFieldState } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; import { ActionType } from '../../../schema-settings/LinkageRules/type'; import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettingsDataScope'; import { SchemaSettingsDateFormat } from '../../../schema-settings/SchemaSettingsDateFormat'; @@ -41,7 +42,6 @@ import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks'; import { isSubMode } from '../association-field/util'; import { removeNullCondition } from '../filter'; import { DynamicComponentProps } from '../filter/DynamicComponent'; -import { getTempFieldState } from '../form-v2/utils'; import { useColorFields } from '../table-v2/Table.Column.Designer'; export const allowAddNew: SchemaSettingsItemType = { 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 e17b0b2f44..b909572637 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 @@ -18,15 +18,16 @@ import { useFormActiveFields } from '../../../block-provider/hooks/useFormActive import { Collection_deprecated } from '../../../collection-manager'; import { CollectionFieldProvider } from '../../../data-source/collection-field/CollectionFieldProvider'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; +import { useDataFormItemProps } from '../../../modules/blocks/data-blocks/form/hooks/useDataFormItemProps'; import { GeneralSchemaDesigner } from '../../../schema-settings'; +import { useContextVariable, useVariables } from '../../../variables'; import { BlockItem } from '../block-item'; import { HTMLEncode } from '../input/shared'; import { FilterFormDesigner } from './FormItem.FilterFormDesigner'; import { useEnsureOperatorsValid } from './SchemaSettingOptions'; import useLazyLoadDisplayAssociationFieldsOfForm from './hooks/useLazyLoadDisplayAssociationFieldsOfForm'; +import { useLinkageRulesForSubTableOrSubForm } from './hooks/useLinkageRulesForSubTableOrSubForm'; import useParseDefaultValue from './hooks/useParseDefaultValue'; -import { useVariables, useContextVariable } from '../../../variables'; -import { useDataFormItemProps } from '../../../modules/blocks/data-blocks/form/hooks/useDataFormItemProps'; Item.displayName = 'FormilyFormItem'; @@ -61,6 +62,7 @@ export const FormItem: any = withDynamicSchemaProps( // 需要放在注冊完变量之后 useParseDefaultValue(); useLazyLoadDisplayAssociationFieldsOfForm(); + useLinkageRulesForSubTableOrSubForm(); useEffect(() => { addActiveFieldName?.(schema.name as string); diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts new file mode 100644 index 0000000000..835b5e6f3b --- /dev/null +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts @@ -0,0 +1,90 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ +import { Field } from '@formily/core'; +import { useField, useFieldSchema } from '@formily/react'; +import { useEffect } from 'react'; +import { useFlag } from '../../../../flag-provider'; +import { bindLinkageRulesToFiled } from '../../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; +import { forEachLinkageRule } from '../../../../schema-settings/LinkageRules/forEachLinkageRule'; +import useLocalVariables from '../../../../variables/hooks/useLocalVariables'; +import useVariables from '../../../../variables/hooks/useVariables'; +import { useSubFormValue } from '../../association-field/hooks'; + +/** + * used to bind the linkage rules of the sub-table or sub-form with the current field + */ +export const useLinkageRulesForSubTableOrSubForm = () => { + const { isInSubForm, isInSubTable } = useFlag(); + + if (!isInSubForm && !isInSubTable) { + return; + } + + const field = useField(); + const fieldSchema = useFieldSchema(); + const { fieldSchema: schemaOfSubTableOrSubForm, formValue } = useSubFormValue(); + const localVariables = useLocalVariables(); + const variables = useVariables(); + + const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm); + + useEffect(() => { + if (!(field.onUnmount as any).__rested) { + const _onUnmount = field.onUnmount; + field.onUnmount = () => { + (field as any).__disposes?.forEach((dispose) => { + dispose(); + }); + _onUnmount(); + }; + (field.onUnmount as any).__rested = true; + } + + if (!linkageRules) { + return; + } + + if ((field as any).__disposes) { + (field as any).__disposes.forEach((dispose) => { + dispose(); + }); + } + + const disposes = ((field as any).__disposes = []); + + forEachLinkageRule(linkageRules, (action, rule) => { + if (action.targetFields?.includes(fieldSchema.name)) { + disposes.push( + bindLinkageRulesToFiled({ + field, + linkageRules, + formValues: formValue, + localVariables, + action, + rule, + variables, + variableNameOfLeftCondition: '$iteration', + }), + ); + } + }); + + (field as any).__linkageRules = linkageRules; + }, [field, fieldSchema?.name, formValue, JSON.stringify(linkageRules), localVariables, variables]); +}; + +function getLinkageRules(fieldSchema) { + if (!fieldSchema) { + return; + } + + return fieldSchema['x-linkage-rules']?.filter((k) => !k.disabled); +} 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 394dfed681..508aa9e3cb 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 @@ -11,27 +11,19 @@ import { css } from '@emotion/css'; import { FormLayout, IFormLayoutProps } from '@formily/antd-v5'; import { Field, Form as FormilyForm, createForm, onFieldInit, onFormInputChange } from '@formily/core'; import { FieldContext, FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; -import { reaction } from '@formily/reactive'; import { uid } from '@formily/shared'; -import { getValuesByPath } from '@nocobase/utils/client'; import { ConfigProvider, Spin, theme } from 'antd'; -import _ from 'lodash'; import React, { useEffect, useMemo } from 'react'; import { useActionContext } from '..'; import { useAttach, useComponent, useDesignable } from '../..'; import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; -import { ActionType } from '../../../schema-settings/LinkageRules/type'; +import { bindLinkageRulesToFiled } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled'; +import { forEachLinkageRule } from '../../../schema-settings/LinkageRules/forEachLinkageRule'; import { useToken } from '../../../style'; import { useLocalVariables, useVariables } from '../../../variables'; -import { VariableOption, VariablesContextType } from '../../../variables/types'; -import { getPath } from '../../../variables/utils/getPath'; -import { getVariableName } from '../../../variables/utils/getVariableName'; -import { getVariablesFromExpression, isVariable } from '../../../variables/utils/isVariable'; -import { getInnermostKeyAndValue, getTargetField } from '../../common/utils/uitls'; import { useProps } from '../../hooks/useProps'; import { useFormBlockHeight } from './hook'; -import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils'; export interface FormProps extends IFormLayoutProps { form?: FormilyForm; @@ -144,49 +136,25 @@ const WithForm = (props: WithFormProps) => { const disposes = []; form.addEffects(id, () => { - linkageRules.forEach((rule) => { - rule.actions?.forEach((action) => { - if (action.targetFields?.length) { - const fields = action.targetFields.join(','); + forEachLinkageRule(linkageRules, (action, rule) => { + if (action.targetFields?.length) { + const fields = action.targetFields.join(','); - // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 - onFieldInit(`*(${fields})`, (field: any, form) => { - 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), - }; - - disposes.push( - reaction( - // 这里共依赖 3 部分,当这 3 部分中的任意一部分发生变更后,需要触发联动规则: - // 1. 条件中的字段值; - // 2. 条件中的变量值; - // 3. value 表达式中的变量值; - () => { - // 获取条件中的字段值 - const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues: form.values }); - - // 获取条件中的变量值 - const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables }); - - // 获取 value 表达式中的变量值 - const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables }); - - const result = [fieldValuesInCondition, variableValuesInCondition, variableValuesInExpression] - .map((item) => JSON.stringify(item)) - .join(','); - return result; - }, - getSubscriber(action, field, rule, variables, localVariables), - { fireImmediately: true, equals: _.isEqual }, - ), - ); - }); - } - }); + // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 + onFieldInit(`*(${fields})`, (field: any, form) => { + disposes.push( + bindLinkageRulesToFiled({ + field, + linkageRules, + formValues: form.values, + localVariables, + action, + rule, + variables, + }), + ); + }); + } }); }); @@ -268,165 +236,3 @@ export const Form: React.FC & { }), { displayName: 'Form' }, ); - -function getSubscriber( - action: any, - field: any, - rule: any, - variables: VariablesContextType, - localVariables: VariableOption[], -): (value: string, oldValue: string) => void { - return () => { - // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 - collectFieldStateOfLinkageRules({ - operator: action.operator, - value: action.value, - field, - condition: rule.condition, - 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; - requestAnimationFrame(() => { - field.setState((state) => { - state[fieldName] = lastState?.value; - }); - }); - //字段隐藏时清空数据 - if (fieldName === 'display' && lastState?.value === 'none') { - field.value = null; - } - } - // 在这里清空 field.stateOfLinkageRules,就可以保证:当条件再次改变时,如果该字段没有和任何条件匹配,则需要把对应的值恢复到初始值; - field.stateOfLinkageRules[fieldName] = null; - }); - }; -} - -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; - } -} - -function getFieldValuesInCondition({ linkageRules, formValues }) { - return linkageRules.map((rule) => { - const run = (condition) => { - const type = Object.keys(condition)[0] || '$and'; - const conditions = condition[type]; - - return conditions - .map((condition) => { - // fix https://nocobase.height.app/T-3251 - if ('$and' in condition || '$or' in condition) { - return run(condition); - } - - const path = getTargetField(condition).join('.'); - return getValuesByPath(formValues, path); - }) - .filter(Boolean); - }; - - return run(rule.condition); - }); -} - -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 getVariablesFromExpression(value) - ?.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 deleted file mode 100644 index a69e530309..0000000000 --- a/packages/core/client/src/schema-component/antd/form-v2/utils.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { Field } from '@formily/core'; -import { evaluators } from '@nocobase/evaluators/client'; -import { uid } from '@nocobase/utils/client'; -import _ from 'lodash'; -import { ActionType } from '../../../schema-settings/LinkageRules/type'; -import { VariableOption, VariablesContextType } from '../../../variables/types'; -import { REGEX_OF_VARIABLE_IN_EXPRESSION } from '../../../variables/utils/isVariable'; -import { conditionAnalyses } from '../../common/utils/uitls'; - -interface Props { - operator; - value; - field: Field & { - [key: string]: any; - }; - condition; - variables: VariablesContextType; - localVariables: VariableOption[]; -} - -/** - * 获取字段临时状态对象 - */ -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, - condition, - variables, - localVariables, -}: Props) => { - 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({ ruleGroup: condition, variables, localVariables }), true), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - required: requiredResult, - }; - break; - case ActionType.InRequired: - requiredResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), false), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - required: requiredResult, - }; - break; - case ActionType.Visible: - case ActionType.None: - case ActionType.Hidden: - displayResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), operator), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - display: displayResult, - }; - break; - case ActionType.Editable: - case ActionType.ReadOnly: - case ActionType.ReadPretty: - patternResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), operator), - ); - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - pattern: patternResult, - }; - break; - case ActionType.Value: - { - const getValue = async () => { - if (value?.mode === 'express') { - if ((value.value || value.result) == null) { - return; - } - - // 解析如 `{{$user.name}}` 之类的变量 - const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { - variables, - localVariables, - }); - - try { - const result = evaluate(exp, { now: () => new Date().toString(), ...expScope }); - return result; - } catch (error) { - console.error(error); - } - } else if (value?.mode === 'constant') { - return value?.value ?? value; - } else { - return null; - } - }; - if (isConditionEmpty(condition)) { - valueResult.push(getTempFieldState(true, getValue())); - } else { - valueResult.push( - getTempFieldState(conditionAnalyses({ ruleGroup: condition, variables, localVariables }), getValue()), - ); - } - field.stateOfLinkageRules = { - ...field.stateOfLinkageRules, - value: valueResult, - }; - } - break; - default: - return null; - } -}; - -export async function replaceVariables( - value: string, - { - variables, - localVariables, - }: { - variables: VariablesContextType; - localVariables: VariableOption[]; - }, -) { - const store = {}; - const scope = {}; - - if (value == null) { - return; - } - - const waitForParsing = value.match(REGEX_OF_VARIABLE_IN_EXPRESSION)?.map(async (item) => { - const { value: parsedValue } = await variables.parseVariable(item, localVariables); - - // 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错 - const id = `_${uid()}`; - - scope[id] = parsedValue; - store[item] = id; - return parsedValue; - }); - - if (waitForParsing) { - await Promise.all(waitForParsing); - } - return { - exp: value.replace(REGEX_OF_VARIABLE_IN_EXPRESSION, (match) => { - return `{{${store[match] || match}}}`; - }), - scope, - }; -} - -function isConditionEmpty(rules: { $and?: any; $or?: any }) { - const type = Object.keys(rules)[0] || '$and'; - const conditions = rules[type]; - - return _.isEmpty(conditions); -} diff --git a/packages/core/client/src/schema-component/antd/quick-edit/QuickEdit.tsx b/packages/core/client/src/schema-component/antd/quick-edit/QuickEdit.tsx index b48491dff3..90b660bc86 100644 --- a/packages/core/client/src/schema-component/antd/quick-edit/QuickEdit.tsx +++ b/packages/core/client/src/schema-component/antd/quick-edit/QuickEdit.tsx @@ -9,14 +9,15 @@ import { css } from '@emotion/css'; // import { FormItem } from '@formily/antd-v5'; +import { IFormItemProps } from '@formily/antd-v5'; import { Field, createForm } from '@formily/core'; import { FormContext, RecursionField, observer, useField, useFieldSchema } from '@formily/react'; import React, { useMemo, useRef } from 'react'; import { useCollectionManager_deprecated } from '../../../collection-manager'; -import { StablePopover } from '../popover'; -import { FormItem } from '../form-item'; -import { IFormItemProps } from '@formily/antd-v5'; import { useCollection } from '../../../data-source/collection/CollectionProvider'; +import { useToken } from '../../../style'; +import { FormItem } from '../form-item'; +import { StablePopover } from '../popover'; export interface QuickEditProps extends IFormItemProps { children?: React.ReactNode; @@ -28,6 +29,7 @@ export const Editable = observer( const containerRef = useRef(null); const fieldSchema = useFieldSchema(); const value = field.value; + const { token } = useToken(); const schema: any = { name: fieldSchema.name, 'x-collection-field': fieldSchema['x-collection-field'], @@ -74,7 +76,15 @@ export const Editable = observer( } `} > -
+
@@ -97,7 +107,11 @@ export const QuickEdit = observer( if (!collectionField) { return null; } - return field.editable ? : ; + return field.editable || field.disabled ? ( + + ) : ( + + ); }, { displayName: 'QuickEdit' }, ); diff --git a/packages/core/client/src/schema-component/antd/quick-edit/__tests__/QuickEdit.test.tsx b/packages/core/client/src/schema-component/antd/quick-edit/__tests__/QuickEdit.test.tsx index 7b43ebe1dc..2e3cf559ac 100644 --- a/packages/core/client/src/schema-component/antd/quick-edit/__tests__/QuickEdit.test.tsx +++ b/packages/core/client/src/schema-component/antd/quick-edit/__tests__/QuickEdit.test.tsx @@ -8,7 +8,7 @@ */ import { BlockSchemaComponentPlugin } from '@nocobase/client'; -import { screen, renderAppOptions, sleep, userEvent, waitFor } from '@nocobase/test/client'; +import { renderAppOptions, screen, sleep, userEvent, waitFor } from '@nocobase/test/client'; describe('QuickEdit', () => { function getRenderOptions(readPretty = false) { diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 95c2a3216c..a06ba65b6d 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -132,7 +132,7 @@ const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) => const index = field.value?.indexOf(record); const basePath = field.address.concat(record.__index || index); return ( - + diff --git a/packages/core/client/src/schema-component/common/utils/logic.js b/packages/core/client/src/schema-component/common/utils/logic.js index df8bb51fab..2306419801 100644 --- a/packages/core/client/src/schema-component/common/utils/logic.js +++ b/packages/core/client/src/schema-component/common/utils/logic.js @@ -120,9 +120,11 @@ export function getJsonLogic() { return a.indexOf(b) !== -1; }, $notIncludes: function (a, b) { - if (!a || typeof a.indexOf === 'undefined') return false; - if (Array.isArray(a)) return !a.some((element) => element.includes(b)); - return !(a.indexOf(b) !== -1); + if (Array.isArray(a)) return !a.some((element) => (element || '').includes(b)); + + a = a || ''; + + return !a.includes(b); }, $anyOf: function (a, b) { if (a.length === 0) { 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 788cc12147..abacb5720f 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -80,10 +80,16 @@ export const conditionAnalyses = async ({ ruleGroup, variables, localVariables, + variableNameOfLeftCondition, }: { ruleGroup; variables: VariablesContextType; localVariables: VariableOption[]; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; }) => { const type = Object.keys(ruleGroup)[0] || '$and'; const conditions = ruleGroup[type]; @@ -101,7 +107,7 @@ export const conditionAnalyses = async ({ return true; } - const targetVariableName = targetFieldToVariableString(getTargetField(condition)); + const targetVariableName = targetFieldToVariableString(getTargetField(condition), variableNameOfLeftCondition); const targetValue = variables .parseVariable(targetVariableName, localVariables, { doNotRequest: true, @@ -140,9 +146,9 @@ export const conditionAnalyses = async ({ * @param targetField * @returns */ -export function targetFieldToVariableString(targetField: string[]) { +export function targetFieldToVariableString(targetField: string[], variableName = '$nForm') { // Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单 - return `{{ $nForm.${targetField.join('.')} }}`; + return `{{ ${variableName}.${targetField.join('.')} }}`; } const getVariablesData = (localVariables) => { diff --git a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx index 89d96e42fa..926a5e8802 100644 --- a/packages/core/client/src/schema-items/GeneralSchemaItems.tsx +++ b/packages/core/client/src/schema-items/GeneralSchemaItems.tsx @@ -14,8 +14,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; import { useDesignable } from '../schema-component'; -import { getTempFieldState } from '../schema-component/antd/form-v2/utils'; import { SchemaSettingsModalItem, SchemaSettingsSwitchItem } from '../schema-settings'; +import { getTempFieldState } from '../schema-settings/LinkageRules/bindLinkageRulesToFiled'; export const GeneralSchemaItems: React.FC<{ required?: boolean; diff --git a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts new file mode 100644 index 0000000000..fefb4e3e8f --- /dev/null +++ b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts @@ -0,0 +1,417 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Field } from '@formily/core'; +import { reaction } from '@formily/reactive'; +import evaluators from '@nocobase/evaluators/client'; +import { getValuesByPath, uid } from '@nocobase/utils/client'; +import _ from 'lodash'; +import { conditionAnalyses, getInnermostKeyAndValue, getTargetField } from '../../schema-component/common/utils/uitls'; +import { VariableOption, VariablesContextType } from '../../variables/types'; +import { getPath } from '../../variables/utils/getPath'; +import { getVariableName } from '../../variables/utils/getVariableName'; +import { + getVariablesFromExpression, + isVariable, + REGEX_OF_VARIABLE_IN_EXPRESSION, +} from '../../variables/utils/isVariable'; +import { ActionType } from './type'; + +interface Props { + operator; + value; + field: Field & { + [key: string]: any; + }; + condition; + variables: VariablesContextType; + localVariables: VariableOption[]; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; +} + +export function bindLinkageRulesToFiled({ + field, + linkageRules, + formValues, + localVariables, + action, + rule, + variables, + variableNameOfLeftCondition, +}: { + field: any; + linkageRules: any[]; + formValues: any; + localVariables: VariableOption[]; + action: any; + rule: any; + variables: VariablesContextType; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; +}) { + 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), + }; + + return reaction( + // 这里共依赖 3 部分,当这 3 部分中的任意一部分发生变更后,需要触发联动规则: + // 1. 条件中的字段值; + // 2. 条件中的变量值; + // 3. value 表达式中的变量值; + () => { + // 获取条件中的字段值 + const fieldValuesInCondition = getFieldValuesInCondition({ linkageRules, formValues }); + + // 获取条件中的变量值 + const variableValuesInCondition = getVariableValuesInCondition({ linkageRules, localVariables }); + + // 获取 value 表达式中的变量值 + const variableValuesInExpression = getVariableValuesInExpression({ action, localVariables }); + + const result = [fieldValuesInCondition, variableValuesInCondition, variableValuesInExpression] + .map((item) => JSON.stringify(item)) + .join(','); + return result; + }, + getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }), + { fireImmediately: true, equals: _.isEqual }, + ); +} + +function getFieldValuesInCondition({ linkageRules, formValues }) { + return linkageRules.map((rule) => { + const run = (condition) => { + const type = Object.keys(condition)[0] || '$and'; + const conditions = condition[type]; + + return conditions + .map((condition) => { + // fix https://nocobase.height.app/T-3251 + if ('$and' in condition || '$or' in condition) { + return run(condition); + } + + const path = getTargetField(condition).join('.'); + return getValuesByPath(formValues, path); + }) + .filter(Boolean); + }; + + return run(rule.condition); + }); +} + +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 getVariablesFromExpression(value) + ?.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)); +} + +function getSubscriber({ + action, + field, + rule, + variables, + localVariables, + variableNameOfLeftCondition, +}: { + action: any; + field: any; + rule: any; + variables: VariablesContextType; + localVariables: VariableOption[]; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; +}): (value: string, oldValue: string) => void { + return () => { + // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 + collectFieldStateOfLinkageRules({ + operator: action.operator, + value: action.value, + field, + condition: rule.condition, + variables, + localVariables, + variableNameOfLeftCondition, + }); + + // 当条件改变时,有可能会触发多个 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; + requestAnimationFrame(() => { + field.setState((state) => { + state[fieldName] = lastState?.value; + }); + }); + //字段隐藏时清空数据 + if (fieldName === 'display' && lastState?.value === 'none') { + field.value = null; + } + } + // 在这里清空 field.stateOfLinkageRules,就可以保证:当条件再次改变时,如果该字段没有和任何条件匹配,则需要把对应的值恢复到初始值; + field.stateOfLinkageRules[fieldName] = null; + }); + }; +} + +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; + } +} + +export const collectFieldStateOfLinkageRules = ({ + operator, + value, + field, + condition, + variables, + localVariables, + variableNameOfLeftCondition, +}: Props) => { + 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'); + const paramsToGetConditionResult = { ruleGroup: condition, variables, localVariables, variableNameOfLeftCondition }; + + switch (operator) { + case ActionType.Required: + requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), true)); + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, + required: requiredResult, + }; + break; + case ActionType.InRequired: + requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), false)); + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, + required: requiredResult, + }; + break; + case ActionType.Visible: + case ActionType.None: + case ActionType.Hidden: + displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, + display: displayResult, + }; + break; + case ActionType.Editable: + case ActionType.ReadOnly: + case ActionType.ReadPretty: + patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, + pattern: patternResult, + }; + break; + case ActionType.Value: + { + const getValue = async () => { + if (value?.mode === 'express') { + if ((value.value || value.result) == null) { + return; + } + + // 解析如 `{{$user.name}}` 之类的变量 + const { exp, scope: expScope } = await replaceVariables(value.value || value.result, { + variables, + localVariables, + }); + + try { + const result = evaluate(exp, { now: () => new Date().toString(), ...expScope }); + return result; + } catch (error) { + console.error(error); + } + } else if (value?.mode === 'constant') { + return value?.value ?? value; + } else { + return null; + } + }; + if (isConditionEmpty(condition)) { + valueResult.push(getTempFieldState(true, getValue())); + } else { + valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), getValue())); + } + field.stateOfLinkageRules = { + ...field.stateOfLinkageRules, + value: valueResult, + }; + } + break; + default: + return null; + } +}; /** + * 获取字段临时状态对象 + */ + +export async function getTempFieldState(condition: boolean | Promise, value: any) { + [condition, value] = await Promise.all([condition, value]); + + return { + condition, + value, + }; +} + +function isConditionEmpty(rules: { $and?: any; $or?: any }) { + const type = Object.keys(rules)[0] || '$and'; + const conditions = rules[type]; + + return _.isEmpty(conditions); +} +export async function replaceVariables( + value: string, + { + variables, + localVariables, + }: { + variables: VariablesContextType; + localVariables: VariableOption[]; + }, +) { + const store = {}; + const scope = {}; + + if (value == null) { + return; + } + + const waitForParsing = value.match(REGEX_OF_VARIABLE_IN_EXPRESSION)?.map(async (item) => { + const { value: parsedValue } = await variables.parseVariable(item, localVariables); + + // 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错 + const id = `_${uid()}`; + + scope[id] = parsedValue; + store[item] = id; + return parsedValue; + }); + + if (waitForParsing) { + await Promise.all(waitForParsing); + } + return { + exp: value.replace(REGEX_OF_VARIABLE_IN_EXPRESSION, (match) => { + return `{{${store[match] || match}}}`; + }), + scope, + }; +} diff --git a/packages/core/client/src/schema-settings/LinkageRules/forEachLinkageRule.ts b/packages/core/client/src/schema-settings/LinkageRules/forEachLinkageRule.ts new file mode 100644 index 0000000000..abb96ef553 --- /dev/null +++ b/packages/core/client/src/schema-settings/LinkageRules/forEachLinkageRule.ts @@ -0,0 +1,16 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +export function forEachLinkageRule(linkageRules: any[], callback: (action: any, rule: any) => void) { + linkageRules.forEach((rule) => { + rule.actions?.forEach((action) => { + callback(action, rule); + }); + }); +} diff --git a/packages/core/client/src/schema-settings/LinkageRules/index.tsx b/packages/core/client/src/schema-settings/LinkageRules/index.tsx index 4b29f7569e..ce8cbc0935 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/index.tsx +++ b/packages/core/client/src/schema-settings/LinkageRules/index.tsx @@ -13,6 +13,7 @@ import React, { useMemo } from 'react'; import { FormBlockContext } from '../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCollectionParentRecordData } from '../../data-source/collection-record/CollectionRecordProvider'; +import { CollectionProvider } from '../../data-source/collection/CollectionProvider'; import { withDynamicSchemaProps } from '../../hoc/withDynamicSchemaProps'; import { RecordProvider } from '../../record-provider'; import { SchemaComponent, useProps } from '../../schema-component'; @@ -23,7 +24,7 @@ import { LinkageRuleActionGroup } from './LinkageRuleActionGroup'; import { EnableLinkage } from './components/EnableLinkage'; import { ArrayCollapse } from './components/LinkageHeader'; -interface Props { +export interface Props { dynamicComponent: any; } @@ -178,7 +179,9 @@ export const FormLinkageRules = withDynamicSchemaProps( - + + + diff --git a/packages/core/client/src/schema-settings/SchemaSettings.tsx b/packages/core/client/src/schema-settings/SchemaSettings.tsx index c8afc6a68b..117d2eda1f 100644 --- a/packages/core/client/src/schema-settings/SchemaSettings.tsx +++ b/packages/core/client/src/schema-settings/SchemaSettings.tsx @@ -1007,7 +1007,7 @@ export const SchemaSettingsDefaultSortingRules = function DefaultSortingRules(pr }; export const SchemaSettingsLinkageRules = function LinkageRules(props) { - const { collectionName, readPretty } = props; + const { collectionName, readPretty, Component, afterSubmit } = props; const fieldSchema = useFieldSchema(); const { form } = useFormBlockContext(); const { dn } = useDesignable(); @@ -1040,7 +1040,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) { title, properties: { fieldReaction: { - 'x-component': FormLinkageRules, + 'x-component': Component || FormLinkageRules, 'x-use-component-props': () => { return { options, @@ -1059,7 +1059,7 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) { }, }, }), - [collectionName, fieldSchema, form, gridSchema, localVariables, record, t, variables, getRules], + [collectionName, fieldSchema, form, gridSchema, localVariables, record, t, variables, getRules, Component], ); const components = useMemo(() => ({ ArrayCollapse, FormLayout }), []); const onSubmit = useCallback( @@ -1079,8 +1079,9 @@ export const SchemaSettingsLinkageRules = function LinkageRules(props) { schema, }); dn.refresh(); + afterSubmit?.(); }, - [dn, getTemplateById, gridSchema, dataKey], + [dn, getTemplateById, gridSchema, dataKey, afterSubmit], ); return ( diff --git a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts index 104cda0c03..6b4b30ccad 100644 --- a/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts +++ b/packages/plugins/@nocobase/plugin-field-m2m-array/src/server/belongs-to-array-field.ts @@ -12,6 +12,7 @@ import { BaseColumnFieldOptions, BelongsToArrayAssociation, Model, RelationField export const elementTypeMap = { nanoid: 'string', sequence: 'string', + uid: 'string', }; export class BelongsToArrayField extends RelationField {