feat: support workflow load dump (#1199)

Reviewed-on: daoyoucloud/tachybase#1199
This commit is contained in:
sealday 2024-06-18 11:44:01 +08:00
parent 12dd3b8ad5
commit 3c23d87c95
4 changed files with 232 additions and 20 deletions

View File

@ -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",

View File

@ -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();
},
};
},
},
},
},
},
},

View File

@ -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;

View File

@ -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==}