Merge branch 'main' into next

This commit is contained in:
GitHub Actions Bot 2024-09-07 15:14:53 +00:00
commit d88adc395b
26 changed files with 711 additions and 456 deletions

View File

@ -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);
}

View File

@ -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';

View File

@ -121,6 +121,27 @@ export interface DataBlockContextValue<T extends {} = {}> {
export const DataBlockContext = createContext<DataBlockContextValue<any>>({} 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 <DataBlockResourceContext.Provider value={value}>{children}</DataBlockResourceContext.Provider>;
};
/**
* @internal
*/
@ -170,7 +191,9 @@ export const DataBlockProvider: FC<Partial<AllDataBlockProps>> = withDynamicSche
<ACLCollectionProvider>
<DataBlockResourceProvider>
<BlockRequestProvider>
<DataBlockCollector params={props.params}>{children}</DataBlockCollector>
<DataBlockCollector params={props.params}>
<RerenderDataBlockProvider>{children}</RerenderDataBlockProvider>
</DataBlockCollector>
</BlockRequestProvider>
</DataBlockResourceProvider>
</ACLCollectionProvider>
@ -195,3 +218,11 @@ export const useDataBlockProps = <T extends {}>(): DataBlockContextValue<T>['pro
const context = useDataBlock<T>();
return context.props;
};
export const useRerenderDataBlock = () => {
const context = useContext(DataBlockResourceContext);
if (!context) {
throw new Error('useRerenderDataBlock() must be used within a DataBlockProvider');
}
return context;
};

View File

@ -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: [

View File

@ -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,
],
});

View File

@ -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],
});

View File

@ -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
<FlagProvider isInSubForm>
<FormLinkageRules {...props} />
</FlagProvider>
);
},
{ 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],
});

View File

@ -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<ArrayField>();
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 (
<FormActiveFieldsProvider name="nester">
<SubFormProvider value={{ value: field.value, collection }}>
<SubFormProvider value={{ value: field.value, collection, fieldSchema: fieldSchema.parent }}>
<RecordProvider isNew={recordV2?.isNew} record={field.value} parent={recordV2?.data}>
<DefaultValueProvider isAllowToSetDefaultValue={isAllowToSetDefaultValue}>
<Card bordered={true}>{props.children}</Card>
@ -200,7 +201,7 @@ const ToManyNester = observer(
)}
</div>
<FormActiveFieldsProvider name="nester">
<SubFormProvider value={{ value, collection }}>
<SubFormProvider value={{ value, collection, fieldSchema: fieldSchema.parent }}>
<RecordProvider isNew={isNewRecord(value)} record={value} parent={recordData}>
<RecordIndexProvider index={index}>
<DefaultValueProvider isAllowToSetDefaultValue={isAllowToSetDefaultValue}>

View File

@ -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(
<CollectionRecordProvider record={null} parentRecord={recordV2}>
<FormActiveFieldsProvider name="nester">
{/* 在这里加,是为了让 “当前对象” 的配置显示正确 */}
<SubFormProvider value={{ value: null, collection }}>
<SubFormProvider value={{ value: null, collection, fieldSchema: fieldSchema.parent }}>
<Table
className={tableClassName}
bordered

View File

@ -8,12 +8,12 @@
*/
import { GeneralField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { Schema, useField, useFieldSchema } from '@formily/react';
import { isString } from 'lodash';
import cloneDeep from 'lodash/cloneDeep';
import { createContext, useCallback, useContext, useMemo } from 'react';
import React, { createContext, FC, useCallback, useContext, useMemo } from 'react';
import { useParsedFilter } from '../../../block-provider/hooks/useParsedFilter';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
import { Collection } from '../../../data-source';
import { isInFilterFormBlock } from '../../../filter-provider';
import { mergeFilter } from '../../../filter-provider/utils';
@ -142,12 +142,23 @@ export const useFieldNames = (
return { label: 'label', value: 'value', ...fieldNames };
};
const SubFormContext = createContext<{
interface SubFormProviderProps {
value: any;
collection: Collection;
}>(null);
/**
* the schema of the current sub-table or sub-form
*/
fieldSchema?: Schema;
}
const SubFormContext = createContext<SubFormProviderProps>(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 <SubFormContext.Provider value={memoValue}>{props.children}</SubFormContext.Provider>;
};
/**
* 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,
};
};

View File

@ -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 = {

View File

@ -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);

View File

@ -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<Field>();
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);
}

View File

@ -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<FormProps> & {
}),
{ 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));
}

View File

@ -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<boolean>, 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);
}

View File

@ -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(
}
`}
>
<div style={{ minHeight: 30, padding: '0 8px' }}>
<div
style={{
minHeight: token.controlHeight,
padding: `1px ${token.paddingXS}px`,
backgroundColor: field.disabled ? token.colorBgContainerDisabled : undefined,
color: field.disabled ? token.colorTextDisabled : undefined,
borderRadius: token.borderRadius,
}}
>
<FormContext.Provider value={form}>
<RecursionField schema={schema} name={fieldSchema.name} />
</FormContext.Provider>
@ -97,7 +107,11 @@ export const QuickEdit = observer(
if (!collectionField) {
return null;
}
return field.editable ? <Editable {...props} /> : <FormItem {...props} style={{ padding: '0 8px' }} />;
return field.editable || field.disabled ? (
<Editable {...props} />
) : (
<FormItem {...props} style={{ padding: '0 8px' }} />
);
},
{ displayName: 'QuickEdit' },
);

View File

@ -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) {

View File

@ -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 (
<SubFormProvider value={{ value: record, collection }}>
<SubFormProvider value={{ value: record, collection, fieldSchema: schema.parent }}>
<RecordIndexProvider index={record.__index || index}>
<RecordProvider isNew={isNewRecord(record)} record={record} parent={parentRecordData}>
<ColumnFieldProvider schema={s} basePath={basePath}>

View File

@ -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) {

View File

@ -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) => {

View File

@ -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;

View File

@ -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<boolean>, 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,
};
}

View File

@ -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);
});
});
}

View File

@ -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(
<FormBlockContext.Provider value={{ form, type: formBlockType, collectionName }}>
<RecordProvider record={record} parent={parentRecordData}>
<FilterContext.Provider value={value}>
<SchemaComponent components={components} schema={schema} />
<CollectionProvider name={collectionName}>
<SchemaComponent components={components} schema={schema} />
</CollectionProvider>
</FilterContext.Provider>
</RecordProvider>
</FormBlockContext.Provider>

View File

@ -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 (

View File

@ -12,6 +12,7 @@ import { BaseColumnFieldOptions, BelongsToArrayAssociation, Model, RelationField
export const elementTypeMap = {
nanoid: 'string',
sequence: 'string',
uid: 'string',
};
export class BelongsToArrayField extends RelationField {