diff --git a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx index c97ca56ff..7d9b31b6f 100644 --- a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx +++ b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx @@ -4,6 +4,7 @@ import { Dropdown, Menu, Button, Tag, Switch, message, Breadcrumb } from 'antd'; import { DownOutlined, RightOutlined, EllipsisOutlined } from '@ant-design/icons'; import { cx } from '@emotion/css'; import classnames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { ActionContext, @@ -40,6 +41,7 @@ function makeNodes(nodes): void { export function WorkflowCanvas() { const history = useHistory(); + const { t } = useTranslation(); const { data, refresh, loading } = useResourceActionContext(); const { resource, targetKey } = useResourceContext(); const { setTitle } = useDocumentTitle(); @@ -79,9 +81,12 @@ export function WorkflowCanvas() { async function onRevision() { const { data: { data: revision } } = await resource.revision({ - filterByTk: workflow[targetKey] + filterByTk: workflow[targetKey], + filter: { + key: workflow.key + } }); - message.success(lang('Operation succeeded')); + message.success(t('Operation succeeded')); history.push(`${revision.id}`); } diff --git a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx index ae070ba61..9b4e0346a 100644 --- a/packages/plugins/workflow/src/client/WorkflowShortcut.tsx +++ b/packages/plugins/workflow/src/client/WorkflowShortcut.tsx @@ -11,6 +11,7 @@ import { WorkflowLink } from './WorkflowLink'; import { ExecutionResourceProvider } from './ExecutionResourceProvider'; import { ExecutionLink } from './ExecutionLink'; import { lang } from './locale'; +import OpenDrawer from './components/OpenDrawer'; export const WorkflowPane = () => { @@ -21,7 +22,8 @@ export const WorkflowPane = () => { components={{ WorkflowLink, ExecutionResourceProvider, - ExecutionLink + ExecutionLink, + OpenDrawer }} /> diff --git a/packages/plugins/workflow/src/client/components/OpenDrawer.tsx b/packages/plugins/workflow/src/client/components/OpenDrawer.tsx new file mode 100644 index 000000000..263c01853 --- /dev/null +++ b/packages/plugins/workflow/src/client/components/OpenDrawer.tsx @@ -0,0 +1,21 @@ +import React, { useState } from 'react'; +import { useFieldSchema } from '@formily/react'; + +import { ActionContext, SchemaComponent } from '@nocobase/client'; + + +export default function ({ component = 'div', children, ...props }) { + const [visible, setVisible] = useState(false); + const fieldSchema = useFieldSchema(); + return ( + + {React.createElement(component, { + ...props, + onClick() { + setVisible(true); + } + }, children)} + + + ); +} diff --git a/packages/plugins/workflow/src/client/locale/zh-CN.ts b/packages/plugins/workflow/src/client/locale/zh-CN.ts index adcf7737c..7d92d209f 100644 --- a/packages/plugins/workflow/src/client/locale/zh-CN.ts +++ b/packages/plugins/workflow/src/client/locale/zh-CN.ts @@ -1,12 +1,14 @@ export default { 'Workflow': '工作流', 'Execution history': '执行历史', + 'Executed': '已执行', 'Trigger type': '触发方式', 'Status': '状态', 'On': '启用', 'Off': '停用', 'Version': '版本', 'Copy to new version': '复制到新版本', + 'Duplicate': '复制', 'Loading': '加载中', 'Load failed': '加载失败', diff --git a/packages/plugins/workflow/src/client/schemas/workflows.ts b/packages/plugins/workflow/src/client/schemas/workflows.ts index 67a14b29f..c4c46e944 100644 --- a/packages/plugins/workflow/src/client/schemas/workflows.ts +++ b/packages/plugins/workflow/src/client/schemas/workflows.ts @@ -1,4 +1,7 @@ import { ISchema } from '@formily/react'; +import { message } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { useRecord, useResourceActionContext, useResourceContext } from '@nocobase/client'; import { NAMESPACE } from '../locale'; import { triggers } from '../triggers'; import { executionSchema } from './executions'; @@ -61,7 +64,18 @@ const collection = { 'x-decorator': 'FormItem', default: false } as ISchema - } + }, + { + type: 'number', + name: 'allExecuted', + interface: 'integer', + uiSchema: { + title: `{{t("Executed", { ns: "${NAMESPACE}" })}}`, + type: 'number', + 'x-component': 'InputNumber', + 'x-decorator': 'FormItem', + } as ISchema + }, ], }; @@ -219,6 +233,25 @@ export const workflowSchema: ISchema = { }, } }, + allExecuted: { + type: 'void', + 'x-decorator': 'Table.Column.Decorator', + 'x-component': 'Table.Column', + properties: { + allExecuted: { + type: 'number', + 'x-decorator': 'OpenDrawer', + 'x-decorator-props': { + component: 'a', + }, + 'x-component': 'CollectionField', + 'x-read-pretty': true, + properties: { + drawer: executionSchema + } + }, + } + }, actions: { type: 'void', title: '{{ t("Actions") }}', @@ -235,17 +268,6 @@ export const workflowSchema: ISchema = { type: 'void', 'x-component': 'WorkflowLink' }, - executions: { - type: 'void', - title: `{{t("Execution history", { ns: "${NAMESPACE}" })}}`, - 'x-component': 'Action.Link', - 'x-component-props': { - type: 'primary', - }, - properties: { - drawer: executionSchema - } - }, update: { type: 'void', title: '{{ t("Edit") }}', @@ -300,6 +322,26 @@ export const workflowSchema: ISchema = { }, }, }, + revision: { + type: 'void', + title: `{{t("Duplicate", { ns: "${NAMESPACE}" })}}`, + 'x-component': 'Action.Link', + 'x-component-props': { + useAction() { + const { t } = useTranslation(); + const { refresh } = useResourceActionContext(); + const { resource, targetKey } = useResourceContext(); + const { [targetKey]: filterByTk } = useRecord(); + return { + async run() { + await resource.revision({ filterByTk }); + message.success(t('Operation succeeded')); + refresh(); + }, + }; + } + } + } // delete: { // type: 'void', // title: '{{ t("Delete") }}', diff --git a/packages/plugins/workflow/src/server/__tests__/actions/workflows.test.ts b/packages/plugins/workflow/src/server/__tests__/actions/workflows.test.ts index d9967f14a..83e914850 100644 --- a/packages/plugins/workflow/src/server/__tests__/actions/workflows.test.ts +++ b/packages/plugins/workflow/src/server/__tests__/actions/workflows.test.ts @@ -125,8 +125,15 @@ describe('workflow > actions > workflows', () => { } }); + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + const { body, status } = await agent.resource(`workflows`).revision({ - filterByTk: w1.id + filterByTk: w1.id, + filter: { + key: w1.key + } }); expect(status).toBe(200); @@ -135,10 +142,8 @@ describe('workflow > actions > workflows', () => { expect(w2.key).toBe(w1.key); expect(w2.current).toBeFalsy(); expect(w2.enabled).toBe(false); - - const p1 = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); + expect(w2.executed).toBe(0); + expect(w2.allExecuted).toBe(1); await WorkflowModel.update({ enabled: true @@ -149,18 +154,21 @@ describe('workflow > actions > workflows', () => { individualHooks: true }); + const p2 = await PostRepo.create({ values: { title: 't2' } }); + + await sleep(500); + const [w1next, w2next] = await WorkflowModel.findAll({ order: [['id', 'ASC']] }); expect(w1next.enabled).toBe(false); expect(w1next.current).toBe(null); + expect(w1next.allExecuted).toBe(2); expect(w2next.enabled).toBe(true); expect(w2next.current).toBe(true); - - const p2 = await PostRepo.create({ values: { title: 't2' } }); - - await sleep(500); + expect(w2next.executed).toBe(1); + expect(w2next.allExecuted).toBe(2); const [e1] = await w1next.getExecutions(); const [e2] = await w2next.getExecutions(); @@ -206,7 +214,10 @@ describe('workflow > actions > workflows', () => { await n1.setDownstream(n2); const { body } = await agent.resource(`workflows`).revision({ - filterByTk: w1.id + filterByTk: w1.id, + filter: { + key: w1.key + } }); const w2 = await WorkflowModel.findByPk(body.data.id, { include: [ @@ -282,10 +293,71 @@ describe('workflow > actions > workflows', () => { }); const { status } = await agent.resource(`workflows`).revision({ - filterByTk: w1.id + filterByTk: w1.id, + filter: { + key: w1.key + } }); expect(status).toBe(400); }); + + it('duplicate workflow', async () => { + const w1 = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts' + } + }); + + const p1 = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const { body, status } = await agent.resource(`workflows`).revision({ + filterByTk: w1.id, + }); + + expect(status).toBe(200); + const { data: w2 } = body; + expect(w2.config).toMatchObject(w1.config); + expect(w2.key).not.toBe(w1.key); + expect(w2.current).toBeTruthy(); + expect(w2.enabled).toBe(false); + expect(w2.allExecuted).toBe(0); + + await WorkflowModel.update({ + enabled: true + }, { + where: { + id: w2.id + }, + individualHooks: true + }); + + const p2 = await PostRepo.create({ values: { title: 't2' } }); + + await sleep(500); + + const [w1next, w2next] = await WorkflowModel.findAll({ + order: [['id', 'ASC']] + }); + + expect(w1next.enabled).toBe(true); + expect(w1next.current).toBe(true); + expect(w1next.executed).toBe(2); + expect(w1next.allExecuted).toBe(2); + expect(w2next.enabled).toBe(true); + expect(w2next.executed).toBe(1); + expect(w2next.allExecuted).toBe(1); + + const [e1] = await w1next.getExecutions(); + const [e2] = await w2next.getExecutions(); + expect(e1.key).not.toBe(e2.key); + expect(e1.workflowId).toBe(w1.id); + expect(e2.workflowId).toBe(w2.id); + }); }); }); diff --git a/packages/plugins/workflow/src/server/actions/workflows.ts b/packages/plugins/workflow/src/server/actions/workflows.ts index b608afccb..9d783ff24 100644 --- a/packages/plugins/workflow/src/server/actions/workflows.ts +++ b/packages/plugins/workflow/src/server/actions/workflows.ts @@ -64,23 +64,30 @@ function migrateConfig(config, oldToNew) { export async function revision(context: Context, next) { const { db } = context; const repository = utils.getRepositoryFromParams(context); - const { filterByTk } = context.action.params; + const { filterByTk, filter = {} } = context.action.params; context.body = await 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, + allExecuted: origin.allExecuted + } : {}; + const instance = await repository.create({ values: { - key: origin.key, - title: origin.title, + title: `${origin.title} copy`, description: origin.description, type: origin.type, - config: origin.config + config: origin.config, + ...revisionData }, transaction });