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