@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,236 @@
|
|
|
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 { InstallOptions, Plugin } from '@nocobase/server';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import flowModelTemplates from './resources/flowModelTemplates';
|
|
13
|
+
import flowModelTemplateUsages from './resources/flowModelTemplateUsages';
|
|
14
|
+
import { FlowModelRepository } from '@nocobase/plugin-flow-engine';
|
|
15
|
+
import { uid } from '@nocobase/utils';
|
|
16
|
+
export class PluginBlockReferenceServer extends Plugin {
|
|
17
|
+
async load() {
|
|
18
|
+
this.app.resourceManager.define(flowModelTemplates);
|
|
19
|
+
this.app.resourceManager.define(flowModelTemplateUsages);
|
|
20
|
+
|
|
21
|
+
this.app.acl.allow('flowModelTemplates', '*', 'loggedIn');
|
|
22
|
+
this.app.acl.allow('flowModelTemplateUsages', '*', 'loggedIn');
|
|
23
|
+
this.app.acl.registerSnippet({
|
|
24
|
+
name: `pm.${this.name}.templates`,
|
|
25
|
+
actions: ['flowModelTemplates:*', 'flowModelTemplateUsages:*'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
this.app.auditManager?.registerActions([
|
|
29
|
+
'flowModelTemplates:create',
|
|
30
|
+
'flowModelTemplates:update',
|
|
31
|
+
'flowModelTemplates:destroy',
|
|
32
|
+
'flowModelTemplateUsages:create',
|
|
33
|
+
'flowModelTemplateUsages:destroy',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const parseOptions = (value: any) => FlowModelRepository.optionsToJson(value || {});
|
|
37
|
+
const omitSubModels = (value: any) => {
|
|
38
|
+
if (!value || typeof value !== 'object') return value;
|
|
39
|
+
const { subModels, ...rest } = value;
|
|
40
|
+
return rest;
|
|
41
|
+
};
|
|
42
|
+
const collectFlowModelsFromTree = (value: any) => {
|
|
43
|
+
const result: Array<any> = [];
|
|
44
|
+
const ensureNodeUid = (node: any): string => {
|
|
45
|
+
const existing = node?.uid;
|
|
46
|
+
if (typeof existing === 'string' && existing) return existing;
|
|
47
|
+
if (typeof existing === 'undefined' || existing === null || existing === '') {
|
|
48
|
+
const next = uid();
|
|
49
|
+
node.uid = next;
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
throw new Error('[block-reference] flowModels uid must be a non-empty string');
|
|
53
|
+
};
|
|
54
|
+
const walk = (node: any, inheritedParentId?: string) => {
|
|
55
|
+
if (!node || typeof node !== 'object') return;
|
|
56
|
+
const nodeUid = ensureNodeUid(node);
|
|
57
|
+
const nextNode = omitSubModels(node);
|
|
58
|
+
if (inheritedParentId && nextNode && typeof nextNode === 'object' && !nextNode.parentId) {
|
|
59
|
+
nextNode.parentId = inheritedParentId;
|
|
60
|
+
}
|
|
61
|
+
if (nodeUid) {
|
|
62
|
+
result.push(nextNode);
|
|
63
|
+
}
|
|
64
|
+
const subModels = node?.subModels;
|
|
65
|
+
if (!subModels || typeof subModels !== 'object') return;
|
|
66
|
+
for (const val of Object.values(subModels)) {
|
|
67
|
+
const items = Array.isArray(val) ? val : [val];
|
|
68
|
+
for (const child of items) {
|
|
69
|
+
walk(child, nodeUid || inheritedParentId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
walk(value);
|
|
74
|
+
return result;
|
|
75
|
+
};
|
|
76
|
+
const extractTemplateUids = (options: any) => {
|
|
77
|
+
const stepParams = options?.stepParams || {};
|
|
78
|
+
const uids = new Set<string>();
|
|
79
|
+
|
|
80
|
+
// 模板引用(不限定 use;后续其它模型也可复用 referenceSettings.useTemplate )
|
|
81
|
+
const mode = _.get(stepParams, ['referenceSettings', 'useTemplate', 'mode']) || 'reference';
|
|
82
|
+
const tplUid = _.get(stepParams, ['referenceSettings', 'useTemplate', 'templateUid']);
|
|
83
|
+
if (tplUid && mode !== 'copy') {
|
|
84
|
+
uids.add(String(tplUid));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// openView(弹窗模板引用)
|
|
88
|
+
// 兼容:openView step key 可能不是 "openView",这里扫描 popupSettings 下所有 stepParams
|
|
89
|
+
const popupSettings = _.get(stepParams, ['popupSettings']);
|
|
90
|
+
if (popupSettings && typeof popupSettings === 'object') {
|
|
91
|
+
for (const val of Object.values(popupSettings)) {
|
|
92
|
+
if (!val || typeof val !== 'object') continue;
|
|
93
|
+
const step = val as Record<string, unknown>;
|
|
94
|
+
const tplUidRaw = step['popupTemplateUid'];
|
|
95
|
+
const tplMode = String(step['popupTemplateMode'] || '') || 'reference';
|
|
96
|
+
if (tplUidRaw !== null && typeof tplUidRaw !== 'undefined' && String(tplUidRaw) && tplMode !== 'copy') {
|
|
97
|
+
uids.add(String(tplUidRaw));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return Array.from(uids);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const syncUsages = async (instanceUid: string, options: any, { transaction }: { transaction?: any } = {}) => {
|
|
106
|
+
const usageRepo = this.db.getRepository('flowModelTemplateUsages');
|
|
107
|
+
const currentUids = _.uniq(extractTemplateUids(options).map(String));
|
|
108
|
+
|
|
109
|
+
await usageRepo.destroy({
|
|
110
|
+
filter: { modelUid: instanceUid },
|
|
111
|
+
transaction,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const templateUid of currentUids) {
|
|
115
|
+
await usageRepo.updateOrCreate({
|
|
116
|
+
filterKeys: ['templateUid', 'modelUid'],
|
|
117
|
+
values: { uid: uid(), templateUid, modelUid: instanceUid },
|
|
118
|
+
transaction,
|
|
119
|
+
context: { disableAfterDestroy: true },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const removeUsagesByInstance = async (
|
|
125
|
+
instanceUid: string,
|
|
126
|
+
_options: any,
|
|
127
|
+
{ transaction }: { transaction?: any } = {},
|
|
128
|
+
) => {
|
|
129
|
+
const usageRepo = this.db.getRepository('flowModelTemplateUsages');
|
|
130
|
+
await usageRepo.destroy({ filter: { modelUid: instanceUid }, transaction });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// 区块删除时自动清理 usage 记录,避免垃圾数据
|
|
134
|
+
this.db.on('flowModels.afterDestroy', async (instance: any, { transaction }: any = {}) => {
|
|
135
|
+
const instanceUid = instance?.get?.('uid') || instance?.uid;
|
|
136
|
+
if (!instanceUid) return;
|
|
137
|
+
const options = parseOptions(instance?.get?.('options') || instance?.options);
|
|
138
|
+
await removeUsagesByInstance(instanceUid, options, { transaction });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 区块保存时维护 usage:
|
|
142
|
+
// - referenceSettings.useTemplate.templateUid(且 mode!==copy)
|
|
143
|
+
// - popupSettings.openView.popupTemplateUid + popupTemplateMode!==copy
|
|
144
|
+
const handleFlowModelSave = async (instance: any, { transaction }: any = {}) => {
|
|
145
|
+
const instanceUid = instance?.get?.('uid') || instance?.uid;
|
|
146
|
+
if (!instanceUid) return;
|
|
147
|
+
|
|
148
|
+
const options = parseOptions(instance?.get?.('options') || instance?.options);
|
|
149
|
+
await syncUsages(instanceUid, options, { transaction });
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
this.db.on('flowModels.afterSaveWithAssociations', handleFlowModelSave);
|
|
153
|
+
|
|
154
|
+
// flowModels:save/destroy 等自定义 action 不会触发 afterSaveWithAssociations/afterDestroy(FlowModelRepository 使用 hooks:false + raw SQL)
|
|
155
|
+
// 这里通过 resourcer middleware 补齐 usages 维护,保持“后端自动维护”为主的设计。
|
|
156
|
+
this.app.resourceManager.use(async (ctx: any, next) => {
|
|
157
|
+
const isFlowModels = ctx?.action?.resourceName === 'flowModels';
|
|
158
|
+
if (!isFlowModels) {
|
|
159
|
+
return next();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const actionName = ctx?.action?.actionName;
|
|
163
|
+
|
|
164
|
+
if (actionName === 'save') {
|
|
165
|
+
const values = ctx?.action?.params?.values;
|
|
166
|
+
if (!values || typeof values !== 'object') {
|
|
167
|
+
ctx.throw(400, {
|
|
168
|
+
code: 'INVALID_PAYLOAD',
|
|
169
|
+
message: 'values is required',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const nodes = collectFlowModelsFromTree(values);
|
|
173
|
+
|
|
174
|
+
await next();
|
|
175
|
+
|
|
176
|
+
for (const node of nodes) {
|
|
177
|
+
const instanceUid = node?.uid;
|
|
178
|
+
if (!instanceUid) continue;
|
|
179
|
+
const currentOptions = parseOptions(node);
|
|
180
|
+
await syncUsages(instanceUid, currentOptions, { transaction: ctx.transaction });
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (actionName === 'destroy') {
|
|
186
|
+
const rootUid = ctx?.action?.params?.filterByTk;
|
|
187
|
+
if (!rootUid || typeof rootUid !== 'string') {
|
|
188
|
+
ctx.throw(400, {
|
|
189
|
+
code: 'INVALID_PAYLOAD',
|
|
190
|
+
message: 'filterByTk is required',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const flowRepo = ctx.db.getRepository('flowModels') as FlowModelRepository;
|
|
194
|
+
const nodes = (await flowRepo.findNodesById(rootUid, {
|
|
195
|
+
includeAsyncNode: true,
|
|
196
|
+
transaction: ctx.transaction,
|
|
197
|
+
})) as Array<{ uid?: string; options?: unknown; parent?: string | null }>;
|
|
198
|
+
|
|
199
|
+
await next();
|
|
200
|
+
|
|
201
|
+
for (const node of nodes) {
|
|
202
|
+
const instanceUid = node?.uid;
|
|
203
|
+
if (!instanceUid) continue;
|
|
204
|
+
const options = parseOptions(node?.options || {});
|
|
205
|
+
if (node?.parent && !options?.parentId) {
|
|
206
|
+
options.parentId = node.parent;
|
|
207
|
+
}
|
|
208
|
+
await removeUsagesByInstance(instanceUid, options, { transaction: ctx.transaction });
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (actionName === 'duplicate') {
|
|
214
|
+
await next();
|
|
215
|
+
const raw = ctx.body?.data ?? ctx.body;
|
|
216
|
+
const nodes = collectFlowModelsFromTree(raw);
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
const instanceUid = node?.uid;
|
|
219
|
+
if (!instanceUid) continue;
|
|
220
|
+
const currentOptions = parseOptions(node);
|
|
221
|
+
await syncUsages(instanceUid, currentOptions, { transaction: ctx.transaction });
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return next();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async install(options?: InstallOptions) {}
|
|
231
|
+
async afterEnable() {}
|
|
232
|
+
async afterDisable() {}
|
|
233
|
+
async remove() {}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default PluginBlockReferenceServer;
|
|
@@ -0,0 +1,61 @@
|
|
|
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 actions, { Context } from '@nocobase/actions';
|
|
11
|
+
import { uid } from '@nocobase/utils';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
name: 'flowModelTemplateUsages',
|
|
15
|
+
actions: {
|
|
16
|
+
async list(ctx: Context, next) {
|
|
17
|
+
await actions.list(ctx, next);
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async get(ctx: Context, next) {
|
|
21
|
+
await actions.get(ctx, next);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async create(ctx: Context, next) {
|
|
25
|
+
const values = ctx.action.params?.values || {};
|
|
26
|
+
const templateUid = values.templateUid;
|
|
27
|
+
const modelUid = values.modelUid;
|
|
28
|
+
if (!templateUid || !modelUid) {
|
|
29
|
+
return ctx.throw(400, 'templateUid and modelUid are required');
|
|
30
|
+
}
|
|
31
|
+
const usageRepo = ctx.db.getRepository('flowModelTemplateUsages');
|
|
32
|
+
const record = await usageRepo.updateOrCreate({
|
|
33
|
+
filterKeys: ['templateUid', 'modelUid'],
|
|
34
|
+
values: {
|
|
35
|
+
uid: values.uid || uid(),
|
|
36
|
+
templateUid,
|
|
37
|
+
modelUid,
|
|
38
|
+
},
|
|
39
|
+
context: ctx,
|
|
40
|
+
});
|
|
41
|
+
ctx.body = record;
|
|
42
|
+
await next();
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async destroy(ctx: Context, next) {
|
|
46
|
+
const params = ctx.action?.params || {};
|
|
47
|
+
const { filterByTk, values = {}, filter } = params;
|
|
48
|
+
const templateUid = values.templateUid;
|
|
49
|
+
const modelUid = values.modelUid;
|
|
50
|
+
if (!filterByTk && templateUid && modelUid && !filter) {
|
|
51
|
+
ctx.action.mergeParams({
|
|
52
|
+
filter: {
|
|
53
|
+
templateUid,
|
|
54
|
+
modelUid,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
await actions.destroy(ctx, next);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,251 @@
|
|
|
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 actions, { Context } from '@nocobase/actions';
|
|
11
|
+
import _ from 'lodash';
|
|
12
|
+
import { uid } from '@nocobase/utils';
|
|
13
|
+
import FlowModelRepository from '@nocobase/plugin-flow-engine/src/server/repository';
|
|
14
|
+
|
|
15
|
+
type PlainRow = Record<string, any>;
|
|
16
|
+
|
|
17
|
+
const toPlain = (row: any): PlainRow => {
|
|
18
|
+
if (!row) return row;
|
|
19
|
+
if (typeof row.toJSON === 'function') return row.toJSON();
|
|
20
|
+
return row;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const buildSearchFilter = (existing: any, search?: string) => {
|
|
24
|
+
const filters = [];
|
|
25
|
+
if (existing) {
|
|
26
|
+
filters.push(existing);
|
|
27
|
+
}
|
|
28
|
+
if (search) {
|
|
29
|
+
filters.push({
|
|
30
|
+
$or: [{ name: { $includes: search } }, { description: { $includes: search } }],
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (!filters.length) return undefined;
|
|
34
|
+
if (filters.length === 1) return filters[0];
|
|
35
|
+
return { $and: filters };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const withUsageCounts = async (ctx: Context, rows: PlainRow[]) => {
|
|
39
|
+
const data = rows.map(toPlain).filter(Boolean);
|
|
40
|
+
const uids = data.map((item) => item?.uid).filter(Boolean);
|
|
41
|
+
if (!uids.length) return data;
|
|
42
|
+
const UsageModel = ctx.db.getModel('flowModelTemplateUsages');
|
|
43
|
+
const usageRows = await UsageModel.findAll({
|
|
44
|
+
attributes: ['templateUid', [UsageModel.sequelize.fn('COUNT', '*'), 'count']],
|
|
45
|
+
where: {
|
|
46
|
+
templateUid: uids,
|
|
47
|
+
},
|
|
48
|
+
group: ['templateUid'],
|
|
49
|
+
raw: true,
|
|
50
|
+
transaction: ctx.transaction,
|
|
51
|
+
});
|
|
52
|
+
const usageMap = new Map<string, number>();
|
|
53
|
+
for (const row of usageRows as any[]) {
|
|
54
|
+
usageMap.set(row.templateUid, Number(row.count) || 0);
|
|
55
|
+
}
|
|
56
|
+
return data.map((item) => ({
|
|
57
|
+
...item,
|
|
58
|
+
usageCount: usageMap.get(item.uid) || 0,
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resolveTemplateUid = (ctx: Context): string | undefined => {
|
|
63
|
+
const params = ctx.action?.params || {};
|
|
64
|
+
if (params.filterByTk) return params.filterByTk;
|
|
65
|
+
if (typeof params?.filter?.uid === 'string') return params.filter.uid;
|
|
66
|
+
if (params.values?.uid) return params.values.uid;
|
|
67
|
+
return undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const normalizeOptions = (options: any) => {
|
|
71
|
+
if (!options) return {};
|
|
72
|
+
if (typeof options === 'string') {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(options);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return options;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const syncTemplateMetaToReferenceBlocks = async (
|
|
83
|
+
ctx: Context,
|
|
84
|
+
template: { uid: string; name: string; description: any; targetUid?: string },
|
|
85
|
+
) => {
|
|
86
|
+
const usageRepo = ctx.db.getRepository('flowModelTemplateUsages');
|
|
87
|
+
const usages = await usageRepo.find({
|
|
88
|
+
filter: { templateUid: template.uid },
|
|
89
|
+
fields: ['modelUid'],
|
|
90
|
+
transaction: ctx.transaction,
|
|
91
|
+
context: ctx,
|
|
92
|
+
});
|
|
93
|
+
if (!Array.isArray(usages) || usages.length === 0) return;
|
|
94
|
+
const modelUids = _.uniq(usages.map((u: any) => u?.modelUid).filter(Boolean));
|
|
95
|
+
if (!modelUids.length) return;
|
|
96
|
+
|
|
97
|
+
const flowRepo = ctx.db.getRepository('flowModels') as FlowModelRepository;
|
|
98
|
+
|
|
99
|
+
for (const modelUid of modelUids) {
|
|
100
|
+
const record = await flowRepo.findOne({
|
|
101
|
+
filter: { uid: modelUid },
|
|
102
|
+
transaction: ctx.transaction,
|
|
103
|
+
});
|
|
104
|
+
if (!record) continue;
|
|
105
|
+
const options = normalizeOptions(record.get('options'));
|
|
106
|
+
if (options?.use !== 'ReferenceBlockModel') continue;
|
|
107
|
+
const stepParams = options?.stepParams || {};
|
|
108
|
+
const useTemplate = _.get(stepParams, ['referenceSettings', 'useTemplate']);
|
|
109
|
+
if (!useTemplate || useTemplate.templateUid !== template.uid) continue;
|
|
110
|
+
const nextOptions = _.cloneDeep(options);
|
|
111
|
+
_.set(nextOptions, ['stepParams', 'referenceSettings', 'useTemplate'], {
|
|
112
|
+
...useTemplate,
|
|
113
|
+
templateName: template.name,
|
|
114
|
+
templateDescription: template.description,
|
|
115
|
+
});
|
|
116
|
+
if (template.targetUid) {
|
|
117
|
+
const currentTarget = _.get(nextOptions, ['stepParams', 'referenceSettings', 'target', 'targetUid']);
|
|
118
|
+
if (!currentTarget) {
|
|
119
|
+
_.set(nextOptions, ['stepParams', 'referenceSettings', 'target', 'targetUid'], template.targetUid);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
await flowRepo.update({
|
|
123
|
+
filter: { uid: modelUid },
|
|
124
|
+
values: {
|
|
125
|
+
options: nextOptions,
|
|
126
|
+
},
|
|
127
|
+
transaction: ctx.transaction,
|
|
128
|
+
context: ctx,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default {
|
|
134
|
+
name: 'flowModelTemplates',
|
|
135
|
+
actions: {
|
|
136
|
+
async list(ctx: Context, next) {
|
|
137
|
+
const search = ctx.action?.params?.search as string | undefined;
|
|
138
|
+
const mergedFilter = buildSearchFilter(ctx.action.params?.filter, search);
|
|
139
|
+
if (mergedFilter) {
|
|
140
|
+
ctx.action.mergeParams({ filter: mergedFilter });
|
|
141
|
+
}
|
|
142
|
+
await actions.list(ctx, next);
|
|
143
|
+
if (Array.isArray(ctx.body)) {
|
|
144
|
+
ctx.body = await withUsageCounts(ctx, ctx.body);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const rows = Array.isArray(ctx.body?.rows) ? ctx.body.rows : [];
|
|
148
|
+
const rowsWithCount = await withUsageCounts(ctx, rows);
|
|
149
|
+
ctx.body = {
|
|
150
|
+
...ctx.body,
|
|
151
|
+
rows: rowsWithCount,
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
async get(ctx: Context, next) {
|
|
156
|
+
await actions.get(ctx, next);
|
|
157
|
+
if (!ctx.body) return;
|
|
158
|
+
const [rowWithCount] = await withUsageCounts(ctx, [ctx.body]);
|
|
159
|
+
ctx.body = rowWithCount;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async create(ctx: Context, next) {
|
|
163
|
+
const values = ctx.action.params?.values || {};
|
|
164
|
+
const detachParent = !!values.detachParent || !!values.detachFromParent;
|
|
165
|
+
if (!values.uid) {
|
|
166
|
+
values.uid = uid();
|
|
167
|
+
}
|
|
168
|
+
if (values.targetUid && typeof values.targetUid === 'object') {
|
|
169
|
+
values.targetUid = values.targetUid.uid || values.targetUid.data?.uid || String(values.targetUid);
|
|
170
|
+
}
|
|
171
|
+
ctx.action.mergeParams({
|
|
172
|
+
values: _.pick(values, [
|
|
173
|
+
'uid',
|
|
174
|
+
'name',
|
|
175
|
+
'description',
|
|
176
|
+
'targetUid',
|
|
177
|
+
'useModel',
|
|
178
|
+
'type',
|
|
179
|
+
'dataSourceKey',
|
|
180
|
+
'collectionName',
|
|
181
|
+
'associationName',
|
|
182
|
+
'filterByTk',
|
|
183
|
+
'sourceId',
|
|
184
|
+
]),
|
|
185
|
+
});
|
|
186
|
+
await actions.create(ctx, next);
|
|
187
|
+
if (ctx.body) {
|
|
188
|
+
ctx.body = {
|
|
189
|
+
...toPlain(ctx.body),
|
|
190
|
+
usageCount: 0,
|
|
191
|
+
};
|
|
192
|
+
if (detachParent && ctx.body.targetUid) {
|
|
193
|
+
const flowRepo = ctx.db.getRepository('flowModels') as FlowModelRepository;
|
|
194
|
+
await flowRepo.clearAncestor(ctx.body.targetUid, { transaction: ctx.transaction });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async update(ctx: Context, next) {
|
|
200
|
+
const values = ctx.action.params?.values || {};
|
|
201
|
+
ctx.action.mergeParams({
|
|
202
|
+
values: _.pick(values, ['name', 'description']),
|
|
203
|
+
});
|
|
204
|
+
await actions.update(ctx, next);
|
|
205
|
+
if (ctx.body) {
|
|
206
|
+
const rawData = ctx.body?.data ?? ctx.body;
|
|
207
|
+
const rows = Array.isArray(rawData) ? rawData : [rawData];
|
|
208
|
+
const rowsWithCount = await withUsageCounts(ctx, rows);
|
|
209
|
+
const normalizedRow = Array.isArray(rawData) ? rowsWithCount : rowsWithCount?.[0];
|
|
210
|
+
ctx.body = ctx.body?.data ? { ...ctx.body, data: normalizedRow } : normalizedRow;
|
|
211
|
+
const templateRow = (Array.isArray(rowsWithCount) ? rowsWithCount[0] : rowsWithCount) as PlainRow;
|
|
212
|
+
const templateUid = resolveTemplateUid(ctx) || templateRow?.uid;
|
|
213
|
+
await syncTemplateMetaToReferenceBlocks(ctx, {
|
|
214
|
+
uid: templateUid,
|
|
215
|
+
name: templateRow?.name,
|
|
216
|
+
description: templateRow?.description,
|
|
217
|
+
targetUid: templateRow?.targetUid,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async destroy(ctx: Context, next) {
|
|
223
|
+
const templateUid = resolveTemplateUid(ctx);
|
|
224
|
+
if (!templateUid) {
|
|
225
|
+
return ctx.throw(400, 'template uid is required');
|
|
226
|
+
}
|
|
227
|
+
const usageRepo = ctx.db.getRepository('flowModelTemplateUsages');
|
|
228
|
+
const usageCount = await usageRepo.count({
|
|
229
|
+
filter: {
|
|
230
|
+
templateUid,
|
|
231
|
+
},
|
|
232
|
+
context: ctx,
|
|
233
|
+
});
|
|
234
|
+
if (usageCount > 0) {
|
|
235
|
+
return ctx.throw(400, {
|
|
236
|
+
code: 'TEMPLATE_IN_USE',
|
|
237
|
+
message: 'Template is in use and cannot be deleted',
|
|
238
|
+
data: { usageCount },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
await actions.destroy(ctx, next);
|
|
242
|
+
// 兜底清理孤立 usage(正常情况下 usageCount 已为 0)
|
|
243
|
+
await usageRepo.destroy({
|
|
244
|
+
filter: {
|
|
245
|
+
templateUid,
|
|
246
|
+
},
|
|
247
|
+
context: ctx,
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|