From c018e5b913d8005c0b3ffdb623286e738fd00570 Mon Sep 17 00:00:00 2001 From: Junyi Date: Thu, 12 May 2022 12:19:25 +0800 Subject: [PATCH] Feat(plugin workflow): revisions (#379) * feat(plugin-workflow): avoid nodes to be added/removed/modified in executed workflow * feat(plugin-workflow): add current field to workflow stand for current version * feat(plugin-workflow): add duplicate action to workflow for revisions * fix(plugin-workflow): fix relation field of workflow --- packages/core/client/src/locale/zh_CN.ts | 11 +- .../client/src/workflow/WorkflowCanvas.tsx | 152 ++++++++--- .../core/client/src/workflow/WorkflowLink.tsx | 2 +- .../core/client/src/workflow/WorkflowPage.tsx | 110 ++------ .../client/src/workflow/WorkflowShortcut.tsx | 33 +-- .../client/src/workflow/WorkflowTable.tsx | 17 -- .../core/client/src/workflow/calculators.tsx | 3 +- packages/core/client/src/workflow/index.tsx | 2 - .../client/src/workflow/nodes/condition.tsx | 14 +- .../core/client/src/workflow/nodes/create.tsx | 10 +- .../core/client/src/workflow/nodes/index.tsx | 249 ++++++++++-------- .../core/client/src/workflow/nodes/update.tsx | 18 +- .../client/src/workflow/schemas/workflows.ts | 19 +- packages/core/client/src/workflow/style.tsx | 43 +++ .../client/src/workflow/triggers/index.tsx | 29 +- .../workflow/src/__tests__/workflow.test.ts | 1 - .../plugins/workflow/src/actions/index.ts | 12 +- .../src/actions/{flow_nodes.ts => nodes.ts} | 92 +++++-- .../plugins/workflow/src/actions/workflows.ts | 145 ++++++++++ .../workflow/src/collections/workflows.ts | 18 +- .../plugins/workflow/src/models/Workflow.ts | 5 + 21 files changed, 646 insertions(+), 339 deletions(-) delete mode 100644 packages/core/client/src/workflow/WorkflowTable.tsx rename packages/plugins/workflow/src/actions/{flow_nodes.ts => nodes.ts} (56%) create mode 100644 packages/plugins/workflow/src/actions/workflows.ts diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 652ba4728..eb0eeefc5 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -46,6 +46,7 @@ export default { "Sign out": "注销", "Cancel": "取消", "Submit": "提交", + "Close": "关闭", "Set the data scope": "设置数据范围", "Data blocks": "数据区块", "Table": "表格", @@ -412,8 +413,11 @@ export default { 'Trigger type': '触发方式', 'Description': '描述', 'Status': '状态', - 'Enabled': '启用', - 'Disabled': '禁用', + 'Started': '启用', + 'Stopped': '停用', + 'Version': '版本', + 'Copy to new version': '复制到新版本', + 'Load failed': '加载失败', 'Trigger': '触发器', @@ -477,6 +481,9 @@ export default { 'Please select collection first': '请先选择数据表', 'Only update records matching conditions': '只更新满足条件的数据', 'Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。', + 'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改', + 'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改', + 'Unsaved changes': '未保存修改', 'Are you sure you don\'t want to save?': '你确定不保存修改吗?', 'Dragging': '拖拽中', diff --git a/packages/core/client/src/workflow/WorkflowCanvas.tsx b/packages/core/client/src/workflow/WorkflowCanvas.tsx index 087b3a3f5..65bc64a18 100644 --- a/packages/core/client/src/workflow/WorkflowCanvas.tsx +++ b/packages/core/client/src/workflow/WorkflowCanvas.tsx @@ -1,17 +1,21 @@ import React, { useContext, useEffect } from 'react'; -import { Dropdown, Menu, Button } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; +import { Dropdown, Menu, Button, Tag, Switch } from 'antd'; +import { PlusOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'; import { cx } from '@emotion/css'; import { useTranslation } from 'react-i18next'; import { - useCollection, + useAPIClient, useCompile, useDocumentTitle, - useResourceActionContext + useRecord, + useResourceActionContext, + useResourceContext } from '..'; import { Instruction, instructions, Node } from './nodes'; -import { addButtonClass, branchBlockClass, branchClass, nodeBlockClass, nodeCardClass, nodeHeaderClass, nodeTitleClass } from './style'; +import { addButtonClass, branchBlockClass, branchClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style'; +import { TriggerConfig } from './triggers'; +import { useHistory } from 'react-router-dom'; @@ -38,8 +42,9 @@ export function useFlowContext() { export function WorkflowCanvas() { const { t } = useTranslation(); + const history = useHistory(); const { data, refresh, loading } = useResourceActionContext(); - + const { resource, targetKey } = useResourceContext(); const { setTitle } = useDocumentTitle(); useEffect(() => { const { title } = data?.data ?? {}; @@ -50,12 +55,38 @@ export function WorkflowCanvas() { return
{t('Load failed')}
; } - const { nodes = [], ...workflow } = data?.data ?? {}; + const { nodes = [], revisions = [], ...workflow } = data?.data ?? {}; makeNodes(nodes); const entry = nodes.find(item => !item.upstream); + function onSwitchVersion({ key }) { + if (key != workflow.id) { + history.push(key); + } + } + + async function onToggle(value) { + await resource.update({ + filterByTk: workflow[targetKey], + values: { + enabled: value, + // NOTE: keep `key` field to adapter for backend + key: workflow.key + } + }); + refresh(); + } + + async function onDuplicate() { + const { data: { data: duplicated } } = await resource.duplicate({ + filterByTk: workflow[targetKey] + }); + + history.push(duplicated.id); + } + return ( -
- +
+
+ {workflow.title} +
+ +
+
+ +
+ +
+
+
+ {t('End')} +
+
-
{t('End')}
); } @@ -104,9 +187,9 @@ interface AddButtonProps { export function AddButton({ upstream, branchIndex = null }: AddButtonProps) { const compile = useCompile(); - const { resource } = useCollection(); - const { data } = useResourceActionContext(); - const { onNodeAdded } = useFlowContext(); + const api = useAPIClient(); + const { workflow, onNodeAdded } = useFlowContext(); + const resource = api.resource('workflows.nodes', workflow.id); async function onCreate({ keyPath }) { const type = keyPath.pop(); @@ -120,7 +203,6 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) { const { data: { data: node } } = await resource.create({ values: { type, - workflowId: data.data.id, upstreamId: upstream?.id ?? null, branchIndex, config @@ -138,25 +220,29 @@ export function AddButton({ upstream, branchIndex = null }: AddButtonProps) { return (
- onCreate(ev)}> - {groups.map(group => ( - - {instructionList.filter(item => item.group === group.value).map(item => item.options - ? ( - - {item.options.map(option => ( - {compile(option.label)} - ))} - - ) - : ( - {compile(item.title)} - ))} - - ))} - - }> + onCreate(ev)}> + {groups.map(group => ( + + {instructionList.filter(item => item.group === group.value).map(item => item.options + ? ( + + {item.options.map(option => ( + {compile(option.label)} + ))} + + ) + : ( + {compile(item.title)} + ))} + + ))} + + } + disabled={workflow.executed} + >
diff --git a/packages/core/client/src/workflow/WorkflowLink.tsx b/packages/core/client/src/workflow/WorkflowLink.tsx index 499a220a1..1e872b39a 100644 --- a/packages/core/client/src/workflow/WorkflowLink.tsx +++ b/packages/core/client/src/workflow/WorkflowLink.tsx @@ -9,6 +9,6 @@ export const WorkflowLink = () => { const { id } = useRecord(); const { setVisible } = useActionContext(); return ( - setVisible(false)}>{t('Configure')} + setVisible(false)}>{t('View')} ); } diff --git a/packages/core/client/src/workflow/WorkflowPage.tsx b/packages/core/client/src/workflow/WorkflowPage.tsx index aa4bc1268..9f5d3ac46 100644 --- a/packages/core/client/src/workflow/WorkflowPage.tsx +++ b/packages/core/client/src/workflow/WorkflowPage.tsx @@ -1,105 +1,47 @@ import { cx } from '@emotion/css'; -import { ISchema } from '@formily/react'; import React from 'react'; import { useRouteMatch } from 'react-router-dom'; import { SchemaComponent } from '..'; import { workflowPageClass } from './style'; -import { TriggerConfig } from './triggers'; import { WorkflowCanvas } from './WorkflowCanvas'; -const workflowCollection = { - name: 'workflow', - fields: [ - { - type: 'string', - name: 'title', - interface: 'input', - uiSchema: { - title: '{{t("Name")}}', - type: 'string', - 'x-component': 'Input', - required: true, - } as ISchema, - }, - ], -}; + export const WorkflowPage = () => { const { params } = useRouteMatch(); return (
-
- -
+ }, + }} + components={{ + WorkflowCanvas, + }} + />
); }; diff --git a/packages/core/client/src/workflow/WorkflowShortcut.tsx b/packages/core/client/src/workflow/WorkflowShortcut.tsx index 0b87064e7..f4ffe620c 100644 --- a/packages/core/client/src/workflow/WorkflowShortcut.tsx +++ b/packages/core/client/src/workflow/WorkflowShortcut.tsx @@ -1,24 +1,16 @@ import React, { useState } from 'react'; import { PartitionOutlined } from '@ant-design/icons'; -import { ISchema, useForm } from '@formily/react'; +import { ISchema } from '@formily/react'; import { uid } from '@formily/shared'; -import { PluginManager } from '../plugin-manager'; -import { ActionContext, SchemaComponent, useActionContext } from '../schema-component'; -import { WorkflowTable } from './WorkflowTable'; import { useTranslation } from 'react-i18next'; +import { PluginManager } from '../plugin-manager'; + +import { ActionContext, SchemaComponent } from '../schema-component'; +import { workflowSchema } from './schemas/workflows'; +import { WorkflowLink } from './WorkflowLink'; +import { ExecutionResourceProvider } from './ExecutionResourceProvider'; + -const useCloseAction = () => { - const { setVisible } = useActionContext(); - const form = useForm(); - return { - async run() { - setVisible(false); - form.submit((values) => { - console.log(values); - }); - }, - }; -}; const schema: ISchema = { type: 'object', @@ -28,10 +20,7 @@ const schema: ISchema = { type: 'void', title: '{{t("Workflow")}}', properties: { - main: { - type: 'void', - 'x-component': 'WorkflowTable', - }, + table: workflowSchema, }, }, }, @@ -52,9 +41,9 @@ export const WorkflowShortcut = () => { ); diff --git a/packages/core/client/src/workflow/WorkflowTable.tsx b/packages/core/client/src/workflow/WorkflowTable.tsx deleted file mode 100644 index c86661267..000000000 --- a/packages/core/client/src/workflow/WorkflowTable.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { SchemaComponent } from '../schema-component'; -import { WorkflowLink, WorkflowPage, ExecutionResourceProvider } from '.'; -import { workflowSchema } from './schemas/workflows'; - -export const WorkflowTable = () => { - return ( - - ); -}; diff --git a/packages/core/client/src/workflow/calculators.tsx b/packages/core/client/src/workflow/calculators.tsx index dd8ffc3c7..1736e6b75 100644 --- a/packages/core/client/src/workflow/calculators.tsx +++ b/packages/core/client/src/workflow/calculators.tsx @@ -261,7 +261,7 @@ export function Operand({ const { component, appendTypeValue } = Types[type] || {}; const VariableComponent = typeof component === 'function' ? component(operand) : NullRender; - console.log(Types); + return (
{ ? parseStringValue(value[field.name], VTypes) : { type: 'constant', value: value[field.name] }; + // TODO: try to use to replace this map return ( } -
); } @@ -111,12 +111,16 @@ function CalculationGroup({ value, onChange }) { ))}
- {t('Add condition')} - {t('Add condition group')} + +
); diff --git a/packages/core/client/src/workflow/nodes/create.tsx b/packages/core/client/src/workflow/nodes/create.tsx index 9a086caad..5332babe1 100644 --- a/packages/core/client/src/workflow/nodes/create.tsx +++ b/packages/core/client/src/workflow/nodes/create.tsx @@ -26,15 +26,7 @@ export default { // disabled: true // } // }, - 'config.params': { - type: 'object', - name: 'config.params', - title: '', - 'x-decorator': 'FormItem', - properties: { - values - } - } + 'config.params.values': values }, view: { diff --git a/packages/core/client/src/workflow/nodes/index.tsx b/packages/core/client/src/workflow/nodes/index.tsx index cdaa9a706..257a70952 100644 --- a/packages/core/client/src/workflow/nodes/index.tsx +++ b/packages/core/client/src/workflow/nodes/index.tsx @@ -2,10 +2,10 @@ import { CloseOutlined, DeleteOutlined } from '@ant-design/icons'; import { css, cx } from '@emotion/css'; import { ISchema, useForm } from '@formily/react'; import { Registry } from '@nocobase/utils'; -import { Button, Modal, Tag } from 'antd'; +import { Button, message, Modal, Tag } from 'antd'; import React, { useContext } from 'react'; import { useTranslation } from 'react-i18next'; -import { SchemaComponent, useActionContext, useAPIClient, useCollection, useCompile, useRequest, useResourceActionContext } from '../..'; +import { SchemaComponent, useActionContext, useAPIClient, useCollection, useCompile, useRecord, useRequest, useResourceActionContext } from '../..'; import { nodeBlockClass, nodeCardClass, nodeClass, nodeHeaderClass, nodeMetaClass, nodeTitleClass } from '../style'; import { AddButton, useFlowContext } from '../WorkflowCanvas'; @@ -44,13 +44,19 @@ instructions.register('parallel', parallel); instructions.register('calculation', calculation); function useUpdateAction() { + const { t } = useTranslation(); const form = useForm(); const api = useAPIClient(); const ctx = useActionContext(); const { refresh } = useResourceActionContext(); const data = useNodeContext(); + const { workflow } = useFlowContext(); return { async run() { + if (workflow.executed) { + message.error(t('Node in executed workflow cannot be modified')); + return; + } await form.submit(); await api.resource('flow_nodes', data.id).update({ filterByTk: data.id, @@ -72,46 +78,51 @@ export function useNodeContext() { } export function Node({ data }) { + const instruction = instructions.get(data.type); return ( -
- {instruction.render - ? instruction.render(data) - : - } - {!instruction.endding - ? - : ( -
+
+ {instruction.render + ? instruction.render(data) + : + } + {!instruction.endding + ? + : ( +
- -
- ) - } -
+ .anticon{ + font-size: 1.5em; + line-height: 100%; + } + `} + > + +
+ ) + } +
+ ); } export function RemoveButton() { const { t } = useTranslation(); - const { resource } = useCollection(); + const api = useAPIClient(); + const { workflow } = useFlowContext(); + const resource = api.resource('workflows.nodes', workflow.id); const current = useNodeContext(); const { nodes, onNodeRemoved } = useFlowContext(); @@ -135,7 +146,9 @@ export function RemoveButton() { }); } - return ( + return workflow.executed + ? null + : (