@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.27
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/components/code-editor/types.d.ts +1 -0
- 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-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -1
- package/es/index.mjs +90 -90
- package/lib/index.js +83 -83
- package/package.json +5 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +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/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- 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/DividerItemModel.tsx +30 -15
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -1
|
@@ -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
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
-
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { FlowEngine, projectLayoutToLegacyRows } from '@nocobase/flow-engine';
|
|
12
12
|
import '../../../../index';
|
|
13
13
|
import { GRID_FLOW_KEY, GRID_STEP } from '../../../base';
|
|
14
14
|
import { FilterFormGridModel } from '../FilterFormGridModel';
|
|
@@ -147,6 +147,71 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
|
|
|
147
147
|
expect(model.props.layout.rows[0].cells[0].items).toEqual(['field-1', 'field-2', 'field-3']);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
it('keeps nested columns inside the first visible row when collapsing a v2 layout', () => {
|
|
151
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
152
|
+
uid: 'filter-grid-collapse-nested-v2',
|
|
153
|
+
use: 'FilterFormGridModel',
|
|
154
|
+
props: {
|
|
155
|
+
layout: {
|
|
156
|
+
version: 2,
|
|
157
|
+
rows: [
|
|
158
|
+
{
|
|
159
|
+
id: 'first',
|
|
160
|
+
cells: [
|
|
161
|
+
{
|
|
162
|
+
id: 'first-cell',
|
|
163
|
+
rows: [
|
|
164
|
+
{
|
|
165
|
+
id: 'first-nested-row',
|
|
166
|
+
cells: [
|
|
167
|
+
{ id: 'first-nested-cell-1', items: ['field-1'] },
|
|
168
|
+
{ id: 'first-nested-cell-2', items: ['field-2'] },
|
|
169
|
+
],
|
|
170
|
+
sizes: [12, 12],
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
sizes: [24],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'second',
|
|
179
|
+
cells: [{ id: 'second-cell', items: ['field-3'] }],
|
|
180
|
+
sizes: [24],
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
structure: {} as any,
|
|
186
|
+
});
|
|
187
|
+
(model as any).subModels = {
|
|
188
|
+
items: [
|
|
189
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
190
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
191
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-3' }),
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
196
|
+
layout: model.props.layout,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
200
|
+
|
|
201
|
+
const collapsedNestedRow = model.props.layout.rows[0].cells[0].rows?.[0];
|
|
202
|
+
expect(collapsedNestedRow?.cells.map((cell) => cell.items)).toEqual([['field-1'], ['field-2']]);
|
|
203
|
+
expect(collapsedNestedRow?.sizes).toEqual([12, 12]);
|
|
204
|
+
expect(model.props.layout.rows).toHaveLength(1);
|
|
205
|
+
|
|
206
|
+
model.toggleFormFieldsCollapse(false, 1);
|
|
207
|
+
|
|
208
|
+
expect(model.props.layout.rows).toHaveLength(2);
|
|
209
|
+
expect(model.props.layout.rows[0].cells[0].rows?.[0].cells.map((cell) => cell.items)).toEqual([
|
|
210
|
+
['field-1'],
|
|
211
|
+
['field-2'],
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
|
|
150
215
|
it('restores the persisted full layout when current props rows were already truncated', () => {
|
|
151
216
|
const model = engine.createModel<FilterFormGridModel>({
|
|
152
217
|
uid: 'filter-grid-collapse-restore',
|
|
@@ -185,4 +250,208 @@ describe('FilterFormGridModel.toggleFormFieldsCollapse', () => {
|
|
|
185
250
|
});
|
|
186
251
|
expect(model.props.rowOrder).toEqual(['first', 'second', 'third']);
|
|
187
252
|
});
|
|
253
|
+
|
|
254
|
+
it('does not reinsert collapsed items when the render layout is normalized again', () => {
|
|
255
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
256
|
+
uid: 'filter-grid-collapse-visible-item-uids',
|
|
257
|
+
use: 'FilterFormGridModel',
|
|
258
|
+
props: {
|
|
259
|
+
rows: {
|
|
260
|
+
first: [['field-1']],
|
|
261
|
+
second: [['field-2']],
|
|
262
|
+
third: [['field-3']],
|
|
263
|
+
},
|
|
264
|
+
rowOrder: ['first', 'second', 'third'],
|
|
265
|
+
},
|
|
266
|
+
structure: {} as any,
|
|
267
|
+
});
|
|
268
|
+
(model as any).subModels = {
|
|
269
|
+
items: [
|
|
270
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
271
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
272
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-3' }),
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
277
|
+
rows: {
|
|
278
|
+
first: [['field-1']],
|
|
279
|
+
second: [['field-2']],
|
|
280
|
+
third: [['field-3']],
|
|
281
|
+
},
|
|
282
|
+
rowOrder: ['first', 'second', 'third'],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
286
|
+
|
|
287
|
+
expect(projectLayoutToLegacyRows(model.getGridLayout()).rows).toEqual({
|
|
288
|
+
first: [['field-1']],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
model.toggleFormFieldsCollapse(false, 1);
|
|
292
|
+
|
|
293
|
+
expect(projectLayoutToLegacyRows(model.getGridLayout()).rows).toEqual({
|
|
294
|
+
first: [['field-1']],
|
|
295
|
+
second: [['field-2']],
|
|
296
|
+
third: [['field-3']],
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('keeps the persisted layout intact when resetRows runs during collapsed mode', () => {
|
|
301
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
302
|
+
uid: 'filter-grid-collapse-reset-rows',
|
|
303
|
+
use: 'FilterFormGridModel',
|
|
304
|
+
props: {
|
|
305
|
+
rows: {
|
|
306
|
+
first: [['field-1']],
|
|
307
|
+
second: [['field-2']],
|
|
308
|
+
third: [['field-3']],
|
|
309
|
+
},
|
|
310
|
+
rowOrder: ['first', 'second', 'third'],
|
|
311
|
+
},
|
|
312
|
+
structure: {} as any,
|
|
313
|
+
});
|
|
314
|
+
(model as any).subModels = {
|
|
315
|
+
items: [
|
|
316
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
317
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
318
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-3' }),
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
323
|
+
rows: {
|
|
324
|
+
first: [['field-1']],
|
|
325
|
+
second: [['field-2']],
|
|
326
|
+
third: [['field-3']],
|
|
327
|
+
},
|
|
328
|
+
rowOrder: ['first', 'second', 'third'],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
332
|
+
model.resetRows(true);
|
|
333
|
+
|
|
334
|
+
const persistedLayout = model.getStepParams(GRID_FLOW_KEY, GRID_STEP).layout;
|
|
335
|
+
const persistedRows = projectLayoutToLegacyRows(persistedLayout).rows;
|
|
336
|
+
const persistedItems = Object.values(persistedRows).flat().flat().sort();
|
|
337
|
+
expect(persistedItems).toEqual(['field-1', 'field-2', 'field-3']);
|
|
338
|
+
expect(projectLayoutToLegacyRows(model.getGridLayout()).rows).toEqual({
|
|
339
|
+
first: [['field-1']],
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('uses the full layout for flow-settings layout reads during collapsed mode', () => {
|
|
344
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
345
|
+
uid: 'filter-grid-collapse-settings-full-layout',
|
|
346
|
+
use: 'FilterFormGridModel',
|
|
347
|
+
props: {
|
|
348
|
+
layout: {
|
|
349
|
+
version: 2,
|
|
350
|
+
rows: [
|
|
351
|
+
{
|
|
352
|
+
id: 'first',
|
|
353
|
+
cells: [{ id: 'first-cell', items: ['field-1'] }],
|
|
354
|
+
sizes: [24],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: 'second',
|
|
358
|
+
cells: [
|
|
359
|
+
{ id: 'second-cell-1', items: ['field-2'] },
|
|
360
|
+
{ id: 'second-cell-2', items: ['field-3'] },
|
|
361
|
+
],
|
|
362
|
+
sizes: [12, 12],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
structure: {} as any,
|
|
368
|
+
});
|
|
369
|
+
(model as any).subModels = {
|
|
370
|
+
items: [
|
|
371
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
372
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
373
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-3' }),
|
|
374
|
+
],
|
|
375
|
+
};
|
|
376
|
+
(model.context as any).flowSettingsEnabled = true;
|
|
377
|
+
|
|
378
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
379
|
+
layout: model.props.layout,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
383
|
+
|
|
384
|
+
expect(projectLayoutToLegacyRows((model as any).normalizeLayoutFromSource()).rows).toEqual({
|
|
385
|
+
first: [['field-1']],
|
|
386
|
+
});
|
|
387
|
+
expect(projectLayoutToLegacyRows(model.getGridLayout()).rows).toEqual({
|
|
388
|
+
first: [['field-1']],
|
|
389
|
+
second: [['field-2'], ['field-3']],
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
model.saveGridLayout(model.getGridLayout());
|
|
393
|
+
|
|
394
|
+
expect(projectLayoutToLegacyRows(model.getStepParams(GRID_FLOW_KEY, GRID_STEP).layout).rows).toEqual({
|
|
395
|
+
first: [['field-1']],
|
|
396
|
+
second: [['field-2'], ['field-3']],
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('restores the latest saved full layout after editing while collapsed in flow settings', () => {
|
|
401
|
+
const model = engine.createModel<FilterFormGridModel>({
|
|
402
|
+
uid: 'filter-grid-collapse-settings-latest-layout',
|
|
403
|
+
use: 'FilterFormGridModel',
|
|
404
|
+
props: {
|
|
405
|
+
layout: {
|
|
406
|
+
version: 2,
|
|
407
|
+
rows: [
|
|
408
|
+
{
|
|
409
|
+
id: 'first',
|
|
410
|
+
cells: [{ id: 'first-cell', items: ['field-1'] }],
|
|
411
|
+
sizes: [24],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: 'second',
|
|
415
|
+
cells: [{ id: 'second-cell', items: ['field-2'] }],
|
|
416
|
+
sizes: [24],
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
structure: {} as any,
|
|
422
|
+
});
|
|
423
|
+
(model as any).subModels = {
|
|
424
|
+
items: [
|
|
425
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-1' }),
|
|
426
|
+
engine.createModel({ use: 'FlowModel', uid: 'field-2' }),
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
(model.context as any).flowSettingsEnabled = true;
|
|
430
|
+
|
|
431
|
+
model.setStepParams(GRID_FLOW_KEY, GRID_STEP, {
|
|
432
|
+
layout: model.props.layout,
|
|
433
|
+
});
|
|
434
|
+
model.toggleFormFieldsCollapse(true, 1);
|
|
435
|
+
|
|
436
|
+
const editedLayout = {
|
|
437
|
+
version: 2 as const,
|
|
438
|
+
rows: [
|
|
439
|
+
{
|
|
440
|
+
id: 'first',
|
|
441
|
+
cells: [
|
|
442
|
+
{ id: 'first-cell-1', items: ['field-1'] },
|
|
443
|
+
{ id: 'first-cell-2', items: ['field-2'] },
|
|
444
|
+
],
|
|
445
|
+
sizes: [12, 12],
|
|
446
|
+
},
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
model.saveGridLayout(editedLayout);
|
|
451
|
+
model.toggleFormFieldsCollapse(false, 1);
|
|
452
|
+
|
|
453
|
+
expect(projectLayoutToLegacyRows(model.props.layout).rows).toEqual({
|
|
454
|
+
first: [['field-1'], ['field-2']],
|
|
455
|
+
});
|
|
456
|
+
});
|
|
188
457
|
});
|
|
@@ -37,6 +37,8 @@ function createEngineWithCollections() {
|
|
|
37
37
|
fields: [
|
|
38
38
|
{ name: 'id', type: 'integer', interface: 'number', filterable: { operators: [] } },
|
|
39
39
|
{ name: 'uid', type: 'string', interface: 'input', filterable: { operators: [] } },
|
|
40
|
+
{ name: 'status', type: 'string', interface: 'select', filterable: { operators: [] } },
|
|
41
|
+
{ name: 'tags', type: 'array', interface: 'multipleSelect', filterable: { operators: [] } },
|
|
40
42
|
{ name: 'createdAt', type: 'date', interface: 'datetime', filterable: { operators: [] } },
|
|
41
43
|
{
|
|
42
44
|
name: 'pluginInSource',
|
|
@@ -106,6 +108,27 @@ describe('custom field operators', () => {
|
|
|
106
108
|
expect(checkboxGroupOps.some((item) => item.value === '$match')).toBe(true);
|
|
107
109
|
});
|
|
108
110
|
|
|
111
|
+
it('uses multi-value scalar operators for multiple select custom fields bound to scalar source fields', () => {
|
|
112
|
+
const engine = createEngineWithCollections();
|
|
113
|
+
|
|
114
|
+
const scalarSourceOps = resolveCustomFieldOperatorList({
|
|
115
|
+
flowEngine: engine,
|
|
116
|
+
fieldModel: 'SelectFieldModel',
|
|
117
|
+
fieldModelProps: { mode: 'multiple' },
|
|
118
|
+
source: ['main', 'users', 'status'],
|
|
119
|
+
});
|
|
120
|
+
expect(scalarSourceOps[0]?.value).toBe('$in');
|
|
121
|
+
expect(scalarSourceOps.some((item) => item.value === '$match')).toBe(false);
|
|
122
|
+
|
|
123
|
+
const arraySourceOps = resolveCustomFieldOperatorList({
|
|
124
|
+
flowEngine: engine,
|
|
125
|
+
fieldModel: 'SelectFieldModel',
|
|
126
|
+
fieldModelProps: { mode: 'multiple' },
|
|
127
|
+
source: ['main', 'users', 'tags'],
|
|
128
|
+
});
|
|
129
|
+
expect(arraySourceOps[0]?.value).toBe('$match');
|
|
130
|
+
});
|
|
131
|
+
|
|
109
132
|
it('resolves record select operators by value field and multiple mode', () => {
|
|
110
133
|
const engine = createEngineWithCollections();
|
|
111
134
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { operators } from '../../../../flow-compat';
|
|
11
|
+
import { isArrayLikeField } from '../shared/filterOperators';
|
|
11
12
|
|
|
12
13
|
type OperatorMeta = {
|
|
13
14
|
label: string;
|
|
@@ -186,12 +187,22 @@ function resolveByModelOrSource(params: ResolveOperatorParams): { operatorList:
|
|
|
186
187
|
return resolveByRecordSelect(params);
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
const sourceField = getSourceField(flowEngine, source);
|
|
191
|
+
if (fieldModel === 'SelectFieldModel' && fieldModelProps?.mode === 'multiple' && sourceField) {
|
|
192
|
+
const sourceOperators = getFieldOperators(sourceField);
|
|
193
|
+
return {
|
|
194
|
+
operatorList: isArrayLikeField(sourceField)
|
|
195
|
+
? getOperatorListByModel(fieldModel, fieldModelProps)
|
|
196
|
+
: toMultiValueOperators(sourceOperators),
|
|
197
|
+
meta: sourceField,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
189
201
|
const modelOperators = getOperatorListByModel(fieldModel, fieldModelProps);
|
|
190
202
|
if (modelOperators.length > 0) {
|
|
191
203
|
return { operatorList: modelOperators, meta: { fieldModel } };
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
const sourceField = getSourceField(flowEngine, source);
|
|
195
206
|
return {
|
|
196
207
|
operatorList: getFieldOperators(sourceField),
|
|
197
208
|
meta: sourceField,
|