@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.
@@ -8,11 +8,12 @@
8
8
  */
9
9
 
10
10
  import React from 'react';
11
- import _ from 'lodash';
12
11
  import { Button, Space } from 'antd';
12
+ import { BlockModel, CommonItemModel, FilterFormBlockModel, FormBlockModel } from '@nocobase/client';
13
13
  import {
14
14
  FlowModel,
15
15
  FlowContext,
16
+ type FlowModelContext,
16
17
  createBlockScopedEngine,
17
18
  FlowExitException,
18
19
  isInheritedFrom,
@@ -21,7 +22,6 @@ import {
21
22
  } from '@nocobase/flow-engine';
22
23
  import { NAMESPACE, tStr } from '../locale';
23
24
  import { renderTemplateSelectLabel, renderTemplateSelectOption } from '../components/TemplateSelectOption';
24
- import { findRefHostInfoFromAncestors } from '../utils/refHost';
25
25
  import {
26
26
  TEMPLATE_LIST_PAGE_SIZE,
27
27
  calcHasMore,
@@ -30,32 +30,117 @@ import {
30
30
  resolveExpectedResourceInfoByModelChain,
31
31
  } from '../utils/templateCompatibility';
32
32
  import { bindInfiniteScrollToFormilySelect, defaultSelectOptionComparator } from '../utils/infiniteSelect';
33
+ import { patchGridOptionsFromTemplateRoot } from '../utils/templateCopy';
34
+ import { REF_HOST_CTX_KEY } from '../constants';
33
35
 
34
36
  type ImporterProps = {
35
- /** 默认从模板根取片段的路径 */
36
- defaultSourcePath?: string;
37
- /** 模板根 use 过滤(可选),支持多个候选 */
38
37
  expectedRootUse?: string | string[];
39
- /** 期望的数据源 key(可选,用于禁用不匹配的模板) */
40
38
  expectedDataSourceKey?: string;
41
- /** 期望的 collectionName(可选,用于禁用不匹配的模板) */
42
39
  expectedCollectionName?: string;
43
- /** 默认挂载到当前模型的 subModels 键(可选),否则使用 importer.subKey */
44
- defaultMountSubKey?: string;
45
- /**
46
- * 引入片段时挂载到第几层父级:
47
- * - 0:挂载到 importer.parent(默认)
48
- * - 1:挂载到 importer.parent.parent
49
- * - 2:以此类推
50
- */
51
- mountToParentLevel?: number;
52
40
  };
53
41
 
54
42
  const FLOW_KEY = 'subModelTemplateImportSettings';
55
43
  const GRID_REF_FLOW_KEY = 'referenceSettings';
56
44
  const GRID_REF_STEP_KEY = 'useTemplate';
57
45
 
58
- export class SubModelTemplateImporterModel extends FlowModel {
46
+ /** 最大递归深度,防止循环引用或过深嵌套 */
47
+ const MAX_NORMALIZE_DEPTH = 20;
48
+
49
+ type NormalizeSubModelTemplateImportNode = (
50
+ node: Record<string, unknown>,
51
+ ctx: { mountTarget: FlowModel; engine: FlowModel['flowEngine'] },
52
+ ) => Record<string, unknown> | undefined;
53
+
54
+ interface ModelWithGetModelClassName {
55
+ getModelClassName?: (name: string) => string | undefined;
56
+ }
57
+
58
+ function resolveMappedFormItemUse(mountTarget: FlowModel): string | undefined {
59
+ const sources: Array<ModelWithGetModelClassName | undefined> = [
60
+ mountTarget as FlowModel & ModelWithGetModelClassName,
61
+ mountTarget.context as ModelWithGetModelClassName | undefined,
62
+ ];
63
+ for (const source of sources) {
64
+ if (typeof source?.getModelClassName === 'function') {
65
+ const mapped = source.getModelClassName('FormItemModel');
66
+ if (mapped) return mapped;
67
+ }
68
+ }
69
+ return undefined;
70
+ }
71
+
72
+ interface NormalizeOptions {
73
+ normalizeMappedItem?: NormalizeSubModelTemplateImportNode;
74
+ mountTarget?: FlowModel;
75
+ }
76
+
77
+ function normalizeGridModelOptionsForMappedFormItemUse(
78
+ input: unknown,
79
+ mappedFormItemUse: string,
80
+ options?: NormalizeOptions,
81
+ depth = 0,
82
+ ) {
83
+ if (depth > MAX_NORMALIZE_DEPTH) {
84
+ return;
85
+ }
86
+
87
+ if (Array.isArray(input)) {
88
+ return input.map((n) => normalizeGridModelOptionsForMappedFormItemUse(n, mappedFormItemUse, options, depth + 1));
89
+ }
90
+ if (!input || typeof input !== 'object') {
91
+ return input;
92
+ }
93
+
94
+ const source = input as Record<string, unknown>;
95
+ const next: Record<string, unknown> = { ...source };
96
+
97
+ if (next.use === 'FormItemModel') {
98
+ next.use = mappedFormItemUse;
99
+ }
100
+
101
+ if (next.subModels) {
102
+ const subModels = next.subModels;
103
+ next.subModels = Object.fromEntries(
104
+ Object.entries(subModels).map(([k, v]) => [
105
+ k,
106
+ normalizeGridModelOptionsForMappedFormItemUse(v, mappedFormItemUse, options, depth + 1),
107
+ ]),
108
+ );
109
+ }
110
+
111
+ const { mountTarget, normalizeMappedItem } = options || {};
112
+ if (mountTarget && typeof normalizeMappedItem === 'function' && next.use === mappedFormItemUse) {
113
+ const normalized = normalizeMappedItem(next, { mountTarget, engine: mountTarget.flowEngine });
114
+ if (normalized && typeof normalized === 'object') {
115
+ return normalized;
116
+ }
117
+ }
118
+
119
+ return next;
120
+ }
121
+
122
+ function findBlockModel(model: FlowModel): BlockModel | undefined {
123
+ if (!model) return;
124
+ if (model instanceof BlockModel) {
125
+ return model;
126
+ }
127
+ return findBlockModel(model.parent);
128
+ }
129
+
130
+ function isModelInsideReferenceBlock(model: FlowModel | undefined): boolean {
131
+ if (!model) return false;
132
+ return model.parent?.use === 'ReferenceBlockModel';
133
+ }
134
+
135
+ function resolveExpectedRootUse(blockModel: FlowModel | undefined): string | string[] {
136
+ // Create/Edit:允许互通
137
+ if (blockModel?.use === 'CreateFormModel' || blockModel?.use === 'EditFormModel') {
138
+ return ['CreateFormModel', 'EditFormModel'];
139
+ }
140
+ return blockModel?.use || '';
141
+ }
142
+
143
+ export class SubModelTemplateImporterModel extends CommonItemModel {
59
144
  declare props: ImporterProps;
60
145
 
61
146
  public resolveExpectedResourceInfo(
@@ -77,82 +162,64 @@ export class SubModelTemplateImporterModel extends FlowModel {
77
162
  }
78
163
 
79
164
  async afterAddAsSubModel() {
80
- // 作为临时“动作模型”,添加后执行导入逻辑并自清理
165
+ // 作为临时"动作模型",添加后执行导入逻辑并自清理
81
166
  const parentGrid = this.parent as FlowModel | undefined;
82
-
83
- const mountLevel = this.props?.mountToParentLevel ?? 0;
84
- let mountTarget: FlowModel | undefined = parentGrid;
85
- for (let i = 0; i < mountLevel; i++) {
86
- mountTarget = mountTarget?.parent as FlowModel | undefined;
87
- }
88
- // mountTarget 兜底:如果按层级拿不到 grid,则继续向上查找
89
- let probe: FlowModel | undefined = mountTarget;
90
- let hops = 0;
91
- while (probe && hops < 5) {
92
- if ((probe.subModels as any)?.grid) {
93
- mountTarget = probe;
94
- break;
95
- }
96
- probe = probe.parent as FlowModel | undefined;
97
- hops++;
98
- }
167
+ const mountTarget = parentGrid?.parent;
99
168
 
100
169
  // 先自清理:避免被保存为真实字段
101
170
  this.remove();
102
171
 
103
172
  // 注意:GridModel 会在 onSubModelRemoved 中触发 saveStepParams(异步且不 await),
104
173
  // 若我们紧接着替换/保存 grid,会与该 save 竞争导致最终落库被覆盖。
105
- // 这里显式等待一次同 uid 的保存完成,确保后续 replaceModel/save 不被“旧 grid saveStepParams”覆盖。
106
- if (parentGrid?.uid) {
107
- await parentGrid.saveStepParams();
108
- }
109
-
110
- if (!mountTarget) return;
111
-
174
+ // 这里显式等待一次同 uid 的保存完成,确保后续 replaceModel/save 不被"旧 grid saveStepParams"覆盖。
175
+ await parentGrid.saveStepParams();
112
176
  const step = (this.getStepParams(FLOW_KEY, 'selectTemplate') || {}) as Record<string, any>;
113
177
  const templateUid = String(step.templateUid || '').trim();
114
178
  const targetUid = String(step.targetUid || '').trim();
115
179
  const templateName = String(step.templateName || '').trim() || undefined;
116
180
  const templateDescription = String(step.templateDescription || '').trim() || undefined;
117
181
  const mode = String(step.mode || 'reference');
118
- const targetPath = String(step.targetPath || 'subModels.grid').trim() || 'subModels.grid';
119
- const mountSubKey = String(step.mountSubKey || 'grid').trim() || 'grid';
120
182
 
121
- // 仅支持表单 fields(grid)引用
122
- if (mountSubKey !== 'grid') {
123
- throw new Error(`[block-reference] Only 'grid' mountSubKey is supported (got '${mountSubKey}').`);
124
- }
125
- if (targetPath !== 'subModels.grid') {
126
- throw new Error(`[block-reference] Only 'subModels.grid' targetPath is supported (got '${targetPath}').`);
127
- }
128
- if (!templateUid) return;
129
- if (!targetUid) {
130
- throw new Error(
131
- `[block-reference] Missing targetUid for template import (templateUid='${templateUid}'). This is required for reference mode.`,
132
- );
133
- }
134
-
135
- const existingGrid = (mountTarget.subModels as any)?.grid as FlowModel | undefined;
136
- if (!existingGrid) {
137
- throw new Error(`[block-reference] Cannot mount to '${mountSubKey}': mountTarget has no grid subModel.`);
138
- }
183
+ const existingGrid = mountTarget.subModels?.grid as FlowModel;
139
184
 
140
185
  if (mode === 'copy') {
141
186
  const scoped = createBlockScopedEngine(mountTarget.flowEngine);
142
187
  const root = await scoped.loadModel<FlowModel>({ uid: targetUid });
143
- const fragment = _.get(root as any, targetPath);
144
- const gridModel =
145
- fragment instanceof FlowModel ? fragment : _.castArray(fragment).find((m) => m instanceof FlowModel);
146
- if (!gridModel) {
147
- throw new Error(`[block-reference] Template fragment is invalid: ${targetPath}`);
148
- }
188
+ const gridModel = root.subModels?.grid as FlowModel;
149
189
 
150
190
  const duplicated = await mountTarget.flowEngine.duplicateModel(gridModel.uid);
191
+ const duplicatedUid = duplicated.uid;
192
+
193
+ // 自定义表单区块可能会对 FormItemModel 做映射:这里按 block 提供的映射将模板字段入口改为目标入口
194
+ const rawMappedUse = resolveMappedFormItemUse(mountTarget);
195
+ const mappedUse =
196
+ rawMappedUse && rawMappedUse !== 'FormItemModel' && mountTarget.flowEngine.getModelClass(rawMappedUse)
197
+ ? rawMappedUse
198
+ : undefined;
199
+
200
+ type ModelClassWithNormalize = { normalizeSubModelTemplateImportNode?: NormalizeSubModelTemplateImportNode };
201
+ const mappedClass = mappedUse
202
+ ? (mountTarget.flowEngine.getModelClass(mappedUse) as ModelClassWithNormalize | undefined)
203
+ : undefined;
204
+ const normalizeFn =
205
+ mappedClass && typeof mappedClass.normalizeSubModelTemplateImportNode === 'function'
206
+ ? mappedClass.normalizeSubModelTemplateImportNode
207
+ : undefined;
208
+
209
+ const normalized = mappedUse
210
+ ? normalizeGridModelOptionsForMappedFormItemUse(duplicated, mappedUse, {
211
+ mountTarget,
212
+ normalizeMappedItem: normalizeFn,
213
+ })
214
+ : duplicated;
215
+
216
+ const merged = patchGridOptionsFromTemplateRoot(root, normalized);
217
+
151
218
  // 将复制出的 grid(默认脱离父级)移动到当前表单 grid 位置,避免再走 replaceModel/save 重建整棵树
152
- await mountTarget.flowEngine.modelRepository.move(duplicated.uid, existingGrid.uid, 'after');
219
+ await mountTarget.flowEngine.modelRepository.move(duplicatedUid, existingGrid.uid, 'after');
153
220
 
154
221
  const newGrid = mountTarget.flowEngine.createModel<FlowModel>({
155
- ...(duplicated as any),
222
+ ...merged.options,
156
223
  parentId: mountTarget.uid,
157
224
  subKey: 'grid',
158
225
  subType: 'object',
@@ -160,13 +227,16 @@ export class SubModelTemplateImporterModel extends FlowModel {
160
227
  mountTarget.setSubModel('grid', newGrid);
161
228
  await newGrid.afterAddAsSubModel();
162
229
  await mountTarget.flowEngine.destroyModel(existingGrid.uid);
230
+ if (merged.patched) {
231
+ await newGrid.saveStepParams();
232
+ }
163
233
 
164
234
  await mountTarget.rerender();
165
235
  return;
166
236
  }
167
237
 
168
- const nextSettings = { templateUid, templateName, templateDescription, targetUid, targetPath, mode };
169
- const isReferenceGrid = existingGrid.use === 'ReferenceFormGridModel';
238
+ const nextSettings = { templateUid, templateName, templateDescription, targetUid, mode };
239
+ const isReferenceGrid = typeof existingGrid.use === 'string' && existingGrid.use === 'ReferenceFormGridModel';
170
240
  if (isReferenceGrid) {
171
241
  existingGrid.setStepParams(GRID_REF_FLOW_KEY, GRID_REF_STEP_KEY, nextSettings);
172
242
  await existingGrid.saveStepParams();
@@ -318,7 +388,42 @@ export class SubModelTemplateImporterModel extends FlowModel {
318
388
  }
319
389
 
320
390
  SubModelTemplateImporterModel.define({
321
- hide: true,
391
+ label: tStr('Field template'),
392
+ sort: -999,
393
+ hide: (ctx: FlowModelContext) => {
394
+ // FilterForm 里暂不支持字段模板入口(避免误创建临时模型)
395
+ const blockModel = findBlockModel(ctx.model);
396
+ if (blockModel instanceof FilterFormBlockModel) {
397
+ return true;
398
+ }
399
+
400
+ // 2) 若当前区块是 ReferenceBlockModel 渲染的 target,隐藏 "From template"
401
+ // 因为在 ReferenceBlockModel 内部编辑字段会直接影响被引用的模板
402
+ if (isModelInsideReferenceBlock(blockModel)) {
403
+ return true;
404
+ }
405
+
406
+ return Object.prototype.hasOwnProperty.call(blockModel.context, REF_HOST_CTX_KEY);
407
+ },
408
+ createModelOptions: (ctx: FlowModelContext) => {
409
+ const blockModel = findBlockModel(ctx.model);
410
+ const expectedRootUse = resolveExpectedRootUse(blockModel);
411
+
412
+ const resourceInit = blockModel?.getStepParams?.('resourceSettings', 'init') || {};
413
+ const expectedDataSourceKey =
414
+ typeof resourceInit?.dataSourceKey === 'string' ? resourceInit.dataSourceKey : undefined;
415
+ const expectedCollectionName =
416
+ typeof resourceInit?.collectionName === 'string' ? resourceInit.collectionName : undefined;
417
+
418
+ return {
419
+ use: 'SubModelTemplateImporterModel',
420
+ props: {
421
+ expectedRootUse,
422
+ expectedDataSourceKey,
423
+ expectedCollectionName,
424
+ },
425
+ };
426
+ },
322
427
  });
323
428
 
324
429
  SubModelTemplateImporterModel.registerFlow({
@@ -336,6 +441,15 @@ SubModelTemplateImporterModel.registerFlow({
336
441
  const templateUid = (step?.templateUid || '').trim();
337
442
  const isNew = !!m.isNew;
338
443
  const disableSelect = !isNew && !!templateUid;
444
+
445
+ // 固定挂载到 parent.parent(即 grid 的父级 block)
446
+ const mountTarget = m.parent?.parent;
447
+
448
+ // 若当前区块对 FormItemModel 做了映射(自定义入口),默认更安全的 copy 模式
449
+ const rawMappedUse = mountTarget ? resolveMappedFormItemUse(mountTarget) : undefined;
450
+ const hasMappedUse =
451
+ rawMappedUse && rawMappedUse !== 'FormItemModel' && !!mountTarget?.flowEngine?.getModelClass?.(rawMappedUse);
452
+
339
453
  const fetchOptions = (keyword?: string, pagination?: { page?: number; pageSize?: number }) =>
340
454
  m.fetchTemplateOptions(ctx as FlowContext, keyword, pagination);
341
455
  return {
@@ -378,7 +492,8 @@ SubModelTemplateImporterModel.registerFlow({
378
492
  { label: tStr('Reference'), value: 'reference' },
379
493
  { label: tStr('Duplicate'), value: 'copy' },
380
494
  ],
381
- default: step?.mode || 'reference',
495
+ // 若当前区块对 FormItemModel 做了映射(自定义入口),默认更安全的 copy 模式
496
+ default: step?.mode || (hasMappedUse ? 'copy' : 'reference'),
382
497
  },
383
498
  modeDescriptionReference: {
384
499
  type: 'void',
@@ -417,16 +532,23 @@ SubModelTemplateImporterModel.registerFlow({
417
532
  async beforeParamsSave(ctx, params) {
418
533
  const importer = ctx.model as SubModelTemplateImporterModel;
419
534
  const parent = importer.parent as FlowModel | undefined;
420
- if (!parent) return;
421
-
422
- const mountLevel = importer.props?.mountToParentLevel ?? 0;
423
- let mountTarget: FlowModel | undefined = parent;
424
- for (let i = 0; i < mountLevel; i++) {
425
- mountTarget = mountTarget?.parent as FlowModel | undefined;
535
+ if (!parent) {
536
+ throw new Error('[block-reference] Cannot resolve mount target: importer has no parent.');
426
537
  }
538
+
539
+ // 固定挂载到 parent.parent(即 grid 的父级 block)
540
+ const mountTarget = parent.parent;
427
541
  const api = (ctx as FlowContext).api;
428
- const templateUid = (params?.templateUid || '').trim();
429
- if (!templateUid) return;
542
+ if (!mountTarget) {
543
+ throw new Error(
544
+ `[block-reference] Cannot resolve mount target from importer parent (uid='${parent.uid}', use='${parent.use}').`,
545
+ );
546
+ }
547
+
548
+ const templateUid = String(params?.templateUid || '').trim();
549
+ if (!templateUid) {
550
+ throw new Error('[block-reference] templateUid is required.');
551
+ }
430
552
 
431
553
  if (!api?.resource) {
432
554
  throw new Error('[block-reference] ctx.api.resource is required to fetch templates.');
@@ -440,48 +562,7 @@ SubModelTemplateImporterModel.registerFlow({
440
562
  }
441
563
  const templateName = (tpl?.name || params?.templateName || '').trim();
442
564
  const templateDescription = (tpl?.description || params?.templateDescription || '').trim();
443
- const mode = params?.mode || 'reference';
444
- const targetPath = (importer.props?.defaultSourcePath || 'subModels.grid').trim();
445
- const mountSubKey = (importer.props?.defaultMountSubKey || importer.subKey || 'grid').trim();
446
-
447
- // 当前仅支持表单 fields(grid)引用
448
- if (mountSubKey !== 'grid') {
449
- throw new Error(`[block-reference] Only 'grid' mountSubKey is supported (got '${mountSubKey}').`);
450
- }
451
- if (targetPath !== 'subModels.grid') {
452
- throw new Error(`[block-reference] Only 'subModels.grid' targetPath is supported (got '${targetPath}').`);
453
- }
454
-
455
- // 若当前位于“引用片段”内部,则禁止再次 From template,避免误清空模板侧字段
456
- const hostInfo = findRefHostInfoFromAncestors(importer);
457
- const hostRef = hostInfo?.ref;
458
- if (hostRef && hostRef.mountSubKey === 'grid' && hostRef.mode !== 'copy') {
459
- const msg =
460
- (ctx as FlowContext).t?.(
461
- 'This block already references field template, please convert fields to copy first',
462
- {
463
- ns: [NAMESPACE, 'client'],
464
- nsMode: 'fallback',
465
- },
466
- ) || '当前区块已引用字段模板,请先将引用字段转换为复制';
467
- const messageApi = (ctx as FlowContext).message || mountTarget.context.message || importer.context.message;
468
- messageApi?.warning?.(msg);
469
- throw new FlowExitException(FLOW_KEY, importer.uid, msg);
470
- }
471
-
472
- // mountTarget 兜底:如果按层级拿不到目标 subModel,则继续向上查找
473
- let probe: FlowModel | undefined = mountTarget;
474
- let hops = 0;
475
- while (probe && hops < 5) {
476
- const hasMount = !!(probe.subModels as any)?.[mountSubKey];
477
- if (hasMount) {
478
- mountTarget = probe;
479
- break;
480
- }
481
- probe = probe.parent as FlowModel | undefined;
482
- hops++;
483
- }
484
- if (!mountTarget) return;
565
+ const mode = String(params?.mode || 'reference');
485
566
 
486
567
  // 禁止跨数据源/数据表使用字段模板(在 UI 侧禁用的同时,这里做一次硬校验,避免绕过)
487
568
  const expectedResource = importer.resolveExpectedResourceInfo(ctx as FlowContext, mountTarget);
@@ -490,14 +571,14 @@ SubModelTemplateImporterModel.registerFlow({
490
571
  throw new Error(disabledReason);
491
572
  }
492
573
 
493
- // 引用模式下,如果目标挂载点已有字段(尤其是 grid),需先提示确认
494
- if (mode !== 'copy' && mountSubKey === 'grid') {
495
- const existingGrid = (mountTarget.subModels as any)?.grid as FlowModel | undefined;
496
- if (!existingGrid) {
497
- throw new Error(`[block-reference] Cannot mount to '${mountSubKey}': mountTarget has no grid subModel.`);
574
+ // 引用模式下,如果目标挂载点已有字段,需先提示确认
575
+ if (mode !== 'copy') {
576
+ const existingGrid = mountTarget.subModels?.grid;
577
+ if (!(existingGrid instanceof FlowModel)) {
578
+ throw new Error('[block-reference] Cannot mount: mountTarget has no grid subModel.');
498
579
  }
499
580
 
500
- const isReferenceGrid = String((existingGrid as any)?.use || '') === 'ReferenceFormGridModel';
581
+ const isReferenceGrid = typeof existingGrid.use === 'string' && existingGrid.use === 'ReferenceFormGridModel';
501
582
 
502
583
  // 已经是引用 grid 且引用未变化:直接退出,避免重复确认
503
584
  if (isReferenceGrid) {
@@ -512,8 +593,6 @@ SubModelTemplateImporterModel.registerFlow({
512
593
  targetUid,
513
594
  templateName,
514
595
  templateDescription,
515
- targetPath,
516
- mountSubKey,
517
596
  mode,
518
597
  });
519
598
  return;
@@ -526,106 +605,106 @@ SubModelTemplateImporterModel.registerFlow({
526
605
  .map((name) => mountTarget.flowEngine.getModelClass(name))
527
606
  .filter(Boolean) as ModelConstructor[]
528
607
  ).filter(Boolean);
529
- const shouldFallbackToAnyItem = fieldItemBaseClasses.length === 0;
530
-
531
- const localItemUids: string[] = [];
532
- const localFieldItemUids: string[] = [];
533
- mountTarget.flowEngine.forEachModel((m: any) => {
534
- if (!m || m.uid === mountTarget.uid || m.uid === importer.uid) return;
535
- if (m?.parent?.uid !== existingGrid.uid || m.subKey !== 'items') return;
536
-
537
- localItemUids.push(String(m.uid));
538
- if (shouldFallbackToAnyItem) {
539
- localFieldItemUids.push(String(m.uid));
540
- return;
608
+ // Details 区块的字段项类型与表单不同,采用"任意 items 都算已有字段"的策略进行确认提示
609
+ const isDetailsBlock = typeof mountTarget.use === 'string' && mountTarget.use === 'DetailsBlockModel';
610
+ const isDetailsGrid = typeof existingGrid.use === 'string' && existingGrid.use === 'DetailsGridModel';
611
+ const shouldFallbackToAnyItem = isDetailsBlock || isDetailsGrid || fieldItemBaseClasses.length === 0;
612
+
613
+ const isFieldItem = (m: FlowModel) => {
614
+ if (shouldFallbackToAnyItem) return true;
615
+ const ctor = m.constructor as ModelConstructor;
616
+ return fieldItemBaseClasses.some((Base) => ctor === Base || isInheritedFrom(ctor, Base));
617
+ };
618
+
619
+ let hasExistingFields = false;
620
+
621
+ // 非 reference grid:直接检查 subModels(避免全量扫描引擎)
622
+ if (!isReferenceGrid) {
623
+ const items = existingGrid.subModels?.items;
624
+ const list = Array.isArray(items) ? items : [];
625
+ for (const item of list) {
626
+ if (!(item instanceof FlowModel)) continue;
627
+ if (item.uid === importer.uid) continue;
628
+ if (isFieldItem(item)) {
629
+ hasExistingFields = true;
630
+ break;
631
+ }
541
632
  }
542
- const ctor = (m as FlowModel).constructor as ModelConstructor;
543
- const isFieldItem = fieldItemBaseClasses.some((Base) => ctor === Base || isInheritedFrom(ctor, Base));
544
- if (isFieldItem) {
545
- localFieldItemUids.push(String(m.uid));
546
- }
547
- });
548
- const hasExistingFields = localFieldItemUids.length > 0;
633
+ } else {
634
+ // reference grid:避免透传模板字段,按当前引擎内的 parent/subKey 关系识别“本地字段”
635
+ mountTarget.flowEngine.forEachModel((m) => {
636
+ if (hasExistingFields) return;
637
+ if (!(m instanceof FlowModel)) return;
638
+ if (m.uid === mountTarget.uid || m.uid === importer.uid) return;
639
+ if (m.parent?.uid !== existingGrid.uid || m.subKey !== 'items') return;
640
+ if (isFieldItem(m)) {
641
+ hasExistingFields = true;
642
+ }
643
+ });
644
+ }
549
645
  if (hasExistingFields) {
550
646
  const viewer = (ctx as FlowContext).viewer || mountTarget.context.viewer || importer.context.viewer;
551
- const message =
552
- (ctx as FlowContext).t?.('Using reference fields will remove existing fields', {
553
- ns: [NAMESPACE, 'client'],
554
- nsMode: 'fallback',
555
- }) || '使用引用字段会将当前已经添加的字段移除,是否继续?';
556
-
557
- // 先关闭 From template 弹窗,再弹二次确认,避免确认框被覆盖
558
- const currentView = (ctx as FlowContext).view;
559
- if (currentView && typeof currentView.close === 'function') {
560
- currentView.close(undefined, true);
561
- }
562
- // 等待一帧,确保上一个弹窗卸载完成
647
+ const message = ctx.t('Using reference fields will remove existing fields', {
648
+ ns: [NAMESPACE, 'client'],
649
+ nsMode: 'fallback',
650
+ });
651
+ ctx.view?.close(undefined, true);
563
652
  await new Promise<void>((resolve) => setTimeout(resolve, 0));
564
653
 
565
- const confirmed =
566
- viewer && typeof viewer.dialog === 'function'
567
- ? await new Promise<boolean>((resolve) => {
568
- let resolved = false;
569
- const resolveOnce = (val: boolean) => {
570
- if (resolved) return;
571
- resolved = true;
572
- resolve(val);
573
- };
574
- viewer.dialog({
575
- title:
576
- (ctx as FlowContext).t?.('Field template', { ns: [NAMESPACE, 'client'], nsMode: 'fallback' }) ||
577
- 'Field template',
578
- width: 520,
579
- destroyOnClose: true,
580
- content: (currentDialog: any) => (
581
- <>
582
- <div style={{ marginBottom: 16 }}>{message}</div>
583
- <currentDialog.Footer>
584
- <Space align="end">
585
- <Button
586
- onClick={() => {
587
- resolveOnce(false);
588
- currentDialog.close(undefined, true);
589
- }}
590
- >
591
- {(ctx as FlowContext).t?.('Cancel') || 'Cancel'}
592
- </Button>
593
- <Button
594
- type="primary"
595
- onClick={() => {
596
- resolveOnce(true);
597
- currentDialog.close(undefined, true);
598
- }}
599
- >
600
- {(ctx as FlowContext).t?.('Confirm') || 'Confirm'}
601
- </Button>
602
- </Space>
603
- </currentDialog.Footer>
604
- </>
605
- ),
606
- onClose: () => resolveOnce(false),
607
- // 保险:再抬高一些 zIndex
608
- zIndex: typeof viewer.getNextZIndex === 'function' ? viewer.getNextZIndex() + 1000 : undefined,
609
- });
610
- })
611
- : window.confirm('使用引用字段会将当前已经添加的字段移除,是否继续?');
654
+ const confirmed = await new Promise<boolean>((resolve) => {
655
+ let resolved = false;
656
+ const resolveOnce = (val: boolean) => {
657
+ if (resolved) return;
658
+ resolved = true;
659
+ resolve(val);
660
+ };
661
+ viewer.dialog({
662
+ title:
663
+ (ctx as FlowContext).t?.('Field template', { ns: [NAMESPACE, 'client'], nsMode: 'fallback' }) ||
664
+ 'Field template',
665
+ width: 520,
666
+ destroyOnClose: true,
667
+ content: (currentDialog: any) => (
668
+ <>
669
+ <div style={{ marginBottom: 16 }}>{message}</div>
670
+ <currentDialog.Footer>
671
+ <Space align="end">
672
+ <Button
673
+ onClick={() => {
674
+ resolveOnce(false);
675
+ currentDialog.close(undefined, true);
676
+ }}
677
+ >
678
+ {(ctx as FlowContext).t?.('Cancel') || 'Cancel'}
679
+ </Button>
680
+ <Button
681
+ type="primary"
682
+ onClick={() => {
683
+ resolveOnce(true);
684
+ currentDialog.close(undefined, true);
685
+ }}
686
+ >
687
+ {(ctx as FlowContext).t?.('Confirm') || 'Confirm'}
688
+ </Button>
689
+ </Space>
690
+ </currentDialog.Footer>
691
+ </>
692
+ ),
693
+ onClose: () => resolveOnce(false),
694
+ zIndex: typeof viewer.getNextZIndex === 'function' ? viewer.getNextZIndex() + 1000 : undefined,
695
+ });
696
+ });
612
697
  if (!confirmed) {
613
698
  throw new FlowExitException(FLOW_KEY, importer.uid, 'User cancelled template import');
614
699
  }
615
700
  }
616
-
617
- // 仅做确认与参数补全;真正的替换/复制在 afterAddAsSubModel 中执行
618
701
  }
619
702
 
620
- // 将解析后的信息写回 stepParams(afterAddAsSubModel 依赖这些值)
621
- // 注意:FlowModel.setStepParams 内部会 clone params,因此不能只改入参对象。
622
703
  importer.setStepParams(FLOW_KEY, 'selectTemplate', {
623
704
  templateUid,
624
705
  targetUid,
625
706
  templateName,
626
707
  templateDescription,
627
- targetPath,
628
- mountSubKey,
629
708
  mode,
630
709
  });
631
710
  },