@nocobase/plugin-ui-templates 2.0.0-alpha.58 → 2.0.0-alpha.60
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/dist/client/index.js +1 -1
- package/dist/client/models/ReferenceFormGridModel.d.ts +16 -2
- package/dist/client/models/SubModelTemplateImporterModel.d.ts +2 -15
- package/dist/client/models/referenceShared.d.ts +2 -1
- package/dist/client/utils/templateCopy.d.ts +21 -0
- package/dist/externalVersion.js +7 -7
- package/dist/locale/zh-CN.json +1 -1
- package/package.json +3 -3
- package/src/client/index.ts +0 -2
- package/src/client/menuExtensions.tsx +53 -5
- package/src/client/models/ReferenceBlockModel.tsx +4 -0
- package/src/client/models/ReferenceFormGridModel.tsx +175 -29
- package/src/client/models/SubModelTemplateImporterModel.tsx +295 -216
- package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +787 -3
- package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +80 -19
- package/src/client/models/referenceShared.tsx +8 -0
- package/src/client/utils/__tests__/templateCopy.test.ts +67 -0
- package/src/client/utils/templateCopy.ts +59 -0
- package/src/locale/zh-CN.json +1 -1
- package/dist/client/subModelMenuExtensions.d.ts +0 -10
- package/dist/client/utils/refHost.d.ts +0 -20
- package/src/client/subModelMenuExtensions.ts +0 -103
- package/src/client/utils/refHost.ts +0 -44
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
10
|
+
import { CollectionFieldModel, FlowEngine, FlowModel } from '@nocobase/flow-engine';
|
|
11
11
|
import { describe, it, expect, vi } from 'vitest';
|
|
12
12
|
import { ReferenceFormGridModel } from '../ReferenceFormGridModel';
|
|
13
13
|
|
|
@@ -45,7 +45,7 @@ describe('ReferenceFormGridModel', () => {
|
|
|
45
45
|
const data = store[query.uid];
|
|
46
46
|
return data ? clone(data) : null;
|
|
47
47
|
}),
|
|
48
|
-
save: vi.fn(async (model) => ({ uid:
|
|
48
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
49
49
|
destroy: vi.fn(async () => true),
|
|
50
50
|
move: vi.fn(async () => {}),
|
|
51
51
|
duplicate: vi.fn(async () => null),
|
|
@@ -94,6 +94,790 @@ describe('ReferenceFormGridModel', () => {
|
|
|
94
94
|
expect(serialized.subModels).toBeUndefined();
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
+
it('bridges host view (inputArgs) to scoped engine (override parent engine view)', async () => {
|
|
98
|
+
class HostModel extends FlowModel {}
|
|
99
|
+
class GridModel extends FlowModel {}
|
|
100
|
+
class ViewReaderModel extends FlowModel {
|
|
101
|
+
onInit(options: any) {
|
|
102
|
+
super.onInit(options);
|
|
103
|
+
const foo = (this.context as any)?.view?.inputArgs?.foo;
|
|
104
|
+
this.setStepParams('viewSettings', 'read', { foo });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const engine = new FlowEngine();
|
|
109
|
+
const store: Record<string, any> = {
|
|
110
|
+
'tpl-root': {
|
|
111
|
+
uid: 'tpl-root',
|
|
112
|
+
use: 'ViewReaderModel',
|
|
113
|
+
subModels: {
|
|
114
|
+
grid: {
|
|
115
|
+
uid: 'tpl-grid',
|
|
116
|
+
use: 'GridModel',
|
|
117
|
+
subKey: 'grid',
|
|
118
|
+
subType: 'object',
|
|
119
|
+
subModels: {
|
|
120
|
+
items: [],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
128
|
+
const mockRepository = {
|
|
129
|
+
findOne: vi.fn(async (query) => {
|
|
130
|
+
const data = store[query.uid];
|
|
131
|
+
return data ? clone(data) : null;
|
|
132
|
+
}),
|
|
133
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
134
|
+
destroy: vi.fn(async () => true),
|
|
135
|
+
move: vi.fn(async () => {}),
|
|
136
|
+
duplicate: vi.fn(async () => null),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
engine.setModelRepository(mockRepository as any);
|
|
140
|
+
engine.registerModels({
|
|
141
|
+
HostModel,
|
|
142
|
+
GridModel,
|
|
143
|
+
ViewReaderModel,
|
|
144
|
+
ReferenceFormGridModel,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 外层(父引擎)view:模拟列表页
|
|
148
|
+
engine.context.defineProperty('view', { value: { inputArgs: { foo: 'from-parent' } } });
|
|
149
|
+
|
|
150
|
+
// 宿主区块 view:模拟抽屉/弹窗的详情页 inputArgs
|
|
151
|
+
const host = engine.createModel<HostModel>({ uid: 'host', use: 'HostModel' });
|
|
152
|
+
host.context.defineProperty('view', { value: { inputArgs: { foo: 'from-host' } } });
|
|
153
|
+
|
|
154
|
+
const refGrid = engine.createModel({
|
|
155
|
+
uid: 'host-grid',
|
|
156
|
+
use: 'ReferenceFormGridModel',
|
|
157
|
+
parentId: host.uid,
|
|
158
|
+
subKey: 'grid',
|
|
159
|
+
subType: 'object',
|
|
160
|
+
stepParams: {
|
|
161
|
+
referenceSettings: {
|
|
162
|
+
useTemplate: {
|
|
163
|
+
templateUid: 'tpl-1',
|
|
164
|
+
targetUid: 'tpl-root',
|
|
165
|
+
targetPath: 'subModels.grid',
|
|
166
|
+
mode: 'reference',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
host.setSubModel('grid', refGrid);
|
|
172
|
+
|
|
173
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
174
|
+
|
|
175
|
+
const targetRoot = (refGrid as any)._targetRoot as FlowModel;
|
|
176
|
+
expect(targetRoot).toBeTruthy();
|
|
177
|
+
expect(targetRoot.getStepParams('viewSettings', 'read')).toEqual({ foo: 'from-host' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('syncs association appends to host block (field template reference)', async () => {
|
|
181
|
+
class HostBlockModel extends FlowModel {
|
|
182
|
+
appends: string[] = [];
|
|
183
|
+
onInit(options: any) {
|
|
184
|
+
super.onInit(options);
|
|
185
|
+
this.context.defineProperty('blockModel', { value: this });
|
|
186
|
+
}
|
|
187
|
+
addAppends(fieldPath: string) {
|
|
188
|
+
this.appends.push(fieldPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class TemplateRootBlockModel extends FlowModel {
|
|
193
|
+
appends: string[] = [];
|
|
194
|
+
onInit(options: any) {
|
|
195
|
+
super.onInit(options);
|
|
196
|
+
this.context.defineProperty('blockModel', { value: this });
|
|
197
|
+
}
|
|
198
|
+
addAppends(fieldPath: string) {
|
|
199
|
+
this.appends.push(fieldPath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class GridModel extends FlowModel {}
|
|
204
|
+
class ItemModel extends CollectionFieldModel {}
|
|
205
|
+
|
|
206
|
+
const engine = new FlowEngine();
|
|
207
|
+
const store: Record<string, any> = {
|
|
208
|
+
'tpl-root': {
|
|
209
|
+
uid: 'tpl-root',
|
|
210
|
+
use: 'TemplateRootBlockModel',
|
|
211
|
+
subModels: {
|
|
212
|
+
grid: {
|
|
213
|
+
uid: 'tpl-grid',
|
|
214
|
+
use: 'GridModel',
|
|
215
|
+
subKey: 'grid',
|
|
216
|
+
subType: 'object',
|
|
217
|
+
subModels: {
|
|
218
|
+
items: [
|
|
219
|
+
{
|
|
220
|
+
uid: 'tpl-item-1',
|
|
221
|
+
use: 'ItemModel',
|
|
222
|
+
subKey: 'items',
|
|
223
|
+
subType: 'array',
|
|
224
|
+
stepParams: {
|
|
225
|
+
fieldSettings: {
|
|
226
|
+
init: {
|
|
227
|
+
dataSourceKey: 'main',
|
|
228
|
+
collectionName: 'users',
|
|
229
|
+
fieldPath: 'roles.name',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
242
|
+
const mockRepository = {
|
|
243
|
+
findOne: vi.fn(async (query) => {
|
|
244
|
+
const data = store[query.uid];
|
|
245
|
+
return data ? clone(data) : null;
|
|
246
|
+
}),
|
|
247
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
248
|
+
destroy: vi.fn(async () => true),
|
|
249
|
+
move: vi.fn(async () => {}),
|
|
250
|
+
duplicate: vi.fn(async () => null),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
engine.setModelRepository(mockRepository as any);
|
|
254
|
+
engine.registerModels({
|
|
255
|
+
HostBlockModel,
|
|
256
|
+
TemplateRootBlockModel,
|
|
257
|
+
GridModel,
|
|
258
|
+
ItemModel,
|
|
259
|
+
ReferenceFormGridModel,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const host = engine.createModel<HostBlockModel>({ uid: 'host', use: 'HostBlockModel' });
|
|
263
|
+
|
|
264
|
+
const refGrid = engine.createModel({
|
|
265
|
+
uid: 'host-grid',
|
|
266
|
+
use: 'ReferenceFormGridModel',
|
|
267
|
+
parentId: host.uid,
|
|
268
|
+
subKey: 'grid',
|
|
269
|
+
subType: 'object',
|
|
270
|
+
stepParams: {
|
|
271
|
+
referenceSettings: {
|
|
272
|
+
useTemplate: {
|
|
273
|
+
templateUid: 'tpl-1',
|
|
274
|
+
targetUid: 'tpl-root',
|
|
275
|
+
targetPath: 'subModels.grid',
|
|
276
|
+
mode: 'reference',
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
host.setSubModel('grid', refGrid);
|
|
282
|
+
|
|
283
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
284
|
+
|
|
285
|
+
// host should receive the top-level association append ('roles') from template fieldPath 'roles.name'
|
|
286
|
+
expect(host.appends).toContain('roles');
|
|
287
|
+
|
|
288
|
+
// template root still receives its own addAppends during onInit, but it should not be the only one
|
|
289
|
+
const tplRoot = (refGrid as any)._targetRoot as TemplateRootBlockModel;
|
|
290
|
+
expect(tplRoot).toBeTruthy();
|
|
291
|
+
expect(tplRoot.appends.length).toBeGreaterThan(0);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('bridges host view (inputArgs) to scoped engine (override parent engine view)', async () => {
|
|
295
|
+
class HostModel extends FlowModel {}
|
|
296
|
+
class GridModel extends FlowModel {}
|
|
297
|
+
class ViewReaderModel extends FlowModel {
|
|
298
|
+
onInit(options: any) {
|
|
299
|
+
super.onInit(options);
|
|
300
|
+
const foo = (this.context as any)?.view?.inputArgs?.foo;
|
|
301
|
+
this.setStepParams('viewSettings', 'read', { foo });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const engine = new FlowEngine();
|
|
306
|
+
const store: Record<string, any> = {
|
|
307
|
+
'tpl-root': {
|
|
308
|
+
uid: 'tpl-root',
|
|
309
|
+
use: 'ViewReaderModel',
|
|
310
|
+
subModels: {
|
|
311
|
+
grid: {
|
|
312
|
+
uid: 'tpl-grid',
|
|
313
|
+
use: 'GridModel',
|
|
314
|
+
subKey: 'grid',
|
|
315
|
+
subType: 'object',
|
|
316
|
+
subModels: {
|
|
317
|
+
items: [],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
325
|
+
const mockRepository = {
|
|
326
|
+
findOne: vi.fn(async (query) => {
|
|
327
|
+
const data = store[query.uid];
|
|
328
|
+
return data ? clone(data) : null;
|
|
329
|
+
}),
|
|
330
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
331
|
+
destroy: vi.fn(async () => true),
|
|
332
|
+
move: vi.fn(async () => {}),
|
|
333
|
+
duplicate: vi.fn(async () => null),
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
engine.setModelRepository(mockRepository as any);
|
|
337
|
+
engine.registerModels({
|
|
338
|
+
HostModel,
|
|
339
|
+
GridModel,
|
|
340
|
+
ViewReaderModel,
|
|
341
|
+
ReferenceFormGridModel,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 外层(父引擎)view:模拟列表页
|
|
345
|
+
engine.context.defineProperty('view', { value: { inputArgs: { foo: 'from-parent' } } });
|
|
346
|
+
|
|
347
|
+
// 宿主区块 view:模拟抽屉/弹窗的详情页 inputArgs
|
|
348
|
+
const host = engine.createModel<HostModel>({ uid: 'host', use: 'HostModel' });
|
|
349
|
+
host.context.defineProperty('view', { value: { inputArgs: { foo: 'from-host' } } });
|
|
350
|
+
|
|
351
|
+
const refGrid = engine.createModel({
|
|
352
|
+
uid: 'host-grid',
|
|
353
|
+
use: 'ReferenceFormGridModel',
|
|
354
|
+
parentId: host.uid,
|
|
355
|
+
subKey: 'grid',
|
|
356
|
+
subType: 'object',
|
|
357
|
+
stepParams: {
|
|
358
|
+
referenceSettings: {
|
|
359
|
+
useTemplate: {
|
|
360
|
+
templateUid: 'tpl-1',
|
|
361
|
+
targetUid: 'tpl-root',
|
|
362
|
+
targetPath: 'subModels.grid',
|
|
363
|
+
mode: 'reference',
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
host.setSubModel('grid', refGrid);
|
|
369
|
+
|
|
370
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
371
|
+
|
|
372
|
+
const targetRoot = (refGrid as any)._targetRoot as FlowModel;
|
|
373
|
+
expect(targetRoot).toBeTruthy();
|
|
374
|
+
expect(targetRoot.getStepParams('viewSettings', 'read')).toEqual({ foo: 'from-host' });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('syncs association appends to host block (field template reference)', async () => {
|
|
378
|
+
class HostBlockModel extends FlowModel {
|
|
379
|
+
appends: string[] = [];
|
|
380
|
+
onInit(options: any) {
|
|
381
|
+
super.onInit(options);
|
|
382
|
+
this.context.defineProperty('blockModel', { value: this });
|
|
383
|
+
}
|
|
384
|
+
addAppends(fieldPath: string) {
|
|
385
|
+
this.appends.push(fieldPath);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
class TemplateRootBlockModel extends FlowModel {
|
|
390
|
+
appends: string[] = [];
|
|
391
|
+
onInit(options: any) {
|
|
392
|
+
super.onInit(options);
|
|
393
|
+
this.context.defineProperty('blockModel', { value: this });
|
|
394
|
+
}
|
|
395
|
+
addAppends(fieldPath: string) {
|
|
396
|
+
this.appends.push(fieldPath);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
class GridModel extends FlowModel {}
|
|
401
|
+
class ItemModel extends CollectionFieldModel {}
|
|
402
|
+
|
|
403
|
+
const engine = new FlowEngine();
|
|
404
|
+
const store: Record<string, any> = {
|
|
405
|
+
'tpl-root': {
|
|
406
|
+
uid: 'tpl-root',
|
|
407
|
+
use: 'TemplateRootBlockModel',
|
|
408
|
+
subModels: {
|
|
409
|
+
grid: {
|
|
410
|
+
uid: 'tpl-grid',
|
|
411
|
+
use: 'GridModel',
|
|
412
|
+
subKey: 'grid',
|
|
413
|
+
subType: 'object',
|
|
414
|
+
subModels: {
|
|
415
|
+
items: [
|
|
416
|
+
{
|
|
417
|
+
uid: 'tpl-item-1',
|
|
418
|
+
use: 'ItemModel',
|
|
419
|
+
subKey: 'items',
|
|
420
|
+
subType: 'array',
|
|
421
|
+
stepParams: {
|
|
422
|
+
fieldSettings: {
|
|
423
|
+
init: {
|
|
424
|
+
dataSourceKey: 'main',
|
|
425
|
+
collectionName: 'users',
|
|
426
|
+
fieldPath: 'roles.name',
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
439
|
+
const mockRepository = {
|
|
440
|
+
findOne: vi.fn(async (query) => {
|
|
441
|
+
const data = store[query.uid];
|
|
442
|
+
return data ? clone(data) : null;
|
|
443
|
+
}),
|
|
444
|
+
save: vi.fn(async (model) => ({ uid: typeof model?.uid === 'string' ? model.uid : (model?.uid as any) })),
|
|
445
|
+
destroy: vi.fn(async () => true),
|
|
446
|
+
move: vi.fn(async () => {}),
|
|
447
|
+
duplicate: vi.fn(async () => null),
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
engine.setModelRepository(mockRepository as any);
|
|
451
|
+
engine.registerModels({
|
|
452
|
+
HostBlockModel,
|
|
453
|
+
TemplateRootBlockModel,
|
|
454
|
+
GridModel,
|
|
455
|
+
ItemModel,
|
|
456
|
+
ReferenceFormGridModel,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const host = engine.createModel<HostBlockModel>({ uid: 'host', use: 'HostBlockModel' });
|
|
460
|
+
|
|
461
|
+
const refGrid = engine.createModel({
|
|
462
|
+
uid: 'host-grid',
|
|
463
|
+
use: 'ReferenceFormGridModel',
|
|
464
|
+
parentId: host.uid,
|
|
465
|
+
subKey: 'grid',
|
|
466
|
+
subType: 'object',
|
|
467
|
+
stepParams: {
|
|
468
|
+
referenceSettings: {
|
|
469
|
+
useTemplate: {
|
|
470
|
+
templateUid: 'tpl-1',
|
|
471
|
+
targetUid: 'tpl-root',
|
|
472
|
+
targetPath: 'subModels.grid',
|
|
473
|
+
mode: 'reference',
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
host.setSubModel('grid', refGrid);
|
|
479
|
+
|
|
480
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
481
|
+
|
|
482
|
+
// host should receive the top-level association append ('roles') from template fieldPath 'roles.name'
|
|
483
|
+
expect(host.appends).toContain('roles');
|
|
484
|
+
|
|
485
|
+
// template root still receives its own addAppends during onInit, but it should not be the only one
|
|
486
|
+
const tplRoot = (refGrid as any)._targetRoot as TemplateRootBlockModel;
|
|
487
|
+
expect(tplRoot).toBeTruthy();
|
|
488
|
+
expect(tplRoot.appends.length).toBeGreaterThan(0);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('bridges host context (record) to referenced grid while keeping scoped engine', async () => {
|
|
492
|
+
const engine = new FlowEngine();
|
|
493
|
+
const store: Record<string, any> = {
|
|
494
|
+
'tpl-root': {
|
|
495
|
+
uid: 'tpl-root',
|
|
496
|
+
use: 'DetailsBlockModel',
|
|
497
|
+
subModels: {
|
|
498
|
+
grid: {
|
|
499
|
+
uid: 'tpl-grid',
|
|
500
|
+
use: 'DetailsGridModel',
|
|
501
|
+
subKey: 'grid',
|
|
502
|
+
subType: 'object',
|
|
503
|
+
subModels: {
|
|
504
|
+
items: [{ uid: 'tpl-i1', use: 'ItemModel', subKey: 'items', subType: 'array' }],
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
512
|
+
const mockRepository = {
|
|
513
|
+
findOne: vi.fn(async (query) => {
|
|
514
|
+
const data = store[query.uid];
|
|
515
|
+
return data ? clone(data) : null;
|
|
516
|
+
}),
|
|
517
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
518
|
+
destroy: vi.fn(async () => true),
|
|
519
|
+
move: vi.fn(async () => {}),
|
|
520
|
+
duplicate: vi.fn(async () => null),
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
class HostDetailsBlockModel extends FlowModel {}
|
|
524
|
+
class DetailsBlockModel extends FlowModel {}
|
|
525
|
+
class DetailsGridModel extends FlowModel {}
|
|
526
|
+
class ItemModel extends FlowModel {}
|
|
527
|
+
|
|
528
|
+
engine.setModelRepository(mockRepository as any);
|
|
529
|
+
engine.registerModels({
|
|
530
|
+
HostDetailsBlockModel,
|
|
531
|
+
DetailsBlockModel,
|
|
532
|
+
DetailsGridModel,
|
|
533
|
+
ItemModel,
|
|
534
|
+
ReferenceFormGridModel,
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const host = engine.createModel<HostDetailsBlockModel>({
|
|
538
|
+
uid: 'host-details',
|
|
539
|
+
use: 'HostDetailsBlockModel',
|
|
540
|
+
});
|
|
541
|
+
host.context.defineProperty('record', { value: { username: 'nocobase' } });
|
|
542
|
+
|
|
543
|
+
const refGrid = engine.createModel({
|
|
544
|
+
uid: 'host-grid',
|
|
545
|
+
use: 'ReferenceFormGridModel',
|
|
546
|
+
parentId: host.uid,
|
|
547
|
+
subKey: 'grid',
|
|
548
|
+
subType: 'object',
|
|
549
|
+
stepParams: {
|
|
550
|
+
referenceSettings: {
|
|
551
|
+
useTemplate: {
|
|
552
|
+
templateUid: 'tpl-1',
|
|
553
|
+
targetUid: 'tpl-root',
|
|
554
|
+
targetPath: 'subModels.grid',
|
|
555
|
+
mode: 'reference',
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
host.setSubModel('grid', refGrid);
|
|
561
|
+
|
|
562
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
563
|
+
|
|
564
|
+
const items = ((refGrid as any).subModels as any)?.items as FlowModel[];
|
|
565
|
+
expect(Array.isArray(items)).toBe(true);
|
|
566
|
+
expect(items.length).toBe(1);
|
|
567
|
+
expect(items[0].context.record).toEqual({ username: 'nocobase' });
|
|
568
|
+
|
|
569
|
+
// 引用渲染应仍使用 scoped engine(避免丢失模型实例/缓存隔离)
|
|
570
|
+
const scoped = (refGrid as any)._scopedEngine;
|
|
571
|
+
expect(scoped).toBeTruthy();
|
|
572
|
+
expect(items[0].context.engine).toBe(scoped);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('delegates layout + linkage rules stepParams to template grid (legacy fallback to template root)', async () => {
|
|
576
|
+
const engine = new FlowEngine();
|
|
577
|
+
const store: Record<string, any> = {
|
|
578
|
+
'tpl-root': {
|
|
579
|
+
uid: 'tpl-root',
|
|
580
|
+
use: 'FormBlockModel',
|
|
581
|
+
stepParams: {
|
|
582
|
+
formModelSettings: {
|
|
583
|
+
layout: {
|
|
584
|
+
layout: 'horizontal',
|
|
585
|
+
labelAlign: 'right',
|
|
586
|
+
labelWidth: 160,
|
|
587
|
+
labelWrap: false,
|
|
588
|
+
colon: false,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
eventSettings: {
|
|
592
|
+
linkageRules: {
|
|
593
|
+
value: [
|
|
594
|
+
{
|
|
595
|
+
key: 'r1',
|
|
596
|
+
title: 'Rule 1',
|
|
597
|
+
enable: true,
|
|
598
|
+
condition: { $and: [] },
|
|
599
|
+
actions: [],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
subModels: {
|
|
606
|
+
grid: {
|
|
607
|
+
uid: 'tpl-grid',
|
|
608
|
+
use: 'GridModel',
|
|
609
|
+
subKey: 'grid',
|
|
610
|
+
subType: 'object',
|
|
611
|
+
stepParams: {},
|
|
612
|
+
subModels: {
|
|
613
|
+
items: [],
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
621
|
+
const mockRepository = {
|
|
622
|
+
findOne: vi.fn(async (query) => {
|
|
623
|
+
const data = store[query.uid];
|
|
624
|
+
return data ? clone(data) : null;
|
|
625
|
+
}),
|
|
626
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
627
|
+
destroy: vi.fn(async () => true),
|
|
628
|
+
move: vi.fn(async () => {}),
|
|
629
|
+
duplicate: vi.fn(async () => null),
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
class HostFormBlockModel extends FlowModel {}
|
|
633
|
+
class GridModel extends FlowModel {}
|
|
634
|
+
|
|
635
|
+
engine.setModelRepository(mockRepository as any);
|
|
636
|
+
engine.registerModels({
|
|
637
|
+
HostFormBlockModel,
|
|
638
|
+
FormBlockModel: MockFormBlockModel,
|
|
639
|
+
GridModel,
|
|
640
|
+
ReferenceFormGridModel,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const host = engine.createModel<HostFormBlockModel>({
|
|
644
|
+
uid: 'host-form',
|
|
645
|
+
use: 'HostFormBlockModel',
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
const refGrid = engine.createModel({
|
|
649
|
+
uid: 'host-grid',
|
|
650
|
+
use: 'ReferenceFormGridModel',
|
|
651
|
+
parentId: host.uid,
|
|
652
|
+
subKey: 'grid',
|
|
653
|
+
subType: 'object',
|
|
654
|
+
stepParams: {
|
|
655
|
+
referenceSettings: {
|
|
656
|
+
useTemplate: {
|
|
657
|
+
templateUid: 'tpl-1',
|
|
658
|
+
targetUid: 'tpl-root',
|
|
659
|
+
targetPath: 'subModels.grid',
|
|
660
|
+
mode: 'reference',
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
host.setSubModel('grid', refGrid);
|
|
666
|
+
|
|
667
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
668
|
+
|
|
669
|
+
// legacy: params live on template root, ReferenceFormGridModel should read them as fallback
|
|
670
|
+
expect(refGrid.getStepParams('formModelSettings', 'layout')).toEqual({
|
|
671
|
+
layout: 'horizontal',
|
|
672
|
+
labelAlign: 'right',
|
|
673
|
+
labelWidth: 160,
|
|
674
|
+
labelWrap: false,
|
|
675
|
+
colon: false,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
expect(refGrid.getStepParams('eventSettings', 'linkageRules')).toEqual({
|
|
679
|
+
value: [
|
|
680
|
+
{
|
|
681
|
+
key: 'r1',
|
|
682
|
+
title: 'Rule 1',
|
|
683
|
+
enable: true,
|
|
684
|
+
condition: { $and: [] },
|
|
685
|
+
actions: [],
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// new: write to template grid
|
|
691
|
+
refGrid.setStepParams('formModelSettings', 'layout', {
|
|
692
|
+
layout: 'vertical',
|
|
693
|
+
labelAlign: 'left',
|
|
694
|
+
labelWidth: 120,
|
|
695
|
+
labelWrap: true,
|
|
696
|
+
colon: true,
|
|
697
|
+
});
|
|
698
|
+
refGrid.setStepParams('eventSettings', 'linkageRules', { value: [] });
|
|
699
|
+
|
|
700
|
+
expect(refGrid.getStepParams('formModelSettings', 'layout')).toEqual({
|
|
701
|
+
layout: 'vertical',
|
|
702
|
+
labelAlign: 'left',
|
|
703
|
+
labelWidth: 120,
|
|
704
|
+
labelWrap: true,
|
|
705
|
+
colon: true,
|
|
706
|
+
});
|
|
707
|
+
expect(refGrid.getStepParams('eventSettings', 'linkageRules')).toEqual({ value: [] });
|
|
708
|
+
|
|
709
|
+
await refGrid.saveStepParams();
|
|
710
|
+
const savedUids = mockRepository.save.mock.calls.map((c) => c[0]?.uid).sort();
|
|
711
|
+
expect(savedUids).toContain('host-grid');
|
|
712
|
+
expect(savedUids).toContain('tpl-grid');
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('flushes local (pre-resolve) stepParams to template grid on resolve/save', async () => {
|
|
716
|
+
const engine = new FlowEngine();
|
|
717
|
+
const store: Record<string, any> = {
|
|
718
|
+
'tpl-root': {
|
|
719
|
+
uid: 'tpl-root',
|
|
720
|
+
use: 'FormBlockModel',
|
|
721
|
+
subModels: {
|
|
722
|
+
grid: {
|
|
723
|
+
uid: 'tpl-grid',
|
|
724
|
+
use: 'GridModel',
|
|
725
|
+
subKey: 'grid',
|
|
726
|
+
subType: 'object',
|
|
727
|
+
stepParams: {},
|
|
728
|
+
subModels: { items: [] },
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
735
|
+
const mockRepository = {
|
|
736
|
+
findOne: vi.fn(async (query) => {
|
|
737
|
+
const data = store[query.uid];
|
|
738
|
+
return data ? clone(data) : null;
|
|
739
|
+
}),
|
|
740
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
741
|
+
destroy: vi.fn(async () => true),
|
|
742
|
+
move: vi.fn(async () => {}),
|
|
743
|
+
duplicate: vi.fn(async () => null),
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
class HostFormBlockModel extends FlowModel {}
|
|
747
|
+
class GridModel extends FlowModel {}
|
|
748
|
+
|
|
749
|
+
engine.setModelRepository(mockRepository as any);
|
|
750
|
+
engine.registerModels({
|
|
751
|
+
HostFormBlockModel,
|
|
752
|
+
GridModel,
|
|
753
|
+
ReferenceFormGridModel,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
const host = engine.createModel<HostFormBlockModel>({
|
|
757
|
+
uid: 'host-form',
|
|
758
|
+
use: 'HostFormBlockModel',
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const refGrid = engine.createModel({
|
|
762
|
+
uid: 'host-grid',
|
|
763
|
+
use: 'ReferenceFormGridModel',
|
|
764
|
+
parentId: host.uid,
|
|
765
|
+
subKey: 'grid',
|
|
766
|
+
subType: 'object',
|
|
767
|
+
stepParams: {
|
|
768
|
+
referenceSettings: {
|
|
769
|
+
useTemplate: {
|
|
770
|
+
templateUid: 'tpl-1',
|
|
771
|
+
targetUid: 'tpl-root',
|
|
772
|
+
targetPath: 'subModels.grid',
|
|
773
|
+
mode: 'reference',
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
host.setSubModel('grid', refGrid);
|
|
779
|
+
|
|
780
|
+
// before target grid resolved, setStepParams should land in local (host-grid) stepParams first
|
|
781
|
+
refGrid.setStepParams('eventSettings', 'linkageRules', { value: [{ key: 'pre' }] });
|
|
782
|
+
expect((refGrid as any).stepParams?.eventSettings?.linkageRules).toEqual({ value: [{ key: 'pre' }] });
|
|
783
|
+
|
|
784
|
+
// save should resolve target, flush local params into template grid, then persist both
|
|
785
|
+
await refGrid.saveStepParams();
|
|
786
|
+
|
|
787
|
+
const targetGrid = (refGrid as any)._targetGrid as FlowModel;
|
|
788
|
+
expect(targetGrid).toBeTruthy();
|
|
789
|
+
expect(targetGrid.getStepParams('eventSettings', 'linkageRules')).toEqual({ value: [{ key: 'pre' }] });
|
|
790
|
+
expect((refGrid as any).stepParams?.eventSettings).toBeUndefined();
|
|
791
|
+
|
|
792
|
+
const savedUids = mockRepository.save.mock.calls.map((c) => c[0]?.uid).sort();
|
|
793
|
+
expect(savedUids).toContain('host-grid');
|
|
794
|
+
expect(savedUids).toContain('tpl-grid');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('bridges host context (record) to referenced grid while keeping scoped engine', async () => {
|
|
798
|
+
const engine = new FlowEngine();
|
|
799
|
+
const store: Record<string, any> = {
|
|
800
|
+
'tpl-root': {
|
|
801
|
+
uid: 'tpl-root',
|
|
802
|
+
use: 'DetailsBlockModel',
|
|
803
|
+
subModels: {
|
|
804
|
+
grid: {
|
|
805
|
+
uid: 'tpl-grid',
|
|
806
|
+
use: 'DetailsGridModel',
|
|
807
|
+
subKey: 'grid',
|
|
808
|
+
subType: 'object',
|
|
809
|
+
subModels: {
|
|
810
|
+
items: [{ uid: 'tpl-i1', use: 'ItemModel', subKey: 'items', subType: 'array' }],
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
|
|
818
|
+
const mockRepository = {
|
|
819
|
+
findOne: vi.fn(async (query) => {
|
|
820
|
+
const data = store[query.uid];
|
|
821
|
+
return data ? clone(data) : null;
|
|
822
|
+
}),
|
|
823
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
824
|
+
destroy: vi.fn(async () => true),
|
|
825
|
+
move: vi.fn(async () => {}),
|
|
826
|
+
duplicate: vi.fn(async () => null),
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
class HostDetailsBlockModel extends FlowModel {}
|
|
830
|
+
class DetailsBlockModel extends FlowModel {}
|
|
831
|
+
class DetailsGridModel extends FlowModel {}
|
|
832
|
+
class ItemModel extends FlowModel {}
|
|
833
|
+
|
|
834
|
+
engine.setModelRepository(mockRepository as any);
|
|
835
|
+
engine.registerModels({
|
|
836
|
+
HostDetailsBlockModel,
|
|
837
|
+
DetailsBlockModel,
|
|
838
|
+
DetailsGridModel,
|
|
839
|
+
ItemModel,
|
|
840
|
+
ReferenceFormGridModel,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
const host = engine.createModel<HostDetailsBlockModel>({
|
|
844
|
+
uid: 'host-details',
|
|
845
|
+
use: 'HostDetailsBlockModel',
|
|
846
|
+
});
|
|
847
|
+
host.context.defineProperty('record', { value: { username: 'nocobase' } });
|
|
848
|
+
|
|
849
|
+
const refGrid = engine.createModel({
|
|
850
|
+
uid: 'host-grid',
|
|
851
|
+
use: 'ReferenceFormGridModel',
|
|
852
|
+
parentId: host.uid,
|
|
853
|
+
subKey: 'grid',
|
|
854
|
+
subType: 'object',
|
|
855
|
+
stepParams: {
|
|
856
|
+
referenceSettings: {
|
|
857
|
+
useTemplate: {
|
|
858
|
+
templateUid: 'tpl-1',
|
|
859
|
+
targetUid: 'tpl-root',
|
|
860
|
+
targetPath: 'subModels.grid',
|
|
861
|
+
mode: 'reference',
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
host.setSubModel('grid', refGrid);
|
|
867
|
+
|
|
868
|
+
await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
869
|
+
|
|
870
|
+
const items = ((refGrid as any).subModels as any)?.items as FlowModel[];
|
|
871
|
+
expect(Array.isArray(items)).toBe(true);
|
|
872
|
+
expect(items.length).toBe(1);
|
|
873
|
+
expect(items[0].context.record).toEqual({ username: 'nocobase' });
|
|
874
|
+
|
|
875
|
+
// 引用渲染应仍使用 scoped engine(避免丢失模型实例/缓存隔离)
|
|
876
|
+
const scoped = (refGrid as any)._scopedEngine;
|
|
877
|
+
expect(scoped).toBeTruthy();
|
|
878
|
+
expect(items[0].context.engine).toBe(scoped);
|
|
879
|
+
});
|
|
880
|
+
|
|
97
881
|
it('syncs host extraTitle with reference template info', async () => {
|
|
98
882
|
MockFormBlockModel.define({
|
|
99
883
|
label: '默认block title',
|
|
@@ -122,7 +906,7 @@ describe('ReferenceFormGridModel', () => {
|
|
|
122
906
|
const data = store[query.uid];
|
|
123
907
|
return data ? clone(data) : null;
|
|
124
908
|
}),
|
|
125
|
-
save: vi.fn(async (model) => ({ uid:
|
|
909
|
+
save: vi.fn(async (model) => ({ uid: model?.uid })),
|
|
126
910
|
destroy: vi.fn(async () => true),
|
|
127
911
|
move: vi.fn(async () => {}),
|
|
128
912
|
duplicate: vi.fn(async () => null),
|