@nocobase/client-v2 2.1.0-beta.23 → 2.1.0-beta.25

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 (42) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/components/Grid/index.d.ts +5 -3
  4. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  5. package/es/flow/models/base/GridModel.d.ts +19 -2
  6. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  7. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  8. package/es/index.mjs +100 -100
  9. package/lib/index.js +100 -100
  10. package/package.json +6 -5
  11. package/src/BaseApplication.tsx +4 -0
  12. package/src/__tests__/globalDeps.test.ts +1 -0
  13. package/src/__tests__/remotePlugins.test.ts +27 -0
  14. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  15. package/src/flow/actions/dataScope.tsx +6 -4
  16. package/src/flow/actions/dataScopeFilter.ts +70 -0
  17. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  18. package/src/flow/components/Grid/index.tsx +66 -20
  19. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  20. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  21. package/src/flow/models/base/BlockGridModel.tsx +2 -2
  22. package/src/flow/models/base/GridModel.tsx +428 -195
  23. package/src/flow/models/base/__tests__/BlockGridModel.dragOverlayConfig.test.ts +44 -0
  24. package/src/flow/models/base/__tests__/GridModel.computeOverlayRect.test.ts +29 -0
  25. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +181 -2
  26. package/src/flow/models/base/__tests__/GridModel.resizeLayout.test.ts +124 -0
  27. package/src/flow/models/base/__tests__/GridModel.visibleLayout.test.ts +55 -15
  28. package/src/flow/models/blocks/details/DetailsGridModel.tsx +6 -6
  29. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  30. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +54 -14
  31. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  32. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +45 -0
  33. package/src/flow/models/blocks/form/FormGridModel.tsx +6 -6
  34. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  35. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  36. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  37. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  38. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  39. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  40. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
  41. package/src/utils/globalDeps.ts +4 -0
  42. package/src/utils/requirejs.ts +1 -1
@@ -0,0 +1,44 @@
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 { FlowEngine, LayoutSlot } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { BlockGridModel } from '../BlockGridModel';
13
+
14
+ describe('BlockGridModel dragOverlayConfig', () => {
15
+ let model: BlockGridModel;
16
+
17
+ beforeEach(() => {
18
+ const engine = new FlowEngine();
19
+ engine.registerModels({ BlockGridModel });
20
+ model = engine.createModel<BlockGridModel>({ use: 'BlockGridModel' });
21
+ });
22
+
23
+ it('applies block insert overlay offsets from dragOverlayConfig', () => {
24
+ const beforeSlot: LayoutSlot = {
25
+ type: 'column',
26
+ rowId: 'row1',
27
+ columnIndex: 0,
28
+ insertIndex: 0,
29
+ position: 'before',
30
+ rect: { top: 100, left: 50, width: 200, height: 48 },
31
+ };
32
+ const afterSlot: LayoutSlot = {
33
+ type: 'column',
34
+ rowId: 'row1',
35
+ columnIndex: 0,
36
+ insertIndex: 1,
37
+ position: 'after',
38
+ rect: { top: 300, left: 50, width: 200, height: 48 },
39
+ };
40
+
41
+ expect((model as any).computeOverlayRect(beforeSlot).top).toBe(88);
42
+ expect((model as any).computeOverlayRect(afterSlot).top).toBe(312);
43
+ });
44
+ });
@@ -203,6 +203,35 @@ describe('GridModel computeOverlayRect', () => {
203
203
  });
204
204
  });
205
205
 
206
+ describe('Item-edge slot', () => {
207
+ it('should apply column-edge config to item-edge overlay', () => {
208
+ model.dragOverlayConfig = {
209
+ columnEdge: {
210
+ right: { width: 24, offsetLeft: 8 },
211
+ },
212
+ };
213
+
214
+ const slot: LayoutSlot = {
215
+ type: 'item-edge',
216
+ rowId: 'row1',
217
+ columnIndex: 0,
218
+ itemIndex: 0,
219
+ itemUid: 'item-1',
220
+ direction: 'right',
221
+ rect: { top: 100, left: 250, width: 16, height: 120 },
222
+ };
223
+
224
+ const result = (model as any).computeOverlayRect(slot);
225
+
226
+ expect(result).toEqual({
227
+ top: 100,
228
+ left: 258,
229
+ width: 24,
230
+ height: 120,
231
+ });
232
+ });
233
+ });
234
+
206
235
  describe('Row-gap slot', () => {
207
236
  it('should use default rect when no config provided', () => {
208
237
  const slot: LayoutSlot = {
@@ -7,9 +7,9 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React from 'react';
11
- import { beforeEach, describe, expect, it } from 'vitest';
12
10
  import { FlowEngine } from '@nocobase/flow-engine';
11
+ import React from 'react';
12
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
13
13
  import { GridModel } from '../GridModel';
14
14
 
15
15
  const createMockRect = ({ top, left, width, height }: { top: number; left: number; width: number; height: number }) => {
@@ -103,5 +103,184 @@ describe('GridModel drag snapshot container', () => {
103
103
  (model as any).updateLayoutSnapshot();
104
104
 
105
105
  expect((model as any).dragState?.slots?.length).toBeGreaterThan(0);
106
+ model.handleDragCancel({} as any);
107
+ });
108
+
109
+ it('uses native pointer position instead of scroll-adjusted drag delta', () => {
110
+ const model = engine.createModel<GridModel>({
111
+ use: 'GridModel',
112
+ uid: 'grid-drag-pointer-position',
113
+ props: {},
114
+ structure: {} as any,
115
+ });
116
+
117
+ (model as any).dragState = {
118
+ sourceUid: 'item-1',
119
+ snapshot: { rows: {}, sizes: {} },
120
+ slots: [],
121
+ containerEl: null,
122
+ containerRect: { top: 0, left: 0, width: 0, height: 0 },
123
+ pointerOrigin: { x: 100, y: 100 },
124
+ pointerPosition: { x: 120, y: 180 },
125
+ activeSlotKey: null,
126
+ previewLayout: undefined,
127
+ refreshTimer: null,
128
+ };
129
+
130
+ const point = (model as any).computePointerPosition({
131
+ delta: { x: 0, y: 600 },
132
+ over: null,
133
+ });
134
+
135
+ expect(point).toEqual({ x: 120, y: 180 });
136
+ });
137
+
138
+ it('removes drag document listeners when dragging finishes', () => {
139
+ const model = engine.createModel<GridModel>({
140
+ use: 'GridModel',
141
+ uid: 'grid-drag-cleanup',
142
+ props: {},
143
+ structure: {} as any,
144
+ });
145
+ const cleanupListeners = vi.fn();
146
+
147
+ (model as any).dragState = {
148
+ sourceUid: 'item-1',
149
+ snapshot: { rows: {}, sizes: {} },
150
+ slots: [],
151
+ containerEl: null,
152
+ containerRect: { top: 0, left: 0, width: 0, height: 0 },
153
+ activeSlotKey: null,
154
+ previewLayout: undefined,
155
+ refreshTimer: null,
156
+ cleanupListeners,
157
+ };
158
+
159
+ model.handleDragCancel({} as any);
160
+
161
+ expect(cleanupListeners).toHaveBeenCalledOnce();
162
+ });
163
+
164
+ it('refreshes drag snapshot after the page scrolls during dragging', () => {
165
+ vi.useFakeTimers();
166
+ try {
167
+ const model = engine.createModel<GridModel>({
168
+ use: 'GridModel',
169
+ uid: 'grid-drag-scroll-refresh',
170
+ props: {},
171
+ structure: {} as any,
172
+ });
173
+ const updateLayoutSnapshot = vi.fn();
174
+ const refreshPreviewFromPointer = vi.fn();
175
+
176
+ (model as any).dragState = {
177
+ sourceUid: 'item-1',
178
+ snapshot: { rows: {}, sizes: {} },
179
+ slots: [],
180
+ containerEl: null,
181
+ containerRect: { top: 0, left: 0, width: 0, height: 0 },
182
+ pointerPosition: { x: 120, y: 180 },
183
+ activeSlotKey: null,
184
+ previewLayout: undefined,
185
+ refreshTimer: null,
186
+ };
187
+ (model as any).updateLayoutSnapshot = updateLayoutSnapshot;
188
+ (model as any).refreshPreviewFromPointer = refreshPreviewFromPointer;
189
+
190
+ (model as any).handleDragScroll();
191
+ vi.advanceTimersByTime(16);
192
+
193
+ expect(updateLayoutSnapshot).toHaveBeenCalledOnce();
194
+ expect(refreshPreviewFromPointer).toHaveBeenCalledOnce();
195
+ } finally {
196
+ vi.useRealTimers();
197
+ }
198
+ });
199
+
200
+ it('uses overlay-sized hit area when resolving drag slot', () => {
201
+ const model = engine.createModel<GridModel>({
202
+ use: 'GridModel',
203
+ uid: 'grid-drag-hit-area',
204
+ props: {},
205
+ structure: {} as any,
206
+ });
207
+
208
+ model.dragOverlayConfig = {
209
+ columnEdge: {
210
+ right: { width: 24, offsetLeft: 8 },
211
+ },
212
+ } as any;
213
+
214
+ (model as any).dragState = {
215
+ sourceUid: 'item-1',
216
+ snapshot: { rows: {}, sizes: {} },
217
+ slots: [
218
+ {
219
+ type: 'column-edge',
220
+ rowId: 'row-1',
221
+ columnIndex: 0,
222
+ direction: 'right',
223
+ rect: { top: 100, left: 200, width: 16, height: 120 },
224
+ },
225
+ ],
226
+ containerEl: null,
227
+ containerRect: { top: 0, left: 0, width: 0, height: 0 },
228
+ activeSlotKey: null,
229
+ previewLayout: undefined,
230
+ refreshTimer: null,
231
+ };
232
+
233
+ const resolved = (model as any).resolveDragSlot({ x: 225, y: 140 });
234
+
235
+ expect(resolved).toMatchObject({
236
+ type: 'column-edge',
237
+ rowId: 'row-1',
238
+ columnIndex: 0,
239
+ direction: 'right',
240
+ });
241
+ });
242
+
243
+ it('keeps overlay coordinates relative to grid root when outer container is scrolled', () => {
244
+ const model = engine.createModel<GridModel>({
245
+ use: 'GridModel',
246
+ uid: 'grid-drag-overlay-scroll',
247
+ props: {},
248
+ structure: {} as any,
249
+ });
250
+ const container = document.createElement('div');
251
+ container.scrollTop = 40;
252
+ container.scrollLeft = 12;
253
+ (model.gridContainerRef as any).current = container;
254
+
255
+ (model as any).dragState = {
256
+ sourceUid: 'item-1',
257
+ snapshot: {
258
+ rows: { 'row-1': [['item-1', 'item-2']] },
259
+ sizes: { 'row-1': [24] },
260
+ },
261
+ slots: [],
262
+ containerEl: container,
263
+ containerRect: { top: 100, left: 50, width: 480, height: 280 },
264
+ activeSlotKey: null,
265
+ previewLayout: undefined,
266
+ refreshTimer: null,
267
+ };
268
+
269
+ (model as any).applyPreview({
270
+ type: 'column',
271
+ rowId: 'row-1',
272
+ columnIndex: 0,
273
+ insertIndex: 1,
274
+ position: 'after',
275
+ rect: { top: 130, left: 90, width: 220, height: 60 },
276
+ });
277
+
278
+ expect(model.props.dragOverlayRect).toMatchObject({
279
+ top: 30,
280
+ left: 40,
281
+ width: 220,
282
+ height: 60,
283
+ type: 'column',
284
+ });
106
285
  });
107
286
  });
@@ -0,0 +1,124 @@
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 { FlowEngine } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { GridModel } from '../GridModel';
13
+
14
+ describe('GridModel resize layout', () => {
15
+ let engine: FlowEngine;
16
+
17
+ beforeEach(() => {
18
+ engine = new FlowEngine();
19
+ engine.registerModels({ GridModel });
20
+ });
21
+
22
+ it('updates v2 layout sizes during resize so the saved layout keeps the new width', () => {
23
+ const itemA = engine.createModel({ use: 'FlowModel', uid: 'a' });
24
+ const itemB = engine.createModel({ use: 'FlowModel', uid: 'b' });
25
+ const model = engine.createModel<GridModel>({
26
+ uid: 'grid-resize-layout',
27
+ use: 'GridModel',
28
+ props: {
29
+ layout: {
30
+ version: 2,
31
+ rows: [
32
+ {
33
+ id: 'rowA',
34
+ cells: [
35
+ { id: 'cellA', items: ['a'] },
36
+ { id: 'cellB', items: ['b'] },
37
+ ],
38
+ sizes: [12, 12],
39
+ },
40
+ ],
41
+ },
42
+ },
43
+ structure: {} as any,
44
+ });
45
+ (model as any).subModels = { items: [itemA, itemB] };
46
+ model.syncLayoutProps(model.getGridLayout());
47
+
48
+ const container = document.createElement('div');
49
+ Object.defineProperty(container, 'clientWidth', {
50
+ configurable: true,
51
+ value: 240,
52
+ });
53
+ (model.gridContainerRef as any).current = container;
54
+ model.onMount();
55
+
56
+ model.emitter.emit('onResizeRight', { resizeDistance: 60, model: itemA });
57
+ model.emitter.emit('onResizeEnd');
58
+
59
+ expect(model.props.layout.rows[0].sizes).toEqual([18, 6]);
60
+ expect(model.getStepParams('gridSettings', 'grid').layout.rows[0].sizes).toEqual([18, 6]);
61
+ });
62
+
63
+ it('uses the nested row container width when resizing v2 nested layouts', () => {
64
+ const itemA = engine.createModel({ use: 'FlowModel', uid: 'a' });
65
+ const itemB = engine.createModel({ use: 'FlowModel', uid: 'b' });
66
+ const model = engine.createModel<GridModel>({
67
+ uid: 'grid-resize-nested-layout',
68
+ use: 'GridModel',
69
+ props: {
70
+ layout: {
71
+ version: 2,
72
+ rows: [
73
+ {
74
+ id: 'outer',
75
+ cells: [
76
+ {
77
+ id: 'outer-cell',
78
+ rows: [
79
+ {
80
+ id: 'nested',
81
+ cells: [
82
+ { id: 'nested-a', items: ['a'] },
83
+ { id: 'nested-b', items: ['b'] },
84
+ ],
85
+ sizes: [12, 12],
86
+ },
87
+ ],
88
+ },
89
+ ],
90
+ sizes: [24],
91
+ },
92
+ ],
93
+ },
94
+ },
95
+ structure: {} as any,
96
+ });
97
+ (model as any).subModels = { items: [itemA, itemB] };
98
+ model.syncLayoutProps(model.getGridLayout());
99
+
100
+ const container = document.createElement('div');
101
+ Object.defineProperty(container, 'clientWidth', {
102
+ configurable: true,
103
+ value: 240,
104
+ });
105
+ const nestedRowParent = document.createElement('div');
106
+ Object.defineProperty(nestedRowParent, 'clientWidth', {
107
+ configurable: true,
108
+ value: 120,
109
+ });
110
+ const nestedRowElement = document.createElement('div');
111
+ nestedRowElement.dataset.gridRowId = 'nested';
112
+ nestedRowElement.dataset.gridPath = JSON.stringify([{ rowId: 'outer', cellId: 'outer-cell' }, { rowId: 'nested' }]);
113
+ nestedRowParent.appendChild(nestedRowElement);
114
+ container.appendChild(nestedRowParent);
115
+ (model.gridContainerRef as any).current = container;
116
+ model.onMount();
117
+
118
+ model.emitter.emit('onResizeRight', { resizeDistance: 30, model: itemA });
119
+ model.emitter.emit('onResizeEnd');
120
+
121
+ const nestedRow = model.props.layout.rows[0].cells[0].rows![0];
122
+ expect(nestedRow.sizes).toEqual([18, 6]);
123
+ });
124
+ });
@@ -7,8 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { describe, it, expect, beforeEach } from 'vitest';
11
10
  import { EMPTY_COLUMN_UID, FlowEngine } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
12
  import { GridModel } from '../GridModel';
13
13
 
14
14
  describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
@@ -74,7 +74,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
74
74
  // 第一列只有 hidden,应该被过滤掉,只保留包含 v 的那一列
75
75
  expect(Object.keys(rows)).toEqual(['row1']);
76
76
  expect(rows.row1).toEqual([['v']]);
77
- expect(sizes.row1).toEqual([16]);
77
+ expect(sizes.row1).toEqual([24]);
78
78
  });
79
79
 
80
80
  it('removes entire row when all columns are hidden-only', async () => {
@@ -109,13 +109,14 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
109
109
  it('uses rowOrder when provided to keep row sequence', async () => {
110
110
  await engine.flowSettings.disable();
111
111
 
112
- const visible = engine.createModel({ use: 'FlowModel', uid: 'v1' });
112
+ const visible1 = engine.createModel({ use: 'FlowModel', uid: 'v1' });
113
+ const visible2 = engine.createModel({ use: 'FlowModel', uid: 'v2' });
113
114
  const model = engine.createModel<GridModel>({
114
115
  use: 'GridModel',
115
116
  uid: 'grid-4',
116
117
  props: {
117
118
  rows: {
118
- second: [['v1']],
119
+ second: [['v2']],
119
120
  first: [['v1']],
120
121
  },
121
122
  sizes: {
@@ -127,7 +128,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
127
128
  structure: {} as any,
128
129
  });
129
130
 
130
- (model as any).subModels = { items: [visible] };
131
+ (model as any).subModels = { items: [visible1, visible2] };
131
132
 
132
133
  const { rows } = (model as any).getVisibleLayout();
133
134
  expect(Object.keys(rows)).toEqual(['first', 'second']);
@@ -136,13 +137,14 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
136
137
  it('falls back to rows key order when rowOrder is missing', async () => {
137
138
  await engine.flowSettings.disable();
138
139
 
139
- const visible = engine.createModel({ use: 'FlowModel', uid: 'v1' });
140
+ const visible1 = engine.createModel({ use: 'FlowModel', uid: 'v1' });
141
+ const visible2 = engine.createModel({ use: 'FlowModel', uid: 'v2' });
140
142
  const model = engine.createModel<GridModel>({
141
143
  use: 'GridModel',
142
144
  uid: 'grid-5',
143
145
  props: {
144
146
  rows: {
145
- second: [['v1']],
147
+ second: [['v2']],
146
148
  first: [['v1']],
147
149
  },
148
150
  sizes: {
@@ -153,7 +155,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
153
155
  structure: {} as any,
154
156
  });
155
157
 
156
- (model as any).subModels = { items: [visible] };
158
+ (model as any).subModels = { items: [visible1, visible2] };
157
159
 
158
160
  const { rows } = (model as any).getVisibleLayout();
159
161
  expect(Object.keys(rows)).toEqual(['second', 'first']);
@@ -163,6 +165,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
163
165
  await engine.flowSettings.disable();
164
166
 
165
167
  const visible = engine.createModel({ use: 'FlowModel', uid: 'v' });
168
+ const visible2 = engine.createModel({ use: 'FlowModel', uid: 'v2' });
166
169
  const hidden = engine.createModel({ use: 'FlowModel', uid: 'h' }) as any;
167
170
  hidden.hidden = true;
168
171
 
@@ -171,7 +174,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
171
174
  uid: 'grid-6',
172
175
  props: {
173
176
  rows: {
174
- row1: [['h', 'v'], ['v']],
177
+ row1: [['h', 'v'], ['v2']],
175
178
  },
176
179
  sizes: {
177
180
  row1: [8, 16],
@@ -180,14 +183,51 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
180
183
  structure: {} as any,
181
184
  });
182
185
 
183
- (model as any).subModels = { items: [hidden, visible] };
186
+ (model as any).subModels = { items: [hidden, visible, visible2] };
184
187
 
185
188
  const { rows, sizes } = (model as any).getVisibleLayout();
186
189
  // 第一个单元格中的 h 应被剔除,只剩 v;列宽保持不变
187
- expect(rows.row1).toEqual([['v'], ['v']]);
190
+ expect(rows.row1).toEqual([['v'], ['v2']]);
188
191
  expect(sizes.row1).toEqual([8, 16]);
189
192
  });
190
193
 
194
+ it('preserves remaining column size ratios when filtering v2 layout columns', () => {
195
+ engine.flowSettings.disable();
196
+
197
+ const visible1 = engine.createModel({ use: 'FlowModel', uid: 'v1' });
198
+ const visible2 = engine.createModel({ use: 'FlowModel', uid: 'v2' });
199
+ const hidden = engine.createModel({ use: 'FlowModel', uid: 'h' }) as any;
200
+ hidden.hidden = true;
201
+
202
+ const model = engine.createModel<GridModel>({
203
+ use: 'GridModel',
204
+ uid: 'grid-7',
205
+ props: {
206
+ layout: {
207
+ version: 2,
208
+ rows: [
209
+ {
210
+ id: 'row1',
211
+ cells: [
212
+ { id: 'cell1', items: ['h'] },
213
+ { id: 'cell2', items: ['v1'] },
214
+ { id: 'cell3', items: ['v2'] },
215
+ ],
216
+ sizes: [4, 8, 12],
217
+ },
218
+ ],
219
+ },
220
+ },
221
+ structure: {} as any,
222
+ });
223
+
224
+ (model as any).subModels = { items: [hidden, visible1, visible2] };
225
+
226
+ const { rows, sizes } = (model as any).getVisibleLayout();
227
+ expect(rows.row1).toEqual([['v1'], ['v2']]);
228
+ expect(sizes.row1).toEqual([10, 14]);
229
+ });
230
+
191
231
  it('ignores EMPTY_COLUMN uid in runtime mode without crashing', async () => {
192
232
  await engine.flowSettings.disable();
193
233
 
@@ -195,7 +235,7 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
195
235
 
196
236
  const model = engine.createModel<GridModel>({
197
237
  use: 'GridModel',
198
- uid: 'grid-7',
238
+ uid: 'grid-8',
199
239
  props: {
200
240
  rows: {
201
241
  row1: [[EMPTY_COLUMN_UID, 'ghost', 'v'], ['ghost-2']],
@@ -210,8 +250,8 @@ describe('GridModel.getVisibleLayout (hidden items filtering)', () => {
210
250
  (model as any).subModels = { items: [visible] };
211
251
 
212
252
  const { rows, sizes } = (model as any).getVisibleLayout();
213
- // unknown uid 视为可见(避免误删),但 EMPTY_COLUMN_UID 必须被剔除
214
- expect(rows.row1).toEqual([['ghost', 'v'], ['ghost-2']]);
215
- expect(sizes.row1).toEqual([8, 16]);
253
+ // 新布局归一化会移除不在 subModels.items 中的 uid,EMPTY_COLUMN_UID 也不会在运行态显示
254
+ expect(rows.row1).toEqual([['v']]);
255
+ expect(sizes.row1).toEqual([24]);
216
256
  });
217
257
  });
@@ -8,11 +8,11 @@
8
8
  */
9
9
 
10
10
  import { SettingOutlined } from '@ant-design/icons';
11
- import { AddSubModelButton, FlowSettingsButton, DragOverlayConfig } from '@nocobase/flow-engine';
11
+ import { AddSubModelButton, DragOverlayConfig, FlowSettingsButton } from '@nocobase/flow-engine';
12
+ import { Skeleton } from 'antd';
12
13
  import React from 'react';
13
14
  import { FieldModel, GridModel } from '../../base';
14
15
  import { DetailsBlockModel } from './DetailsBlockModel';
15
- import { Skeleton } from 'antd';
16
16
 
17
17
  export class DetailsGridModel extends GridModel<{
18
18
  parent: DetailsBlockModel;
@@ -32,8 +32,8 @@ export class DetailsGridModel extends GridModel<{
32
32
  dragOverlayConfig: DragOverlayConfig = {
33
33
  // 列内插入
34
34
  columnInsert: {
35
- before: { offsetTop: -12, height: 24 },
36
- after: { offsetTop: 7, height: 24 },
35
+ before: { offsetTop: -6, height: 24 },
36
+ after: { offsetTop: 3, height: 24 },
37
37
  },
38
38
  // 列边缘
39
39
  columnEdge: {
@@ -42,8 +42,8 @@ export class DetailsGridModel extends GridModel<{
42
42
  },
43
43
  // 行间隙
44
44
  rowGap: {
45
- above: { offsetTop: 0, height: 24 },
46
- below: { offsetTop: -14, height: 24 },
45
+ above: { offsetTop: -2, height: 24 },
46
+ below: { offsetTop: -12, height: 24 },
47
47
  },
48
48
  };
49
49
 
@@ -102,15 +102,19 @@ export class FilterFormBlockModel extends FilterBlockModel<{
102
102
  // 首次进入页面:等待子模型 beforeRender 完成(例如 name 初始化),再应用表单级默认值并触发筛选
103
103
  void this.applyDefaultsAndInitialFilter();
104
104
 
105
- // 监听页面区块删除,自动清理已失效的筛选字段
105
+ // 监听页面区块删除,自动清理已失效的筛选字段。
106
+ // 这里使用 onSubModelDestroyed 而不是 onSubModelRemoved,避免弹窗关闭时
107
+ // 的临时模型卸载被误判成“用户删除了目标区块”。
106
108
  const blockGridModel = this.context.blockGridModel;
107
109
  if (blockGridModel?.emitter) {
108
- const handleTargetRemoved = (model) => {
110
+ const handleTargetDestroyed = (model) => {
109
111
  if (!model?.uid || model.uid === this.uid) return;
110
- this.handleTargetBlockRemoved(model.uid);
112
+ void this.handleTargetBlockRemoved(model.uid).catch((error) => {
113
+ console.error('Failed to handle destroyed target block in FilterFormBlockModel:', error);
114
+ });
111
115
  };
112
- blockGridModel.emitter.on('onSubModelRemoved', handleTargetRemoved);
113
- this.removeTargetBlockListener = () => blockGridModel.emitter.off('onSubModelRemoved', handleTargetRemoved);
116
+ blockGridModel.emitter.on('onSubModelDestroyed', handleTargetDestroyed);
117
+ this.removeTargetBlockListener = () => blockGridModel.emitter.off('onSubModelDestroyed', handleTargetDestroyed);
114
118
  }
115
119
  }
116
120