mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
fix(subtable): prevent empty rows (#5990)
* fix(subtable): prevent empty rows * refactor: rename params * chore: fix unit tests
This commit is contained in:
parent
122d33aaf5
commit
98098ab6cc
@ -87,7 +87,7 @@ const tableClassName = css`
|
|||||||
export const SubTable: any = observer(
|
export const SubTable: any = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { openSize } = props;
|
const { openSize } = props;
|
||||||
const { field, options: collectionField } = useAssociationFieldContext<ArrayField>();
|
const { field, options: collectionField, fieldSchema: schema } = useAssociationFieldContext<ArrayField>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visibleSelector, setVisibleSelector] = useState(false);
|
const [visibleSelector, setVisibleSelector] = useState(false);
|
||||||
const [selectedRows, setSelectedRows] = useState([]);
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
@ -98,7 +98,9 @@ export const SubTable: any = observer(
|
|||||||
const recordV2 = useCollectionRecord();
|
const recordV2 = useCollectionRecord();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
const { allowSelectExistingRecord, allowAddnew, allowDisassociation } = field.componentProps;
|
const { allowSelectExistingRecord, allowAddnew, allowDisassociation } = field.componentProps;
|
||||||
useSubTableSpecialCase({ field });
|
|
||||||
|
useSubTableSpecialCase({ rootField: field, rootSchema: schema });
|
||||||
|
|
||||||
const move = (fromIndex: number, toIndex: number) => {
|
const move = (fromIndex: number, toIndex: number) => {
|
||||||
if (toIndex === undefined) return;
|
if (toIndex === undefined) return;
|
||||||
if (!isArr(field.value)) return;
|
if (!isArr(field.value)) return;
|
||||||
|
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Schema } from '@formily/json-schema';
|
||||||
|
import { renderHook } from '@nocobase/test/client';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { useHasUsedVariable } from '../hooks/useSpecialCase';
|
||||||
|
|
||||||
|
describe('useHasUsedVariable', () => {
|
||||||
|
it('should return true when FormItem has variable in default value', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: '{{$context.field1}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when FormItem has no variable', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: 'static value',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true when nested FormItem has variable', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
parent: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
child: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: '{{$context.child}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when schema has no FormItem decorator', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
type: 'string',
|
||||||
|
default: '{{$context.field1}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when default value is not a string', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: 123,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when default value is not a variable', () => {
|
||||||
|
const schema = new Schema({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
field1: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
default: '$context.field1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useHasUsedVariable());
|
||||||
|
const { hasUsedVariable } = result.current;
|
||||||
|
expect(hasUsedVariable('$context', schema)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -128,10 +128,8 @@ const useParseDefaultValue = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (value == null || value === '') {
|
if (value == null || value === '') {
|
||||||
// fix https://nocobase.height.app/T-4350/description
|
|
||||||
// 如果 field.mounted 为 false,说明 field 已经被卸载了,不需要再设置默认值
|
// 如果 field.mounted 为 false,说明 field 已经被卸载了,不需要再设置默认值
|
||||||
if (field.mounted) {
|
if (field.mounted) {
|
||||||
// fix https://nocobase.height.app/T-2805
|
|
||||||
field.setInitialValue(null);
|
field.setInitialValue(null);
|
||||||
await field.reset({ forceClear: true });
|
await field.reset({ forceClear: true });
|
||||||
}
|
}
|
||||||
|
@ -10,15 +10,17 @@
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { Schema, useFieldSchema, useForm } from '@formily/react';
|
import { Schema, useFieldSchema, useForm } from '@formily/react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
|
||||||
import {
|
import {
|
||||||
CollectionFieldOptions_deprecated,
|
CollectionFieldOptions_deprecated,
|
||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
} from '../../../../collection-manager';
|
} from '../../../../collection-manager';
|
||||||
import { markRecordAsNew } from '../../../../data-source/collection-record/isNewRecord';
|
import { markRecordAsNew } from '../../../../data-source/collection-record/isNewRecord';
|
||||||
|
import { isVariable } from '../../../../variables/utils/isVariable';
|
||||||
import { isSubMode } from '../../association-field/util';
|
import { isSubMode } from '../../association-field/util';
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* #### 处理 `子表单` 和 `子表格` 中的特殊情况
|
* #### 处理 `子表单` 和 `子表格` 中的特殊情况
|
||||||
*
|
*
|
||||||
@ -187,20 +189,22 @@ export function isFromDatabase(value: Record<string, any>) {
|
|||||||
* 3. 如果子表格中没有设置默认值,就会再把子表格重置为空。
|
* 3. 如果子表格中没有设置默认值,就会再把子表格重置为空。
|
||||||
* @param param0
|
* @param param0
|
||||||
*/
|
*/
|
||||||
export const useSubTableSpecialCase = ({ field }) => {
|
export const useSubTableSpecialCase = ({ rootField, rootSchema }) => {
|
||||||
|
const { hasUsedVariable } = useHasUsedVariable();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (_.isEmpty(field.value)) {
|
if (_.isEmpty(rootField.value) && hasUsedVariable('$context', rootSchema)) {
|
||||||
const emptyValue = field.value;
|
const emptyValue = rootField.value;
|
||||||
const newValue = [markRecordAsNew({})];
|
const newValue = [markRecordAsNew({})];
|
||||||
field.value = newValue;
|
rootField.value = newValue;
|
||||||
// 因为默认值的解析是异步的,所以下面的代码会优先于默认值的设置,这样就防止了设置完默认值后又被清空的问题
|
// 因为默认值的解析是异步的,所以下面的代码会优先于默认值的设置,这样就防止了设置完默认值后又被清空的问题
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (JSON.stringify(field.value) === JSON.stringify(newValue)) {
|
if (JSON.stringify(rootField.value) === JSON.stringify(newValue)) {
|
||||||
field.value = emptyValue;
|
rootField.value = emptyValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, [rootField, rootSchema, hasUsedVariable]);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,3 +232,48 @@ export function isSubset(subset: any[], superset: any[]): boolean {
|
|||||||
// All elements match, it's a subset
|
// All elements match, it's a subset
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively checks a schema and its properties for usage of a variable name
|
||||||
|
* @param schema The Schema object to check
|
||||||
|
* @param variableName The variable name to search for
|
||||||
|
* @returns True if the variable is used in the schema or its properties
|
||||||
|
*/
|
||||||
|
const checkSchema = (schema: Schema, variableName: string): boolean => {
|
||||||
|
// Check if current node is a FormItem and has a default value containing the variable
|
||||||
|
if (schema['x-decorator'] === 'FormItem') {
|
||||||
|
const defaultValue = schema.default;
|
||||||
|
if (
|
||||||
|
defaultValue &&
|
||||||
|
typeof defaultValue === 'string' &&
|
||||||
|
isVariable(defaultValue) &&
|
||||||
|
defaultValue.includes(variableName)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively check all child properties
|
||||||
|
let hasVariable = false;
|
||||||
|
schema.mapProperties((childSchema) => {
|
||||||
|
if (checkSchema(childSchema, variableName)) {
|
||||||
|
hasVariable = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasVariable;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useHasUsedVariable() {
|
||||||
|
/**
|
||||||
|
* Checks if a variable name is used anywhere in a schema
|
||||||
|
* @param variableName The variable name to search for
|
||||||
|
* @param rootSchema The root Schema object to check
|
||||||
|
* @returns True if the variable is used in the schema
|
||||||
|
*/
|
||||||
|
const hasUsedVariable = useCallback((variableName: string, rootSchema: Schema): boolean => {
|
||||||
|
return checkSchema(rootSchema, variableName);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { hasUsedVariable };
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user