@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
|
@@ -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
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
219
|
+
await mountTarget.flowEngine.modelRepository.move(duplicatedUid, existingGrid.uid, 'after');
|
|
153
220
|
|
|
154
221
|
const newGrid = mountTarget.flowEngine.createModel<FlowModel>({
|
|
155
|
-
...
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
//
|
|
494
|
-
if (mode !== 'copy'
|
|
495
|
-
const existingGrid =
|
|
496
|
-
if (!existingGrid) {
|
|
497
|
-
throw new Error(
|
|
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 =
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
},
|