mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 05:36:05 +00:00
Merge branch 'main' into next
This commit is contained in:
commit
d88adc395b
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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: [
|
||||
|
@ -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,
|
||||
],
|
||||
});
|
||||
|
@ -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],
|
||||
});
|
||||
|
@ -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],
|
||||
});
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
@ -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 = {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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' },
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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}>
|
||||
|
@ -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) {
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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>
|
||||
|
@ -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 (
|
||||
|
@ -12,6 +12,7 @@ import { BaseColumnFieldOptions, BelongsToArrayAssociation, Model, RelationField
|
||||
export const elementTypeMap = {
|
||||
nanoid: 'string',
|
||||
sequence: 'string',
|
||||
uid: 'string',
|
||||
};
|
||||
|
||||
export class BelongsToArrayField extends RelationField {
|
||||
|
Loading…
Reference in New Issue
Block a user