mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-08 06:59:26 +08:00
* fix(linkageRules): fix invalid issue of form linkage rules (#5876) * test: add e2e test * fix: complete the fix * fix: fix e2e errors * fix: fix e2e errors * feat(table): add support for conditional styling based on pretty mode * feat(FormItem): enhance wrapperStyle to conditionally apply padding based on background color * chore: skip e2e test --------- Co-authored-by: sheldon guo <sheldon_66@163.com>
This commit is contained in:
parent
736aa9ee93
commit
ea82812222
@ -13,7 +13,7 @@ import {
|
|||||||
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
|
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
|
||||||
test,
|
test,
|
||||||
} from '@nocobase/test/e2e';
|
} from '@nocobase/test/e2e';
|
||||||
import { T2165, T3251, T3806, T3815, expressionTemplateInLinkageRules, T4891 } from './templatesOfBug';
|
import { expressionTemplateInLinkageRules, T2165, T3251, T3806, T3815, T4891 } from './templatesOfBug';
|
||||||
|
|
||||||
test.describe('linkage rules', () => {
|
test.describe('linkage rules', () => {
|
||||||
test('basic usage', async ({ page, mockPage }) => {
|
test('basic usage', async ({ page, mockPage }) => {
|
||||||
@ -276,7 +276,6 @@ test.describe('linkage rules', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://nocobase.height.app/T-T3815 &&T-3802
|
|
||||||
test('fireImmediately in create form & edit form', async ({ page, mockPage, mockRecord }) => {
|
test('fireImmediately in create form & edit form', async ({ page, mockPage, mockRecord }) => {
|
||||||
const nocoPage = await mockPage(T3815).waitForInit();
|
const nocoPage = await mockPage(T3815).waitForInit();
|
||||||
await mockRecord('general', {
|
await mockRecord('general', {
|
||||||
|
@ -51,7 +51,7 @@ export const FormItem: any = withDynamicSchemaProps(
|
|||||||
const field = useField<Field>();
|
const field = useField<Field>();
|
||||||
const schema = useFieldSchema();
|
const schema = useFieldSchema();
|
||||||
const { addActiveFieldName } = useFormActiveFields() || {};
|
const { addActiveFieldName } = useFormActiveFields() || {};
|
||||||
const { wrapperStyle } = useDataFormItemProps();
|
const { wrapperStyle }: { wrapperStyle: any } = useDataFormItemProps();
|
||||||
|
|
||||||
useParseDefaultValue();
|
useParseDefaultValue();
|
||||||
useLazyLoadDisplayAssociationFieldsOfForm();
|
useLazyLoadDisplayAssociationFieldsOfForm();
|
||||||
@ -95,7 +95,15 @@ export const FormItem: any = withDynamicSchemaProps(
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ACLCollectionFieldProvider>
|
<ACLCollectionFieldProvider>
|
||||||
<Item className={className} {...props} extra={extra} wrapperStyle={wrapperStyle} />
|
<Item
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
extra={extra}
|
||||||
|
wrapperStyle={{
|
||||||
|
...(wrapperStyle.backgroundColor ? { paddingLeft: '5px', paddingRight: '5px' } : {}),
|
||||||
|
...wrapperStyle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ACLCollectionFieldProvider>
|
</ACLCollectionFieldProvider>
|
||||||
</BlockItem>
|
</BlockItem>
|
||||||
</CollectionFieldProvider>
|
</CollectionFieldProvider>
|
||||||
|
@ -16,7 +16,9 @@ import { ConfigProvider, Spin, theme } from 'antd';
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useActionContext } from '..';
|
import { useActionContext } from '..';
|
||||||
import { useAttach, useComponent } from '../..';
|
import { useAttach, useComponent } from '../..';
|
||||||
|
import { getCardItemSchema } from '../../../block-provider';
|
||||||
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
|
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
|
||||||
|
import { useDataBlockRequest } from '../../../data-source/data-block/DataBlockRequestProvider';
|
||||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||||
import { bindLinkageRulesToFiled } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled';
|
import { bindLinkageRulesToFiled } from '../../../schema-settings/LinkageRules/bindLinkageRulesToFiled';
|
||||||
import { forEachLinkageRule } from '../../../schema-settings/LinkageRules/forEachLinkageRule';
|
import { forEachLinkageRule } from '../../../schema-settings/LinkageRules/forEachLinkageRule';
|
||||||
@ -24,7 +26,6 @@ import { useToken } from '../../../style';
|
|||||||
import { useLocalVariables, useVariables } from '../../../variables';
|
import { useLocalVariables, useVariables } from '../../../variables';
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
import { useFormBlockHeight } from './hook';
|
import { useFormBlockHeight } from './hook';
|
||||||
import { getCardItemSchema } from '../../../block-provider';
|
|
||||||
|
|
||||||
export interface FormProps extends IFormLayoutProps {
|
export interface FormProps extends IFormLayoutProps {
|
||||||
form?: FormilyForm;
|
form?: FormilyForm;
|
||||||
@ -128,6 +129,7 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
const localVariables = useLocalVariables({ currentForm: form });
|
const localVariables = useLocalVariables({ currentForm: form });
|
||||||
const { templateFinished } = useTemplateBlockContext();
|
const { templateFinished } = useTemplateBlockContext();
|
||||||
|
const { loading } = useDataBlockRequest() || {};
|
||||||
const linkageRules: any[] =
|
const linkageRules: any[] =
|
||||||
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
||||||
|
|
||||||
@ -150,29 +152,36 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
}, [form, props.disabled, setFormValueChanged]);
|
}, [form, props.disabled, setFormValueChanged]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const id = uid();
|
const id = uid();
|
||||||
const disposes = [];
|
const disposes = [];
|
||||||
|
|
||||||
form.addEffects(id, () => {
|
// 如果不延迟执行,那么一开始获取到的 form.values 的值是旧的,会导致详情区块的联动规则出现一些问题
|
||||||
forEachLinkageRule(linkageRules, (action, rule) => {
|
setTimeout(() => {
|
||||||
if (action.targetFields?.length) {
|
form.addEffects(id, () => {
|
||||||
const fields = action.targetFields.join(',');
|
forEachLinkageRule(linkageRules, (action, rule) => {
|
||||||
|
if (action.targetFields?.length) {
|
||||||
|
const fields = action.targetFields.join(',');
|
||||||
|
|
||||||
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替
|
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替
|
||||||
onFieldInit(`*(${fields})`, (field: any, form) => {
|
onFieldInit(`*(${fields})`, (field: any, form) => {
|
||||||
disposes.push(
|
disposes.push(
|
||||||
bindLinkageRulesToFiled({
|
bindLinkageRulesToFiled({
|
||||||
field,
|
field,
|
||||||
linkageRules,
|
linkageRules,
|
||||||
formValues: form.values,
|
formValues: form.values,
|
||||||
localVariables,
|
localVariables,
|
||||||
action,
|
action,
|
||||||
rule,
|
rule,
|
||||||
variables,
|
variables,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -182,7 +191,7 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
dispose();
|
dispose();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [linkageRules, templateFinished]);
|
}, [linkageRules, templateFinished, loading]);
|
||||||
|
|
||||||
return fieldSchema['x-decorator'] === 'FormV2' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
|
return fieldSchema['x-decorator'] === 'FormV2' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
|
||||||
};
|
};
|
||||||
|
@ -500,6 +500,8 @@ const InternalBodyCellComponent = (props) => {
|
|||||||
const { record, schema, rowIndex, isSubTable, ...others } = props;
|
const { record, schema, rowIndex, isSubTable, ...others } = props;
|
||||||
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema });
|
const { valueMap } = useSatisfiedActionValues({ formValues: record, category: 'style', schema });
|
||||||
const style = useMemo(() => Object.assign({ ...props.style }, valueMap), [props.style, valueMap]);
|
const style = useMemo(() => Object.assign({ ...props.style }, valueMap), [props.style, valueMap]);
|
||||||
|
const isReadyPrettyMode =
|
||||||
|
!!schema?.properties && Object.values(schema.properties).some((item) => item['x-read-pretty'] === true);
|
||||||
const skeletonStyle = {
|
const skeletonStyle = {
|
||||||
height: '1em',
|
height: '1em',
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.06)',
|
backgroundColor: 'rgba(0, 0, 0, 0.06)',
|
||||||
@ -507,7 +509,7 @@ const InternalBodyCellComponent = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td {...others} className={classNames(props.className, cellClass)} style={style}>
|
<td {...others} className={classNames(props.className, cellClass)} style={isReadyPrettyMode ? style : {}}>
|
||||||
{/* Lazy rendering cannot be used in sub-tables. */}
|
{/* Lazy rendering cannot be used in sub-tables. */}
|
||||||
{isSubTable || inView || isIndex ? props.children : <div style={skeletonStyle} />}
|
{isSubTable || inView || isIndex ? props.children : <div style={skeletonStyle} />}
|
||||||
</td>
|
</td>
|
||||||
|
@ -95,7 +95,6 @@ export const conditionAnalyses = async ({
|
|||||||
const conditions = ruleGroup[type];
|
const conditions = ruleGroup[type];
|
||||||
|
|
||||||
let results = conditions.map(async (condition) => {
|
let results = conditions.map(async (condition) => {
|
||||||
// fix https://nocobase.height.app/T-3152
|
|
||||||
if ('$and' in condition || '$or' in condition) {
|
if ('$and' in condition || '$or' in condition) {
|
||||||
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables });
|
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables });
|
||||||
}
|
}
|
||||||
@ -155,7 +154,7 @@ export const conditionAnalyses = async ({
|
|||||||
* @param targetField
|
* @param targetField
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function targetFieldToVariableString(targetField: string[], variableName = '$nForm') {
|
function targetFieldToVariableString(targetField: string[], variableName = '$nForm') {
|
||||||
// Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单
|
// Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单
|
||||||
return `{{ ${variableName}.${targetField.join('.')} }}`;
|
return `{{ ${variableName}.${targetField.join('.')} }}`;
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,6 @@ function getFieldValuesInCondition({ linkageRules, formValues }) {
|
|||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
.map((condition) => {
|
.map((condition) => {
|
||||||
// fix https://nocobase.height.app/T-3251
|
|
||||||
if ('$and' in condition || '$or' in condition) {
|
if ('$and' in condition || '$or' in condition) {
|
||||||
return run(condition);
|
return run(condition);
|
||||||
}
|
}
|
||||||
|
@ -77,16 +77,13 @@ const useCurrentFormData = () => {
|
|||||||
*/
|
*/
|
||||||
export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => {
|
export const useCurrentFormContext = ({ form: _form }: Pick<Props, 'form'> = {}) => {
|
||||||
const { form } = useFormBlockContext();
|
const { form } = useFormBlockContext();
|
||||||
const formData = useCurrentFormData();
|
|
||||||
const { isVariableParsedInOtherContext } = useFlag();
|
const { isVariableParsedInOtherContext } = useFlag();
|
||||||
|
|
||||||
const formInstance = _form || form;
|
const formInstance = _form || form;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
/** 变量值 */
|
/** 变量值 */
|
||||||
currentFormCtx:
|
currentFormCtx: formInstance?.values,
|
||||||
formInstance?.readPretty === false && formInstance?.values && Object.keys(formInstance?.values)?.length
|
|
||||||
? formInstance?.values
|
|
||||||
: formData || formInstance?.values,
|
|
||||||
/** 用来判断是否可以显示`当前表单`变量 */
|
/** 用来判断是否可以显示`当前表单`变量 */
|
||||||
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
|
shouldDisplayCurrentForm: formInstance && !formInstance.readPretty && !isVariableParsedInOtherContext,
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 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 { expect, test } from '@nocobase/test/e2e';
|
||||||
|
import { formFieldDependsOnSubtableFieldsWithLinkageRules } from './template';
|
||||||
|
|
||||||
|
test.describe('linkage rules', () => {
|
||||||
|
test.skip('form field depends on subtable fields with linkage rules', async ({ page, mockPage, mockRecord }) => {
|
||||||
|
const nocoPage = await mockPage(formFieldDependsOnSubtableFieldsWithLinkageRules).waitForInit();
|
||||||
|
const record = await mockRecord('test');
|
||||||
|
await nocoPage.goto();
|
||||||
|
|
||||||
|
// 1. 点击 Add new,打开新增表单弹窗
|
||||||
|
await page.getByLabel('action-Action-Add new-create-').click();
|
||||||
|
|
||||||
|
// 2. 新增表单中,填上子表单的值,result 会自动计算结果
|
||||||
|
await page.getByLabel('action-AssociationField.').click();
|
||||||
|
await page.getByLabel('block-item-CollectionField-A-form-A.count-count').getByRole('spinbutton').fill('10');
|
||||||
|
await page.getByLabel('block-item-CollectionField-A-form-A.price-price').getByRole('spinbutton').fill('10');
|
||||||
|
// 10 * 10 = 100
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('block-item-CollectionField-test-form-test.result-result').getByRole('spinbutton'),
|
||||||
|
).toHaveValue('100');
|
||||||
|
|
||||||
|
// 3. 关闭弹窗,点击 Edit 按钮打开编辑表单弹窗
|
||||||
|
await page.getByLabel('drawer-Action.Container-test-Add record-mask').click();
|
||||||
|
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||||
|
await page.getByLabel('action-Action.Link-Edit-').click();
|
||||||
|
|
||||||
|
// 4. 编辑表单中的 result 字段值,应该是由子表格中填入的值计算得出的
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('block-item-CollectionField-test-form-test.result-result').getByRole('spinbutton'),
|
||||||
|
).toHaveValue(String(calcResult(record)));
|
||||||
|
|
||||||
|
// 5. 在子表格中新增并输入新的值,result 字段值会自动计算
|
||||||
|
await page.getByLabel('action-AssociationField.').click();
|
||||||
|
await page
|
||||||
|
.getByRole('row', { name: `table-index-${record.subtable.length + 1} block-item-` })
|
||||||
|
.getByRole('spinbutton')
|
||||||
|
.first()
|
||||||
|
.fill('10');
|
||||||
|
await page
|
||||||
|
.getByRole('row', { name: `table-index-${record.subtable.length + 1} block-item-` })
|
||||||
|
.getByRole('spinbutton')
|
||||||
|
.nth(1)
|
||||||
|
.fill('10');
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('block-item-CollectionField-test-form-test.result-result').getByRole('spinbutton'),
|
||||||
|
).toHaveValue(String(calcResult(record) + 10 * 10));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function calcResult(record) {
|
||||||
|
return record.subtable.reduce((acc, item) => acc + item.count * item.price, 0);
|
||||||
|
}
|
1076
packages/core/client/src/schema-settings/__e2e__/template.ts
Normal file
1076
packages/core/client/src/schema-settings/__e2e__/template.ts
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user