@nocobase/plugin-ui-templates 2.0.0-alpha.57
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/LICENSE.txt +172 -0
- package/build.config.ts +12 -0
- package/client.js +1 -0
- package/dist/client/collections/flowModelTemplates.d.ts +67 -0
- package/dist/client/components/FlowModelTemplatesPage.d.ts +12 -0
- package/dist/client/components/TemplateSelectOption.d.ts +20 -0
- package/dist/client/constants.d.ts +9 -0
- package/dist/client/hooks/useFlowModelTemplateActions.d.ts +24 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.js +10 -0
- package/dist/client/locale.d.ts +18 -0
- package/dist/client/menuExtensions.d.ts +9 -0
- package/dist/client/models/ReferenceBlockModel.d.ts +47 -0
- package/dist/client/models/ReferenceFormGridModel.d.ts +38 -0
- package/dist/client/models/SubModelTemplateImporterModel.d.ts +55 -0
- package/dist/client/models/referenceShared.d.ts +23 -0
- package/dist/client/openViewActionExtensions.d.ts +10 -0
- package/dist/client/schemas/flowModelTemplates.d.ts +11 -0
- package/dist/client/subModelMenuExtensions.d.ts +10 -0
- package/dist/client/utils/infiniteSelect.d.ts +28 -0
- package/dist/client/utils/refHost.d.ts +20 -0
- package/dist/client/utils/templateCompatibility.d.ts +91 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +42 -0
- package/dist/externalVersion.js +24 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +48 -0
- package/dist/locale/de-DE.json +14 -0
- package/dist/locale/en-US.json +72 -0
- package/dist/locale/es-ES.json +14 -0
- package/dist/locale/fr-FR.json +14 -0
- package/dist/locale/hu-HU.json +14 -0
- package/dist/locale/id-ID.json +14 -0
- package/dist/locale/it-IT.json +14 -0
- package/dist/locale/ja-JP.json +14 -0
- package/dist/locale/ko-KR.json +14 -0
- package/dist/locale/nl-NL.json +14 -0
- package/dist/locale/pt-BR.json +14 -0
- package/dist/locale/ru-RU.json +14 -0
- package/dist/locale/tr-TR.json +14 -0
- package/dist/locale/uk-UA.json +14 -0
- package/dist/locale/vi-VN.json +14 -0
- package/dist/locale/zh-CN.json +71 -0
- package/dist/locale/zh-TW.json +14 -0
- package/dist/server/collections/flowModelTemplateUsages.d.ts +11 -0
- package/dist/server/collections/flowModelTemplateUsages.js +71 -0
- package/dist/server/collections/flowModelTemplates.d.ts +11 -0
- package/dist/server/collections/flowModelTemplates.js +96 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +42 -0
- package/dist/server/plugin.d.ts +17 -0
- package/dist/server/plugin.js +242 -0
- package/dist/server/resources/flowModelTemplateUsages.d.ts +19 -0
- package/dist/server/resources/flowModelTemplateUsages.js +91 -0
- package/dist/server/resources/flowModelTemplates.d.ts +20 -0
- package/dist/server/resources/flowModelTemplates.js +267 -0
- package/package.json +37 -0
- package/server.js +1 -0
- package/src/client/__tests__/openViewActionExtensions.test.ts +1208 -0
- package/src/client/collections/flowModelTemplates.ts +131 -0
- package/src/client/components/FlowModelTemplatesPage.tsx +78 -0
- package/src/client/components/TemplateSelectOption.tsx +106 -0
- package/src/client/constants.ts +10 -0
- package/src/client/hooks/useFlowModelTemplateActions.tsx +137 -0
- package/src/client/index.ts +54 -0
- package/src/client/locale.ts +40 -0
- package/src/client/menuExtensions.tsx +1033 -0
- package/src/client/models/ReferenceBlockModel.tsx +793 -0
- package/src/client/models/ReferenceFormGridModel.tsx +302 -0
- package/src/client/models/SubModelTemplateImporterModel.tsx +634 -0
- package/src/client/models/__tests__/ReferenceBlockModel.test.tsx +482 -0
- package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +175 -0
- package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +447 -0
- package/src/client/models/referenceShared.tsx +99 -0
- package/src/client/openViewActionExtensions.tsx +981 -0
- package/src/client/schemas/flowModelTemplates.ts +264 -0
- package/src/client/subModelMenuExtensions.ts +103 -0
- package/src/client/utils/infiniteSelect.ts +150 -0
- package/src/client/utils/refHost.ts +44 -0
- package/src/client/utils/templateCompatibility.ts +374 -0
- package/src/client.ts +10 -0
- package/src/index.ts +11 -0
- package/src/locale/de-DE.json +14 -0
- package/src/locale/en-US.json +72 -0
- package/src/locale/es-ES.json +14 -0
- package/src/locale/fr-FR.json +14 -0
- package/src/locale/hu-HU.json +14 -0
- package/src/locale/id-ID.json +14 -0
- package/src/locale/it-IT.json +14 -0
- package/src/locale/ja-JP.json +14 -0
- package/src/locale/ko-KR.json +14 -0
- package/src/locale/nl-NL.json +14 -0
- package/src/locale/pt-BR.json +14 -0
- package/src/locale/ru-RU.json +14 -0
- package/src/locale/tr-TR.json +14 -0
- package/src/locale/uk-UA.json +14 -0
- package/src/locale/vi-VN.json +14 -0
- package/src/locale/zh-CN.json +71 -0
- package/src/locale/zh-TW.json +14 -0
- package/src/server/__tests__/template-usage.test.ts +351 -0
- package/src/server/collections/flowModelTemplateUsages.ts +51 -0
- package/src/server/collections/flowModelTemplates.ts +76 -0
- package/src/server/index.ts +10 -0
- package/src/server/plugin.ts +236 -0
- package/src/server/resources/flowModelTemplateUsages.ts +61 -0
- package/src/server/resources/flowModelTemplates.ts +251 -0
|
@@ -0,0 +1,482 @@
|
|
|
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 { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
import { ReferenceBlockModel } from '../ReferenceBlockModel';
|
|
13
|
+
|
|
14
|
+
class MockGridModel extends FlowModel {}
|
|
15
|
+
|
|
16
|
+
class MockFormBlockModel extends FlowModel {
|
|
17
|
+
constructor(options: any) {
|
|
18
|
+
super(options);
|
|
19
|
+
const titleFromProps = options?.props?.title || options?.title;
|
|
20
|
+
if (titleFromProps) {
|
|
21
|
+
this.setTitle(titleFromProps);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TEST_TIMEOUT = 10_000;
|
|
27
|
+
|
|
28
|
+
describe('ReferenceBlockModel', () => {
|
|
29
|
+
let engine: FlowEngine;
|
|
30
|
+
let gridModel: FlowModel;
|
|
31
|
+
let targetBlockModel: FlowModel;
|
|
32
|
+
let referenceBlockModel: ReferenceBlockModel;
|
|
33
|
+
let scopedEngine: FlowEngine;
|
|
34
|
+
let store: Record<string, any>;
|
|
35
|
+
let lastSavedSnapshot: Record<string, any>;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.spyOn(ReferenceBlockModel.prototype as any, 'rerender').mockResolvedValue(undefined);
|
|
39
|
+
engine = new FlowEngine();
|
|
40
|
+
scopedEngine = new FlowEngine();
|
|
41
|
+
lastSavedSnapshot = {};
|
|
42
|
+
store = {
|
|
43
|
+
'grid-uid': {
|
|
44
|
+
uid: 'grid-uid',
|
|
45
|
+
use: 'GridModel',
|
|
46
|
+
parentId: 'page-uid',
|
|
47
|
+
subKey: 'items',
|
|
48
|
+
subType: 'array',
|
|
49
|
+
},
|
|
50
|
+
'target-block-uid': {
|
|
51
|
+
uid: 'target-block-uid',
|
|
52
|
+
use: 'FormBlockModel',
|
|
53
|
+
parentId: 'grid-uid',
|
|
54
|
+
subKey: 'items',
|
|
55
|
+
subType: 'array',
|
|
56
|
+
props: { title: 'Test Form Block' },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Mock api to avoid context errors
|
|
61
|
+
engine.context.defineProperty('api', {
|
|
62
|
+
value: {
|
|
63
|
+
auth: {
|
|
64
|
+
role: 'admin',
|
|
65
|
+
locale: 'zh-CN',
|
|
66
|
+
token: 'test-token',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
scopedEngine.context.defineProperty('api', { value: engine.context.api });
|
|
71
|
+
|
|
72
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
73
|
+
const mockRepository = {
|
|
74
|
+
findOne: vi.fn(async (query) => {
|
|
75
|
+
const data = store[query.uid];
|
|
76
|
+
return data ? clone(data) : null;
|
|
77
|
+
}),
|
|
78
|
+
save: vi.fn(async (model) => {
|
|
79
|
+
const data = typeof model.serialize === 'function' ? model.serialize() : model;
|
|
80
|
+
lastSavedSnapshot[data.uid] = clone(data);
|
|
81
|
+
store[data.uid] = { ...(store[data.uid] || {}), ...clone(data) };
|
|
82
|
+
const sub = data.subModels || {};
|
|
83
|
+
if (sub && typeof sub === 'object') {
|
|
84
|
+
Object.values(sub).forEach((child: any) => {
|
|
85
|
+
if (!child) return;
|
|
86
|
+
if (Array.isArray(child)) {
|
|
87
|
+
child.forEach((c) => {
|
|
88
|
+
if (c?.uid) {
|
|
89
|
+
store[c.uid] = { ...(store[c.uid] || {}), ...clone(c) };
|
|
90
|
+
lastSavedSnapshot[c.uid] = clone(c);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
} else if (child.uid) {
|
|
94
|
+
store[child.uid] = { ...(store[child.uid] || {}), ...clone(child) };
|
|
95
|
+
lastSavedSnapshot[child.uid] = clone(child);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { uid: data.uid };
|
|
100
|
+
}),
|
|
101
|
+
destroy: vi.fn(async (uid) => {
|
|
102
|
+
const key = typeof uid === 'string' ? uid : uid?.uid;
|
|
103
|
+
if (key && store[key]) {
|
|
104
|
+
delete store[key];
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}),
|
|
108
|
+
move: vi.fn(async () => {}),
|
|
109
|
+
duplicate: vi.fn(async (uid) => {
|
|
110
|
+
const data = store[uid];
|
|
111
|
+
if (!data) return null;
|
|
112
|
+
const copy = { ...clone(data), uid: `${uid}-copy` };
|
|
113
|
+
store[copy.uid] = copy;
|
|
114
|
+
return { uid: copy.uid };
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
engine.setModelRepository(mockRepository);
|
|
119
|
+
scopedEngine.setModelRepository(mockRepository);
|
|
120
|
+
|
|
121
|
+
// 注册 ReferenceBlockModel
|
|
122
|
+
engine.registerModels({
|
|
123
|
+
GridModel: MockGridModel,
|
|
124
|
+
FormBlockModel: MockFormBlockModel,
|
|
125
|
+
ReferenceBlockModel,
|
|
126
|
+
});
|
|
127
|
+
scopedEngine.registerModels({
|
|
128
|
+
GridModel: MockGridModel,
|
|
129
|
+
FormBlockModel: MockFormBlockModel,
|
|
130
|
+
ReferenceBlockModel,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 创建 Grid 模型
|
|
134
|
+
gridModel = engine.createModel({
|
|
135
|
+
uid: 'grid-uid',
|
|
136
|
+
use: 'GridModel',
|
|
137
|
+
parentId: 'page-uid',
|
|
138
|
+
subKey: 'items',
|
|
139
|
+
subType: 'array',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 创建目标区块模型(表单区块)
|
|
143
|
+
targetBlockModel = engine.createModel({
|
|
144
|
+
uid: 'target-block-uid',
|
|
145
|
+
use: 'FormBlockModel',
|
|
146
|
+
parentId: 'grid-uid',
|
|
147
|
+
subKey: 'items',
|
|
148
|
+
subType: 'array',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 添加目标区块到 Grid
|
|
152
|
+
gridModel.addSubModel('items', targetBlockModel);
|
|
153
|
+
|
|
154
|
+
vi.spyOn(ReferenceBlockModel.prototype as any, '_ensureScopedEngine').mockReturnValue(scopedEngine);
|
|
155
|
+
vi.spyOn(ReferenceBlockModel.prototype as any, '_resolveFinalTarget').mockImplementation(async (uid: string) => {
|
|
156
|
+
if (!uid) return null;
|
|
157
|
+
return scopedEngine.loadModel({ uid });
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(() => {
|
|
162
|
+
vi.restoreAllMocks();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('Original block should remain', () => {
|
|
166
|
+
it(
|
|
167
|
+
'should not change target parentId when creating a reference block',
|
|
168
|
+
async () => {
|
|
169
|
+
// 记录目标区块的初始 parentId
|
|
170
|
+
const originalParentId = (targetBlockModel as any)._options.parentId;
|
|
171
|
+
expect(originalParentId).toBe('grid-uid');
|
|
172
|
+
|
|
173
|
+
// 创建引用区块
|
|
174
|
+
referenceBlockModel = engine.createModel({
|
|
175
|
+
uid: 'reference-block-uid',
|
|
176
|
+
use: 'ReferenceBlockModel',
|
|
177
|
+
parentId: 'grid-uid',
|
|
178
|
+
subKey: 'items',
|
|
179
|
+
subType: 'array',
|
|
180
|
+
stepParams: {
|
|
181
|
+
referenceSettings: {
|
|
182
|
+
target: {
|
|
183
|
+
targetUid: 'target-block-uid',
|
|
184
|
+
mode: 'reference',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}) as ReferenceBlockModel;
|
|
189
|
+
|
|
190
|
+
// 添加引用区块到 Grid
|
|
191
|
+
gridModel.addSubModel('items', referenceBlockModel);
|
|
192
|
+
|
|
193
|
+
// 触发 beforeRender 事件,加载目标区块
|
|
194
|
+
await referenceBlockModel.dispatchEvent('beforeRender');
|
|
195
|
+
|
|
196
|
+
// 验证目标区块的 parentId 没有被修改
|
|
197
|
+
const currentParentId = (targetBlockModel as any)._options.parentId;
|
|
198
|
+
expect(currentParentId).toBe('grid-uid');
|
|
199
|
+
expect(currentParentId).toBe(originalParentId);
|
|
200
|
+
expect(store['target-block-uid'].parentId).toBe('grid-uid');
|
|
201
|
+
|
|
202
|
+
// 验证目标区块不是引用区块的 parent(只是内存中的引用关系)
|
|
203
|
+
expect((targetBlockModel as any)._options.parentId).not.toBe('reference-block-uid');
|
|
204
|
+
},
|
|
205
|
+
TEST_TIMEOUT,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
it(
|
|
209
|
+
'should not persist target block when saving the reference block',
|
|
210
|
+
async () => {
|
|
211
|
+
referenceBlockModel = engine.createModel({
|
|
212
|
+
uid: 'reference-block-uid',
|
|
213
|
+
use: 'ReferenceBlockModel',
|
|
214
|
+
parentId: 'grid-uid',
|
|
215
|
+
subKey: 'items',
|
|
216
|
+
subType: 'array',
|
|
217
|
+
stepParams: {
|
|
218
|
+
referenceSettings: {
|
|
219
|
+
target: {
|
|
220
|
+
targetUid: 'target-block-uid',
|
|
221
|
+
mode: 'reference',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
}) as ReferenceBlockModel;
|
|
226
|
+
|
|
227
|
+
gridModel.addSubModel('items', referenceBlockModel);
|
|
228
|
+
|
|
229
|
+
await referenceBlockModel.onDispatchEventStart('beforeRender');
|
|
230
|
+
await referenceBlockModel.save();
|
|
231
|
+
|
|
232
|
+
const savedRef = store['reference-block-uid'];
|
|
233
|
+
expect(savedRef.subModels?.target).toBeUndefined();
|
|
234
|
+
expect(savedRef.parentId).toBe('grid-uid');
|
|
235
|
+
expect(store['target-block-uid'].parentId).toBe('grid-uid');
|
|
236
|
+
expect(lastSavedSnapshot['reference-block-uid']?.subModels?.target).toBeUndefined();
|
|
237
|
+
expect(lastSavedSnapshot['target-block-uid']).toBeUndefined();
|
|
238
|
+
},
|
|
239
|
+
TEST_TIMEOUT,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
it(
|
|
243
|
+
'should find target block by grid parentId after a reload',
|
|
244
|
+
async () => {
|
|
245
|
+
// 模拟刷新页面的场景:重新从 repository 加载
|
|
246
|
+
const reloadedTargetBlock = await engine.loadModel({ uid: 'target-block-uid' });
|
|
247
|
+
|
|
248
|
+
// 验证加载的目标区块的 parentId 仍然指向 Grid
|
|
249
|
+
expect((reloadedTargetBlock as any).parentId).toBe('grid-uid');
|
|
250
|
+
|
|
251
|
+
// 验证 Grid 可以通过 findModelByParentId 找到目标区块
|
|
252
|
+
const mockRepo = engine.modelRepository as any;
|
|
253
|
+
const foundBlock = await mockRepo.findOne({ uid: 'target-block-uid' });
|
|
254
|
+
expect(foundBlock.parentId).toBe('grid-uid');
|
|
255
|
+
expect(foundBlock.parentId).not.toBe('reference-block-uid');
|
|
256
|
+
},
|
|
257
|
+
TEST_TIMEOUT,
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('Template option disabled rules', () => {
|
|
262
|
+
it('disables template when associationName mismatches in association context', async () => {
|
|
263
|
+
referenceBlockModel = engine.createModel({
|
|
264
|
+
uid: 'reference-block-uid',
|
|
265
|
+
use: 'ReferenceBlockModel',
|
|
266
|
+
parentId: 'grid-uid',
|
|
267
|
+
subKey: 'items',
|
|
268
|
+
subType: 'array',
|
|
269
|
+
stepParams: {
|
|
270
|
+
resourceSettings: {
|
|
271
|
+
init: {
|
|
272
|
+
associationName: 'users.profile',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}) as ReferenceBlockModel;
|
|
277
|
+
|
|
278
|
+
const list = vi.fn(async () => ({
|
|
279
|
+
data: {
|
|
280
|
+
rows: [
|
|
281
|
+
{ uid: 'tpl-ok', name: 'OK', associationName: 'users.profile' },
|
|
282
|
+
{ uid: 'tpl-mismatch', name: 'Mismatch', associationName: 'users.posts' },
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
}));
|
|
286
|
+
|
|
287
|
+
const ctx: any = {
|
|
288
|
+
model: referenceBlockModel,
|
|
289
|
+
api: {
|
|
290
|
+
resource: (name: string) => {
|
|
291
|
+
if (name !== 'flowModelTemplates') throw new Error('unexpected resource');
|
|
292
|
+
return { list };
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
t: (k: string) => k,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const flow: any = referenceBlockModel.getFlow('referenceSettings');
|
|
299
|
+
const step: any = flow?.getStep?.('useTemplate');
|
|
300
|
+
expect(typeof step?.uiSchema).toBe('function');
|
|
301
|
+
const schema: any = step.uiSchema(ctx);
|
|
302
|
+
const reactions = schema?.templateUid?.['x-reactions'] || [];
|
|
303
|
+
expect(Array.isArray(reactions)).toBe(true);
|
|
304
|
+
expect(typeof reactions[0]).toBe('function');
|
|
305
|
+
|
|
306
|
+
const field: any = { componentProps: {}, data: {} };
|
|
307
|
+
reactions[0](field);
|
|
308
|
+
await field.componentProps.onDropdownVisibleChange(true);
|
|
309
|
+
|
|
310
|
+
const opts = field.dataSource;
|
|
311
|
+
expect(Array.isArray(opts)).toBe(true);
|
|
312
|
+
expect(opts[0].value).toBe('tpl-ok');
|
|
313
|
+
expect(opts[0].disabled).toBe(false);
|
|
314
|
+
expect(opts[1].value).toBe('tpl-mismatch');
|
|
315
|
+
expect(opts[1].disabled).toBe(true);
|
|
316
|
+
expect(String(opts[1].disabledReason || '')).toContain('Template association mismatch');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('disables association templates when current context is not an association resource', async () => {
|
|
320
|
+
referenceBlockModel = engine.createModel({
|
|
321
|
+
uid: 'reference-block-uid',
|
|
322
|
+
use: 'ReferenceBlockModel',
|
|
323
|
+
parentId: 'grid-uid',
|
|
324
|
+
subKey: 'items',
|
|
325
|
+
subType: 'array',
|
|
326
|
+
stepParams: {
|
|
327
|
+
resourceSettings: {
|
|
328
|
+
init: {
|
|
329
|
+
associationName: 'users',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
}) as ReferenceBlockModel;
|
|
334
|
+
|
|
335
|
+
const list = vi.fn(async () => ({
|
|
336
|
+
data: {
|
|
337
|
+
rows: [{ uid: 'tpl-assoc', name: 'Assoc', associationName: 'users.profile' }],
|
|
338
|
+
},
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
const ctx: any = {
|
|
342
|
+
model: referenceBlockModel,
|
|
343
|
+
api: {
|
|
344
|
+
resource: (name: string) => {
|
|
345
|
+
if (name !== 'flowModelTemplates') throw new Error('unexpected resource');
|
|
346
|
+
return { list };
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
t: (k: string) => k,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const flow: any = referenceBlockModel.getFlow('referenceSettings');
|
|
353
|
+
const step: any = flow?.getStep?.('useTemplate');
|
|
354
|
+
const schema: any = step.uiSchema(ctx);
|
|
355
|
+
const reactions = schema?.templateUid?.['x-reactions'] || [];
|
|
356
|
+
const field: any = { componentProps: {}, data: {} };
|
|
357
|
+
reactions[0](field);
|
|
358
|
+
await field.componentProps.onDropdownVisibleChange(true);
|
|
359
|
+
|
|
360
|
+
const opts = field.dataSource;
|
|
361
|
+
expect(opts[0].value).toBe('tpl-assoc');
|
|
362
|
+
expect(opts[0].disabled).toBe(true);
|
|
363
|
+
expect(String(opts[0].disabledReason || '')).toContain('Template association mismatch');
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('Reference block basics', () => {
|
|
368
|
+
it(
|
|
369
|
+
'should resolve the target block correctly',
|
|
370
|
+
async () => {
|
|
371
|
+
gridModel.addSubModel('items', targetBlockModel);
|
|
372
|
+
|
|
373
|
+
referenceBlockModel = engine.createModel({
|
|
374
|
+
uid: 'reference-block-uid',
|
|
375
|
+
use: 'ReferenceBlockModel',
|
|
376
|
+
parentId: 'grid-uid',
|
|
377
|
+
stepParams: {
|
|
378
|
+
referenceSettings: {
|
|
379
|
+
target: {
|
|
380
|
+
targetUid: 'target-block-uid',
|
|
381
|
+
mode: 'reference',
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
}) as ReferenceBlockModel;
|
|
386
|
+
|
|
387
|
+
gridModel.addSubModel('items', referenceBlockModel);
|
|
388
|
+
|
|
389
|
+
await referenceBlockModel.onDispatchEventStart('beforeRender');
|
|
390
|
+
|
|
391
|
+
// 验证 _targetModel 被正确设置
|
|
392
|
+
const targetModel = (referenceBlockModel as any)._targetModel;
|
|
393
|
+
expect(targetModel).toBeDefined();
|
|
394
|
+
expect(targetModel?.uid).toBe('target-block-uid');
|
|
395
|
+
},
|
|
396
|
+
TEST_TIMEOUT,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
it(
|
|
400
|
+
'should clear target model when target UID is empty',
|
|
401
|
+
async () => {
|
|
402
|
+
referenceBlockModel = engine.createModel({
|
|
403
|
+
uid: 'reference-block-uid',
|
|
404
|
+
use: 'ReferenceBlockModel',
|
|
405
|
+
parentId: 'grid-uid',
|
|
406
|
+
stepParams: {
|
|
407
|
+
referenceSettings: {
|
|
408
|
+
target: {
|
|
409
|
+
targetUid: '',
|
|
410
|
+
mode: 'reference',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}) as ReferenceBlockModel;
|
|
415
|
+
|
|
416
|
+
await referenceBlockModel.onDispatchEventStart('beforeRender');
|
|
417
|
+
|
|
418
|
+
// 验证 _targetModel 为 undefined
|
|
419
|
+
const targetModel = (referenceBlockModel as any)._targetModel;
|
|
420
|
+
expect(targetModel).toBeUndefined();
|
|
421
|
+
},
|
|
422
|
+
TEST_TIMEOUT,
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
it(
|
|
426
|
+
'should render reference block title from target',
|
|
427
|
+
async () => {
|
|
428
|
+
// 创建目标区块,包含标题
|
|
429
|
+
const targetWithTitle = engine.createModel({
|
|
430
|
+
uid: 'target-block-with-title',
|
|
431
|
+
use: 'FormBlockModel',
|
|
432
|
+
parentId: 'grid-uid',
|
|
433
|
+
props: {
|
|
434
|
+
title: 'Test Form Block',
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
gridModel.addSubModel('items', targetWithTitle);
|
|
439
|
+
|
|
440
|
+
referenceBlockModel = engine.createModel({
|
|
441
|
+
uid: 'reference-block-uid',
|
|
442
|
+
use: 'ReferenceBlockModel',
|
|
443
|
+
parentId: 'grid-uid',
|
|
444
|
+
stepParams: {
|
|
445
|
+
referenceSettings: {
|
|
446
|
+
target: {
|
|
447
|
+
targetUid: 'target-block-with-title',
|
|
448
|
+
mode: 'reference',
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
}) as ReferenceBlockModel;
|
|
453
|
+
|
|
454
|
+
gridModel.addSubModel('items', referenceBlockModel);
|
|
455
|
+
|
|
456
|
+
// Mock repository 返回目标区块
|
|
457
|
+
const mockRepo = engine.modelRepository as any;
|
|
458
|
+
mockRepo.findOne = vi.fn(async (query) => {
|
|
459
|
+
if (query.uid === 'target-block-with-title') {
|
|
460
|
+
return {
|
|
461
|
+
uid: 'target-block-with-title',
|
|
462
|
+
use: 'FormBlockModel',
|
|
463
|
+
parentId: 'grid-uid',
|
|
464
|
+
props: {
|
|
465
|
+
title: 'Test Form Block',
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await referenceBlockModel.onDispatchEventStart('beforeRender');
|
|
473
|
+
|
|
474
|
+
// title 展示目标标题;模板信息放在 extraTitle
|
|
475
|
+
const title = referenceBlockModel.title;
|
|
476
|
+
expect(title).toContain('Test Form Block');
|
|
477
|
+
expect((referenceBlockModel as any).extraTitle).toMatch(/Reference template|引用模板/);
|
|
478
|
+
},
|
|
479
|
+
TEST_TIMEOUT,
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
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 { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { ReferenceFormGridModel } from '../ReferenceFormGridModel';
|
|
13
|
+
|
|
14
|
+
class MockFormBlockModel extends FlowModel {}
|
|
15
|
+
class MockGridModel extends FlowModel {}
|
|
16
|
+
class MockFieldModel extends FlowModel {}
|
|
17
|
+
|
|
18
|
+
describe('ReferenceFormGridModel', () => {
|
|
19
|
+
it('proxies subModels to template grid but serializes as leaf', async () => {
|
|
20
|
+
const engine = new FlowEngine();
|
|
21
|
+
const store: Record<string, any> = {
|
|
22
|
+
'tpl-root': {
|
|
23
|
+
uid: 'tpl-root',
|
|
24
|
+
use: 'FormBlockModel',
|
|
25
|
+
subModels: {
|
|
26
|
+
grid: {
|
|
27
|
+
uid: 'tpl-grid',
|
|
28
|
+
use: 'GridModel',
|
|
29
|
+
subKey: 'grid',
|
|
30
|
+
subType: 'object',
|
|
31
|
+
subModels: {
|
|
32
|
+
items: [
|
|
33
|
+
{ uid: 'tpl-f1', use: 'FieldModel', subKey: 'items', subType: 'array' },
|
|
34
|
+
{ uid: 'tpl-f2', use: 'FieldModel', subKey: 'items', subType: 'array' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
43
|
+
const mockRepository = {
|
|
44
|
+
findOne: vi.fn(async (query) => {
|
|
45
|
+
const data = store[query.uid];
|
|
46
|
+
return data ? clone(data) : null;
|
|
47
|
+
}),
|
|
48
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
49
|
+
destroy: vi.fn(async () => true),
|
|
50
|
+
move: vi.fn(async () => {}),
|
|
51
|
+
duplicate: vi.fn(async () => null),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
engine.setModelRepository(mockRepository as any);
|
|
55
|
+
engine.registerModels({
|
|
56
|
+
FormBlockModel: MockFormBlockModel,
|
|
57
|
+
GridModel: MockGridModel,
|
|
58
|
+
FieldModel: MockFieldModel,
|
|
59
|
+
ReferenceFormGridModel,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const form = engine.createModel({
|
|
63
|
+
uid: 'host-form',
|
|
64
|
+
use: 'FormBlockModel',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const refGrid = engine.createModel({
|
|
68
|
+
uid: 'host-grid',
|
|
69
|
+
use: 'ReferenceFormGridModel',
|
|
70
|
+
parentId: form.uid,
|
|
71
|
+
subKey: 'grid',
|
|
72
|
+
subType: 'object',
|
|
73
|
+
stepParams: {
|
|
74
|
+
referenceSettings: {
|
|
75
|
+
useTemplate: {
|
|
76
|
+
templateUid: 'tpl-1',
|
|
77
|
+
targetUid: 'tpl-root',
|
|
78
|
+
targetPath: 'subModels.grid',
|
|
79
|
+
mode: 'reference',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
form.setSubModel('grid', refGrid);
|
|
85
|
+
|
|
86
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
87
|
+
|
|
88
|
+
const items = ((refGrid as any).subModels as any)?.items as FlowModel[];
|
|
89
|
+
expect(Array.isArray(items)).toBe(true);
|
|
90
|
+
expect(items.length).toBe(2);
|
|
91
|
+
expect(items.map((m) => m.uid).sort()).toEqual(['tpl-f1', 'tpl-f2']);
|
|
92
|
+
|
|
93
|
+
const serialized = refGrid.serialize();
|
|
94
|
+
expect(serialized.subModels).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('syncs host extraTitle with reference template info', async () => {
|
|
98
|
+
MockFormBlockModel.define({
|
|
99
|
+
label: '默认block title',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const engine = new FlowEngine();
|
|
103
|
+
const store: Record<string, any> = {
|
|
104
|
+
'tpl-root': {
|
|
105
|
+
uid: 'tpl-root',
|
|
106
|
+
use: 'FormBlockModel',
|
|
107
|
+
subModels: {
|
|
108
|
+
grid: {
|
|
109
|
+
uid: 'tpl-grid',
|
|
110
|
+
use: 'GridModel',
|
|
111
|
+
subKey: 'grid',
|
|
112
|
+
subType: 'object',
|
|
113
|
+
subModels: { items: [] },
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
120
|
+
const mockRepository = {
|
|
121
|
+
findOne: vi.fn(async (query) => {
|
|
122
|
+
const data = store[query.uid];
|
|
123
|
+
return data ? clone(data) : null;
|
|
124
|
+
}),
|
|
125
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
126
|
+
destroy: vi.fn(async () => true),
|
|
127
|
+
move: vi.fn(async () => {}),
|
|
128
|
+
duplicate: vi.fn(async () => null),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
engine.setModelRepository(mockRepository as any);
|
|
132
|
+
engine.registerModels({
|
|
133
|
+
FormBlockModel: MockFormBlockModel,
|
|
134
|
+
GridModel: MockGridModel,
|
|
135
|
+
FieldModel: MockFieldModel,
|
|
136
|
+
ReferenceFormGridModel,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const form = engine.createModel({
|
|
140
|
+
uid: 'host-form',
|
|
141
|
+
use: 'FormBlockModel',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const refGrid = engine.createModel({
|
|
145
|
+
uid: 'host-grid',
|
|
146
|
+
use: 'ReferenceFormGridModel',
|
|
147
|
+
parentId: form.uid,
|
|
148
|
+
subKey: 'grid',
|
|
149
|
+
subType: 'object',
|
|
150
|
+
stepParams: {
|
|
151
|
+
referenceSettings: {
|
|
152
|
+
useTemplate: {
|
|
153
|
+
templateUid: 'tpl-1',
|
|
154
|
+
templateName: '模板名称',
|
|
155
|
+
targetUid: 'tpl-root',
|
|
156
|
+
targetPath: 'subModels.grid',
|
|
157
|
+
mode: 'reference',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
form.setSubModel('grid', refGrid);
|
|
163
|
+
|
|
164
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
165
|
+
expect(form.title).toBe('默认block title');
|
|
166
|
+
expect((form as any).extraTitle).toBe('Reference template: 模板名称 (Fields only)');
|
|
167
|
+
|
|
168
|
+
// clear settings should restore base title
|
|
169
|
+
(refGrid as any).stepParams.referenceSettings.useTemplate.templateUid = '';
|
|
170
|
+
(refGrid as any).stepParams.referenceSettings.useTemplate.targetUid = '';
|
|
171
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
172
|
+
expect(form.title).toBe('默认block title');
|
|
173
|
+
expect((form as any).extraTitle).toBe('');
|
|
174
|
+
});
|
|
175
|
+
});
|