feat: support workflow load dump (#1199)
Reviewed-on: daoyoucloud/tachybase#1199
This commit is contained in:
parent
12dd3b8ad5
commit
3c23d87c95
@ -20,10 +20,12 @@
|
||||
"@tachybase/components": "workspace:*",
|
||||
"@tachybase/plugin-workflow-test": "workspace:*",
|
||||
"@types/ejs": "^3.1.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"antd": "5.18.1",
|
||||
"axios": "^1.6.2",
|
||||
"cron-parser": "4.4.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "8.0.5",
|
||||
"react": "~18.3.1",
|
||||
|
@ -1,8 +1,15 @@
|
||||
import React from 'react';
|
||||
import { useActionContext, useRecord, useResourceActionContext, useResourceContext } from '@tachybase/client';
|
||||
import {
|
||||
useActionContext,
|
||||
useAPIClient,
|
||||
useRecord,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
} from '@tachybase/client';
|
||||
import { ISchema, useForm } from '@tachybase/schema';
|
||||
|
||||
import { message } from 'antd';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { NAMESPACE } from '../locale';
|
||||
@ -198,6 +205,85 @@ export const workflowSchema: ISchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
load: {
|
||||
type: 'void',
|
||||
title: `{{t("Load", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
icon: 'UploadOutlined',
|
||||
openSize: 'small',
|
||||
},
|
||||
properties: {
|
||||
modal: {
|
||||
type: 'void',
|
||||
title: `{{t("Load a workflow", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormV2',
|
||||
'x-component': 'Action.Modal',
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: '{{t("Title")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
title: '{{ t("File") }}',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Upload.Attachment',
|
||||
'x-component-props': {
|
||||
action: 'attachments:create',
|
||||
multiple: false,
|
||||
},
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Modal.Footer',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction() {
|
||||
const { t } = useTranslation();
|
||||
const api = useAPIClient();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
const { setVisible } = useActionContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
const { values } = useForm();
|
||||
return {
|
||||
async run() {
|
||||
const { data } = await api.request({
|
||||
url: values.file.url,
|
||||
baseURL: '/',
|
||||
});
|
||||
await resource.load({ filterByTk, values: { ...values, workflow: data } });
|
||||
message.success(t('Operation succeeded'));
|
||||
refresh();
|
||||
setVisible(false);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
type: 'void',
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{t("Add new")}}',
|
||||
@ -460,6 +546,29 @@ export const workflowSchema: ISchema = {
|
||||
useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
|
||||
},
|
||||
},
|
||||
dump: {
|
||||
type: 'void',
|
||||
title: '{{ t("Dump") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
useAction() {
|
||||
const { t } = useTranslation();
|
||||
const { refresh } = useResourceActionContext();
|
||||
const { resource, targetKey } = useResourceContext();
|
||||
const { [targetKey]: filterByTk } = useRecord();
|
||||
const { values } = useForm();
|
||||
return {
|
||||
async run() {
|
||||
const { data } = await resource.dump({ filterByTk, values });
|
||||
const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' });
|
||||
saveAs(blob, data.data.title + '-' + data.data.key + '.json');
|
||||
message.success(t('Operation succeeded'));
|
||||
refresh();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -54,7 +54,114 @@ export async function destroy(context: Context, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
export async function revision(context: Context, next) {
|
||||
export async function dump(context: Context, next: Next) {
|
||||
const repository = utils.getRepositoryFromParams(context);
|
||||
const { filterByTk, filter = {}, values = {} } = context.action.params;
|
||||
|
||||
context.body = await context.db.sequelize.transaction(async (transaction) => {
|
||||
const origin = await repository.findOne({
|
||||
filterByTk,
|
||||
filter,
|
||||
appends: ['nodes'],
|
||||
context,
|
||||
transaction,
|
||||
});
|
||||
|
||||
const revisionData = filter.key
|
||||
? {
|
||||
key: filter.key,
|
||||
title: origin.title,
|
||||
triggerTitle: origin.triggerTitle,
|
||||
allExecuted: origin.allExecuted,
|
||||
sync: origin.sync,
|
||||
}
|
||||
: values;
|
||||
|
||||
const dumpOne = {
|
||||
...origin.toJSON(),
|
||||
...revisionData,
|
||||
};
|
||||
|
||||
return dumpOne;
|
||||
});
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function load(context: Context, next: Next) {
|
||||
const plugin = context.app.getPlugin(Plugin);
|
||||
const repository = utils.getRepositoryFromParams(context);
|
||||
const { values = {} } = context.action.params;
|
||||
|
||||
context.body = await context.db.sequelize.transaction(async (transaction) => {
|
||||
const origin = values.workflow;
|
||||
|
||||
const trigger = plugin.triggers.get(origin.type);
|
||||
|
||||
const instance = await repository.create({
|
||||
values: {
|
||||
title: values.title,
|
||||
description: origin.description,
|
||||
type: origin.type,
|
||||
triggerTitle: origin.triggerTitle,
|
||||
allExecuted: origin.allExecuted,
|
||||
sync: origin.sync,
|
||||
config:
|
||||
typeof trigger.duplicateConfig === 'function'
|
||||
? await trigger.duplicateConfig(origin, { transaction })
|
||||
: origin.config,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
const originalNodesMap = new Map();
|
||||
origin.nodes.forEach((node) => {
|
||||
originalNodesMap.set(node.id, node);
|
||||
});
|
||||
|
||||
const oldToNew = new Map();
|
||||
const newToOld = new Map();
|
||||
for await (const node of origin.nodes) {
|
||||
const instruction = plugin.instructions.get(node.type);
|
||||
const newNode = await instance.createNode(
|
||||
{
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
config:
|
||||
typeof instruction.duplicateConfig === 'function'
|
||||
? await instruction.duplicateConfig(node, { transaction })
|
||||
: node.config,
|
||||
title: node.title,
|
||||
branchIndex: node.branchIndex,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
// NOTE: keep original node references for later replacement
|
||||
oldToNew.set(node.id, newNode);
|
||||
newToOld.set(newNode.id, node);
|
||||
}
|
||||
|
||||
for await (const [oldId, newNode] of oldToNew.entries()) {
|
||||
const oldNode = originalNodesMap.get(oldId);
|
||||
const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : null;
|
||||
const newDownstream = oldNode.downstreamId ? oldToNew.get(oldNode.downstreamId) : null;
|
||||
|
||||
await newNode.update(
|
||||
{
|
||||
upstreamId: newUpstream?.id ?? null,
|
||||
downstreamId: newDownstream?.id ?? null,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
}
|
||||
|
||||
return instance;
|
||||
});
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
export async function revision(context: Context, next: Next) {
|
||||
const plugin = context.app.getPlugin(Plugin);
|
||||
const repository = utils.getRepositoryFromParams(context);
|
||||
const { filterByTk, filter = {}, values = {} } = context.action.params;
|
||||
|
@ -4019,6 +4019,9 @@ importers:
|
||||
'@types/ejs':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.5
|
||||
'@types/file-saver':
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.5
|
||||
version: 4.17.5
|
||||
@ -4031,6 +4034,9 @@ importers:
|
||||
cron-parser:
|
||||
specifier: 4.4.0
|
||||
version: 4.4.0
|
||||
file-saver:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
lodash:
|
||||
specifier: 4.17.21
|
||||
version: 4.17.21
|
||||
@ -6360,7 +6366,7 @@ packages:
|
||||
'@babel/traverse': 7.24.5(supports-color@5.5.0)
|
||||
'@babel/types': 7.24.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.5(supports-color@5.5.0)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
@ -10276,8 +10282,8 @@ packages:
|
||||
engines: {node: '>=4.0'}
|
||||
dependencies:
|
||||
async: 2.6.4
|
||||
axios: 0.21.4(debug@4.3.4)
|
||||
debug: 4.3.4
|
||||
axios: 0.21.4(debug@4.3.5)
|
||||
debug: 4.3.5(supports-color@5.5.0)
|
||||
eventemitter2: 6.4.9
|
||||
ws: 7.5.9
|
||||
transitivePeerDependencies:
|
||||
@ -14962,10 +14968,10 @@ packages:
|
||||
is-buffer: 2.0.5
|
||||
dev: true
|
||||
|
||||
/axios@0.21.4(debug@4.3.4):
|
||||
/axios@0.21.4(debug@4.3.5):
|
||||
resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6(debug@4.3.4)
|
||||
follow-redirects: 1.15.6(debug@4.3.5)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
@ -14982,7 +14988,7 @@ packages:
|
||||
/axios@1.6.8:
|
||||
resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6(debug@4.3.4)
|
||||
follow-redirects: 1.15.6(debug@4.3.5)
|
||||
form-data: 4.0.0
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
@ -19135,17 +19141,6 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
/follow-redirects@1.15.6(debug@4.3.4):
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
|
||||
/follow-redirects@1.15.6(debug@4.3.5):
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
@ -19156,7 +19151,6 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
debug: 4.3.5(supports-color@5.5.0)
|
||||
dev: false
|
||||
|
||||
/fontkit@2.0.2:
|
||||
resolution: {integrity: sha512-jc4k5Yr8iov8QfS6u8w2CnHWVmbOGtdBtOXMze5Y+QD966Rx6PEVWXSEGwXlsDlKtu1G12cJjcsybnqhSk/+LA==}
|
||||
|
Loading…
Reference in New Issue
Block a user