@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.
Files changed (106) hide show
  1. package/LICENSE.txt +172 -0
  2. package/build.config.ts +12 -0
  3. package/client.js +1 -0
  4. package/dist/client/collections/flowModelTemplates.d.ts +67 -0
  5. package/dist/client/components/FlowModelTemplatesPage.d.ts +12 -0
  6. package/dist/client/components/TemplateSelectOption.d.ts +20 -0
  7. package/dist/client/constants.d.ts +9 -0
  8. package/dist/client/hooks/useFlowModelTemplateActions.d.ts +24 -0
  9. package/dist/client/index.d.ts +13 -0
  10. package/dist/client/index.js +10 -0
  11. package/dist/client/locale.d.ts +18 -0
  12. package/dist/client/menuExtensions.d.ts +9 -0
  13. package/dist/client/models/ReferenceBlockModel.d.ts +47 -0
  14. package/dist/client/models/ReferenceFormGridModel.d.ts +38 -0
  15. package/dist/client/models/SubModelTemplateImporterModel.d.ts +55 -0
  16. package/dist/client/models/referenceShared.d.ts +23 -0
  17. package/dist/client/openViewActionExtensions.d.ts +10 -0
  18. package/dist/client/schemas/flowModelTemplates.d.ts +11 -0
  19. package/dist/client/subModelMenuExtensions.d.ts +10 -0
  20. package/dist/client/utils/infiniteSelect.d.ts +28 -0
  21. package/dist/client/utils/refHost.d.ts +20 -0
  22. package/dist/client/utils/templateCompatibility.d.ts +91 -0
  23. package/dist/client.d.ts +9 -0
  24. package/dist/client.js +42 -0
  25. package/dist/externalVersion.js +24 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +48 -0
  28. package/dist/locale/de-DE.json +14 -0
  29. package/dist/locale/en-US.json +72 -0
  30. package/dist/locale/es-ES.json +14 -0
  31. package/dist/locale/fr-FR.json +14 -0
  32. package/dist/locale/hu-HU.json +14 -0
  33. package/dist/locale/id-ID.json +14 -0
  34. package/dist/locale/it-IT.json +14 -0
  35. package/dist/locale/ja-JP.json +14 -0
  36. package/dist/locale/ko-KR.json +14 -0
  37. package/dist/locale/nl-NL.json +14 -0
  38. package/dist/locale/pt-BR.json +14 -0
  39. package/dist/locale/ru-RU.json +14 -0
  40. package/dist/locale/tr-TR.json +14 -0
  41. package/dist/locale/uk-UA.json +14 -0
  42. package/dist/locale/vi-VN.json +14 -0
  43. package/dist/locale/zh-CN.json +71 -0
  44. package/dist/locale/zh-TW.json +14 -0
  45. package/dist/server/collections/flowModelTemplateUsages.d.ts +11 -0
  46. package/dist/server/collections/flowModelTemplateUsages.js +71 -0
  47. package/dist/server/collections/flowModelTemplates.d.ts +11 -0
  48. package/dist/server/collections/flowModelTemplates.js +96 -0
  49. package/dist/server/index.d.ts +9 -0
  50. package/dist/server/index.js +42 -0
  51. package/dist/server/plugin.d.ts +17 -0
  52. package/dist/server/plugin.js +242 -0
  53. package/dist/server/resources/flowModelTemplateUsages.d.ts +19 -0
  54. package/dist/server/resources/flowModelTemplateUsages.js +91 -0
  55. package/dist/server/resources/flowModelTemplates.d.ts +20 -0
  56. package/dist/server/resources/flowModelTemplates.js +267 -0
  57. package/package.json +37 -0
  58. package/server.js +1 -0
  59. package/src/client/__tests__/openViewActionExtensions.test.ts +1208 -0
  60. package/src/client/collections/flowModelTemplates.ts +131 -0
  61. package/src/client/components/FlowModelTemplatesPage.tsx +78 -0
  62. package/src/client/components/TemplateSelectOption.tsx +106 -0
  63. package/src/client/constants.ts +10 -0
  64. package/src/client/hooks/useFlowModelTemplateActions.tsx +137 -0
  65. package/src/client/index.ts +54 -0
  66. package/src/client/locale.ts +40 -0
  67. package/src/client/menuExtensions.tsx +1033 -0
  68. package/src/client/models/ReferenceBlockModel.tsx +793 -0
  69. package/src/client/models/ReferenceFormGridModel.tsx +302 -0
  70. package/src/client/models/SubModelTemplateImporterModel.tsx +634 -0
  71. package/src/client/models/__tests__/ReferenceBlockModel.test.tsx +482 -0
  72. package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +175 -0
  73. package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +447 -0
  74. package/src/client/models/referenceShared.tsx +99 -0
  75. package/src/client/openViewActionExtensions.tsx +981 -0
  76. package/src/client/schemas/flowModelTemplates.ts +264 -0
  77. package/src/client/subModelMenuExtensions.ts +103 -0
  78. package/src/client/utils/infiniteSelect.ts +150 -0
  79. package/src/client/utils/refHost.ts +44 -0
  80. package/src/client/utils/templateCompatibility.ts +374 -0
  81. package/src/client.ts +10 -0
  82. package/src/index.ts +11 -0
  83. package/src/locale/de-DE.json +14 -0
  84. package/src/locale/en-US.json +72 -0
  85. package/src/locale/es-ES.json +14 -0
  86. package/src/locale/fr-FR.json +14 -0
  87. package/src/locale/hu-HU.json +14 -0
  88. package/src/locale/id-ID.json +14 -0
  89. package/src/locale/it-IT.json +14 -0
  90. package/src/locale/ja-JP.json +14 -0
  91. package/src/locale/ko-KR.json +14 -0
  92. package/src/locale/nl-NL.json +14 -0
  93. package/src/locale/pt-BR.json +14 -0
  94. package/src/locale/ru-RU.json +14 -0
  95. package/src/locale/tr-TR.json +14 -0
  96. package/src/locale/uk-UA.json +14 -0
  97. package/src/locale/vi-VN.json +14 -0
  98. package/src/locale/zh-CN.json +71 -0
  99. package/src/locale/zh-TW.json +14 -0
  100. package/src/server/__tests__/template-usage.test.ts +351 -0
  101. package/src/server/collections/flowModelTemplateUsages.ts +51 -0
  102. package/src/server/collections/flowModelTemplates.ts +76 -0
  103. package/src/server/index.ts +10 -0
  104. package/src/server/plugin.ts +236 -0
  105. package/src/server/resources/flowModelTemplateUsages.ts +61 -0
  106. package/src/server/resources/flowModelTemplates.ts +251 -0
@@ -0,0 +1,447 @@
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 { SubModelTemplateImporterModel } from '../SubModelTemplateImporterModel';
13
+
14
+ describe('SubModelTemplateImporterModel', () => {
15
+ it('stores derived targetUid into stepParams in beforeParamsSave', async () => {
16
+ const engine = new FlowEngine();
17
+
18
+ class FormBlockModel extends FlowModel {}
19
+ class GridModel extends FlowModel {}
20
+
21
+ engine.registerModels({
22
+ FormBlockModel,
23
+ GridModel,
24
+ SubModelTemplateImporterModel,
25
+ });
26
+
27
+ const form = engine.createModel<FormBlockModel>({ uid: 'host-form', use: 'FormBlockModel' });
28
+ const grid = engine.createModel<GridModel>({
29
+ uid: 'host-grid',
30
+ use: 'GridModel',
31
+ parentId: form.uid,
32
+ subKey: 'grid',
33
+ subType: 'object',
34
+ });
35
+ form.setSubModel('grid', grid);
36
+
37
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
38
+ uid: 'importer-1',
39
+ use: 'SubModelTemplateImporterModel',
40
+ parentId: grid.uid,
41
+ subKey: 'items',
42
+ subType: 'array',
43
+ props: {
44
+ mountToParentLevel: 1,
45
+ defaultSourcePath: 'subModels.grid',
46
+ defaultMountSubKey: 'grid',
47
+ },
48
+ });
49
+ importer.setParent(grid);
50
+
51
+ const flow: any = importer.getFlow('subModelTemplateImportSettings');
52
+ const step: any = flow?.getStep?.('selectTemplate')?.serialize?.();
53
+ expect(typeof step?.beforeParamsSave).toBe('function');
54
+
55
+ const ctx: any = {
56
+ model: importer,
57
+ api: {
58
+ resource: (name: string) => {
59
+ if (name !== 'flowModelTemplates') throw new Error('unexpected resource');
60
+ return {
61
+ get: async () => ({ data: { data: { uid: 'tpl-1', name: 'Template 1', targetUid: 'tpl-root' } } }),
62
+ };
63
+ },
64
+ },
65
+ t: (k: string) => k,
66
+ message: { warning: vi.fn(), error: vi.fn() },
67
+ };
68
+
69
+ const params: any = { templateUid: 'tpl-1', mode: 'reference' };
70
+ // FlowSettings.open 会在 beforeParamsSave 之前 setStepParams
71
+ importer.setStepParams('subModelTemplateImportSettings', 'selectTemplate', params);
72
+ await step.beforeParamsSave(ctx, params);
73
+
74
+ const saved = importer.getStepParams('subModelTemplateImportSettings', 'selectTemplate');
75
+ expect(saved.templateUid).toBe('tpl-1');
76
+ expect(saved.mode).toBe('reference');
77
+ expect(saved.targetUid).toBe('tpl-root');
78
+ expect(saved.templateName).toBe('Template 1');
79
+ expect(saved.targetPath).toBe('subModels.grid');
80
+ expect(saved.mountSubKey).toBe('grid');
81
+ });
82
+
83
+ it('filters by expectedRootUse and disables mismatched dataSource/collection', async () => {
84
+ const engine = new FlowEngine();
85
+ engine.registerModels({ SubModelTemplateImporterModel });
86
+
87
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
88
+ uid: 'importer-1',
89
+ use: 'SubModelTemplateImporterModel',
90
+ props: {
91
+ expectedRootUse: ['CreateFormModel', 'EditFormModel'],
92
+ expectedDataSourceKey: 'main',
93
+ expectedCollectionName: 'orders',
94
+ },
95
+ });
96
+
97
+ const list = vi.fn(async () => ({
98
+ data: {
99
+ rows: [
100
+ {
101
+ uid: 'tpl-mismatch',
102
+ name: 'Mismatch',
103
+ useModel: 'EditFormModel',
104
+ dataSourceKey: 'main',
105
+ collectionName: 'customers',
106
+ targetUid: 'root-mis',
107
+ },
108
+ {
109
+ uid: 'tpl-ok',
110
+ name: 'OK',
111
+ useModel: 'CreateFormModel',
112
+ dataSourceKey: 'main',
113
+ collectionName: 'orders',
114
+ targetUid: 'root-ok',
115
+ },
116
+ {
117
+ uid: 'tpl-other',
118
+ name: 'Other',
119
+ useModel: 'TableBlockModel',
120
+ dataSourceKey: 'main',
121
+ collectionName: 'orders',
122
+ targetUid: 'root-other',
123
+ },
124
+ ],
125
+ },
126
+ }));
127
+
128
+ const ctx: any = {
129
+ api: {
130
+ resource: () => ({ list }),
131
+ },
132
+ t: (k: string) => k,
133
+ };
134
+
135
+ const { options } = await (importer as any).fetchTemplateOptions(ctx, '');
136
+ expect(Array.isArray(options)).toBe(true);
137
+
138
+ const values = options.map((o: any) => o.value).sort();
139
+ expect(values).toEqual(['tpl-mismatch', 'tpl-ok']);
140
+
141
+ // enabled items should come first
142
+ expect(options[0].value).toBe('tpl-ok');
143
+ expect(options[0].disabled).toBe(false);
144
+ expect(options[1].value).toBe('tpl-mismatch');
145
+ expect(options[1].disabled).toBe(true);
146
+
147
+ const mismatch = options.find((o: any) => o.value === 'tpl-mismatch');
148
+ expect(mismatch.disabled).toBe(true);
149
+ expect(String(mismatch.disabledReason || '')).toContain('Template collection mismatch');
150
+ });
151
+
152
+ it('resolves expected collection via associationName when collectionName is missing/wrong', async () => {
153
+ const engine = new FlowEngine();
154
+ engine.registerModels({ SubModelTemplateImporterModel });
155
+
156
+ const dsManager = engine.context.dataSourceManager;
157
+ const main = dsManager.getDataSource('main');
158
+ if (!main) throw new Error('missing main dataSource');
159
+ main.collectionManager.addCollection({
160
+ name: 'users',
161
+ fields: [{ name: 'roles', type: 'hasMany', target: 'roles' }],
162
+ });
163
+ main.collectionManager.addCollection({ name: 'roles' });
164
+
165
+ class FormBlockModel extends FlowModel {}
166
+ engine.registerModels({ FormBlockModel });
167
+
168
+ const form = engine.createModel<FormBlockModel>({
169
+ uid: 'form',
170
+ use: 'FormBlockModel',
171
+ stepParams: {
172
+ resourceSettings: {
173
+ init: {
174
+ dataSourceKey: 'main',
175
+ collectionName: 'bad',
176
+ associationName: 'users.roles',
177
+ },
178
+ },
179
+ },
180
+ });
181
+
182
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
183
+ uid: 'importer-1',
184
+ use: 'SubModelTemplateImporterModel',
185
+ parentId: form.uid,
186
+ subKey: 'items',
187
+ subType: 'array',
188
+ });
189
+ importer.setParent(form);
190
+
191
+ const list = vi.fn(async () => ({
192
+ data: {
193
+ rows: [
194
+ { uid: 'tpl-users', name: 'Users', dataSourceKey: 'main', collectionName: 'users', targetUid: 't1' },
195
+ { uid: 'tpl-roles', name: 'Roles', dataSourceKey: 'main', collectionName: 'roles', targetUid: 't2' },
196
+ ],
197
+ },
198
+ }));
199
+
200
+ const ctx: any = {
201
+ api: { resource: () => ({ list }) },
202
+ t: (k: string) => k,
203
+ dataSourceManager: dsManager,
204
+ };
205
+
206
+ const { options } = await (importer as any).fetchTemplateOptions(ctx, '');
207
+ expect(options[0].value).toBe('tpl-roles');
208
+ expect(options[0].disabled).toBe(false);
209
+ expect(options[1].value).toBe('tpl-users');
210
+ expect(options[1].disabled).toBe(true);
211
+ });
212
+
213
+ it('resolves template collection via associationName when template collectionName is missing/wrong', async () => {
214
+ const engine = new FlowEngine();
215
+ engine.registerModels({ SubModelTemplateImporterModel });
216
+
217
+ const dsManager = engine.context.dataSourceManager;
218
+ const main = dsManager.getDataSource('main');
219
+ if (!main) throw new Error('missing main dataSource');
220
+ main.collectionManager.addCollection({
221
+ name: 'users',
222
+ fields: [{ name: 'roles', type: 'hasMany', target: 'roles' }],
223
+ });
224
+ main.collectionManager.addCollection({ name: 'roles' });
225
+
226
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
227
+ uid: 'importer-1',
228
+ use: 'SubModelTemplateImporterModel',
229
+ props: {
230
+ expectedDataSourceKey: 'main',
231
+ expectedCollectionName: 'roles',
232
+ },
233
+ });
234
+
235
+ const list = vi.fn(async () => ({
236
+ data: {
237
+ rows: [
238
+ {
239
+ uid: 'tpl-bad-collection',
240
+ name: 'Roles via association',
241
+ dataSourceKey: 'main',
242
+ collectionName: 'bad',
243
+ associationName: 'users.roles',
244
+ targetUid: 't1',
245
+ },
246
+ { uid: 'tpl-users', name: 'Users', dataSourceKey: 'main', collectionName: 'users', targetUid: 't2' },
247
+ ],
248
+ },
249
+ }));
250
+
251
+ const ctx: any = {
252
+ api: { resource: () => ({ list }) },
253
+ t: (k: string) => k,
254
+ dataSourceManager: dsManager,
255
+ };
256
+
257
+ const { options } = await (importer as any).fetchTemplateOptions(ctx, '');
258
+ expect(options[0].value).toBe('tpl-bad-collection');
259
+ expect(options[0].disabled).toBe(false);
260
+ expect(options[1].value).toBe('tpl-users');
261
+ expect(options[1].disabled).toBe(true);
262
+ });
263
+
264
+ it('skips confirm when current grid has no FormItem fields', async () => {
265
+ const engine = new FlowEngine();
266
+
267
+ class FormBlockModel extends FlowModel {}
268
+ class GridModel extends FlowModel {}
269
+ class FormItemModel extends FlowModel {}
270
+ class FormCustomItemModel extends FlowModel {}
271
+ class FormJSFieldItemModel extends FlowModel {}
272
+ class DummyItemModel extends FlowModel {}
273
+
274
+ engine.registerModels({
275
+ FormBlockModel,
276
+ GridModel,
277
+ FormItemModel,
278
+ FormCustomItemModel,
279
+ FormJSFieldItemModel,
280
+ DummyItemModel,
281
+ SubModelTemplateImporterModel,
282
+ });
283
+
284
+ const form = engine.createModel<FormBlockModel>({ uid: 'host-form', use: 'FormBlockModel' });
285
+ const grid = engine.createModel<GridModel>({
286
+ uid: 'host-grid',
287
+ use: 'GridModel',
288
+ parentId: form.uid,
289
+ subKey: 'grid',
290
+ subType: 'object',
291
+ });
292
+ form.setSubModel('grid', grid);
293
+
294
+ // A non-field item exists in grid.items (should NOT trigger the "remove existing fields" confirm)
295
+ const dummy = engine.createModel<DummyItemModel>({
296
+ uid: 'dummy-item',
297
+ use: 'DummyItemModel',
298
+ parentId: grid.uid,
299
+ subKey: 'items',
300
+ subType: 'array',
301
+ });
302
+ grid.addSubModel('items', dummy);
303
+
304
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
305
+ uid: 'importer-1',
306
+ use: 'SubModelTemplateImporterModel',
307
+ parentId: grid.uid,
308
+ subKey: 'items',
309
+ subType: 'array',
310
+ props: {
311
+ mountToParentLevel: 1,
312
+ defaultSourcePath: 'subModels.grid',
313
+ defaultMountSubKey: 'grid',
314
+ },
315
+ });
316
+ grid.addSubModel('items', importer);
317
+
318
+ const flow: any = importer.getFlow('subModelTemplateImportSettings');
319
+ const step: any = flow?.getStep?.('selectTemplate')?.serialize?.();
320
+ expect(typeof step?.beforeParamsSave).toBe('function');
321
+
322
+ const viewer = { dialog: vi.fn() };
323
+ const view = { close: vi.fn() };
324
+ const ctx: any = {
325
+ model: importer,
326
+ api: {
327
+ resource: () => ({
328
+ get: async () => ({ data: { data: { uid: 'tpl-1', name: 'Template 1', targetUid: 'tpl-root' } } }),
329
+ }),
330
+ },
331
+ viewer,
332
+ view,
333
+ t: (k: string) => k,
334
+ message: { warning: vi.fn(), error: vi.fn() },
335
+ };
336
+
337
+ const params: any = { templateUid: 'tpl-1', mode: 'reference' };
338
+ importer.setStepParams('subModelTemplateImportSettings', 'selectTemplate', params);
339
+ await step.beforeParamsSave(ctx, params);
340
+
341
+ expect(viewer.dialog).not.toHaveBeenCalled();
342
+ expect(view.close).not.toHaveBeenCalled();
343
+ });
344
+
345
+ it('waits parent grid saveStepParams before replaceModel save', async () => {
346
+ const engine = new FlowEngine();
347
+
348
+ class PageModel extends FlowModel {}
349
+ class FormBlockModel extends FlowModel {}
350
+ class GridModel extends FlowModel {}
351
+ class ReferenceFormGridModel extends FlowModel {}
352
+
353
+ const saveCalls: Array<{ uid: string; use: string; onlyStepParams?: boolean }> = [];
354
+ let resolveStepParamsSave: (() => void) | undefined;
355
+ const stepParamsSavePromise = new Promise<{ uid: string }>((resolve) => {
356
+ resolveStepParamsSave = () => resolve({ uid: 'host-grid' });
357
+ });
358
+
359
+ engine.setModelRepository({
360
+ findOne: vi.fn(async () => null),
361
+ save: vi.fn(async (model: any, options?: { onlyStepParams?: boolean }) => {
362
+ saveCalls.push({
363
+ uid: String(model?.uid || ''),
364
+ use: String(model?.use || ''),
365
+ onlyStepParams: options?.onlyStepParams,
366
+ });
367
+ if (options?.onlyStepParams) {
368
+ return await stepParamsSavePromise;
369
+ }
370
+ return { uid: model?.uid };
371
+ }),
372
+ destroy: vi.fn(async () => true),
373
+ move: vi.fn(async () => {}),
374
+ duplicate: vi.fn(async () => null),
375
+ });
376
+
377
+ engine.registerModels({
378
+ PageModel,
379
+ FormBlockModel,
380
+ GridModel,
381
+ ReferenceFormGridModel,
382
+ SubModelTemplateImporterModel,
383
+ });
384
+
385
+ const page = engine.createModel<PageModel>({ uid: 'page', use: 'PageModel' });
386
+ const form = engine.createModel<FormBlockModel>({
387
+ uid: 'host-form',
388
+ use: 'FormBlockModel',
389
+ parentId: page.uid,
390
+ subKey: 'items',
391
+ subType: 'array',
392
+ });
393
+ page.addSubModel('items', form);
394
+
395
+ const grid = engine.createModel<GridModel>({
396
+ uid: 'host-grid',
397
+ use: 'GridModel',
398
+ parentId: form.uid,
399
+ subKey: 'grid',
400
+ subType: 'object',
401
+ });
402
+ form.setSubModel('grid', grid);
403
+
404
+ grid.emitter.on('onSubModelRemoved', () => {
405
+ void grid.saveStepParams();
406
+ });
407
+
408
+ const importer = engine.createModel<SubModelTemplateImporterModel>({
409
+ uid: 'importer-1',
410
+ use: 'SubModelTemplateImporterModel',
411
+ parentId: grid.uid,
412
+ subKey: 'items',
413
+ subType: 'array',
414
+ props: {
415
+ mountToParentLevel: 1,
416
+ defaultSourcePath: 'subModels.grid',
417
+ defaultMountSubKey: 'grid',
418
+ },
419
+ stepParams: {
420
+ subModelTemplateImportSettings: {
421
+ selectTemplate: {
422
+ templateUid: 'tpl-1',
423
+ targetUid: 'tpl-root',
424
+ templateName: 'Template 1',
425
+ mode: 'reference',
426
+ targetPath: 'subModels.grid',
427
+ mountSubKey: 'grid',
428
+ },
429
+ },
430
+ },
431
+ });
432
+ grid.addSubModel('items', importer);
433
+
434
+ const run = importer.afterAddAsSubModel();
435
+
436
+ await Promise.resolve();
437
+ expect(saveCalls).toHaveLength(1);
438
+ expect(saveCalls[0]).toMatchObject({ uid: 'host-grid', use: 'GridModel', onlyStepParams: true });
439
+
440
+ resolveStepParamsSave?.();
441
+ await run;
442
+
443
+ expect(saveCalls).toHaveLength(2);
444
+ expect(saveCalls[1]).toMatchObject({ uid: 'host-grid', use: 'ReferenceFormGridModel' });
445
+ expect((form.subModels as any)?.grid?.use).toBe('ReferenceFormGridModel');
446
+ });
447
+ });
@@ -0,0 +1,99 @@
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 React from 'react';
11
+ import { Card, Empty, Result, Skeleton } from 'antd';
12
+ import {
13
+ FlowContext,
14
+ type FlowEngine,
15
+ FlowEngineProvider,
16
+ type FlowModel,
17
+ FlowModelRenderer,
18
+ FlowViewContextProvider,
19
+ createBlockScopedEngine,
20
+ useFlowViewContext,
21
+ } from '@nocobase/flow-engine';
22
+ import { NAMESPACE } from '../locale';
23
+
24
+ export function ensureBlockScopedEngine(flowEngine: FlowEngine, scopedEngine?: FlowEngine): FlowEngine {
25
+ return scopedEngine ?? createBlockScopedEngine(flowEngine);
26
+ }
27
+
28
+ export function unlinkScopedEngine(engine?: FlowEngine): void {
29
+ engine?.unlinkFromStack?.();
30
+ }
31
+
32
+ function tClient(model: { translate?: (key: string, options?: any) => string }, key: string) {
33
+ const t = model?.translate;
34
+ if (typeof t !== 'function') return key;
35
+ return t(key, { ns: [NAMESPACE, 'client'] }) || key;
36
+ }
37
+
38
+ export function renderReferenceTargetPlaceholder(
39
+ model: { translate?: (key: string, options?: any) => string },
40
+ state: 'unconfigured' | 'invalid' | 'resolving',
41
+ ) {
42
+ if (state === 'unconfigured') {
43
+ return (
44
+ <Card>
45
+ <div style={{ padding: 24 }}>
46
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={tClient(model, 'Please configure target block')} />
47
+ </div>
48
+ </Card>
49
+ );
50
+ }
51
+
52
+ if (state === 'resolving') {
53
+ return (
54
+ <Card>
55
+ <div style={{ padding: 24 }}>
56
+ <Skeleton active title={false} paragraph={{ rows: 3 }} />
57
+ </div>
58
+ </Card>
59
+ );
60
+ }
61
+
62
+ return (
63
+ <Card>
64
+ <div style={{ padding: 24 }}>
65
+ <Result status="error" subTitle={tClient(model, 'Target block is invalid')} />
66
+ </div>
67
+ </Card>
68
+ );
69
+ }
70
+
71
+ // 桥接父视图上下文:
72
+ // - ctx.engine 指向 scoped engine;
73
+ // - 同时继承父级 view/popup 等变量。
74
+ export const RefViewBridge: React.FC<{ engine: FlowEngine; model: FlowModel }> = ({ engine, model }) => {
75
+ const parentViewCtx = useFlowViewContext();
76
+ const viewCtx = React.useMemo(() => {
77
+ const c = new FlowContext();
78
+ c.defineProperty('engine', { value: engine });
79
+ c.addDelegate(engine.context);
80
+ if (parentViewCtx && parentViewCtx instanceof FlowContext) {
81
+ c.addDelegate(parentViewCtx);
82
+ }
83
+ return c;
84
+ }, [engine, parentViewCtx]);
85
+
86
+ return (
87
+ <FlowViewContextProvider context={viewCtx}>
88
+ <FlowModelRenderer key={model.uid} model={model} showFlowSettings={false} showErrorFallback />
89
+ </FlowViewContextProvider>
90
+ );
91
+ };
92
+
93
+ export const ReferenceScopedRenderer: React.FC<{ engine: FlowEngine; model: FlowModel }> = ({ engine, model }) => {
94
+ return (
95
+ <FlowEngineProvider engine={engine}>
96
+ <RefViewBridge engine={engine} model={model} />
97
+ </FlowEngineProvider>
98
+ );
99
+ };