@nocobase/client-v2 2.1.0-alpha.30 → 2.1.0-alpha.31
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.
- package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +90 -90
- package/lib/index.js +87 -87
- package/package.json +5 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/index.ts +1 -0
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -0,0 +1,250 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { BaseRecordResource, FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import {
|
|
13
|
+
applyDisassociateAction,
|
|
14
|
+
ActionModel,
|
|
15
|
+
applyAssociateAction,
|
|
16
|
+
AssociateActionModel,
|
|
17
|
+
CollectionActionGroupModel,
|
|
18
|
+
DisassociateActionModel,
|
|
19
|
+
getAssociationTargetResourceSettings,
|
|
20
|
+
PopupActionModel,
|
|
21
|
+
RecordActionGroupModel,
|
|
22
|
+
} from '../../..';
|
|
23
|
+
|
|
24
|
+
class TestAssociationResource<T = any> extends BaseRecordResource<T> {
|
|
25
|
+
async refresh(): Promise<void> {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createEngine = () => {
|
|
29
|
+
const engine = new FlowEngine();
|
|
30
|
+
engine.registerModels({
|
|
31
|
+
ActionModel,
|
|
32
|
+
AssociateActionModel,
|
|
33
|
+
DisassociateActionModel,
|
|
34
|
+
PopupActionModel,
|
|
35
|
+
CollectionActionGroupModel,
|
|
36
|
+
RecordActionGroupModel,
|
|
37
|
+
});
|
|
38
|
+
return engine;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createContext = (engine: FlowEngine, resourceSettingsInit: any) => {
|
|
42
|
+
const collection = {
|
|
43
|
+
options: {
|
|
44
|
+
availableActions: ['list', 'update'],
|
|
45
|
+
},
|
|
46
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
engine,
|
|
50
|
+
collection,
|
|
51
|
+
blockModel: {
|
|
52
|
+
collection,
|
|
53
|
+
getResourceSettingsInitParams: () => resourceSettingsInit,
|
|
54
|
+
},
|
|
55
|
+
} as any;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe('association action models', () => {
|
|
59
|
+
it('uses update permission for association operations', () => {
|
|
60
|
+
expect(AssociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
61
|
+
expect(DisassociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
62
|
+
expect(AssociateActionModel.capabilityActionName).toBe('update');
|
|
63
|
+
expect(DisassociateActionModel.capabilityActionName).toBe('update');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses association target collection for selector table blocks', () => {
|
|
67
|
+
const ctx: any = {
|
|
68
|
+
blockModel: {
|
|
69
|
+
association: {
|
|
70
|
+
target: 'transports',
|
|
71
|
+
targetCollection: {
|
|
72
|
+
name: 'transports',
|
|
73
|
+
dataSourceKey: 'main',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
getResourceSettingsInitParams: () => ({
|
|
77
|
+
dataSourceKey: 'main',
|
|
78
|
+
collectionName: 'orders',
|
|
79
|
+
associationName: 'products.transports',
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
expect(getAssociationTargetResourceSettings(ctx)).toEqual({
|
|
85
|
+
dataSourceKey: 'main',
|
|
86
|
+
collectionName: 'transports',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows Associate only in collection actions of association blocks', async () => {
|
|
91
|
+
const engine = createEngine();
|
|
92
|
+
|
|
93
|
+
const relationItems = await CollectionActionGroupModel.defineChildren(
|
|
94
|
+
createContext(engine, {
|
|
95
|
+
dataSourceKey: 'main',
|
|
96
|
+
collectionName: 'orders',
|
|
97
|
+
associationName: 'products.o2m_orders',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
const normalItems = await CollectionActionGroupModel.defineChildren(
|
|
101
|
+
createContext(engine, {
|
|
102
|
+
dataSourceKey: 'main',
|
|
103
|
+
collectionName: 'orders',
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('AssociateActionModel');
|
|
108
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
109
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows Disassociate only in record actions of association blocks', async () => {
|
|
113
|
+
const engine = createEngine();
|
|
114
|
+
|
|
115
|
+
const relationItems = await RecordActionGroupModel.defineChildren(
|
|
116
|
+
createContext(engine, {
|
|
117
|
+
dataSourceKey: 'main',
|
|
118
|
+
collectionName: 'orders',
|
|
119
|
+
associationName: 'products.o2m_orders',
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
const normalItems = await RecordActionGroupModel.defineChildren(
|
|
123
|
+
createContext(engine, {
|
|
124
|
+
dataSourceKey: 'main',
|
|
125
|
+
collectionName: 'orders',
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('DisassociateActionModel');
|
|
130
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
131
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('disassociates the current record through association resource remove action', async () => {
|
|
135
|
+
const resource = {
|
|
136
|
+
runAction: vi.fn(async () => ({})),
|
|
137
|
+
refresh: vi.fn(async () => {}),
|
|
138
|
+
};
|
|
139
|
+
const collection = {
|
|
140
|
+
getFilterByTK: vi.fn(() => 12),
|
|
141
|
+
};
|
|
142
|
+
const ctx: any = {
|
|
143
|
+
blockModel: {
|
|
144
|
+
collection,
|
|
145
|
+
resource,
|
|
146
|
+
getResourceSettingsInitParams: () => ({
|
|
147
|
+
dataSourceKey: 'main',
|
|
148
|
+
collectionName: 'orders',
|
|
149
|
+
associationName: 'products.o2m_orders',
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
record: {
|
|
153
|
+
id: 12,
|
|
154
|
+
},
|
|
155
|
+
message: {
|
|
156
|
+
success: vi.fn(),
|
|
157
|
+
error: vi.fn(),
|
|
158
|
+
},
|
|
159
|
+
t: (value: string) => value,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
await applyDisassociateAction(ctx);
|
|
163
|
+
|
|
164
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith(ctx.record);
|
|
165
|
+
expect(resource.runAction).toHaveBeenCalledWith('remove', {
|
|
166
|
+
data: [12],
|
|
167
|
+
});
|
|
168
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
169
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record disassociated successfully');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('associates selected records through association resource add action', async () => {
|
|
173
|
+
const resource = {
|
|
174
|
+
runAction: vi.fn(async () => ({})),
|
|
175
|
+
refresh: vi.fn(async () => {}),
|
|
176
|
+
};
|
|
177
|
+
const collection = {
|
|
178
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
179
|
+
};
|
|
180
|
+
const ctx: any = {
|
|
181
|
+
blockModel: {
|
|
182
|
+
collection,
|
|
183
|
+
resource,
|
|
184
|
+
getResourceSettingsInitParams: () => ({
|
|
185
|
+
dataSourceKey: 'main',
|
|
186
|
+
collectionName: 'orders',
|
|
187
|
+
associationName: 'products.o2m_orders',
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
message: {
|
|
191
|
+
success: vi.fn(),
|
|
192
|
+
warning: vi.fn(),
|
|
193
|
+
error: vi.fn(),
|
|
194
|
+
},
|
|
195
|
+
t: (value: string) => value,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await applyAssociateAction(ctx, [{ id: 11 }, { id: 12 }]);
|
|
199
|
+
|
|
200
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 11 });
|
|
201
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 12 });
|
|
202
|
+
expect(resource.runAction).toHaveBeenCalledWith('add', {
|
|
203
|
+
data: [11, 12],
|
|
204
|
+
});
|
|
205
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
206
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record associated successfully');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('uses nested association resource url when adding associated records', async () => {
|
|
210
|
+
const engine = createEngine();
|
|
211
|
+
const resource = engine.createResource(TestAssociationResource);
|
|
212
|
+
const api = {
|
|
213
|
+
request: vi.fn().mockResolvedValue({ data: { data: {} } }),
|
|
214
|
+
};
|
|
215
|
+
resource.setAPIClient(api as any);
|
|
216
|
+
resource.setResourceName('products.o2m_orders');
|
|
217
|
+
resource.setSourceId('362872646860800');
|
|
218
|
+
|
|
219
|
+
const ctx: any = {
|
|
220
|
+
blockModel: {
|
|
221
|
+
collection: {
|
|
222
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
223
|
+
},
|
|
224
|
+
resource,
|
|
225
|
+
getResourceSettingsInitParams: () => ({
|
|
226
|
+
dataSourceKey: 'main',
|
|
227
|
+
collectionName: 'orders',
|
|
228
|
+
associationName: 'products.o2m_orders',
|
|
229
|
+
sourceId: '362872646860800',
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
message: {
|
|
233
|
+
success: vi.fn(),
|
|
234
|
+
warning: vi.fn(),
|
|
235
|
+
error: vi.fn(),
|
|
236
|
+
},
|
|
237
|
+
t: (value: string) => value,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await applyAssociateAction(ctx, [{ id: 11 }]);
|
|
241
|
+
|
|
242
|
+
expect(api.request).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
method: 'post',
|
|
245
|
+
url: 'products/362872646860800/o2m_orders:add',
|
|
246
|
+
data: [11],
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export * from './AddNewActionModel';
|
|
11
|
+
export * from './AssociateActionModel';
|
|
12
|
+
export * from './AssociationActionUtils';
|
|
11
13
|
export * from './BulkDeleteActionModel';
|
|
12
14
|
export * from './DeleteActionModel';
|
|
15
|
+
export * from './DisassociateActionModel';
|
|
13
16
|
export * from './EditActionModel';
|
|
14
17
|
export * from './FilterActionModel';
|
|
15
18
|
export * from './JSActionModel';
|
|
@@ -118,6 +118,14 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
118
118
|
private dragState?: DragState;
|
|
119
119
|
private _memoItemFlowSettings?: Exclude<FlowModelRendererProps['showFlowSettings'], boolean>;
|
|
120
120
|
|
|
121
|
+
onInit(options) {
|
|
122
|
+
super.onInit(options);
|
|
123
|
+
// 历史数据里可能残留拖拽高亮框,初始化时立即清理,避免刷新页面后常驻显示。
|
|
124
|
+
if (this.props.dragOverlayRect) {
|
|
125
|
+
this.setProps('dragOverlayRect', null);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
121
129
|
private updateDragPointerPosition = (event: Event) => {
|
|
122
130
|
if (!this.dragState) {
|
|
123
131
|
return;
|
|
@@ -243,6 +251,12 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
243
251
|
return this.normalizeLayoutFromSource();
|
|
244
252
|
}
|
|
245
253
|
|
|
254
|
+
serialize(): Record<string, any> {
|
|
255
|
+
const data = super.serialize();
|
|
256
|
+
data.props = _.omit(data.props, ['dragOverlayRect']);
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
|
|
246
260
|
syncLayoutProps(layout: GridLayoutV2) {
|
|
247
261
|
const projection = projectLayoutToLegacyRows(layout);
|
|
248
262
|
this.setProps('layout', layout);
|
|
@@ -801,11 +815,17 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
801
815
|
this.setProps('dragOverlayRect', null);
|
|
802
816
|
}
|
|
803
817
|
|
|
804
|
-
handleDragEnd(
|
|
818
|
+
handleDragEnd(event: DragEndEvent) {
|
|
805
819
|
if (!this.dragState) {
|
|
806
820
|
return;
|
|
807
821
|
}
|
|
808
822
|
|
|
823
|
+
const finalPoint = this.computePointerPosition(event);
|
|
824
|
+
if (finalPoint) {
|
|
825
|
+
const finalSlot = this.resolveDragSlot(finalPoint);
|
|
826
|
+
this.applyPreview(finalSlot);
|
|
827
|
+
}
|
|
828
|
+
|
|
809
829
|
const previewLayout = this.dragState.previewLayout;
|
|
810
830
|
if (previewLayout) {
|
|
811
831
|
if (previewLayout.layout) {
|
|
@@ -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
|
});
|
|
@@ -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
|
|
75
|
-
|
|
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
|
|
317
|
+
const fullLayout =
|
|
318
|
+
layout ||
|
|
319
|
+
normalizeGridLayout({
|
|
320
|
+
rows: fullRows,
|
|
321
|
+
rowOrder,
|
|
322
|
+
itemUids: this.getItemUids(),
|
|
323
|
+
});
|
|
163
324
|
const limitedLayout = normalizeGridLayout({
|
|
164
|
-
|
|
165
|
-
|
|
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
|
}
|