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 { 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}`);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
'Workflow': '工作流',
|
||||
'Execution history': '执行历史',
|
||||
'Executed': '已执行',
|
||||
'Trigger type': '触发方式',
|
||||
'Status': '状态',
|
||||
'On': '启用',
|
||||
'Off': '停用',
|
||||
'Version': '版本',
|
||||
'Copy to new version': '复制到新版本',
|
||||
'Duplicate': '复制',
|
||||
|
||||
'Loading': '加载中',
|
||||
'Load failed': '加载失败',
|
||||
|
@ -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") }}',
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user