mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +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(
|
||||
(props: any) => {
|
||||
const { openSize } = props;
|
||||
const { field, options: collectionField } = useAssociationFieldContext<ArrayField>();
|
||||
const { field, options: collectionField, fieldSchema: schema } = useAssociationFieldContext<ArrayField>();
|
||||
const { t } = useTranslation();
|
||||
const [visibleSelector, setVisibleSelector] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState([]);
|
||||
@ -98,7 +98,9 @@ export const SubTable: any = observer(
|
||||
const recordV2 = useCollectionRecord();
|
||||
const collection = useCollection();
|
||||
const { allowSelectExistingRecord, allowAddnew, allowDisassociation } = field.componentProps;
|
||||
useSubTableSpecialCase({ field });
|
||||
|
||||
useSubTableSpecialCase({ rootField: field, rootSchema: schema });
|
||||
|
||||
const move = (fromIndex: number, toIndex: number) => {
|
||||
if (toIndex === undefined) 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 === '') {
|
||||
// fix https://nocobase.height.app/T-4350/description
|
||||
// 如果 field.mounted 为 false,说明 field 已经被卸载了,不需要再设置默认值
|
||||
if (field.mounted) {
|
||||
// fix https://nocobase.height.app/T-2805
|
||||
field.setInitialValue(null);
|
||||
await field.reset({ forceClear: true });
|
||||
}
|
||||
|
@ -10,15 +10,17 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { Schema, useFieldSchema, useForm } from '@formily/react';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
CollectionFieldOptions_deprecated,
|
||||
useCollectionManager_deprecated,
|
||||
useCollection_deprecated,
|
||||
} from '../../../../collection-manager';
|
||||
import { markRecordAsNew } from '../../../../data-source/collection-record/isNewRecord';
|
||||
import { isVariable } from '../../../../variables/utils/isVariable';
|
||||
import { isSubMode } from '../../association-field/util';
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
/**
|
||||
* #### 处理 `子表单` 和 `子表格` 中的特殊情况
|
||||
*
|
||||
@ -187,20 +189,22 @@ export function isFromDatabase(value: Record<string, any>) {
|
||||
* 3. 如果子表格中没有设置默认值,就会再把子表格重置为空。
|
||||
* @param param0
|
||||
*/
|
||||
export const useSubTableSpecialCase = ({ field }) => {
|
||||
export const useSubTableSpecialCase = ({ rootField, rootSchema }) => {
|
||||
const { hasUsedVariable } = useHasUsedVariable();
|
||||
|
||||
useEffect(() => {
|
||||
if (_.isEmpty(field.value)) {
|
||||
const emptyValue = field.value;
|
||||
if (_.isEmpty(rootField.value) && hasUsedVariable('$context', rootSchema)) {
|
||||
const emptyValue = rootField.value;
|
||||
const newValue = [markRecordAsNew({})];
|
||||
field.value = newValue;
|
||||
rootField.value = newValue;
|
||||
// 因为默认值的解析是异步的,所以下面的代码会优先于默认值的设置,这样就防止了设置完默认值后又被清空的问题
|
||||
setTimeout(() => {
|
||||
if (JSON.stringify(field.value) === JSON.stringify(newValue)) {
|
||||
field.value = emptyValue;
|
||||
if (JSON.stringify(rootField.value) === JSON.stringify(newValue)) {
|
||||
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
|
||||
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