@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.
- package/LICENSE.txt +172 -0
- package/build.config.ts +12 -0
- package/client.js +1 -0
- package/dist/client/collections/flowModelTemplates.d.ts +67 -0
- package/dist/client/components/FlowModelTemplatesPage.d.ts +12 -0
- package/dist/client/components/TemplateSelectOption.d.ts +20 -0
- package/dist/client/constants.d.ts +9 -0
- package/dist/client/hooks/useFlowModelTemplateActions.d.ts +24 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.js +10 -0
- package/dist/client/locale.d.ts +18 -0
- package/dist/client/menuExtensions.d.ts +9 -0
- package/dist/client/models/ReferenceBlockModel.d.ts +47 -0
- package/dist/client/models/ReferenceFormGridModel.d.ts +38 -0
- package/dist/client/models/SubModelTemplateImporterModel.d.ts +55 -0
- package/dist/client/models/referenceShared.d.ts +23 -0
- package/dist/client/openViewActionExtensions.d.ts +10 -0
- package/dist/client/schemas/flowModelTemplates.d.ts +11 -0
- package/dist/client/subModelMenuExtensions.d.ts +10 -0
- package/dist/client/utils/infiniteSelect.d.ts +28 -0
- package/dist/client/utils/refHost.d.ts +20 -0
- package/dist/client/utils/templateCompatibility.d.ts +91 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +42 -0
- package/dist/externalVersion.js +24 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +48 -0
- package/dist/locale/de-DE.json +14 -0
- package/dist/locale/en-US.json +72 -0
- package/dist/locale/es-ES.json +14 -0
- package/dist/locale/fr-FR.json +14 -0
- package/dist/locale/hu-HU.json +14 -0
- package/dist/locale/id-ID.json +14 -0
- package/dist/locale/it-IT.json +14 -0
- package/dist/locale/ja-JP.json +14 -0
- package/dist/locale/ko-KR.json +14 -0
- package/dist/locale/nl-NL.json +14 -0
- package/dist/locale/pt-BR.json +14 -0
- package/dist/locale/ru-RU.json +14 -0
- package/dist/locale/tr-TR.json +14 -0
- package/dist/locale/uk-UA.json +14 -0
- package/dist/locale/vi-VN.json +14 -0
- package/dist/locale/zh-CN.json +71 -0
- package/dist/locale/zh-TW.json +14 -0
- package/dist/server/collections/flowModelTemplateUsages.d.ts +11 -0
- package/dist/server/collections/flowModelTemplateUsages.js +71 -0
- package/dist/server/collections/flowModelTemplates.d.ts +11 -0
- package/dist/server/collections/flowModelTemplates.js +96 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.js +42 -0
- package/dist/server/plugin.d.ts +17 -0
- package/dist/server/plugin.js +242 -0
- package/dist/server/resources/flowModelTemplateUsages.d.ts +19 -0
- package/dist/server/resources/flowModelTemplateUsages.js +91 -0
- package/dist/server/resources/flowModelTemplates.d.ts +20 -0
- package/dist/server/resources/flowModelTemplates.js +267 -0
- package/package.json +37 -0
- package/server.js +1 -0
- package/src/client/__tests__/openViewActionExtensions.test.ts +1208 -0
- package/src/client/collections/flowModelTemplates.ts +131 -0
- package/src/client/components/FlowModelTemplatesPage.tsx +78 -0
- package/src/client/components/TemplateSelectOption.tsx +106 -0
- package/src/client/constants.ts +10 -0
- package/src/client/hooks/useFlowModelTemplateActions.tsx +137 -0
- package/src/client/index.ts +54 -0
- package/src/client/locale.ts +40 -0
- package/src/client/menuExtensions.tsx +1033 -0
- package/src/client/models/ReferenceBlockModel.tsx +793 -0
- package/src/client/models/ReferenceFormGridModel.tsx +302 -0
- package/src/client/models/SubModelTemplateImporterModel.tsx +634 -0
- package/src/client/models/__tests__/ReferenceBlockModel.test.tsx +482 -0
- package/src/client/models/__tests__/ReferenceFormGridModel.test.tsx +175 -0
- package/src/client/models/__tests__/SubModelTemplateImporterModel.test.ts +447 -0
- package/src/client/models/referenceShared.tsx +99 -0
- package/src/client/openViewActionExtensions.tsx +981 -0
- package/src/client/schemas/flowModelTemplates.ts +264 -0
- package/src/client/subModelMenuExtensions.ts +103 -0
- package/src/client/utils/infiniteSelect.ts +150 -0
- package/src/client/utils/refHost.ts +44 -0
- package/src/client/utils/templateCompatibility.ts +374 -0
- package/src/client.ts +10 -0
- package/src/index.ts +11 -0
- package/src/locale/de-DE.json +14 -0
- package/src/locale/en-US.json +72 -0
- package/src/locale/es-ES.json +14 -0
- package/src/locale/fr-FR.json +14 -0
- package/src/locale/hu-HU.json +14 -0
- package/src/locale/id-ID.json +14 -0
- package/src/locale/it-IT.json +14 -0
- package/src/locale/ja-JP.json +14 -0
- package/src/locale/ko-KR.json +14 -0
- package/src/locale/nl-NL.json +14 -0
- package/src/locale/pt-BR.json +14 -0
- package/src/locale/ru-RU.json +14 -0
- package/src/locale/tr-TR.json +14 -0
- package/src/locale/uk-UA.json +14 -0
- package/src/locale/vi-VN.json +14 -0
- package/src/locale/zh-CN.json +71 -0
- package/src/locale/zh-TW.json +14 -0
- package/src/server/__tests__/template-usage.test.ts +351 -0
- package/src/server/collections/flowModelTemplateUsages.ts +51 -0
- package/src/server/collections/flowModelTemplates.ts +76 -0
- package/src/server/index.ts +10 -0
- package/src/server/plugin.ts +236 -0
- package/src/server/resources/flowModelTemplateUsages.ts +61 -0
- package/src/server/resources/flowModelTemplates.ts +251 -0
|
@@ -0,0 +1,634 @@
|
|
|
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 _ from 'lodash';
|
|
12
|
+
import { Button, Space } from 'antd';
|
|
13
|
+
import {
|
|
14
|
+
FlowModel,
|
|
15
|
+
FlowContext,
|
|
16
|
+
createBlockScopedEngine,
|
|
17
|
+
FlowExitException,
|
|
18
|
+
isInheritedFrom,
|
|
19
|
+
type ModelConstructor,
|
|
20
|
+
tExpr,
|
|
21
|
+
} from '@nocobase/flow-engine';
|
|
22
|
+
import { NAMESPACE, tStr } from '../locale';
|
|
23
|
+
import { renderTemplateSelectLabel, renderTemplateSelectOption } from '../components/TemplateSelectOption';
|
|
24
|
+
import { findRefHostInfoFromAncestors } from '../utils/refHost';
|
|
25
|
+
import {
|
|
26
|
+
TEMPLATE_LIST_PAGE_SIZE,
|
|
27
|
+
calcHasMore,
|
|
28
|
+
getTemplateAvailabilityDisabledReason,
|
|
29
|
+
parseResourceListResponse,
|
|
30
|
+
resolveExpectedResourceInfoByModelChain,
|
|
31
|
+
} from '../utils/templateCompatibility';
|
|
32
|
+
import { bindInfiniteScrollToFormilySelect, defaultSelectOptionComparator } from '../utils/infiniteSelect';
|
|
33
|
+
|
|
34
|
+
type ImporterProps = {
|
|
35
|
+
/** 默认从模板根取片段的路径 */
|
|
36
|
+
defaultSourcePath?: string;
|
|
37
|
+
/** 模板根 use 过滤(可选),支持多个候选 */
|
|
38
|
+
expectedRootUse?: string | string[];
|
|
39
|
+
/** 期望的数据源 key(可选,用于禁用不匹配的模板) */
|
|
40
|
+
expectedDataSourceKey?: string;
|
|
41
|
+
/** 期望的 collectionName(可选,用于禁用不匹配的模板) */
|
|
42
|
+
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
|
+
};
|
|
53
|
+
|
|
54
|
+
const FLOW_KEY = 'subModelTemplateImportSettings';
|
|
55
|
+
const GRID_REF_FLOW_KEY = 'referenceSettings';
|
|
56
|
+
const GRID_REF_STEP_KEY = 'useTemplate';
|
|
57
|
+
|
|
58
|
+
export class SubModelTemplateImporterModel extends FlowModel {
|
|
59
|
+
declare props: ImporterProps;
|
|
60
|
+
|
|
61
|
+
public resolveExpectedResourceInfo(
|
|
62
|
+
ctx?: FlowContext,
|
|
63
|
+
start?: FlowModel,
|
|
64
|
+
): { dataSourceKey?: string; collectionName?: string } {
|
|
65
|
+
const fromPropsDataSourceKey = String(this.props?.expectedDataSourceKey || '').trim();
|
|
66
|
+
const fromPropsCollectionName = String(this.props?.expectedCollectionName || '').trim();
|
|
67
|
+
if (fromPropsDataSourceKey && fromPropsCollectionName) {
|
|
68
|
+
return { dataSourceKey: fromPropsDataSourceKey, collectionName: fromPropsCollectionName };
|
|
69
|
+
}
|
|
70
|
+
const resolved = resolveExpectedResourceInfoByModelChain(ctx, start || this.parent, {
|
|
71
|
+
dataSourceManager: this.context.dataSourceManager,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
dataSourceKey: String(resolved?.dataSourceKey || fromPropsDataSourceKey || '').trim() || undefined,
|
|
75
|
+
collectionName: String(resolved?.collectionName || fromPropsCollectionName || '').trim() || undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async afterAddAsSubModel() {
|
|
80
|
+
// 作为临时“动作模型”,添加后执行导入逻辑并自清理
|
|
81
|
+
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
|
+
}
|
|
99
|
+
|
|
100
|
+
// 先自清理:避免被保存为真实字段
|
|
101
|
+
this.remove();
|
|
102
|
+
|
|
103
|
+
// 注意:GridModel 会在 onSubModelRemoved 中触发 saveStepParams(异步且不 await),
|
|
104
|
+
// 若我们紧接着替换/保存 grid,会与该 save 竞争导致最终落库被覆盖。
|
|
105
|
+
// 这里显式等待一次同 uid 的保存完成,确保后续 replaceModel/save 不被“旧 grid saveStepParams”覆盖。
|
|
106
|
+
if (parentGrid?.uid) {
|
|
107
|
+
await parentGrid.saveStepParams();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!mountTarget) return;
|
|
111
|
+
|
|
112
|
+
const step = (this.getStepParams(FLOW_KEY, 'selectTemplate') || {}) as Record<string, any>;
|
|
113
|
+
const templateUid = String(step.templateUid || '').trim();
|
|
114
|
+
const targetUid = String(step.targetUid || '').trim();
|
|
115
|
+
const templateName = String(step.templateName || '').trim() || undefined;
|
|
116
|
+
const templateDescription = String(step.templateDescription || '').trim() || undefined;
|
|
117
|
+
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
|
+
|
|
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
|
+
}
|
|
139
|
+
|
|
140
|
+
if (mode === 'copy') {
|
|
141
|
+
const scoped = createBlockScopedEngine(mountTarget.flowEngine);
|
|
142
|
+
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
|
+
}
|
|
149
|
+
|
|
150
|
+
const duplicated = await mountTarget.flowEngine.duplicateModel(gridModel.uid);
|
|
151
|
+
// 将复制出的 grid(默认脱离父级)移动到当前表单 grid 位置,避免再走 replaceModel/save 重建整棵树
|
|
152
|
+
await mountTarget.flowEngine.modelRepository.move(duplicated.uid, existingGrid.uid, 'after');
|
|
153
|
+
|
|
154
|
+
const newGrid = mountTarget.flowEngine.createModel<FlowModel>({
|
|
155
|
+
...(duplicated as any),
|
|
156
|
+
parentId: mountTarget.uid,
|
|
157
|
+
subKey: 'grid',
|
|
158
|
+
subType: 'object',
|
|
159
|
+
});
|
|
160
|
+
mountTarget.setSubModel('grid', newGrid);
|
|
161
|
+
await newGrid.afterAddAsSubModel();
|
|
162
|
+
await mountTarget.flowEngine.destroyModel(existingGrid.uid);
|
|
163
|
+
|
|
164
|
+
await mountTarget.rerender();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextSettings = { templateUid, templateName, templateDescription, targetUid, targetPath, mode };
|
|
169
|
+
const isReferenceGrid = existingGrid.use === 'ReferenceFormGridModel';
|
|
170
|
+
if (isReferenceGrid) {
|
|
171
|
+
existingGrid.setStepParams(GRID_REF_FLOW_KEY, GRID_REF_STEP_KEY, nextSettings);
|
|
172
|
+
await existingGrid.saveStepParams();
|
|
173
|
+
await mountTarget.rerender();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const uidToReplace = existingGrid.uid;
|
|
178
|
+
const oldStepParams: Record<string, any> =
|
|
179
|
+
existingGrid.stepParams && typeof existingGrid.stepParams === 'object'
|
|
180
|
+
? (existingGrid.stepParams as Record<string, any>)
|
|
181
|
+
: {};
|
|
182
|
+
const prevRefSettings =
|
|
183
|
+
oldStepParams[GRID_REF_FLOW_KEY] && typeof oldStepParams[GRID_REF_FLOW_KEY] === 'object'
|
|
184
|
+
? (oldStepParams[GRID_REF_FLOW_KEY] as Record<string, any>)
|
|
185
|
+
: {};
|
|
186
|
+
const nextStepParams = {
|
|
187
|
+
[GRID_REF_FLOW_KEY]: {
|
|
188
|
+
...prevRefSettings,
|
|
189
|
+
[GRID_REF_STEP_KEY]: nextSettings,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 需要清理旧 grid 子树(否则旧字段会残留并被 serialize 落库)
|
|
194
|
+
await mountTarget.flowEngine.destroyModel(uidToReplace);
|
|
195
|
+
|
|
196
|
+
const newGrid = mountTarget.flowEngine.createModel<FlowModel>({
|
|
197
|
+
uid: uidToReplace,
|
|
198
|
+
use: 'ReferenceFormGridModel',
|
|
199
|
+
props: existingGrid.props,
|
|
200
|
+
sortIndex: existingGrid.sortIndex,
|
|
201
|
+
parentId: mountTarget.uid,
|
|
202
|
+
subKey: 'grid',
|
|
203
|
+
subType: 'object',
|
|
204
|
+
stepParams: nextStepParams,
|
|
205
|
+
});
|
|
206
|
+
mountTarget.setSubModel('grid', newGrid);
|
|
207
|
+
await newGrid.afterAddAsSubModel();
|
|
208
|
+
await newGrid.save();
|
|
209
|
+
|
|
210
|
+
await mountTarget.rerender();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async save() {
|
|
214
|
+
// 禁止持久化
|
|
215
|
+
return { uid: this.uid };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async saveStepParams() {
|
|
219
|
+
// 禁止持久化(FlowSettingsDialog 会调用 saveStepParams)
|
|
220
|
+
return { uid: this.uid };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
render() {
|
|
224
|
+
// 临时模型不渲染任何内容
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public getTemplateDisabledReason(
|
|
229
|
+
ctx: FlowContext,
|
|
230
|
+
tpl: Record<string, any>,
|
|
231
|
+
expected?: { dataSourceKey?: string; collectionName?: string },
|
|
232
|
+
): string | undefined {
|
|
233
|
+
const expectedDataSourceKey = String(expected?.dataSourceKey || this.props?.expectedDataSourceKey || '').trim();
|
|
234
|
+
const expectedCollectionName = String(expected?.collectionName || this.props?.expectedCollectionName || '').trim();
|
|
235
|
+
if (!expectedDataSourceKey || !expectedCollectionName) return undefined;
|
|
236
|
+
return getTemplateAvailabilityDisabledReason(
|
|
237
|
+
ctx,
|
|
238
|
+
tpl,
|
|
239
|
+
{ dataSourceKey: expectedDataSourceKey, collectionName: expectedCollectionName },
|
|
240
|
+
{ dataSourceManager: this.context.dataSourceManager },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
public async fetchTemplateOptions(
|
|
245
|
+
ctx: FlowContext,
|
|
246
|
+
keyword?: string,
|
|
247
|
+
pagination?: { page?: number; pageSize?: number },
|
|
248
|
+
): Promise<{ options: any[]; hasMore: boolean }> {
|
|
249
|
+
const api = ctx.api;
|
|
250
|
+
if (!api?.resource) return { options: [], hasMore: false };
|
|
251
|
+
const page = Math.max(1, Number(pagination?.page || 1));
|
|
252
|
+
const pageSize = Math.max(1, Number(pagination?.pageSize || TEMPLATE_LIST_PAGE_SIZE));
|
|
253
|
+
try {
|
|
254
|
+
const expectedRootUse = this.props?.expectedRootUse;
|
|
255
|
+
const expects = expectedRootUse
|
|
256
|
+
? Array.isArray(expectedRootUse)
|
|
257
|
+
? expectedRootUse.map((u) => String(u))
|
|
258
|
+
: [String(expectedRootUse)]
|
|
259
|
+
: [];
|
|
260
|
+
// useModel 不匹配直接不显示
|
|
261
|
+
const useModelFilter =
|
|
262
|
+
expects.length > 0
|
|
263
|
+
? {
|
|
264
|
+
$or: [{ useModel: { $in: expects } }, { useModel: null }, { useModel: '' }],
|
|
265
|
+
}
|
|
266
|
+
: undefined;
|
|
267
|
+
// 排除弹窗模板(popup templates),避免污染区块/字段模板列表
|
|
268
|
+
const nonPopupFilter = {
|
|
269
|
+
$and: [{ $or: [{ type: { $notIn: ['popup'] } }, { type: null }, { type: '' }] }],
|
|
270
|
+
};
|
|
271
|
+
const mergedFilter = useModelFilter ? { $and: [useModelFilter, ...nonPopupFilter.$and] } : nonPopupFilter;
|
|
272
|
+
const res = await api.resource('flowModelTemplates').list({
|
|
273
|
+
page,
|
|
274
|
+
pageSize,
|
|
275
|
+
search: keyword || undefined,
|
|
276
|
+
filter: mergedFilter,
|
|
277
|
+
});
|
|
278
|
+
const { rows: rawRows, count } = parseResourceListResponse<any>(res);
|
|
279
|
+
const rawLength = rawRows.length;
|
|
280
|
+
|
|
281
|
+
const expectedResource = this.resolveExpectedResourceInfo(ctx);
|
|
282
|
+
const withIndex = rawRows.flatMap((r, idx) => {
|
|
283
|
+
const useModel = r?.useModel;
|
|
284
|
+
if (expects.length > 0) {
|
|
285
|
+
if (!useModel) return [];
|
|
286
|
+
if (!expects.includes(String(useModel))) return [];
|
|
287
|
+
}
|
|
288
|
+
const name = r?.name || r?.uid || '';
|
|
289
|
+
const desc = r?.description;
|
|
290
|
+
const disabledReason = this.getTemplateDisabledReason(ctx, r, expectedResource);
|
|
291
|
+
return [
|
|
292
|
+
{
|
|
293
|
+
__idx: (page - 1) * pageSize + idx,
|
|
294
|
+
label: renderTemplateSelectLabel(name),
|
|
295
|
+
value: r?.uid,
|
|
296
|
+
description: desc,
|
|
297
|
+
rawName: name,
|
|
298
|
+
targetUid: r?.targetUid,
|
|
299
|
+
disabled: !!disabledReason,
|
|
300
|
+
disabledReason,
|
|
301
|
+
dataSourceKey: r?.dataSourceKey,
|
|
302
|
+
collectionName: r?.collectionName,
|
|
303
|
+
useModel,
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
});
|
|
307
|
+
// enabled 模板放在最前面,其余保持原有顺序(稳定排序)
|
|
308
|
+
withIndex.sort(defaultSelectOptionComparator);
|
|
309
|
+
return {
|
|
310
|
+
options: withIndex,
|
|
311
|
+
hasMore: calcHasMore({ page, pageSize, rowsLength: rawLength, count }),
|
|
312
|
+
};
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error('fetch template options failed', e);
|
|
315
|
+
return { options: [], hasMore: false };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
SubModelTemplateImporterModel.define({
|
|
321
|
+
hide: true,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
SubModelTemplateImporterModel.registerFlow({
|
|
325
|
+
key: FLOW_KEY,
|
|
326
|
+
title: tExpr('Field template'),
|
|
327
|
+
manual: true,
|
|
328
|
+
sort: -999,
|
|
329
|
+
steps: {
|
|
330
|
+
selectTemplate: {
|
|
331
|
+
preset: true,
|
|
332
|
+
title: tStr('Field template'),
|
|
333
|
+
uiSchema: (ctx) => {
|
|
334
|
+
const m = ctx.model as SubModelTemplateImporterModel;
|
|
335
|
+
const step = m.getStepParams(FLOW_KEY, 'selectTemplate') || {};
|
|
336
|
+
const templateUid = (step?.templateUid || '').trim();
|
|
337
|
+
const isNew = !!m.isNew;
|
|
338
|
+
const disableSelect = !isNew && !!templateUid;
|
|
339
|
+
const fetchOptions = (keyword?: string, pagination?: { page?: number; pageSize?: number }) =>
|
|
340
|
+
m.fetchTemplateOptions(ctx as FlowContext, keyword, pagination);
|
|
341
|
+
return {
|
|
342
|
+
templateUid: {
|
|
343
|
+
title: tStr('Template'),
|
|
344
|
+
description: tStr('Template field section description'),
|
|
345
|
+
'x-decorator': 'FormItem',
|
|
346
|
+
'x-component': 'Select',
|
|
347
|
+
'x-component-props': {
|
|
348
|
+
showSearch: true,
|
|
349
|
+
filterOption: false,
|
|
350
|
+
allowClear: true,
|
|
351
|
+
placeholder: tStr('Search templates'),
|
|
352
|
+
disabled: disableSelect,
|
|
353
|
+
optionLabelProp: 'label',
|
|
354
|
+
dropdownMatchSelectWidth: true,
|
|
355
|
+
dropdownStyle: { maxWidth: 560 },
|
|
356
|
+
getPopupContainer: () => document.body,
|
|
357
|
+
optionRender: renderTemplateSelectOption,
|
|
358
|
+
},
|
|
359
|
+
default: templateUid || undefined,
|
|
360
|
+
'x-validator': [{ required: true }],
|
|
361
|
+
'x-reactions': [
|
|
362
|
+
(field) => {
|
|
363
|
+
bindInfiniteScrollToFormilySelect(
|
|
364
|
+
field,
|
|
365
|
+
async (keyword: string, page: number, pageSize: number) => {
|
|
366
|
+
return fetchOptions(keyword, { page, pageSize });
|
|
367
|
+
},
|
|
368
|
+
{ pageSize: TEMPLATE_LIST_PAGE_SIZE, composingKey: '__templateComposing' },
|
|
369
|
+
);
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
mode: {
|
|
374
|
+
title: tStr('Mode'),
|
|
375
|
+
'x-decorator': 'FormItem',
|
|
376
|
+
'x-component': 'Radio.Group',
|
|
377
|
+
enum: [
|
|
378
|
+
{ label: tStr('Reference'), value: 'reference' },
|
|
379
|
+
{ label: tStr('Duplicate'), value: 'copy' },
|
|
380
|
+
],
|
|
381
|
+
default: step?.mode || 'reference',
|
|
382
|
+
},
|
|
383
|
+
modeDescriptionReference: {
|
|
384
|
+
type: 'void',
|
|
385
|
+
'x-decorator': 'FormItem',
|
|
386
|
+
'x-decorator-props': { colon: false },
|
|
387
|
+
'x-component': 'Alert',
|
|
388
|
+
'x-component-props': {
|
|
389
|
+
type: 'info',
|
|
390
|
+
showIcon: false,
|
|
391
|
+
message: tStr('Reference mode description'),
|
|
392
|
+
style: { marginTop: -8 },
|
|
393
|
+
},
|
|
394
|
+
'x-reactions': {
|
|
395
|
+
dependencies: ['mode'],
|
|
396
|
+
fulfill: { state: { hidden: '{{$deps[0] === "copy"}}' } },
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
modeDescriptionDuplicate: {
|
|
400
|
+
type: 'void',
|
|
401
|
+
'x-decorator': 'FormItem',
|
|
402
|
+
'x-decorator-props': { colon: false },
|
|
403
|
+
'x-component': 'Alert',
|
|
404
|
+
'x-component-props': {
|
|
405
|
+
type: 'info',
|
|
406
|
+
showIcon: false,
|
|
407
|
+
message: tStr('Duplicate mode description'),
|
|
408
|
+
style: { marginTop: -8 },
|
|
409
|
+
},
|
|
410
|
+
'x-reactions': {
|
|
411
|
+
dependencies: ['mode'],
|
|
412
|
+
fulfill: { state: { hidden: '{{$deps[0] !== "copy"}}' } },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
async beforeParamsSave(ctx, params) {
|
|
418
|
+
const importer = ctx.model as SubModelTemplateImporterModel;
|
|
419
|
+
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;
|
|
426
|
+
}
|
|
427
|
+
const api = (ctx as FlowContext).api;
|
|
428
|
+
const templateUid = (params?.templateUid || '').trim();
|
|
429
|
+
if (!templateUid) return;
|
|
430
|
+
|
|
431
|
+
if (!api?.resource) {
|
|
432
|
+
throw new Error('[block-reference] ctx.api.resource is required to fetch templates.');
|
|
433
|
+
}
|
|
434
|
+
const res = await api.resource('flowModelTemplates').get({ filterByTk: templateUid });
|
|
435
|
+
const tpl = res?.data?.data || res?.data || res;
|
|
436
|
+
|
|
437
|
+
const targetUid = (tpl?.targetUid || params?.targetUid || '').trim();
|
|
438
|
+
if (!targetUid) {
|
|
439
|
+
throw new Error(`[block-reference] Template '${templateUid}' has no targetUid.`);
|
|
440
|
+
}
|
|
441
|
+
const templateName = (tpl?.name || params?.templateName || '').trim();
|
|
442
|
+
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;
|
|
485
|
+
|
|
486
|
+
// 禁止跨数据源/数据表使用字段模板(在 UI 侧禁用的同时,这里做一次硬校验,避免绕过)
|
|
487
|
+
const expectedResource = importer.resolveExpectedResourceInfo(ctx as FlowContext, mountTarget);
|
|
488
|
+
const disabledReason = importer.getTemplateDisabledReason(ctx as FlowContext, tpl || {}, expectedResource);
|
|
489
|
+
if (disabledReason) {
|
|
490
|
+
throw new Error(disabledReason);
|
|
491
|
+
}
|
|
492
|
+
|
|
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.`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const isReferenceGrid = String((existingGrid as any)?.use || '') === 'ReferenceFormGridModel';
|
|
501
|
+
|
|
502
|
+
// 已经是引用 grid 且引用未变化:直接退出,避免重复确认
|
|
503
|
+
if (isReferenceGrid) {
|
|
504
|
+
const existing = existingGrid.getStepParams(GRID_REF_FLOW_KEY, GRID_REF_STEP_KEY) || {};
|
|
505
|
+
const norm = (v: any) => String(v || '').trim();
|
|
506
|
+
const isSame =
|
|
507
|
+
norm(existing.templateUid) === norm(templateUid) && norm(existing.targetUid) === norm(targetUid);
|
|
508
|
+
if (isSame) {
|
|
509
|
+
// 仍需写入当前 stepParams,避免 afterAddAsSubModel 无法读取 targetUid 等
|
|
510
|
+
importer.setStepParams(FLOW_KEY, 'selectTemplate', {
|
|
511
|
+
templateUid,
|
|
512
|
+
targetUid,
|
|
513
|
+
templateName,
|
|
514
|
+
templateDescription,
|
|
515
|
+
targetPath,
|
|
516
|
+
mountSubKey,
|
|
517
|
+
mode,
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 仅检测“当前区块”已持久化/在当前引擎内的字段,避免 reference grid 透传模板字段导致误提示
|
|
524
|
+
const fieldItemBaseClasses = (
|
|
525
|
+
['FormItemModel', 'FormCustomItemModel', 'FormJSFieldItemModel']
|
|
526
|
+
.map((name) => mountTarget.flowEngine.getModelClass(name))
|
|
527
|
+
.filter(Boolean) as ModelConstructor[]
|
|
528
|
+
).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;
|
|
541
|
+
}
|
|
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;
|
|
549
|
+
if (hasExistingFields) {
|
|
550
|
+
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
|
+
// 等待一帧,确保上一个弹窗卸载完成
|
|
563
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
564
|
+
|
|
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('使用引用字段会将当前已经添加的字段移除,是否继续?');
|
|
612
|
+
if (!confirmed) {
|
|
613
|
+
throw new FlowExitException(FLOW_KEY, importer.uid, 'User cancelled template import');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 仅做确认与参数补全;真正的替换/复制在 afterAddAsSubModel 中执行
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// 将解析后的信息写回 stepParams(afterAddAsSubModel 依赖这些值)
|
|
621
|
+
// 注意:FlowModel.setStepParams 内部会 clone params,因此不能只改入参对象。
|
|
622
|
+
importer.setStepParams(FLOW_KEY, 'selectTemplate', {
|
|
623
|
+
templateUid,
|
|
624
|
+
targetUid,
|
|
625
|
+
templateName,
|
|
626
|
+
templateDescription,
|
|
627
|
+
targetPath,
|
|
628
|
+
mountSubKey,
|
|
629
|
+
mode,
|
|
630
|
+
});
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
});
|