@nocobase/client-v2 2.1.0-alpha.30 → 2.1.0-alpha.32

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 (103) hide show
  1. package/es/components/form/JsonTextArea.d.ts +18 -0
  2. package/es/components/index.d.ts +1 -0
  3. package/es/flow/actions/dateRangeLimit.d.ts +9 -0
  4. package/es/flow/actions/index.d.ts +1 -0
  5. package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
  6. package/es/flow/index.d.ts +1 -0
  7. package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
  8. package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
  9. package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
  10. package/es/flow/models/actions/index.d.ts +3 -0
  11. package/es/flow/models/base/GridModel.d.ts +3 -1
  12. package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
  13. package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
  14. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
  15. package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
  16. package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
  17. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
  18. package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
  19. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
  20. package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -0
  21. package/es/flow-compat/data.d.ts +9 -2
  22. package/es/flow-compat/index.d.ts +1 -1
  23. package/es/index.d.ts +1 -0
  24. package/es/index.mjs +100 -93
  25. package/lib/index.js +101 -94
  26. package/package.json +6 -5
  27. package/src/BaseApplication.tsx +1 -1
  28. package/src/__tests__/app.test.tsx +23 -6
  29. package/src/__tests__/globalDeps.test.ts +5 -0
  30. package/src/components/form/JsonTextArea.tsx +129 -0
  31. package/src/components/index.ts +1 -0
  32. package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
  33. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
  34. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
  35. package/src/flow/actions/__tests__/pattern.test.ts +190 -0
  36. package/src/flow/actions/dateRangeLimit.tsx +66 -0
  37. package/src/flow/actions/index.ts +1 -0
  38. package/src/flow/actions/linkageRules.tsx +119 -14
  39. package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
  40. package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
  41. package/src/flow/actions/openView.tsx +2 -1
  42. package/src/flow/actions/pattern.tsx +25 -2
  43. package/src/flow/actions/titleField.tsx +8 -3
  44. package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
  45. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
  46. package/src/flow/components/FieldAssignValueInput.tsx +1 -0
  47. package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
  48. package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
  49. package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
  50. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
  51. package/src/flow/index.ts +1 -0
  52. package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
  53. package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
  54. package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
  55. package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
  56. package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
  57. package/src/flow/models/actions/FilterActionModel.tsx +17 -9
  58. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
  59. package/src/flow/models/actions/index.ts +3 -0
  60. package/src/flow/models/base/GridModel.tsx +21 -1
  61. package/src/flow/models/base/PageModel/PageModel.tsx +15 -3
  62. package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
  63. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
  64. package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -0
  65. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
  66. package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
  67. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
  68. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
  69. package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
  70. package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
  71. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
  72. package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
  73. package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
  74. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
  75. package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
  76. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +167 -1
  77. package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
  78. package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
  79. package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
  80. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
  81. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +48 -8
  82. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
  83. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
  84. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
  85. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
  86. package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
  87. package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
  88. package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
  89. package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
  90. package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
  91. package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
  92. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
  93. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
  94. package/src/flow/models/fields/DividerItemModel.tsx +30 -15
  95. package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
  96. package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
  97. package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
  98. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
  99. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
  100. package/src/flow-compat/data.ts +25 -3
  101. package/src/flow-compat/index.ts +7 -1
  102. package/src/index.ts +1 -0
  103. package/src/utils/globalDeps.ts +6 -0
@@ -12,10 +12,32 @@ import { RootPageModel } from '../RootPageModel';
12
12
 
13
13
  // Mock PageModel
14
14
  const mockPageModelSaveStepParams = vi.fn();
15
+ const mockPageModelOpenFlowSettings = vi.fn();
15
16
  vi.mock('../PageModel', () => ({
16
17
  PageModel: class {
18
+ props: any = {};
19
+ stepParams: any = {};
20
+
17
21
  static registerFlow() {}
18
22
 
23
+ setProps(key: string, value: any) {
24
+ this.props[key] = value;
25
+ }
26
+
27
+ setStepParams(flowKey: string, stepKey: string, params: Record<string, any>) {
28
+ if (!this.stepParams[flowKey]) {
29
+ this.stepParams[flowKey] = {};
30
+ }
31
+ this.stepParams[flowKey][stepKey] = {
32
+ ...this.stepParams[flowKey][stepKey],
33
+ ...params,
34
+ };
35
+ }
36
+
37
+ async openFlowSettings(options?: any) {
38
+ return mockPageModelOpenFlowSettings(options);
39
+ }
40
+
19
41
  async saveStepParams() {
20
42
  return mockPageModelSaveStepParams();
21
43
  }
@@ -26,6 +48,7 @@ describe('RootPageModel', () => {
26
48
  let rootPageModel: RootPageModel;
27
49
  let mockContext: any;
28
50
  let mockApi: any;
51
+ let mockRefreshDesktopRoutes: any;
29
52
  let mockFlowEngine: any;
30
53
 
31
54
  beforeEach(() => {
@@ -35,6 +58,7 @@ describe('RootPageModel', () => {
35
58
  mockApi = {
36
59
  request: vi.fn().mockResolvedValue({ data: { success: true } }),
37
60
  };
61
+ mockRefreshDesktopRoutes = vi.fn().mockResolvedValue(undefined);
38
62
 
39
63
  // Mock FlowEngine
40
64
  mockFlowEngine = {
@@ -45,6 +69,11 @@ describe('RootPageModel', () => {
45
69
  // Mock context
46
70
  mockContext = {
47
71
  api: mockApi,
72
+ refreshDesktopRoutes: mockRefreshDesktopRoutes,
73
+ currentRoute: {
74
+ id: 'route-123',
75
+ enableTabs: true,
76
+ },
48
77
  };
49
78
 
50
79
  // Create RootPageModel instance
@@ -63,6 +92,65 @@ describe('RootPageModel', () => {
63
92
  };
64
93
  });
65
94
 
95
+ describe('openFlowSettings', () => {
96
+ it('should use desktop route enableTabs as settings dialog initial value', async () => {
97
+ mockContext.currentRoute.enableTabs = false;
98
+ (rootPageModel as any).stepParams = {
99
+ pageSettings: {
100
+ general: {
101
+ displayTitle: true,
102
+ enableTabs: true,
103
+ },
104
+ },
105
+ };
106
+
107
+ await rootPageModel.openFlowSettings({ flowKey: 'pageSettings', stepKey: 'general' } as any);
108
+
109
+ expect((rootPageModel as any).stepParams.pageSettings.general).toMatchObject({
110
+ displayTitle: true,
111
+ enableTabs: false,
112
+ });
113
+ expect(mockPageModelOpenFlowSettings).toHaveBeenCalledWith({
114
+ flowKey: 'pageSettings',
115
+ stepKey: 'general',
116
+ });
117
+ });
118
+
119
+ it('should keep flow model enableTabs when route status is unavailable', async () => {
120
+ mockContext.currentRoute = {};
121
+ (rootPageModel as any).stepParams = {
122
+ pageSettings: {
123
+ general: {
124
+ enableTabs: true,
125
+ },
126
+ },
127
+ };
128
+
129
+ await rootPageModel.openFlowSettings({ flowKey: 'pageSettings', stepKey: 'general' } as any);
130
+
131
+ expect((rootPageModel as any).stepParams.pageSettings.general.enableTabs).toBe(true);
132
+ });
133
+
134
+ it('should not sync enableTabs when opening other settings steps', async () => {
135
+ mockContext.currentRoute.enableTabs = false;
136
+ (rootPageModel as any).stepParams = {
137
+ pageSettings: {
138
+ general: {
139
+ enableTabs: true,
140
+ },
141
+ },
142
+ };
143
+
144
+ await rootPageModel.openFlowSettings({ flowKey: 'otherSettings', stepKey: 'general' } as any);
145
+
146
+ expect((rootPageModel as any).stepParams.pageSettings.general.enableTabs).toBe(true);
147
+ expect(mockPageModelOpenFlowSettings).toHaveBeenCalledWith({
148
+ flowKey: 'otherSettings',
149
+ stepKey: 'general',
150
+ });
151
+ });
152
+ });
153
+
66
154
  describe('saveStepParams', () => {
67
155
  it('should call parent saveStepParams method', async () => {
68
156
  await rootPageModel.saveStepParams();
@@ -89,6 +177,34 @@ describe('RootPageModel', () => {
89
177
  },
90
178
  });
91
179
  });
180
+
181
+ it('should refresh desktop routes after route update is persisted', async () => {
182
+ await rootPageModel.saveStepParams();
183
+
184
+ expect(mockApi.request).toHaveBeenCalledTimes(1);
185
+ expect(mockRefreshDesktopRoutes).toHaveBeenCalledTimes(1);
186
+ expect(mockApi.request.mock.invocationCallOrder[0]).toBeLessThan(
187
+ mockRefreshDesktopRoutes.mock.invocationCallOrder[0],
188
+ );
189
+ });
190
+
191
+ it('should apply enableTabs to current page immediately after route update is persisted', async () => {
192
+ (rootPageModel as any).stepParams = {
193
+ pageSettings: {
194
+ general: {
195
+ enableTabs: false,
196
+ },
197
+ },
198
+ };
199
+
200
+ await rootPageModel.saveStepParams();
201
+
202
+ expect(mockContext.currentRoute.enableTabs).toBe(false);
203
+ expect((rootPageModel as any).props.enableTabs).toBe(false);
204
+ expect(mockApi.request.mock.invocationCallOrder[0]).toBeLessThan(
205
+ mockRefreshDesktopRoutes.mock.invocationCallOrder[0],
206
+ );
207
+ });
92
208
  });
93
209
 
94
210
  describe('handleDragEnd', () => {
@@ -283,4 +283,102 @@ describe('GridModel drag snapshot container', () => {
283
283
  type: 'column',
284
284
  });
285
285
  });
286
+
287
+ it('clears persisted drag overlay on init', () => {
288
+ const model = engine.createModel<GridModel>({
289
+ use: 'GridModel',
290
+ uid: 'grid-hidden-stale-overlay',
291
+ props: {
292
+ dragOverlayRect: {
293
+ top: 10,
294
+ left: 20,
295
+ width: 100,
296
+ height: 40,
297
+ type: 'column',
298
+ },
299
+ },
300
+ structure: {} as any,
301
+ });
302
+
303
+ expect(model.props.dragOverlayRect).toBeNull();
304
+ });
305
+
306
+ it('omits drag overlay rect from serialized props', () => {
307
+ const model = engine.createModel<GridModel>({
308
+ use: 'GridModel',
309
+ uid: 'grid-serialize-overlay',
310
+ props: {
311
+ rowGap: 24,
312
+ dragOverlayRect: {
313
+ top: 10,
314
+ left: 20,
315
+ width: 100,
316
+ height: 40,
317
+ type: 'column',
318
+ },
319
+ },
320
+ structure: {} as any,
321
+ });
322
+
323
+ const serialized = model.serialize();
324
+
325
+ expect(serialized.props).toMatchObject({ rowGap: 24 });
326
+ expect(serialized.props).not.toHaveProperty('dragOverlayRect');
327
+ });
328
+
329
+ it('recomputes the final slot from drag end position before saving layout', () => {
330
+ const model = engine.createModel<GridModel>({
331
+ use: 'GridModel',
332
+ uid: 'grid-drag-final-slot',
333
+ props: {},
334
+ structure: {} as any,
335
+ });
336
+ const applyPreview = vi.fn((slot) => {
337
+ (model as any).dragState.previewLayout = slot
338
+ ? {
339
+ rows: { 'row-final': [['item-1']] },
340
+ sizes: { 'row-final': [24] },
341
+ }
342
+ : undefined;
343
+ });
344
+ const resolveDragSlot = vi.fn(() => ({
345
+ type: 'row-gap',
346
+ targetRowId: 'row-final',
347
+ position: 'below',
348
+ rect: { top: 200, left: 20, width: 440, height: 32 },
349
+ }));
350
+ const saveGridLayout = vi.fn();
351
+ const syncLayoutProps = vi.fn();
352
+ const finishDrag = vi.fn();
353
+
354
+ (model as any).applyPreview = applyPreview;
355
+ (model as any).resolveDragSlot = resolveDragSlot;
356
+ (model as any).saveGridLayout = saveGridLayout;
357
+ (model as any).syncLayoutProps = syncLayoutProps;
358
+ (model as any).finishDrag = finishDrag;
359
+ (model as any).dragState = {
360
+ sourceUid: 'item-1',
361
+ snapshot: { rows: {}, sizes: {} },
362
+ slots: [],
363
+ containerEl: null,
364
+ containerRect: { top: 0, left: 0, width: 0, height: 0 },
365
+ pointerOrigin: { x: 100, y: 100 },
366
+ activeSlotKey: null,
367
+ previewLayout: undefined,
368
+ refreshTimer: null,
369
+ generatedIds: new Map(),
370
+ };
371
+
372
+ model.handleDragEnd({
373
+ delta: { x: 30, y: 220 },
374
+ } as any);
375
+
376
+ expect(resolveDragSlot).toHaveBeenCalledWith({ x: 130, y: 320 });
377
+ expect(applyPreview).toHaveBeenCalledOnce();
378
+ expect(saveGridLayout).toHaveBeenCalledWith({
379
+ rows: { 'row-final': [['item-1']] },
380
+ sizes: { 'row-final': [24] },
381
+ });
382
+ expect(finishDrag).toHaveBeenCalledOnce();
383
+ });
286
384
  });
@@ -379,6 +379,9 @@ DetailsItemModel.registerFlow({
379
379
  steps: {
380
380
  aclCheckRefresh: {
381
381
  use: 'aclCheckRefresh',
382
+ defaultParams: {
383
+ strategy: 'formItem',
384
+ },
382
385
  },
383
386
  },
384
387
  });
@@ -12,7 +12,10 @@ import {
12
12
  AddSubModelButton,
13
13
  DragOverlayConfig,
14
14
  FlowSettingsButton,
15
+ GridCellV2,
16
+ GridLayoutData,
15
17
  GridLayoutV2,
18
+ GridRowV2,
16
19
  normalizeGridLayout,
17
20
  observable,
18
21
  projectLayoutToLegacyRows,
@@ -24,6 +27,8 @@ import { FilterFormItemModel } from './FilterFormItemModel';
24
27
 
25
28
  export class FilterFormGridModel extends GridModel {
26
29
  private fullLayoutBeforeCollapse?: GridLayoutV2;
30
+ private normalizedItemUidsOverride?: string[];
31
+ private readingFullLayoutForSettingsInteraction = false;
27
32
  itemSettingsMenuLevel = 2;
28
33
  itemFlowSettings = {
29
34
  showBackground: true,
@@ -64,6 +69,182 @@ export class FilterFormGridModel extends GridModel {
64
69
  return filterTargetKey || 'id';
65
70
  }
66
71
 
72
+ private normalizeFilterFormLayout(
73
+ source?: Partial<GridLayoutData>,
74
+ options?: {
75
+ useVisibleItemUids?: boolean;
76
+ },
77
+ ): GridLayoutV2 {
78
+ const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
79
+ const useVisibleItemUids = options?.useVisibleItemUids !== false;
80
+
81
+ return normalizeGridLayout({
82
+ layout: source?.layout ?? this.props.layout ?? params.layout,
83
+ rows: source?.rows ?? this.props.rows ?? params.rows,
84
+ sizes: source?.sizes ?? this.props.sizes ?? params.sizes,
85
+ rowOrder: source?.rowOrder ?? this.props.rowOrder ?? params.rowOrder,
86
+ // 折叠态只保留当前可见字段,避免归一化时把被裁掉的字段重新补回布局。
87
+ itemUids: useVisibleItemUids ? this.normalizedItemUidsOverride ?? this.getItemUids() : this.getItemUids(),
88
+ gridUid: this.uid,
89
+ logger: console,
90
+ });
91
+ }
92
+
93
+ private normalizeStoredFullLayout(): GridLayoutV2 {
94
+ const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
95
+
96
+ return normalizeGridLayout({
97
+ layout: this.fullLayoutBeforeCollapse ?? params.layout ?? this.props.layout,
98
+ rows: params.rows ?? this.props.rows,
99
+ sizes: params.sizes ?? this.props.sizes,
100
+ rowOrder: params.rowOrder ?? this.props.rowOrder,
101
+ itemUids: this.getItemUids(),
102
+ gridUid: this.uid,
103
+ logger: console,
104
+ });
105
+ }
106
+
107
+ protected override normalizeLayoutFromSource(source?: Partial<GridLayoutData>): GridLayoutV2 {
108
+ // 只有运行时读取当前展示布局时才应用折叠态可见字段覆盖;
109
+ // 带 source 的路径通常用于重算/持久化布局,必须始终基于完整字段集。
110
+ if (!source && this.readingFullLayoutForSettingsInteraction) {
111
+ return this.normalizeStoredFullLayout();
112
+ }
113
+
114
+ return this.normalizeFilterFormLayout(source, {
115
+ useVisibleItemUids: !source && !this.readingFullLayoutForSettingsInteraction,
116
+ });
117
+ }
118
+
119
+ getGridLayout(): GridLayoutV2 {
120
+ if (this.context.flowSettingsEnabled && this.normalizedItemUidsOverride) {
121
+ return this.normalizeStoredFullLayout();
122
+ }
123
+
124
+ return super.getGridLayout();
125
+ }
126
+
127
+ saveGridLayout(layout?: GridLayoutData | GridLayoutV2) {
128
+ super.saveGridLayout(layout);
129
+
130
+ if (this.context.flowSettingsEnabled && this.normalizedItemUidsOverride) {
131
+ const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
132
+ this.fullLayoutBeforeCollapse = normalizeGridLayout({
133
+ layout: params.layout ?? this.props.layout,
134
+ rows: params.rows ?? this.props.rows,
135
+ sizes: params.sizes ?? this.props.sizes,
136
+ rowOrder: params.rowOrder ?? this.props.rowOrder,
137
+ itemUids: this.getItemUids(),
138
+ gridUid: this.uid,
139
+ logger: console,
140
+ });
141
+ }
142
+ }
143
+
144
+ handleDragStart(event: Parameters<GridModel['handleDragStart']>[0]) {
145
+ this.readingFullLayoutForSettingsInteraction = Boolean(
146
+ this.context.flowSettingsEnabled && this.normalizedItemUidsOverride,
147
+ );
148
+ try {
149
+ super.handleDragStart(event);
150
+ } finally {
151
+ this.readingFullLayoutForSettingsInteraction = false;
152
+ }
153
+ }
154
+
155
+ private collectLayoutItemUids(rows: GridLayoutV2['rows']) {
156
+ const itemUids = new Set<string>();
157
+
158
+ const visitRows = (currentRows: GridLayoutV2['rows']) => {
159
+ currentRows.forEach((row) => {
160
+ row.cells.forEach((cell) => {
161
+ cell.items?.forEach((uid) => {
162
+ if (uid && this.flowEngine.getModel(uid)) {
163
+ itemUids.add(uid);
164
+ }
165
+ });
166
+
167
+ if (cell.rows?.length) {
168
+ visitRows(cell.rows);
169
+ }
170
+ });
171
+ });
172
+ };
173
+
174
+ visitRows(rows);
175
+
176
+ return Array.from(itemUids);
177
+ }
178
+
179
+ private getCellVisibleRowCount(cell: GridCellV2): number {
180
+ if (cell.rows?.length) {
181
+ return cell.rows.reduce((count, row) => count + this.getRowVisibleRowCount(row), 0);
182
+ }
183
+
184
+ return cell.items?.length || 0;
185
+ }
186
+
187
+ private getRowVisibleRowCount(row: GridRowV2): number {
188
+ return row.cells.reduce((count, cell) => Math.max(count, this.getCellVisibleRowCount(cell)), 0);
189
+ }
190
+
191
+ private limitCellByVisibleCount(cell: GridCellV2, visibleRows: number): GridCellV2 | null {
192
+ if (visibleRows <= 0) {
193
+ return null;
194
+ }
195
+
196
+ if (cell.rows?.length) {
197
+ const rows = this.limitLayoutRowsByVisibleCount(cell.rows, visibleRows);
198
+ return rows.length ? { id: cell.id, rows } : null;
199
+ }
200
+
201
+ if (cell.items) {
202
+ const items = cell.items.slice(0, visibleRows);
203
+ return items.length ? { id: cell.id, items } : null;
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ private limitRowByVisibleCount(row: GridRowV2, visibleRows: number): GridRowV2 | null {
210
+ const cellsWithSizes = row.cells
211
+ .map((cell, index) => {
212
+ const limitedCell = this.limitCellByVisibleCount(cell, visibleRows);
213
+ return limitedCell ? { cell: limitedCell, size: row.sizes?.[index] } : null;
214
+ })
215
+ .filter(Boolean) as { cell: GridCellV2; size?: number }[];
216
+
217
+ if (!cellsWithSizes.length) {
218
+ return null;
219
+ }
220
+
221
+ return {
222
+ id: row.id,
223
+ cells: cellsWithSizes.map((entry) => entry.cell),
224
+ sizes: cellsWithSizes.map((entry) => entry.size ?? 1),
225
+ };
226
+ }
227
+
228
+ private limitLayoutRowsByVisibleCount(rows: GridLayoutV2['rows'], visibleRows: number): GridLayoutV2['rows'] {
229
+ const limitedRows: GridLayoutV2['rows'] = [];
230
+ let remainingRows = visibleRows;
231
+
232
+ rows.forEach((row) => {
233
+ if (remainingRows <= 0) {
234
+ return;
235
+ }
236
+
237
+ const rowHeight = this.getRowVisibleRowCount(row);
238
+ const limitedRow = this.limitRowByVisibleCount(row, Math.min(rowHeight, remainingRows));
239
+ if (limitedRow) {
240
+ limitedRows.push(limitedRow);
241
+ remainingRows -= Math.min(rowHeight, remainingRows);
242
+ }
243
+ });
244
+
245
+ return limitedRows;
246
+ }
247
+
67
248
  /**
68
249
  * 获取筛选表单当前“完整布局”。
69
250
  * 折叠态会临时裁剪 props.rows,因此这里优先选取行数更多的那份布局,
@@ -71,8 +252,12 @@ export class FilterFormGridModel extends GridModel {
71
252
  */
72
253
  private getFullLayout() {
73
254
  const params = this.getStepParams(GRID_FLOW_KEY, GRID_STEP) || {};
74
- const currentLayout = this.props.layout ? this.getGridLayout() : undefined;
75
- const savedLayout = params.layout ? this.normalizeLayoutFromSource(params) : undefined;
255
+ const currentLayout = this.props.layout
256
+ ? this.normalizeFilterFormLayout(undefined, { useVisibleItemUids: false })
257
+ : undefined;
258
+ const savedLayout = params.layout
259
+ ? this.normalizeFilterFormLayout(params, { useVisibleItemUids: false })
260
+ : undefined;
76
261
  const currentProjection = currentLayout
77
262
  ? projectLayoutToLegacyRows(currentLayout)
78
263
  : { rows: this.props.rows || {}, rowOrder: this.props.rowOrder };
@@ -103,41 +288,11 @@ export class FilterFormGridModel extends GridModel {
103
288
  };
104
289
  }
105
290
 
106
- /**
107
- * 按“可视字段行数”裁剪布局,而不是只按 grid row 数裁剪。
108
- * 这样即使拖拽后多个字段被排进同一个 cell,也仍然可以正确折叠。
109
- */
110
- private limitRowsByVisibleCount(
111
- rows: Record<string, string[][]>,
112
- rowOrder: string[],
113
- visibleRows: number,
114
- ): Record<string, string[][]> {
115
- const limitedRows: Record<string, string[][]> = {};
116
- let remainingRows = visibleRows;
117
-
118
- rowOrder.forEach((rowKey) => {
119
- if (remainingRows <= 0 || !rows[rowKey]) {
120
- return;
121
- }
122
-
123
- const cells = rows[rowKey];
124
- const rowHeight = cells.reduce((max, cell) => Math.max(max, cell.length), 0);
125
- const visibleCount = Math.min(rowHeight, remainingRows);
126
- const nextCells = cells.map((cell) => cell.slice(0, visibleCount));
127
-
128
- if (nextCells.some((cell) => cell.length > 0)) {
129
- limitedRows[rowKey] = nextCells;
130
- remainingRows -= visibleCount;
131
- }
132
- });
133
-
134
- return limitedRows;
135
- }
136
-
137
291
  toggleFormFieldsCollapse(collapse: boolean, visibleRows: number) {
138
292
  const { rows: fullRows, rowOrder, layout } = this.getFullLayout();
139
293
 
140
294
  if (!collapse) {
295
+ this.normalizedItemUidsOverride = undefined;
141
296
  const restoredLayout = this.fullLayoutBeforeCollapse || layout;
142
297
  if (restoredLayout) {
143
298
  this.syncLayoutProps(restoredLayout);
@@ -159,12 +314,21 @@ export class FilterFormGridModel extends GridModel {
159
314
  });
160
315
  }
161
316
 
162
- const limitedRows = this.limitRowsByVisibleCount(fullRows, rowOrder, visibleRows);
317
+ const fullLayout =
318
+ layout ||
319
+ normalizeGridLayout({
320
+ rows: fullRows,
321
+ rowOrder,
322
+ itemUids: this.getItemUids(),
323
+ });
163
324
  const limitedLayout = normalizeGridLayout({
164
- rows: limitedRows,
165
- rowOrder,
325
+ layout: {
326
+ version: 2,
327
+ rows: this.limitLayoutRowsByVisibleCount(fullLayout.rows, visibleRows),
328
+ },
166
329
  });
167
330
 
331
+ this.normalizedItemUidsOverride = this.collectLayoutItemUids(limitedLayout.rows);
168
332
  this.syncLayoutProps(limitedLayout);
169
333
  this.setProps('rowOrder', rowOrder);
170
334
  }