From 84c62a4ab7e6b74a67c6ab9aaaf958ad0e2aae1d Mon Sep 17 00:00:00 2001 From: mytharcher Date: Sat, 2 Nov 2024 13:41:33 +0800 Subject: [PATCH] feat(plugin-workflow): add preset and branching config before add node --- .../src/client/LoopInstruction.tsx | 1 + .../src/client/ParallelInstruction.tsx | 3 + .../src/locale/zh-CN.json | 2 +- .../plugin-workflow/src/client/AddButton.tsx | 83 +++--- .../src/client/AddNodeContext.tsx | 249 ++++++++++++++++++ .../src/client/CanvasContent.tsx | 67 ++--- .../src/client/nodes/condition.tsx | 54 +++- .../src/client/nodes/index.tsx | 28 +- .../plugin-workflow/src/locale/zh-CN.json | 9 +- .../src/server/actions/index.ts | 2 +- 10 files changed, 389 insertions(+), 109 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-workflow/src/client/AddNodeContext.tsx diff --git a/packages/plugins/@nocobase/plugin-workflow-loop/src/client/LoopInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-loop/src/client/LoopInstruction.tsx index 6a26da0a39..b3e72c902d 100644 --- a/packages/plugins/@nocobase/plugin-workflow-loop/src/client/LoopInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-loop/src/client/LoopInstruction.tsx @@ -355,6 +355,7 @@ export default class extends Instruction { default: 0, }, }; + branching = true; scope = { renderEngineReference, }; diff --git a/packages/plugins/@nocobase/plugin-workflow-parallel/src/client/ParallelInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-parallel/src/client/ParallelInstruction.tsx index 1a72976203..13006e1bc2 100644 --- a/packages/plugins/@nocobase/plugin-workflow-parallel/src/client/ParallelInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-parallel/src/client/ParallelInstruction.tsx @@ -58,6 +58,7 @@ export default class extends Instruction { default: 'all', }, }; + branching = true; components = { RadioWithTooltip, }; @@ -118,6 +119,7 @@ export default class extends Instruction { icon={} onClick={() => setBranchCount(branchCount - 1)} disabled={workflow.executed} + size="small" /> ) : null @@ -145,6 +147,7 @@ export default class extends Instruction { transform: rotate(-45deg); } `} + size="small" onClick={() => setBranchCount(branchCount + 1)} disabled={workflow.executed} /> diff --git a/packages/plugins/@nocobase/plugin-workflow-parallel/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-parallel/src/locale/zh-CN.json index 62a87c8d8a..5a822552a7 100644 --- a/packages/plugins/@nocobase/plugin-workflow-parallel/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-workflow-parallel/src/locale/zh-CN.json @@ -1,5 +1,5 @@ { - "Parallel branch": "分支", + "Parallel branch": "并行分支", "Run multiple branch processes in parallel.": "并行运行多个分支流程。", "Add branch": "增加分支", "Mode": "执行模式", diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/AddButton.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/AddButton.tsx index c3a6d6d657..500f9e2af5 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/AddButton.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/AddButton.tsx @@ -8,16 +8,17 @@ */ import React, { useCallback, useMemo, useState } from 'react'; -import { Button, Dropdown, MenuProps } from 'antd'; +import { Button, Dropdown, MenuProps, Modal } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; -import { css, useAPIClient, useCompile, usePlugin } from '@nocobase/client'; +import { css, SchemaComponent, useAPIClient, useCompile, usePlugin } from '@nocobase/client'; import WorkflowPlugin from '.'; import { useFlowContext } from './FlowContext'; import { NAMESPACE } from './locale'; import { Instruction } from './nodes'; import useStyles from './style'; +import { useAddNodeContext } from './AddNodeContext'; interface AddButtonProps { upstream; @@ -29,14 +30,13 @@ export function AddButton(props: AddButtonProps) { const { upstream, branchIndex = null } = props; const engine = usePlugin(WorkflowPlugin); const compile = useCompile(); - const api = useAPIClient(); - const { workflow, refresh } = useFlowContext() ?? {}; + const { workflow } = useFlowContext() ?? {}; const instructionList = Array.from(engine.instructions.getValues()) as Instruction[]; const { styles } = useStyles(); - const [creating, setCreating] = useState(false); + const { onPreset, onCreate, creating } = useAddNodeContext(); const groups = useMemo(() => { - return [ + const result = [ { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` }, { key: 'calculation', label: `{{t("Calculation", { ns: "${NAMESPACE}" })}}` }, { key: 'collection', label: `{{t("Collection operations", { ns: "${NAMESPACE}" })}}` }, @@ -58,62 +58,36 @@ export function AddButton(props: AddButtonProps) { 'aria-label': item.type, key: item.type, label: item.title, - type: item.options ? 'subMenu' : null, - children: item.options - ? item.options.map((option) => ({ - role: 'button', - 'aria-label': option.key, - key: option.key, - label: option.label, - })) - : null, })), }; }) .filter((group) => group.children.length); - }, [branchIndex, engine, instructionList, upstream, workflow]); - const onCreate = useCallback( + return compile(result); + }, [branchIndex, compile, engine, instructionList, upstream, workflow]); + + const onClick = useCallback( async ({ keyPath }) => { - const type = keyPath.pop(); - const [optionKey] = keyPath; + const [type] = keyPath; const instruction = engine.instructions.get(type); - const config = instruction.createDefaultConfig(); - if (optionKey) { - const { value } = instruction.options?.find((item) => item.key === optionKey) ?? {}; - Object.assign(config, typeof value === 'function' ? value() : value); + if (!instruction) { + return; } - - if (workflow) { - setCreating(true); - try { - await api.resource('workflows.nodes', workflow.id).create({ - values: { - type, - upstreamId: upstream?.id ?? null, - branchIndex, - title: compile(instruction.title), - config, - }, - }); - refresh(); - } catch (err) { - console.error(err); - } finally { - setCreating(false); - } + const title = compile(instruction.title); + const config = instruction.createDefaultConfig?.() ?? {}; + const data = { instruction, type, title, config, upstream, branchIndex }; + if ( + instruction.presetFieldset || + (typeof instruction.branching === 'function' ? instruction.branching(config) : instruction.branching) + ) { + onPreset(data); + } else { + onCreate(data); } }, - [api, branchIndex, engine.instructions, refresh, upstream?.id, workflow], + [branchIndex, engine.instructions, onCreate, upstream], ); - const menu = useMemo(() => { - return { - onClick: onCreate, - items: compile(groups), - }; - }, [groups, onCreate]); - if (!workflow) { return null; } @@ -121,8 +95,10 @@ export function AddButton(props: AddButtonProps) { return (
} - loading={creating} + loading={creating?.upstream === upstream && creating?.branchIndex === branchIndex} + size="small" />
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/AddNodeContext.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/AddNodeContext.tsx new file mode 100644 index 0000000000..a9ebb46e1b --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/AddNodeContext.tsx @@ -0,0 +1,249 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import { createForm } from '@formily/core'; +import { observer, useForm } from '@formily/react'; + +import { + ActionContextProvider, + FormItem, + SchemaComponent, + useActionContext, + useAPIClient, + useCancelAction, +} from '@nocobase/client'; + +import { useFlowContext } from './FlowContext'; +import { lang, NAMESPACE } from './locale'; +import { RadioWithTooltip } from './components'; + +function useAddNodeSubmitAction() { + const form = useForm(); + return { + async run() { + console.log('=======', form.values); + }, + }; +} + +const AddNodeContext = createContext(null); + +export function useAddNodeContext() { + return useContext(AddNodeContext); +} + +const defaultBranchingOptions = [ + { + label: `{{t('After end of branches', { ns: "${NAMESPACE}" })}}`, + value: false, + }, + { + label: `{{t('Inside of branch', { ns: "${NAMESPACE}" })}}`, + value: 0, + }, +]; + +const DownstreamBranchIndex = observer((props) => { + const { presetting } = useAddNodeContext(); + const { nodes } = useFlowContext(); + const { values } = useForm(); + const options = useMemo(() => { + if (!presetting?.instruction) { + return []; + } + const { instruction, upstream, branchIndex } = presetting || {}; + const downstream = upstream + ? nodes.find((item) => item.upstreamId === upstream.id && item.branchIndex === branchIndex) + : nodes.find((item) => item.upstreamId === null); + if (!downstream) { + return []; + } + const branching = + typeof instruction.branching === 'function' ? instruction.branching(values.config) : instruction.branching; + if (!branching) { + return []; + } + return branching === true ? defaultBranchingOptions : branching; + }, [presetting, nodes, values.config]); + + if (!options.length) { + return null; + } + + return ( + + ); + + // return ( + // + // + // + // ); +}); + +function PresetFieldset({ useSchema }) { + const schema = useSchema(); + return ; +} + +export function AddNodeContextProvider(props) { + const api = useAPIClient(); + const [creating, setCreating] = useState(null); + const [presetting, setPresetting] = useState(null); + const [formValueChanged, setFormValueChanged] = useState(false); + const { workflow, refresh } = useFlowContext() ?? {}; + + const form = useMemo(() => { + return createForm({ + initialValues: {}, + values: {}, + }); + }, [presetting]); + + const onModalCancel = useCallback( + (visible) => { + if (!visible) { + // form.setValues({}); + // form.clearFormGraph('.config', true); + form.reset(); + setTimeout(() => { + setPresetting(null); + }); + } + }, + [form], + ); + + const onCreate = useCallback( + async ({ type, title, config, upstream, branchIndex }) => { + setCreating({ upstream, branchIndex }); + try { + await api.resource('workflows.nodes', workflow.id).create({ + values: { + type, + upstreamId: upstream?.id ?? null, + branchIndex, + title, + config, + }, + }); + refresh(); + } catch (err) { + console.error(err); + } finally { + setCreating(null); + } + }, + [api, refresh, workflow.id], + ); + + const usePresetSchema = useCallback(() => presetting?.instruction.presetFieldset, [presetting]); + + return ( + + {props.children} + + + + + ); +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/CanvasContent.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/CanvasContent.tsx index 77044c9d5f..6a345f959b 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/CanvasContent.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/CanvasContent.tsx @@ -18,6 +18,7 @@ import { useFlowContext } from './FlowContext'; import { lang } from './locale'; import useStyles from './style'; import { TriggerConfig } from './triggers'; +import { AddNodeContextProvider } from './AddNodeContext'; export function CanvasContent({ entry }) { const { styles } = useStyles(); @@ -27,41 +28,43 @@ export function CanvasContent({ entry }) { return (
-
-
-
- {workflow?.executed ? ( - - ) : null} - -
- + +
+
+
+ {workflow?.executed ? ( + + ) : null} + +
+ +
+
{lang('End')}
-
{lang('End')}
-
+
diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/condition.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/condition.tsx index a4c70cd447..c22382a470 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/condition.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/condition.tsx @@ -20,6 +20,12 @@ import useStyles from '../style'; import { useWorkflowVariableOptions, WorkflowVariableTextArea } from '../variable'; import { CalculationConfig } from '../components/Calculation'; +const BRANCH_INDEX = { + DEFAULT: null, + ON_TRUE: 1, + ON_FALSE: 0, +} as const; + export default class extends Instruction { title = `{{t("Condition", { ns: "${NAMESPACE}" })}}`; type = 'condition'; @@ -107,18 +113,44 @@ export default class extends Instruction { required: true, }, }; - options = [ - { - label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`, - key: 'rejectOnFalse', - value: { rejectOnFalse: true }, + presetFieldset = { + rejectOnFalse: { + type: 'boolean', + title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Radio.Group', + enum: [ + { + label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`, + value: true, + }, + { + label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`, + value: false, + }, + ], + default: true, }, - { - label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`, - key: 'branch', - value: { rejectOnFalse: false }, - }, - ]; + }; + + branching = ({ rejectOnFalse = true } = {}) => { + return rejectOnFalse + ? false + : [ + { + label: `{{t('After end of branches', { ns: "${NAMESPACE}" })}}`, + value: false, + }, + { + label: `{{t('Inside of "Yes" branch', { ns: "${NAMESPACE}" })}}`, + value: BRANCH_INDEX.ON_TRUE, + }, + { + label: `{{t('Inside of "No" branch', { ns: "${NAMESPACE}" })}}`, + value: BRANCH_INDEX.ON_FALSE, + }, + ]; + }; scope = { renderEngineReference, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx index 4c8b68968e..d4a60e42a0 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/nodes/index.tsx @@ -51,27 +51,40 @@ export type NodeAvailableContext = { branchIndex: number; }; +type Config = Record; + +type Options = { label: string; value: any }[]; + export abstract class Instruction { title: string; type: string; group: string; description?: string; /** - * @experimental + * @deprecated migrate to `presetFieldset` instead */ options?: { label: string; value: any; key: string }[]; fieldset: Record; + /** + * @experimental + */ + presetFieldset?: Record; + /** + * To presentation if the instruction is creating a branch + * @experimental + */ + branching?: boolean | Options | ((config: Config) => boolean | Options); /** * @experimental */ view?: ISchema; - scope?: { [key: string]: any }; - components?: { [key: string]: any }; + scope?: Record; + components?: Record; Component?(props): JSX.Element; /** * @experimental */ - createDefaultConfig?(): Record { + createDefaultConfig?(): Config { return {}; } useVariables?(node, options?: UseVariableOptions): VariableOption; @@ -568,12 +581,7 @@ export function NodeDefaultView(props) { 'Node with unknown type will cause error. Please delete it or check plugin which provide this type.', )} > -
+
{lang('Unknown node')} {data.id} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json index c16784bc33..43baf66a8c 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-workflow/src/locale/zh-CN.json @@ -160,6 +160,8 @@ "Continue when \"Yes\"": "“是”则继续", "Branch into \"Yes\" and \"No\"": "“是”和“否”分别继续", "Condition expression": "条件表达式", + "Inside of \"Yes\" branch": "“是”分支内", + "Inside of \"No\" branch": "“否”分支内", "Create record": "新增数据", "Add new record to a collection. You can use variables from upstream nodes to assign values to fields.": "向一个数据表中添加新的数据。可以使用上游节点里的变量为字段赋值。", @@ -206,5 +208,10 @@ "Succeeded": "成功", "Test run": "测试执行", "Test run will do the actual data manipulating or API calling, please use with caution.": "测试执行会进行实际的数据操作或 API 调用,请谨慎使用。", - "No variable": "无变量" + "No variable": "无变量", + + "Add node": "添加节点", + "Move all downstream nodes to": "将所有下游节点移至", + "After end of branches": "分支结束后", + "Inside of branch": "分支内" } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/index.ts index e0b100700b..028ef3b1e1 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/index.ts @@ -22,7 +22,7 @@ function make(name, mod) { } export default function ({ app }) { - app.actions({ + app.resourceManager.registerActionHandlers({ ...make('workflows', workflows), ...make('workflows.nodes', { create: nodes.create,