@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.
- 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/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/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 +3 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -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 -0
- package/es/index.mjs +100 -93
- package/lib/index.js +101 -94
- package/package.json +6 -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/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__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -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 +119 -14
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- 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/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/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/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/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/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +48 -8
- 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/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -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/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,57 @@
|
|
|
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 { tExpr } from '@nocobase/flow-engine';
|
|
11
|
+
import type { ButtonProps } from 'antd';
|
|
12
|
+
import { ActionModel, ActionSceneEnum } from '../base';
|
|
13
|
+
import { applyDisassociateAction, isAssociationBlockContext } from './AssociationActionUtils';
|
|
14
|
+
|
|
15
|
+
export class DisassociateActionModel extends ActionModel {
|
|
16
|
+
static scene = ActionSceneEnum.record;
|
|
17
|
+
static capabilityActionName = 'update';
|
|
18
|
+
|
|
19
|
+
defaultProps: ButtonProps = {
|
|
20
|
+
type: 'link',
|
|
21
|
+
title: tExpr('Disassociate'),
|
|
22
|
+
icon: 'DisconnectOutlined',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
getAclActionName() {
|
|
26
|
+
return 'update';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DisassociateActionModel.define({
|
|
31
|
+
label: tExpr('Disassociate'),
|
|
32
|
+
sort: 65,
|
|
33
|
+
hide(ctx) {
|
|
34
|
+
return !isAssociationBlockContext(ctx);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
DisassociateActionModel.registerFlow({
|
|
39
|
+
key: 'disassociateSettings',
|
|
40
|
+
title: tExpr('Disassociate settings'),
|
|
41
|
+
on: 'click',
|
|
42
|
+
steps: {
|
|
43
|
+
confirm: {
|
|
44
|
+
use: 'confirm',
|
|
45
|
+
defaultParams: {
|
|
46
|
+
enable: true,
|
|
47
|
+
title: tExpr('Disassociate record'),
|
|
48
|
+
content: tExpr('Are you sure you want to disassociate it?'),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
disassociate: {
|
|
52
|
+
async handler(ctx) {
|
|
53
|
+
await applyDisassociateAction(ctx);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { tExpr, MobilePopup, MultiRecordResource, useFlowSettingsContext } from '@nocobase/flow-engine';
|
|
11
|
+
import type { CollectionFieldInterfaceDataSourceManager } from '@nocobase/flow-engine';
|
|
11
12
|
import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
|
|
12
13
|
import { ButtonProps, Popover, Transfer } from 'antd';
|
|
13
14
|
import React from 'react';
|
|
@@ -15,6 +16,7 @@ import { FilterGroup, VariableFilterItem } from '../../components/filter';
|
|
|
15
16
|
import { ActionModel, CollectionBlockModel } from '../base';
|
|
16
17
|
import { FilterContainer } from '../../components/filter/FilterContainer';
|
|
17
18
|
import _ from 'lodash';
|
|
19
|
+
import { getFlowFieldInterfaceOptions } from '../../../flow-compat';
|
|
18
20
|
|
|
19
21
|
export class FilterActionModel extends ActionModel {
|
|
20
22
|
static scene = 'collection';
|
|
@@ -135,9 +137,11 @@ FilterActionModel.registerFlow({
|
|
|
135
137
|
'x-component': (props) => {
|
|
136
138
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
137
139
|
const { model } = useFlowSettingsContext();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const filterable = getFilterableFields(
|
|
141
|
+
model.context.blockModel.collection,
|
|
142
|
+
model.context.dataSourceManager,
|
|
143
|
+
model.context.app?.dataSourceManager,
|
|
144
|
+
);
|
|
141
145
|
const dataSource = filterable.map((field: any) => ({ key: field.name, title: field.title }));
|
|
142
146
|
return (
|
|
143
147
|
<Transfer
|
|
@@ -157,9 +161,11 @@ FilterActionModel.registerFlow({
|
|
|
157
161
|
},
|
|
158
162
|
defaultParams(ctx) {
|
|
159
163
|
// 默认仅包含“可筛选”的字段(与 1.0 一致),以避免 JSON 等未提供 operators 的字段出现在默认允许集合中
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
const names = getFilterableFields(
|
|
165
|
+
ctx.blockModel.collection,
|
|
166
|
+
ctx.model?.context?.dataSourceManager,
|
|
167
|
+
ctx.model?.context?.app?.dataSourceManager,
|
|
168
|
+
).map((field: any) => field.name);
|
|
163
169
|
return {
|
|
164
170
|
filterableFieldNames: names || [],
|
|
165
171
|
};
|
|
@@ -304,13 +310,15 @@ FilterActionModel.registerFlow({
|
|
|
304
310
|
},
|
|
305
311
|
});
|
|
306
312
|
|
|
307
|
-
function getFilterableFields(
|
|
313
|
+
function getFilterableFields(
|
|
314
|
+
collection: any,
|
|
315
|
+
...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
|
|
316
|
+
) {
|
|
308
317
|
const fields = collection?.getFields?.() || [];
|
|
309
|
-
if (!fiMgr) return [];
|
|
310
318
|
return fields.filter((field: any) => {
|
|
311
319
|
if (!field?.interface) return false;
|
|
312
320
|
if (field?.filterable === false) return false;
|
|
313
|
-
const fi =
|
|
321
|
+
const fi = getFlowFieldInterfaceOptions(field.interface, ...dataSourceManagers);
|
|
314
322
|
return !!fi?.filterable;
|
|
315
323
|
});
|
|
316
324
|
}
|
|
@@ -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) {
|
|
@@ -49,6 +49,17 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
49
49
|
private unmounted = false;
|
|
50
50
|
private documentTitleUpdateVersion = 0;
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 根页面标签页开关以路由表为准,避免 flow model 里的旧配置覆盖路由管理设置。
|
|
54
|
+
*/
|
|
55
|
+
private getEnableTabs(): boolean {
|
|
56
|
+
const routeEnableTabs = (this.context as any)?.currentRoute?.enableTabs;
|
|
57
|
+
if (this.props.routeId != null && typeof routeEnableTabs === 'boolean') {
|
|
58
|
+
return routeEnableTabs;
|
|
59
|
+
}
|
|
60
|
+
return !!this.props.enableTabs;
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
private getActiveTabKey(): string | undefined {
|
|
53
64
|
const viewParams = this.context.view?.navigation?.viewParams;
|
|
54
65
|
if (viewParams) {
|
|
@@ -193,7 +204,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
193
204
|
};
|
|
194
205
|
|
|
195
206
|
let nextTitle = '';
|
|
196
|
-
if (this.
|
|
207
|
+
if (this.getEnableTabs()) {
|
|
197
208
|
const activeTabKey = preferredActiveTabKey || this.getActiveTabKey();
|
|
198
209
|
const activeTabModel = activeTabKey
|
|
199
210
|
? (this.flowEngine.getModel(activeTabKey) as BasePageTabModel | undefined)
|
|
@@ -356,13 +367,14 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
356
367
|
headerStyle.paddingBlock = token.paddingSM;
|
|
357
368
|
headerStyle.paddingInline = token.paddingLG;
|
|
358
369
|
}
|
|
359
|
-
|
|
370
|
+
const enableTabs = this.getEnableTabs();
|
|
371
|
+
if (enableTabs) {
|
|
360
372
|
headerStyle.paddingBottom = 0;
|
|
361
373
|
}
|
|
362
374
|
return (
|
|
363
375
|
<>
|
|
364
376
|
{this.props.displayTitle && <PageHeader title={this.props.title} style={headerStyle} />}
|
|
365
|
-
{
|
|
377
|
+
{enableTabs ? this.renderTabs() : this.renderFirstTab()}
|
|
366
378
|
</>
|
|
367
379
|
);
|
|
368
380
|
}
|
|
@@ -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 = {
|