@nocobase/plugin-ui-templates 2.0.2 → 2.0.3

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.
Files changed (53) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +99 -0
  3. package/dist/externalVersion.js +7 -7
  4. package/package.json +3 -3
  5. package/LICENSE.txt +0 -172
  6. package/src/client/__tests__/openViewActionExtensions.test.ts +0 -1208
  7. package/src/client/collections/flowModelTemplates.ts +0 -131
  8. package/src/client/components/FlowModelTemplatesPage.tsx +0 -78
  9. package/src/client/components/TemplateSelectOption.tsx +0 -106
  10. package/src/client/constants.ts +0 -10
  11. package/src/client/hooks/useFlowModelTemplateActions.tsx +0 -137
  12. package/src/client/index.ts +0 -52
  13. package/src/client/locale.ts +0 -40
  14. package/src/client/menuExtensions.tsx +0 -1091
  15. package/src/client/models/ReferenceBlockModel.tsx +0 -841
  16. package/src/client/models/ReferenceFormGridModel.tsx +0 -448
  17. package/src/client/models/SubModelTemplateImporterModel.tsx +0 -725
  18. package/src/client/models/__tests__/ReferenceBlockModel.test.tsx +0 -547
  19. package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +0 -965
  20. package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +0 -529
  21. package/src/client/models/referenceShared.tsx +0 -107
  22. package/src/client/openViewActionExtensions.tsx +0 -986
  23. package/src/client/schemas/flowModelTemplates.ts +0 -264
  24. package/src/client/utils/__tests__/templateCopy.test.ts +0 -67
  25. package/src/client/utils/infiniteSelect.ts +0 -150
  26. package/src/client/utils/templateCompatibility.ts +0 -374
  27. package/src/client/utils/templateCopy.ts +0 -59
  28. package/src/client.ts +0 -10
  29. package/src/index.ts +0 -11
  30. package/src/locale/de-DE.json +0 -14
  31. package/src/locale/en-US.json +0 -72
  32. package/src/locale/es-ES.json +0 -14
  33. package/src/locale/fr-FR.json +0 -14
  34. package/src/locale/hu-HU.json +0 -14
  35. package/src/locale/id-ID.json +0 -14
  36. package/src/locale/it-IT.json +0 -14
  37. package/src/locale/ja-JP.json +0 -14
  38. package/src/locale/ko-KR.json +0 -14
  39. package/src/locale/nl-NL.json +0 -14
  40. package/src/locale/pt-BR.json +0 -14
  41. package/src/locale/ru-RU.json +0 -14
  42. package/src/locale/tr-TR.json +0 -14
  43. package/src/locale/uk-UA.json +0 -14
  44. package/src/locale/vi-VN.json +0 -14
  45. package/src/locale/zh-CN.json +0 -71
  46. package/src/locale/zh-TW.json +0 -14
  47. package/src/server/__tests__/template-usage.test.ts +0 -351
  48. package/src/server/collections/flowModelTemplateUsages.ts +0 -51
  49. package/src/server/collections/flowModelTemplates.ts +0 -76
  50. package/src/server/index.ts +0 -10
  51. package/src/server/plugin.ts +0 -236
  52. package/src/server/resources/flowModelTemplateUsages.ts +0 -61
  53. package/src/server/resources/flowModelTemplates.ts +0 -251
@@ -1,965 +0,0 @@
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 { CollectionFieldModel, 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: model?.uid })),
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('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/assignRules/linkageRules 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
- const assignRulesParams = { value: [{ key: 'a1', targetPath: 'title', value: 'hello' }] };
699
- refGrid.setStepParams('formModelSettings', 'assignRules', assignRulesParams);
700
- refGrid.setStepParams('eventSettings', 'linkageRules', { value: [] });
701
-
702
- expect(refGrid.getStepParams('formModelSettings', 'layout')).toEqual({
703
- layout: 'vertical',
704
- labelAlign: 'left',
705
- labelWidth: 120,
706
- labelWrap: true,
707
- colon: true,
708
- });
709
- expect(refGrid.getStepParams('formModelSettings', 'assignRules')).toEqual(assignRulesParams);
710
- expect(refGrid.getStepParams('eventSettings', 'linkageRules')).toEqual({ value: [] });
711
-
712
- await refGrid.saveStepParams();
713
- const savedUids = mockRepository.save.mock.calls.map((c) => c[0]?.uid).sort();
714
- expect(savedUids).toContain('host-grid');
715
- expect(savedUids).toContain('tpl-grid');
716
-
717
- const savedTplGrid = mockRepository.save.mock.calls.find((c) => c[0]?.uid === 'tpl-grid')?.[0];
718
- expect(savedTplGrid?.stepParams?.formModelSettings?.assignRules).toEqual(assignRulesParams);
719
- });
720
-
721
- it('flushes local (pre-resolve) stepParams to template grid on resolve/save', async () => {
722
- const engine = new FlowEngine();
723
- const store: Record<string, any> = {
724
- 'tpl-root': {
725
- uid: 'tpl-root',
726
- use: 'FormBlockModel',
727
- subModels: {
728
- grid: {
729
- uid: 'tpl-grid',
730
- use: 'GridModel',
731
- subKey: 'grid',
732
- subType: 'object',
733
- stepParams: {},
734
- subModels: { items: [] },
735
- },
736
- },
737
- },
738
- };
739
-
740
- const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
741
- const mockRepository = {
742
- findOne: vi.fn(async (query) => {
743
- const data = store[query.uid];
744
- return data ? clone(data) : null;
745
- }),
746
- save: vi.fn(async (model) => ({ uid: model?.uid })),
747
- destroy: vi.fn(async () => true),
748
- move: vi.fn(async () => {}),
749
- duplicate: vi.fn(async () => null),
750
- };
751
-
752
- class HostFormBlockModel extends FlowModel {}
753
- class GridModel extends FlowModel {}
754
-
755
- engine.setModelRepository(mockRepository as any);
756
- engine.registerModels({
757
- HostFormBlockModel,
758
- GridModel,
759
- ReferenceFormGridModel,
760
- });
761
-
762
- const host = engine.createModel<HostFormBlockModel>({
763
- uid: 'host-form',
764
- use: 'HostFormBlockModel',
765
- });
766
-
767
- const refGrid = engine.createModel({
768
- uid: 'host-grid',
769
- use: 'ReferenceFormGridModel',
770
- parentId: host.uid,
771
- subKey: 'grid',
772
- subType: 'object',
773
- stepParams: {
774
- referenceSettings: {
775
- useTemplate: {
776
- templateUid: 'tpl-1',
777
- targetUid: 'tpl-root',
778
- targetPath: 'subModels.grid',
779
- mode: 'reference',
780
- },
781
- },
782
- },
783
- });
784
- host.setSubModel('grid', refGrid);
785
-
786
- // before target grid resolved, setStepParams should land in local (host-grid) stepParams first
787
- refGrid.setStepParams('eventSettings', 'linkageRules', { value: [{ key: 'pre' }] });
788
- expect((refGrid as any).stepParams?.eventSettings?.linkageRules).toEqual({ value: [{ key: 'pre' }] });
789
-
790
- // save should resolve target, flush local params into template grid, then persist both
791
- await refGrid.saveStepParams();
792
-
793
- const targetGrid = (refGrid as any)._targetGrid as FlowModel;
794
- expect(targetGrid).toBeTruthy();
795
- expect(targetGrid.getStepParams('eventSettings', 'linkageRules')).toEqual({ value: [{ key: 'pre' }] });
796
- expect((refGrid as any).stepParams?.eventSettings).toBeUndefined();
797
-
798
- const savedUids = mockRepository.save.mock.calls.map((c) => c[0]?.uid).sort();
799
- expect(savedUids).toContain('host-grid');
800
- expect(savedUids).toContain('tpl-grid');
801
- });
802
-
803
- it('bridges host context (record) to referenced grid while keeping scoped engine', async () => {
804
- const engine = new FlowEngine();
805
- const store: Record<string, any> = {
806
- 'tpl-root': {
807
- uid: 'tpl-root',
808
- use: 'DetailsBlockModel',
809
- subModels: {
810
- grid: {
811
- uid: 'tpl-grid',
812
- use: 'DetailsGridModel',
813
- subKey: 'grid',
814
- subType: 'object',
815
- subModels: {
816
- items: [{ uid: 'tpl-i1', use: 'ItemModel', subKey: 'items', subType: 'array' }],
817
- },
818
- },
819
- },
820
- },
821
- };
822
-
823
- const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
824
- const mockRepository = {
825
- findOne: vi.fn(async (query) => {
826
- const data = store[query.uid];
827
- return data ? clone(data) : null;
828
- }),
829
- save: vi.fn(async (model) => ({ uid: model?.uid })),
830
- destroy: vi.fn(async () => true),
831
- move: vi.fn(async () => {}),
832
- duplicate: vi.fn(async () => null),
833
- };
834
-
835
- class HostDetailsBlockModel extends FlowModel {}
836
- class DetailsBlockModel extends FlowModel {}
837
- class DetailsGridModel extends FlowModel {}
838
- class ItemModel extends FlowModel {}
839
-
840
- engine.setModelRepository(mockRepository as any);
841
- engine.registerModels({
842
- HostDetailsBlockModel,
843
- DetailsBlockModel,
844
- DetailsGridModel,
845
- ItemModel,
846
- ReferenceFormGridModel,
847
- });
848
-
849
- const host = engine.createModel<HostDetailsBlockModel>({
850
- uid: 'host-details',
851
- use: 'HostDetailsBlockModel',
852
- });
853
- host.context.defineProperty('record', { value: { username: 'nocobase' } });
854
-
855
- const refGrid = engine.createModel({
856
- uid: 'host-grid',
857
- use: 'ReferenceFormGridModel',
858
- parentId: host.uid,
859
- subKey: 'grid',
860
- subType: 'object',
861
- stepParams: {
862
- referenceSettings: {
863
- useTemplate: {
864
- templateUid: 'tpl-1',
865
- targetUid: 'tpl-root',
866
- targetPath: 'subModels.grid',
867
- mode: 'reference',
868
- },
869
- },
870
- },
871
- });
872
- host.setSubModel('grid', refGrid);
873
-
874
- await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
875
-
876
- const items = ((refGrid as any).subModels as any)?.items as FlowModel[];
877
- expect(Array.isArray(items)).toBe(true);
878
- expect(items.length).toBe(1);
879
- expect(items[0].context.record).toEqual({ username: 'nocobase' });
880
-
881
- // 引用渲染应仍使用 scoped engine(避免丢失模型实例/缓存隔离)
882
- const scoped = (refGrid as any)._scopedEngine;
883
- expect(scoped).toBeTruthy();
884
- expect(items[0].context.engine).toBe(scoped);
885
- });
886
-
887
- it('syncs host extraTitle with reference template info', async () => {
888
- MockFormBlockModel.define({
889
- label: '默认block title',
890
- });
891
-
892
- const engine = new FlowEngine();
893
- const store: Record<string, any> = {
894
- 'tpl-root': {
895
- uid: 'tpl-root',
896
- use: 'FormBlockModel',
897
- subModels: {
898
- grid: {
899
- uid: 'tpl-grid',
900
- use: 'GridModel',
901
- subKey: 'grid',
902
- subType: 'object',
903
- subModels: { items: [] },
904
- },
905
- },
906
- },
907
- };
908
-
909
- const clone = (obj: any) => JSON.parse(JSON.stringify(obj));
910
- const mockRepository = {
911
- findOne: vi.fn(async (query) => {
912
- const data = store[query.uid];
913
- return data ? clone(data) : null;
914
- }),
915
- save: vi.fn(async (model) => ({ uid: model?.uid })),
916
- destroy: vi.fn(async () => true),
917
- move: vi.fn(async () => {}),
918
- duplicate: vi.fn(async () => null),
919
- };
920
-
921
- engine.setModelRepository(mockRepository as any);
922
- engine.registerModels({
923
- FormBlockModel: MockFormBlockModel,
924
- GridModel: MockGridModel,
925
- FieldModel: MockFieldModel,
926
- ReferenceFormGridModel,
927
- });
928
-
929
- const form = engine.createModel({
930
- uid: 'host-form',
931
- use: 'FormBlockModel',
932
- });
933
-
934
- const refGrid = engine.createModel({
935
- uid: 'host-grid',
936
- use: 'ReferenceFormGridModel',
937
- parentId: form.uid,
938
- subKey: 'grid',
939
- subType: 'object',
940
- stepParams: {
941
- referenceSettings: {
942
- useTemplate: {
943
- templateUid: 'tpl-1',
944
- templateName: '模板名称',
945
- targetUid: 'tpl-root',
946
- targetPath: 'subModels.grid',
947
- mode: 'reference',
948
- },
949
- },
950
- },
951
- });
952
- form.setSubModel('grid', refGrid);
953
-
954
- await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
955
- expect(form.title).toBe('默认block title');
956
- expect((form as any).extraTitle).toBe('Reference template: 模板名称 (Fields only)');
957
-
958
- // clear settings should restore base title
959
- (refGrid as any).stepParams.referenceSettings.useTemplate.templateUid = '';
960
- (refGrid as any).stepParams.referenceSettings.useTemplate.targetUid = '';
961
- await refGrid.dispatchEvent('beforeRender', undefined, { useCache: false });
962
- expect(form.title).toBe('默认block title');
963
- expect((form as any).extraTitle).toBe('');
964
- });
965
- });