feat(plugin-workflow): add duplicate action (#1171)

* feat(plugin-workflow): add duplicate action

* fix(plugin-workflow): fix test case

* feat(plugin-workflow): move execution link to column
This commit is contained in:
Junyi 2022-12-01 06:52:46 -08:00 committed by GitHub
parent 86a23c0d9f
commit 400320d175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 181 additions and 30 deletions

View File

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

View File

@ -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
}}
/>
</Card>

View File

@ -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 (
<ActionContext.Provider value={{ visible, setVisible, fieldSchema }}>
{React.createElement(component, {
...props,
onClick() {
setVisible(true);
}
}, children)}
<SchemaComponent schema={fieldSchema} onlyRenderProperties />
</ActionContext.Provider>
);
}

View File

@ -1,12 +1,14 @@
export default {
'Workflow': '工作流',
'Execution history': '执行历史',
'Executed': '已执行',
'Trigger type': '触发方式',
'Status': '状态',
'On': '启用',
'Off': '停用',
'Version': '版本',
'Copy to new version': '复制到新版本',
'Duplicate': '复制',
'Loading': '加载中',
'Load failed': '加载失败',

View File

@ -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") }}',

View File

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

View File

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