@nocobase/client-v2 2.1.0-beta.26 → 2.1.0-beta.29
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/components/form/JsonTextArea.d.ts +18 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/actions/dateRangeLimit.d.ts +9 -0
- package/es/flow/actions/index.d.ts +1 -0
- package/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/models/base/PageModel/PageModel.d.ts +4 -0
- package/es/flow/models/base/PageModel/RootPageModel.d.ts +9 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +7 -0
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +2 -0
- package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +20 -0
- package/es/flow/models/fields/JSEditableFieldModel.d.ts +4 -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 +97 -90
- package/lib/index.js +99 -92
- package/package.json +6 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/components/form/JsonTextArea.tsx +129 -0
- package/src/components/index.ts +1 -0
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +478 -0
- package/src/flow/actions/__tests__/pattern.test.ts +190 -0
- package/src/flow/actions/dateRangeLimit.tsx +66 -0
- package/src/flow/actions/index.ts +1 -0
- package/src/flow/actions/linkageRules.tsx +117 -19
- package/src/flow/actions/openView.tsx +2 -1
- package/src/flow/actions/pattern.tsx +25 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/admin-shell/AdminLayoutRouteCoordinator.ts +7 -1
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +117 -0
- 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/base/PageModel/PageModel.tsx +15 -3
- package/src/flow/models/base/PageModel/RootPageModel.tsx +37 -2
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +73 -0
- package/src/flow/models/base/PageModel/__tests__/RootPageModel.test.ts +116 -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/form/value-runtime/__tests__/runtime.test.ts +167 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +103 -11
- 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/SubTableFieldModel/SubTableColumnModel.tsx +27 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +47 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +42 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.refresh.test.tsx +122 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +2 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +21 -9
- package/src/flow/models/fields/DateTimeFieldModel/DateOnlyFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeFieldModel.tsx +4 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeNoTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/DateTimeTzFieldModel.tsx +9 -0
- package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +242 -0
- package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +152 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/JSEditableFieldModel.tsx +110 -14
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +87 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +210 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -1
|
@@ -17,6 +17,31 @@ import { PageModel } from './PageModel';
|
|
|
17
17
|
export class RootPageModel extends PageModel {
|
|
18
18
|
mounted = false;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 打开页面设置前,把标签页开关表单值同步为路由表中的当前状态。
|
|
22
|
+
*/
|
|
23
|
+
private syncPageSettingsEnableTabsFromRoute() {
|
|
24
|
+
const routeEnableTabs = (this.context as any)?.currentRoute?.enableTabs;
|
|
25
|
+
if (typeof routeEnableTabs !== 'boolean') {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.setStepParams('pageSettings', 'general', {
|
|
29
|
+
enableTabs: routeEnableTabs,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 保存页面设置后立即同步当前页面状态,让标签页显隐无需等路由列表刷新或页面重载。
|
|
35
|
+
*/
|
|
36
|
+
private syncEnableTabsToCurrentPage(enableTabs: boolean) {
|
|
37
|
+
const currentRoute = (this.context as any)?.currentRoute;
|
|
38
|
+
const routeId = this.props.routeId;
|
|
39
|
+
if (currentRoute && (routeId == null || currentRoute.id == null || String(currentRoute.id) === String(routeId))) {
|
|
40
|
+
currentRoute.enableTabs = enableTabs;
|
|
41
|
+
}
|
|
42
|
+
this.setProps('enableTabs', enableTabs);
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
/**
|
|
21
46
|
* 新建 tab 在首次保存完成前,前端 route 里可能还没有数据库 id。
|
|
22
47
|
* 拖拽前兜底触发一次保存,确保 move 接口拿到真实主键。
|
|
@@ -65,18 +90,28 @@ export class RootPageModel extends PageModel {
|
|
|
65
90
|
);
|
|
66
91
|
}
|
|
67
92
|
|
|
93
|
+
async openFlowSettings(options?: Parameters<PageModel['openFlowSettings']>[0]) {
|
|
94
|
+
if (options?.flowKey === 'pageSettings' && options?.stepKey === 'general') {
|
|
95
|
+
this.syncPageSettingsEnableTabsFromRoute();
|
|
96
|
+
}
|
|
97
|
+
return super.openFlowSettings(options);
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
async saveStepParams() {
|
|
69
101
|
await super.saveStepParams();
|
|
70
102
|
|
|
71
103
|
if (this.stepParams.pageSettings) {
|
|
104
|
+
const enableTabs = !!this.stepParams.pageSettings.general.enableTabs;
|
|
72
105
|
// 更新路由
|
|
73
|
-
this.context.api.request({
|
|
106
|
+
await this.context.api.request({
|
|
74
107
|
url: `desktopRoutes:update?filter[id]=${this.props.routeId}`,
|
|
75
108
|
method: 'post',
|
|
76
109
|
data: {
|
|
77
|
-
enableTabs
|
|
110
|
+
enableTabs,
|
|
78
111
|
},
|
|
79
112
|
});
|
|
113
|
+
this.syncEnableTabsToCurrentPage(enableTabs);
|
|
114
|
+
await this.context.refreshDesktopRoutes?.();
|
|
80
115
|
}
|
|
81
116
|
}
|
|
82
117
|
|
|
@@ -412,6 +412,7 @@ describe('PageModel', () => {
|
|
|
412
412
|
describe('render header spacing with tabs', () => {
|
|
413
413
|
it('should compact page header bottom spacing when tabs are enabled', () => {
|
|
414
414
|
pageModel.props = {
|
|
415
|
+
routeId: 'route-1',
|
|
415
416
|
displayTitle: true,
|
|
416
417
|
enableTabs: true,
|
|
417
418
|
title: 'Title',
|
|
@@ -430,6 +431,7 @@ describe('PageModel', () => {
|
|
|
430
431
|
|
|
431
432
|
it('should keep original header style when tabs are disabled', () => {
|
|
432
433
|
pageModel.props = {
|
|
434
|
+
routeId: 'route-1',
|
|
433
435
|
displayTitle: true,
|
|
434
436
|
enableTabs: false,
|
|
435
437
|
title: 'Title',
|
|
@@ -442,6 +444,57 @@ describe('PageModel', () => {
|
|
|
442
444
|
|
|
443
445
|
expect(header.props.style).toEqual({ backgroundColor: 'var(--colorBgLayout)' });
|
|
444
446
|
});
|
|
447
|
+
|
|
448
|
+
it('should use desktop route enableTabs=false before flow model props', () => {
|
|
449
|
+
pageModel.props = {
|
|
450
|
+
routeId: 'route-1',
|
|
451
|
+
displayTitle: true,
|
|
452
|
+
enableTabs: true,
|
|
453
|
+
title: 'Title',
|
|
454
|
+
headerStyle: { backgroundColor: 'var(--colorBgLayout)' },
|
|
455
|
+
} as any;
|
|
456
|
+
(pageModel as any).context = {
|
|
457
|
+
currentRoute: {
|
|
458
|
+
enableTabs: false,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
pageModel.renderTabs = vi.fn(() => null);
|
|
462
|
+
pageModel.renderFirstTab = vi.fn(() => null);
|
|
463
|
+
|
|
464
|
+
const result = pageModel.render() as any;
|
|
465
|
+
const header = result.props.children[0];
|
|
466
|
+
|
|
467
|
+
expect(pageModel.renderTabs).not.toHaveBeenCalled();
|
|
468
|
+
expect(pageModel.renderFirstTab).toHaveBeenCalled();
|
|
469
|
+
expect(header.props.style).toEqual({ backgroundColor: 'var(--colorBgLayout)' });
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should use desktop route enableTabs=true before flow model props', () => {
|
|
473
|
+
pageModel.props = {
|
|
474
|
+
routeId: 'route-1',
|
|
475
|
+
displayTitle: true,
|
|
476
|
+
enableTabs: false,
|
|
477
|
+
title: 'Title',
|
|
478
|
+
headerStyle: { backgroundColor: 'var(--colorBgLayout)' },
|
|
479
|
+
} as any;
|
|
480
|
+
(pageModel as any).context = {
|
|
481
|
+
currentRoute: {
|
|
482
|
+
enableTabs: true,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
pageModel.renderTabs = vi.fn(() => null);
|
|
486
|
+
pageModel.renderFirstTab = vi.fn(() => null);
|
|
487
|
+
|
|
488
|
+
const result = pageModel.render() as any;
|
|
489
|
+
const header = result.props.children[0];
|
|
490
|
+
|
|
491
|
+
expect(pageModel.renderTabs).toHaveBeenCalled();
|
|
492
|
+
expect(pageModel.renderFirstTab).not.toHaveBeenCalled();
|
|
493
|
+
expect(header.props.style).toMatchObject({
|
|
494
|
+
backgroundColor: 'var(--colorBgLayout)',
|
|
495
|
+
paddingBottom: 0,
|
|
496
|
+
});
|
|
497
|
+
});
|
|
445
498
|
});
|
|
446
499
|
|
|
447
500
|
describe('dirty refresh signal', () => {
|
|
@@ -574,6 +627,26 @@ describe('PageModel', () => {
|
|
|
574
627
|
expect(document.title).toBe('Resolved tab doc title');
|
|
575
628
|
});
|
|
576
629
|
|
|
630
|
+
it('should use page documentTitle when desktop route disables tabs even if flow model enables tabs', async () => {
|
|
631
|
+
pageModel.props = { routeId: 'route-1', enableTabs: true, title: 'Route page title' } as any;
|
|
632
|
+
(pageModel as any).context.currentRoute = {
|
|
633
|
+
enableTabs: false,
|
|
634
|
+
};
|
|
635
|
+
(pageModel as any).stepParams = {
|
|
636
|
+
pageSettings: {
|
|
637
|
+
general: {
|
|
638
|
+
documentTitle: 'Route page doc title',
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
(pageModel as any).context.resolveJsonTemplate = vi.fn(async () => 'Resolved route page doc title');
|
|
643
|
+
|
|
644
|
+
await (pageModel as any).updateDocumentTitle();
|
|
645
|
+
|
|
646
|
+
expect((pageModel as any).context.resolveJsonTemplate).toHaveBeenCalledWith('Route page doc title');
|
|
647
|
+
expect(document.title).toBe('Resolved route page doc title');
|
|
648
|
+
});
|
|
649
|
+
|
|
577
650
|
it('should fallback to tab title when active tab documentTitle is empty', async () => {
|
|
578
651
|
pageModel.props = { enableTabs: true } as any;
|
|
579
652
|
const activeTab = {
|
|
@@ -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', () => {
|
|
@@ -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
|
}
|