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