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:
parent
86a23c0d9f
commit
400320d175
@ -4,6 +4,7 @@ import { Dropdown, Menu, Button, Tag, Switch, message, Breadcrumb } from 'antd';
|
|||||||
import { DownOutlined, RightOutlined, EllipsisOutlined } from '@ant-design/icons';
|
import { DownOutlined, RightOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionContext,
|
ActionContext,
|
||||||
@ -40,6 +41,7 @@ function makeNodes(nodes): void {
|
|||||||
|
|
||||||
export function WorkflowCanvas() {
|
export function WorkflowCanvas() {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { data, refresh, loading } = useResourceActionContext();
|
const { data, refresh, loading } = useResourceActionContext();
|
||||||
const { resource, targetKey } = useResourceContext();
|
const { resource, targetKey } = useResourceContext();
|
||||||
const { setTitle } = useDocumentTitle();
|
const { setTitle } = useDocumentTitle();
|
||||||
@ -79,9 +81,12 @@ export function WorkflowCanvas() {
|
|||||||
|
|
||||||
async function onRevision() {
|
async function onRevision() {
|
||||||
const { data: { data: revision } } = await resource.revision({
|
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}`);
|
history.push(`${revision.id}`);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { WorkflowLink } from './WorkflowLink';
|
|||||||
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
import { ExecutionResourceProvider } from './ExecutionResourceProvider';
|
||||||
import { ExecutionLink } from './ExecutionLink';
|
import { ExecutionLink } from './ExecutionLink';
|
||||||
import { lang } from './locale';
|
import { lang } from './locale';
|
||||||
|
import OpenDrawer from './components/OpenDrawer';
|
||||||
|
|
||||||
|
|
||||||
export const WorkflowPane = () => {
|
export const WorkflowPane = () => {
|
||||||
@ -21,7 +22,8 @@ export const WorkflowPane = () => {
|
|||||||
components={{
|
components={{
|
||||||
WorkflowLink,
|
WorkflowLink,
|
||||||
ExecutionResourceProvider,
|
ExecutionResourceProvider,
|
||||||
ExecutionLink
|
ExecutionLink,
|
||||||
|
OpenDrawer
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,14 @@
|
|||||||
export default {
|
export default {
|
||||||
'Workflow': '工作流',
|
'Workflow': '工作流',
|
||||||
'Execution history': '执行历史',
|
'Execution history': '执行历史',
|
||||||
|
'Executed': '已执行',
|
||||||
'Trigger type': '触发方式',
|
'Trigger type': '触发方式',
|
||||||
'Status': '状态',
|
'Status': '状态',
|
||||||
'On': '启用',
|
'On': '启用',
|
||||||
'Off': '停用',
|
'Off': '停用',
|
||||||
'Version': '版本',
|
'Version': '版本',
|
||||||
'Copy to new version': '复制到新版本',
|
'Copy to new version': '复制到新版本',
|
||||||
|
'Duplicate': '复制',
|
||||||
|
|
||||||
'Loading': '加载中',
|
'Loading': '加载中',
|
||||||
'Load failed': '加载失败',
|
'Load failed': '加载失败',
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { ISchema } from '@formily/react';
|
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 { NAMESPACE } from '../locale';
|
||||||
import { triggers } from '../triggers';
|
import { triggers } from '../triggers';
|
||||||
import { executionSchema } from './executions';
|
import { executionSchema } from './executions';
|
||||||
@ -61,7 +64,18 @@ const collection = {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
default: false
|
default: false
|
||||||
} as ISchema
|
} 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: {
|
actions: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Actions") }}',
|
title: '{{ t("Actions") }}',
|
||||||
@ -235,17 +268,6 @@ export const workflowSchema: ISchema = {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'WorkflowLink'
|
'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: {
|
update: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: '{{ t("Edit") }}',
|
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: {
|
// delete: {
|
||||||
// type: 'void',
|
// type: 'void',
|
||||||
// title: '{{ t("Delete") }}',
|
// title: '{{ t("Delete") }}',
|
||||||
|
@ -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({
|
const { body, status } = await agent.resource(`workflows`).revision({
|
||||||
filterByTk: w1.id
|
filterByTk: w1.id,
|
||||||
|
filter: {
|
||||||
|
key: w1.key
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@ -135,10 +142,8 @@ describe('workflow > actions > workflows', () => {
|
|||||||
expect(w2.key).toBe(w1.key);
|
expect(w2.key).toBe(w1.key);
|
||||||
expect(w2.current).toBeFalsy();
|
expect(w2.current).toBeFalsy();
|
||||||
expect(w2.enabled).toBe(false);
|
expect(w2.enabled).toBe(false);
|
||||||
|
expect(w2.executed).toBe(0);
|
||||||
const p1 = await PostRepo.create({ values: { title: 't1' } });
|
expect(w2.allExecuted).toBe(1);
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
await WorkflowModel.update({
|
await WorkflowModel.update({
|
||||||
enabled: true
|
enabled: true
|
||||||
@ -149,18 +154,21 @@ describe('workflow > actions > workflows', () => {
|
|||||||
individualHooks: true
|
individualHooks: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const p2 = await PostRepo.create({ values: { title: 't2' } });
|
||||||
|
|
||||||
|
await sleep(500);
|
||||||
|
|
||||||
const [w1next, w2next] = await WorkflowModel.findAll({
|
const [w1next, w2next] = await WorkflowModel.findAll({
|
||||||
order: [['id', 'ASC']]
|
order: [['id', 'ASC']]
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(w1next.enabled).toBe(false);
|
expect(w1next.enabled).toBe(false);
|
||||||
expect(w1next.current).toBe(null);
|
expect(w1next.current).toBe(null);
|
||||||
|
expect(w1next.allExecuted).toBe(2);
|
||||||
expect(w2next.enabled).toBe(true);
|
expect(w2next.enabled).toBe(true);
|
||||||
expect(w2next.current).toBe(true);
|
expect(w2next.current).toBe(true);
|
||||||
|
expect(w2next.executed).toBe(1);
|
||||||
const p2 = await PostRepo.create({ values: { title: 't2' } });
|
expect(w2next.allExecuted).toBe(2);
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const [e1] = await w1next.getExecutions();
|
const [e1] = await w1next.getExecutions();
|
||||||
const [e2] = await w2next.getExecutions();
|
const [e2] = await w2next.getExecutions();
|
||||||
@ -206,7 +214,10 @@ describe('workflow > actions > workflows', () => {
|
|||||||
await n1.setDownstream(n2);
|
await n1.setDownstream(n2);
|
||||||
|
|
||||||
const { body } = await agent.resource(`workflows`).revision({
|
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, {
|
const w2 = await WorkflowModel.findByPk(body.data.id, {
|
||||||
include: [
|
include: [
|
||||||
@ -282,10 +293,71 @@ describe('workflow > actions > workflows', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await agent.resource(`workflows`).revision({
|
const { status } = await agent.resource(`workflows`).revision({
|
||||||
filterByTk: w1.id
|
filterByTk: w1.id,
|
||||||
|
filter: {
|
||||||
|
key: w1.key
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -64,23 +64,30 @@ function migrateConfig(config, oldToNew) {
|
|||||||
export async function revision(context: Context, next) {
|
export async function revision(context: Context, next) {
|
||||||
const { db } = context;
|
const { db } = context;
|
||||||
const repository = utils.getRepositoryFromParams(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 => {
|
context.body = await db.sequelize.transaction(async transaction => {
|
||||||
const origin = await repository.findOne({
|
const origin = await repository.findOne({
|
||||||
filterByTk,
|
filterByTk,
|
||||||
|
filter,
|
||||||
appends: ['nodes'],
|
appends: ['nodes'],
|
||||||
context,
|
context,
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const revisionData = filter.key ? {
|
||||||
|
key: filter.key,
|
||||||
|
title: origin.title,
|
||||||
|
allExecuted: origin.allExecuted
|
||||||
|
} : {};
|
||||||
|
|
||||||
const instance = await repository.create({
|
const instance = await repository.create({
|
||||||
values: {
|
values: {
|
||||||
key: origin.key,
|
title: `${origin.title} copy`,
|
||||||
title: origin.title,
|
|
||||||
description: origin.description,
|
description: origin.description,
|
||||||
type: origin.type,
|
type: origin.type,
|
||||||
config: origin.config
|
config: origin.config,
|
||||||
|
...revisionData
|
||||||
},
|
},
|
||||||
transaction
|
transaction
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user