@nocobase/client-v2 2.4.0-alpha.5 → 2.4.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  2. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  3. package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
  4. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -0
  5. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  6. package/es/index.mjs +20 -20
  7. package/lib/index.js +111 -111
  8. package/package.json +9 -8
  9. package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
  10. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
  11. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
  12. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  13. package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
  14. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  15. package/src/flow/actions/__tests__/validation.test.ts +48 -0
  16. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +39 -0
  17. package/src/flow/components/FieldAssignValueInput.tsx +4 -0
  18. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
  19. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
  20. package/src/flow/components/filter/VariableFilterItem.tsx +11 -3
  21. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  22. package/src/flow/models/base/GridModel.tsx +0 -1
  23. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  24. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -3
  25. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  26. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  27. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  28. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  29. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  30. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
  31. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
@@ -47,6 +47,146 @@ class MockFlowModelRepository implements IFlowModelRepository {
47
47
  }
48
48
 
49
49
  describe('assignFieldValuesFlow (editor)', () => {
50
+ it('shows the deleted-field placeholder for a stale assigned value', async () => {
51
+ const engine = new FlowEngine();
52
+ engine.setModelRepository(new MockFlowModelRepository());
53
+ engine.registerModels({
54
+ AssignFormModel,
55
+ AssignFormGridModel,
56
+ AssignFormItemModel,
57
+ InputFieldModel,
58
+ VariableFieldFormModel,
59
+ });
60
+ engine.context.defineProperty('location', { value: { search: '' } });
61
+ engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
62
+ engine.context.defineProperty('flowSettingsEnabled', { value: true });
63
+
64
+ const main = engine.context.dataSourceManager.getDataSource('main');
65
+ main.addCollection({
66
+ name: 'users',
67
+ fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
68
+ });
69
+ const users = engine.context.dataSourceManager.getCollection('main', 'users');
70
+
71
+ const action = engine.createModel({
72
+ use: 'FlowModel',
73
+ uid: 'act-assign-deleted-field',
74
+ });
75
+ action.setStepParams('assignSettings', 'assignFieldValues', {
76
+ assignedValues: { deletedField: 'stale value' },
77
+ });
78
+ action.context.defineProperty('blockModel', { value: { collection: users } });
79
+
80
+ const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
81
+ const Editor = step.uiSchema().editor?.['x-component'] as React.ComponentType;
82
+ const flowSettingsCtx = new FlowRuntimeContext(action, 'assignSettings', 'settings');
83
+
84
+ render(
85
+ <FlowEngineProvider engine={engine}>
86
+ <ConfigProvider>
87
+ <App>
88
+ <FlowSettingsContextProvider value={flowSettingsCtx}>
89
+ <Editor />
90
+ </FlowSettingsContextProvider>
91
+ </App>
92
+ </ConfigProvider>
93
+ </FlowEngineProvider>,
94
+ );
95
+
96
+ await waitFor(() => {
97
+ expect(screen.getByRole('button', { name: /Fields/ })).toBeInTheDocument();
98
+ expect(screen.getByText(/deletedField.*may have been deleted/)).toBeInTheDocument();
99
+ const form = engine.findModelByParentId<AssignFormModel>(action.uid, 'assignForm');
100
+ expect(form).toBeDefined();
101
+ expect(form?.subModels.grid.subModels.items || []).toHaveLength(1);
102
+ expect(form?.getAssignedValues()).toEqual({});
103
+ });
104
+ });
105
+
106
+ it('shows the standard deleted-field placeholder for a persisted assigned field model', async () => {
107
+ const engine = new FlowEngine();
108
+ engine.setModelRepository(new MockFlowModelRepository());
109
+ engine.registerModels({
110
+ AssignFormModel,
111
+ AssignFormGridModel,
112
+ AssignFormItemModel,
113
+ InputFieldModel,
114
+ VariableFieldFormModel,
115
+ });
116
+ engine.context.defineProperty('location', { value: { search: '' } });
117
+ engine.context.defineProperty('themeToken', { value: { marginLG: 24 } });
118
+ engine.context.defineProperty('flowSettingsEnabled', { value: true });
119
+
120
+ const main = engine.context.dataSourceManager.getDataSource('main');
121
+ main.addCollection({
122
+ name: 'users',
123
+ fields: [{ name: 'nickname', type: 'string', interface: 'input' }],
124
+ });
125
+ const users = engine.context.dataSourceManager.getCollection('main', 'users');
126
+
127
+ const action = engine.createModel({
128
+ use: 'FlowModel',
129
+ uid: 'act-assign-persisted-deleted-field',
130
+ });
131
+ action.setStepParams('assignSettings', 'assignFieldValues', {
132
+ assignedValues: { deletedField: 'stale value' },
133
+ });
134
+ action.context.defineProperty('blockModel', { value: { collection: users } });
135
+
136
+ const form = engine.createModel<AssignFormModel>({
137
+ use: 'AssignFormModel',
138
+ uid: 'form-assign-persisted-deleted-field',
139
+ parentId: action.uid,
140
+ subKey: 'assignForm',
141
+ stepParams: {
142
+ resourceSettings: {
143
+ init: {
144
+ dataSourceKey: 'main',
145
+ collectionName: 'users',
146
+ },
147
+ },
148
+ },
149
+ });
150
+ action.setSubModel('assignForm', form);
151
+ form.subModels.grid.addSubModel('items', {
152
+ use: 'AssignFormItemModel',
153
+ uid: 'item-assign-persisted-deleted-field',
154
+ stepParams: {
155
+ fieldSettings: {
156
+ init: {
157
+ dataSourceKey: 'main',
158
+ collectionName: 'users',
159
+ fieldPath: 'deletedField',
160
+ },
161
+ assignValue: { value: 'stale value' },
162
+ },
163
+ },
164
+ });
165
+ form.subModels.grid.resetRows(true);
166
+
167
+ const step = createAssignFieldValuesStep({ settingsFlowKey: 'assignSettings' });
168
+ const Editor = step.uiSchema().editor?.['x-component'] as React.ComponentType;
169
+ const flowSettingsCtx = new FlowRuntimeContext(action, 'assignSettings', 'settings');
170
+
171
+ render(
172
+ <FlowEngineProvider engine={engine}>
173
+ <ConfigProvider>
174
+ <App>
175
+ <FlowSettingsContextProvider value={flowSettingsCtx}>
176
+ <Editor />
177
+ </FlowSettingsContextProvider>
178
+ </App>
179
+ </ConfigProvider>
180
+ </FlowEngineProvider>,
181
+ );
182
+
183
+ await waitFor(() => {
184
+ expect(screen.getByText(/deletedField.*may have been deleted/)).toBeInTheDocument();
185
+ expect(form.subModels.grid.subModels.items).toHaveLength(1);
186
+ expect(form.getAssignedValues()).toEqual({});
187
+ });
188
+ });
189
+
50
190
  it('repairs AssignFormModel resource init and clears cached collection', async () => {
51
191
  const engine = new FlowEngine();
52
192
  engine.setModelRepository(new MockFlowModelRepository());
@@ -21,6 +21,35 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
21
21
  engine.registerModels({ FilterFormGridModel });
22
22
  });
23
23
 
24
+ it.each([false, true])('preserves collapsed fields in mobile layout with settings enabled: %s', (settingsEnabled) => {
25
+ engine.flowSettings[settingsEnabled ? 'enable' : 'disable']();
26
+ const rows = {
27
+ first: [['field-1']],
28
+ second: [['field-2']],
29
+ third: [['field-3']],
30
+ };
31
+ const model = engine.createModel<FilterFormGridModel>({
32
+ uid: 'mobile-filter-grid',
33
+ use: 'FilterFormGridModel',
34
+ props: { rows },
35
+ subModels: {
36
+ items: ['field-1', 'field-2', 'field-3'].map((uid) => ({ use: 'FlowModel', uid })),
37
+ },
38
+ });
39
+ model.context.defineProperty('isMobileLayout', { value: true });
40
+ model.setStepParams(GRID_FLOW_KEY, GRID_STEP, { rows });
41
+ type VisibleLayoutReader = { getVisibleLayout(): { rows: Record<string, string[][]> } };
42
+ const renderedItems = () =>
43
+ Object.values((model as unknown as VisibleLayoutReader).getVisibleLayout().rows).flat(2);
44
+
45
+ expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
46
+ model.toggleFormFieldsCollapse(true, 1);
47
+ expect(renderedItems()).toEqual(['field-1']);
48
+ model.toggleFormFieldsCollapse(false, 1);
49
+ expect(renderedItems()).toEqual(['field-1', 'field-2', 'field-3']);
50
+ expect(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).rows).toEqual(rows);
51
+ });
52
+
24
53
  it('uses rowOrder from the full layout when collapsing after reorder', () => {
25
54
  const model = engine.createModel<FilterFormGridModel>({
26
55
  uid: 'filter-grid-collapse-order',
@@ -351,7 +351,7 @@ export const FieldComponentProps: React.FC<{ fieldModel: string; source: string[
351
351
  <FormItem label={t('Options')}>
352
352
  <Space direction="vertical" size={8} style={{ width: '100%' }}>
353
353
  {options.map((option: any, index: number) => (
354
- <Space key={`${option.value}-${index}`} style={{ width: '100%' }} size={8} wrap align="start">
354
+ <Space key={index} style={{ width: '100%' }} size={8} wrap align="start">
355
355
  <Input
356
356
  style={{ flex: 1, minWidth: 120 }}
357
357
  placeholder={t('Option label')}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { createForm } from '@formily/core';
13
+ import { Field, FormProvider } from '@formily/react';
14
+ import { render, screen } from '@nocobase/test/client';
15
+ import userEvent from '@testing-library/user-event';
16
+ import { FlowEngine, FlowEngineProvider, FlowModel, FlowModelProvider } from '@nocobase/flow-engine';
17
+ import { FieldComponentProps } from '../FieldComponentProps';
18
+
19
+ class HostModel extends FlowModel {
20
+ render() {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ describe.each(['RadioGroupFieldModel', 'CheckboxGroupFieldModel', 'SelectFieldModel'])('%s options', (fieldModel) => {
26
+ it('keeps focus while editing option values and preserves remaining rows after removal', async () => {
27
+ const form = createForm();
28
+ const engine = new FlowEngine();
29
+ engine.registerModels({ HostModel });
30
+ const model = engine.createModel<HostModel>({ use: 'HostModel' });
31
+ render(
32
+ <FlowEngineProvider engine={engine}>
33
+ <FlowModelProvider model={model}>
34
+ <FormProvider form={form}>
35
+ <Field name="props" component={[FieldComponentProps, { fieldModel, source: [] }]} />
36
+ </FormProvider>
37
+ </FlowModelProvider>
38
+ </FlowEngineProvider>,
39
+ );
40
+
41
+ await userEvent.click(screen.getByRole('button', { name: 'plus Add' }));
42
+ const valueInput = screen.getByPlaceholderText('Option value');
43
+ await userEvent.type(valueInput, 'abcdef');
44
+ expect(screen.getByPlaceholderText('Option value')).toHaveValue('abcdef');
45
+ expect(valueInput).toHaveFocus();
46
+
47
+ await userEvent.keyboard('{Backspace}{Backspace}xy');
48
+ expect(valueInput).toHaveValue('abcdxy');
49
+ expect(valueInput).toHaveFocus();
50
+ await userEvent.type(screen.getByPlaceholderText('Option label'), 'First');
51
+ await userEvent.click(screen.getByRole('button', { name: 'plus Add' }));
52
+ await userEvent.type(screen.getAllByPlaceholderText('Option label')[1], 'Second');
53
+ await userEvent.type(screen.getAllByPlaceholderText('Option value')[1], 'second');
54
+ await userEvent.click(screen.getAllByRole('button', { name: 'close' })[0]);
55
+ expect(screen.getByPlaceholderText('Option label')).toHaveValue('Second');
56
+ expect(screen.getByPlaceholderText('Option value')).toHaveValue('second');
57
+ await userEvent.type(screen.getByPlaceholderText('Option value'), '2');
58
+ expect(screen.getByPlaceholderText('Option value')).toHaveFocus();
59
+ expect(form.values.props.options).toEqual([{ label: 'Second', value: 'second2' }]);
60
+ });
61
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import React from 'react';
11
+ import { act, render, screen, waitFor, cleanup } from '@testing-library/react';
12
+ import { Form, Input } from 'antd';
13
+ import { afterEach, expect, it, vi } from 'vitest';
14
+ import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
15
+ import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
16
+ import { CreateFormModel, EditFormModel, FormGridModel, FormItemModel, FormComponent } from '../../../..';
17
+ import { fieldLinkageRules, linkageAssignField } from '../../../../actions/linkageRules';
18
+
19
+ afterEach(cleanup);
20
+
21
+ it.each([
22
+ { use: 'CreateFormModel', mode: 'default', expected: 'STAFF-001' },
23
+ { use: 'EditFormModel', mode: 'default', expected: '' },
24
+ { use: 'EditFormModel', mode: 'override', expected: 'STAFF-001' },
25
+ ])('$use respects $mode linkage after a delayed popup variable response', async ({ use, mode, expected }) => {
26
+ const uid = `popup-${use}-${mode}`;
27
+ const engine = new FlowEngine();
28
+ engine.registerModels({ CreateFormModel, EditFormModel, FormGridModel, FormItemModel });
29
+ engine.registerActions({ fieldLinkageRules, linkageAssignField });
30
+ engine.context.dataSourceManager.getDataSource('main').addCollection({
31
+ name: 't1_user',
32
+ filterTargetKey: 'id',
33
+ fields: [
34
+ { name: 'id', type: 'integer', interface: 'number' },
35
+ { name: 'staffseq', type: 'string', interface: 'input' },
36
+ { name: 'staffname', type: 'string', interface: 'input' },
37
+ ],
38
+ });
39
+ const form = engine.createModel<CreateFormModel | EditFormModel>({
40
+ uid,
41
+ use,
42
+ stepParams: { resourceSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', filterByTk: 1 } } },
43
+ subModels: {
44
+ grid: {
45
+ uid: `${uid}-grid`,
46
+ use: 'FormGridModel',
47
+ subModels: {
48
+ items: [
49
+ {
50
+ uid: `${uid}-staffname`,
51
+ use: 'FormItemModel',
52
+ props: { name: 'staffname' },
53
+ stepParams: {
54
+ fieldSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', fieldPath: 'staffname' } },
55
+ },
56
+ },
57
+ ],
58
+ },
59
+ },
60
+ },
61
+ });
62
+ const configured = '{{ ctx.popup.record.staffseq }}';
63
+ const rules = {
64
+ value: [
65
+ {
66
+ key: 'rule',
67
+ title: 'Rule',
68
+ enable: true,
69
+ condition: { logic: '$and', items: [] },
70
+ actions: [
71
+ {
72
+ key: 'assign',
73
+ name: 'linkageAssignField',
74
+ params: {
75
+ value: [
76
+ {
77
+ key: 'field',
78
+ enable: true,
79
+ mode,
80
+ condition: { logic: '$and', items: [] },
81
+ targetPath: 'staffname',
82
+ value: configured,
83
+ },
84
+ ],
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ };
91
+ form.setStepParams('eventSettings', 'linkageRules', rules);
92
+ const saved = JSON.parse(JSON.stringify(form.serialize()));
93
+ expect(saved.subModels.grid.stepParams.eventSettings.linkageRules).toEqual(rules);
94
+ expect(saved.stepParams.eventSettings?.linkageRules).toBeUndefined();
95
+
96
+ const token = `test.${Buffer.from(JSON.stringify({ userId: 2, signInTime: 'member-form' })).toString('base64url')}.sig`;
97
+ type Item = { id: string; rd: string; template: unknown; contextParams: unknown };
98
+ let resolveResponse: (() => void) | undefined;
99
+ const responseReady = new Promise<void>((resolve) => {
100
+ resolveResponse = resolve;
101
+ });
102
+ const request = vi.fn(async ({ data }: { data: { values: { batch: Item[] } } }) => {
103
+ const batch = data.values.batch;
104
+ expect(batch[0].rd).toBe(generateFlowModelRdFromToken(form.uid, token));
105
+ expect(batch[0].contextParams).toEqual({
106
+ 'popup.record': { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' },
107
+ });
108
+ await responseReady;
109
+ return {
110
+ data: {
111
+ data: {
112
+ results: batch.map((item) => ({
113
+ id: item.id,
114
+ data: JSON.parse(JSON.stringify(item.template).replaceAll(configured, 'STAFF-001')),
115
+ })),
116
+ },
117
+ },
118
+ };
119
+ });
120
+ engine.context.defineProperty('api', { value: { auth: { token, role: 'member' }, request } });
121
+ form.context.defineProperty('popup', {
122
+ value: { record: { id: 1 } },
123
+ resolveOnServer: true,
124
+ meta: {
125
+ type: 'object',
126
+ buildVariablesParams: () => ({ record: { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' } }),
127
+ },
128
+ });
129
+ const resource = form.resource as SingleRecordResource;
130
+ resource.setData(use === 'EditFormModel' ? { id: 1, staffname: null } : {});
131
+ function View() {
132
+ form.useHooksBeforeRender();
133
+ return (
134
+ <FormComponent model={form}>
135
+ <Form.Item name="staffname">
136
+ <Input aria-label="staffname" />
137
+ </Form.Item>
138
+ </FormComponent>
139
+ );
140
+ }
141
+ const dispatchEvent = vi.spyOn(form, 'dispatchEvent');
142
+ const view = render(<View />);
143
+ try {
144
+ form.formValueRuntime?.mount({ sync: true });
145
+ // Simulate the field mounting, without involving the page layout and field-renderer plugins.
146
+ engine.emitter.emit('model:mounted', { model: form.subModels.grid.subModels.items[0] });
147
+ let pending: Promise<unknown> | undefined;
148
+ await act(async () => {
149
+ pending = form.applyFlow('eventSettings');
150
+ });
151
+ await waitFor(() => expect(request).toHaveBeenCalled());
152
+ await act(async () => {
153
+ resolveResponse?.();
154
+ await pending;
155
+ });
156
+ if (mode === 'default') {
157
+ // Resolving a default value must not overwrite an existing edit record.
158
+ expect(form.subModels.grid.subModels.items[0].props.initialValue).toBe('STAFF-001');
159
+ }
160
+ await waitFor(() => expect((screen.getByLabelText('staffname') as HTMLInputElement).value).toBe(expected));
161
+ expect(form.form.getFieldValue('staffname')).toBe(expected || null);
162
+ } finally {
163
+ resolveResponse?.();
164
+ await act(async () => {
165
+ view.unmount();
166
+ form.formValueRuntime?.dispose();
167
+ // Drain automatic linkage refreshes before another engine starts its variable batch.
168
+ await Promise.all(
169
+ dispatchEvent.mock.results.filter((result) => result.type === 'return').map((result) => result.value),
170
+ );
171
+ });
172
+ dispatchEvent.mockRestore();
173
+ engine.disposeScheduler();
174
+ }
175
+ });
@@ -19,7 +19,7 @@ import {
19
19
  createCurrentRecordMetaFactory,
20
20
  useFlowModel,
21
21
  } from '@nocobase/flow-engine';
22
- import { Table, Button, Space } from 'antd';
22
+ import { Table, Button, Space, theme } from 'antd';
23
23
  import { uid } from '@formily/shared';
24
24
  import classNames from 'classnames';
25
25
  import { DragEndEvent } from '@dnd-kit/core';
@@ -190,6 +190,7 @@ const AddFieldColumn = ({ model }) => {
190
190
  };
191
191
 
192
192
  const DisplayTable = (props) => {
193
+ const { token } = theme.useToken();
193
194
  const {
194
195
  pageSize,
195
196
  value,
@@ -269,7 +270,7 @@ const DisplayTable = (props) => {
269
270
  },
270
271
  },
271
272
  ...baseColumns,
272
- { key: 'empty' },
273
+ { key: 'empty', render: () => <div style={{ minHeight: token.controlHeight }} /> },
273
274
  ].filter(Boolean),
274
275
  ) as any[];
275
276
 
@@ -283,7 +284,7 @@ const DisplayTable = (props) => {
283
284
  }
284
285
 
285
286
  return cols;
286
- }, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model]);
287
+ }, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model, token.controlHeight]);
287
288
  return (
288
289
  <Table
289
290
  tableLayout="fixed"
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { CloseOutlined, ZoomInOutlined } from '@ant-design/icons';
11
- import { Table, Form, Space, Button } from 'antd';
11
+ import { Table, Form, Space, Button, theme } from 'antd';
12
12
  import { css } from '@emotion/css';
13
13
  import { useTranslation } from 'react-i18next';
14
14
  import { PlusOutlined } from '@ant-design/icons';
@@ -51,6 +51,7 @@ function shouldRefreshForChangedPaths(fieldPath: unknown, changedPaths: unknown)
51
51
 
52
52
  export function SubTableField(props) {
53
53
  const { t } = useTranslation();
54
+ const { token } = theme.useToken();
54
55
  const {
55
56
  onChange,
56
57
  columns,
@@ -180,6 +181,9 @@ export function SubTableField(props) {
180
181
  // the current array index so Form.Item can rebind after reordering/removal.
181
182
  const rowBindingKey = `${rowIdentity}:${pageRowIdx}`;
182
183
  const columnKey = col.dataIndex ?? col.key ?? 'cell';
184
+ if (col.key === '_empty') {
185
+ return <div style={{ minHeight: token.controlHeight }} />;
186
+ }
183
187
  if (!col.render) {
184
188
  return;
185
189
  }