From 238af440e3a4138d20ad6dcf3bcf3e60c5f26861 Mon Sep 17 00:00:00 2001 From: Junyi Date: Tue, 16 May 2023 08:45:45 +0700 Subject: [PATCH] feat(plugin-workflow): loop (#1787) * feat(plugin-workflow): add loop instruction * fix(plugin-workflow): fix lint error * feat(plugin-workflow): add loop variable in client * feat(plugin-workflow): refactor and add job list to nodes in execution * feat(plugin-workflow): allow to query multiple records * fix(plugin-workflow): fix i18n * fix(plugin-workflow): fix undefined value in component * fix(plugin-workflow): fix parse context value with current node * fix(plugin-workflow): fix revision with scope variable * test(plugin-workflow): add failing case * fix(plugin-workflow): fix revision with scope variable * chore(plugin-workflow): fix lint errors * fix(plugin-workflow): fix workflow canvas page style * fix(plugin-workflow): revert abstracted node config drawer back to each node * fix(plugin-workflow): fix parallel extra call * fix(plugin-workflow): fix parallel branch end * fix(plugin-workflow): fix jobs variable in processor * fix(plugin-workflow): fix workflow canvas scroll style * fix(plugin-workflow): fix slowly opening job modal * fix(plugin-workflow): fix cycling reference --- .../hooks/useCollectionDataSource.ts | 4 +- .../schema-component/antd/variable/Input.tsx | 9 +- .../workflow/src/client/CanvasContent.tsx | 24 ++ .../workflow/src/client/ExecutionCanvas.tsx | 131 +++++-- .../workflow/src/client/WorkflowCanvas.tsx | 17 +- .../client/components/CollectionFieldset.tsx | 4 +- .../workflow/src/client/locale/zh-CN.ts | 9 + .../workflow/src/client/nodes/calculation.tsx | 2 +- .../workflow/src/client/nodes/create.tsx | 2 +- .../workflow/src/client/nodes/index.tsx | 167 ++++----- .../workflow/src/client/nodes/loop.tsx | 162 +++++++++ .../src/client/nodes/manual/index.tsx | 2 +- .../workflow/src/client/nodes/query.tsx | 17 +- .../plugins/workflow/src/client/style.tsx | 63 ++-- .../workflow/src/client/triggers/index.tsx | 4 +- .../plugins/workflow/src/client/variable.tsx | 42 ++- .../plugins/workflow/src/server/Processor.ts | 39 ++- .../server/__tests__/collections/comments.ts | 4 + .../__tests__/instructions/loop.test.ts | 331 ++++++++++++++++++ .../workflow/src/server/actions/workflows.ts | 4 +- .../src/server/instructions/calculation.ts | 2 +- .../src/server/instructions/condition.ts | 2 +- .../src/server/instructions/create.ts | 2 +- .../src/server/instructions/destroy.ts | 2 +- .../workflow/src/server/instructions/index.ts | 3 + .../workflow/src/server/instructions/loop.ts | 100 ++++++ .../src/server/instructions/parallel.ts | 11 +- .../workflow/src/server/instructions/query.ts | 2 +- .../src/server/instructions/request.ts | 2 +- .../src/server/instructions/update.ts | 2 +- yarn.lock | 223 +----------- 31 files changed, 966 insertions(+), 422 deletions(-) create mode 100644 packages/plugins/workflow/src/client/CanvasContent.tsx create mode 100644 packages/plugins/workflow/src/client/nodes/loop.tsx create mode 100644 packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts create mode 100644 packages/plugins/workflow/src/server/instructions/loop.ts diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionDataSource.ts b/packages/core/client/src/collection-manager/hooks/useCollectionDataSource.ts index bb940136c..a82af34ad 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionDataSource.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionDataSource.ts @@ -4,9 +4,9 @@ import { useCollectionManager } from '.'; import { useCompile } from '../../schema-component'; export function useCollectionDataSource(filter?: Function) { + const compile = useCompile(); + const { collections = [] } = useCollectionManager(); return (field: any) => { - const compile = useCompile(); - const { collections = [] } = useCollectionManager(); action.bound((data: any) => { const filtered = typeof filter === 'function' ? data.filter(filter) : data; field.dataSource = filtered.map((item) => ({ diff --git a/packages/core/client/src/schema-component/antd/variable/Input.tsx b/packages/core/client/src/schema-component/antd/variable/Input.tsx index 4607cb80f..73885ba79 100644 --- a/packages/core/client/src/schema-component/antd/variable/Input.tsx +++ b/packages/core/client/src/schema-component/antd/variable/Input.tsx @@ -97,11 +97,14 @@ const ConstantTypes = { }, }; -function getTypedConstantOption(type) { +function getTypedConstantOption(type: string, types?: true | string[]) { + const allTypes = Object.values(ConstantTypes); return { value: '', label: '{{t("Constant")}}', - children: Object.values(ConstantTypes), + children: types + ? allTypes.filter((item) => (Array.isArray(types) && types.includes(item.value)) || types === true) + : allTypes, component: ConstantTypes[type]?.component, }; } @@ -129,7 +132,7 @@ export function Input(props) { label: '{{t("Constant")}}', } : useTypedConstant - ? getTypedConstantOption(type) + ? getTypedConstantOption(type, useTypedConstant) : { value: '', label: '{{t("Null")}}', diff --git a/packages/plugins/workflow/src/client/CanvasContent.tsx b/packages/plugins/workflow/src/client/CanvasContent.tsx new file mode 100644 index 000000000..d00ab33d7 --- /dev/null +++ b/packages/plugins/workflow/src/client/CanvasContent.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Tag } from 'antd'; +import { cx } from '@emotion/css'; + +import { Branch } from './Branch'; +import { lang } from './locale'; +import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style'; +import { TriggerConfig } from './triggers'; + +export function CanvasContent({ entry }) { + return ( +
+ +
+ +
+
+
+ {lang('End')} +
+
+
+ ); +} diff --git a/packages/plugins/workflow/src/client/ExecutionCanvas.tsx b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx index a517f5b66..03ebe779b 100644 --- a/packages/plugins/workflow/src/client/ExecutionCanvas.tsx +++ b/packages/plugins/workflow/src/client/ExecutionCanvas.tsx @@ -1,35 +1,117 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Tag, Breadcrumb } from 'antd'; -import { cx } from '@emotion/css'; +import { css } from '@emotion/css'; import { Link } from 'react-router-dom'; -import { useCompile, useDocumentTitle, useResourceActionContext } from '@nocobase/client'; +import { + ActionContext, + SchemaComponent, + useCompile, + useDocumentTitle, + useResourceActionContext, +} from '@nocobase/client'; import { str2moment } from '@nocobase/utils/client'; -import { FlowContext } from './FlowContext'; -import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style'; -import { TriggerConfig } from './triggers'; -import { Branch } from './Branch'; -import { ExecutionStatusOptionsMap } from './constants'; -import { lang } from './locale'; +import { FlowContext, useFlowContext } from './FlowContext'; +import { nodeTitleClass } from './style'; +import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants'; +import { lang, NAMESPACE } from './locale'; import { linkNodes } from './utils'; +import { instructions } from './nodes'; +import { CanvasContent } from './CanvasContent'; function attachJobs(nodes, jobs: any[] = []): void { const nodesMap = new Map(); - nodes.forEach((item) => nodesMap.set(item.id, item)); - const jobsMap = new Map(); - jobs.forEach((item) => jobsMap.set(item.nodeId, item)); - for (const node of nodesMap.values()) { - if (jobsMap.has(node.id)) { - node.job = jobsMap.get(node.id); - } - } + nodes.forEach((item) => { + item.jobs = []; + nodesMap.set(item.id, item); + }); + jobs.forEach((item) => { + const node = nodesMap.get(item.nodeId); + node.jobs.push(item); + item.node = { + id: node.id, + title: node.title, + type: node.type, + }; + }); + nodes.forEach((item) => { + item.jobs = item.jobs.sort((a, b) => a.id - b.id); + }); +} + +function JobModal() { + const compile = useCompile(); + const { viewJob: job, setViewJob } = useFlowContext(); + const { node = {} } = job ?? {}; + const instruction = instructions.get(node.type); + + return ( + + + {compile(instruction?.title)} + {node.title} + #{node.id} + + ), + properties: { + status: { + type: 'number', + title: `{{t("Status", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Select', + enum: JobStatusOptions, + 'x-read-pretty': true, + }, + updatedAt: { + type: 'string', + title: `{{t("Executed at", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'DatePicker', + 'x-component-props': { + showTime: true, + }, + 'x-read-pretty': true, + }, + result: { + type: 'object', + title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Input.JSON', + 'x-component-props': { + className: css` + padding: 1em; + background-color: #eee; + `, + }, + 'x-read-pretty': true, + }, + }, + }, + }, + }} + /> + + ); } export function ExecutionCanvas() { const compile = useCompile(); const { data, loading } = useResourceActionContext(); const { setTitle } = useDocumentTitle(); + const [viewJob, setViewJob] = useState(null); useEffect(() => { const { workflow } = data?.data ?? {}; setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`); @@ -58,6 +140,8 @@ export function ExecutionCanvas() { workflow: workflow.type ? workflow : null, nodes, execution, + viewJob, + setViewJob }} >
@@ -79,17 +163,8 @@ export function ExecutionCanvas() {
-
- -
- -
-
-
- {lang('End')} -
-
-
+ + ); } diff --git a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx index 6f89fab4e..a7ba981ea 100644 --- a/packages/plugins/workflow/src/client/WorkflowCanvas.tsx +++ b/packages/plugins/workflow/src/client/WorkflowCanvas.tsx @@ -16,13 +16,12 @@ import { } from '@nocobase/client'; import { FlowContext, useFlowContext } from './FlowContext'; -import { branchBlockClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style'; -import { TriggerConfig } from './triggers'; -import { Branch } from './Branch'; +import { workflowVersionDropdownClass } from './style'; import { executionSchema } from './schemas/executions'; import { ExecutionLink } from './ExecutionLink'; import { lang } from './locale'; import { linkNodes } from './utils'; +import { CanvasContent } from './CanvasContent'; function ExecutionResourceProvider({ request, filter = {}, ...others }) { const { workflow } = useFlowContext(); @@ -214,17 +213,7 @@ export function WorkflowCanvas() { -
- -
- -
-
-
- {lang('End')} -
-
-
+ ); } diff --git a/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx b/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx index 992950fec..0d2b0c499 100644 --- a/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx +++ b/packages/plugins/workflow/src/client/components/CollectionFieldset.tsx @@ -48,7 +48,7 @@ export default observer(({ value, disabled, onChange }: any) => { !['formula'].includes(field.type), ); - const unassignedFields = fields.filter((field) => !(field.name in value)); + const unassignedFields = fields.filter((field) => !value || !(field.name in value)); const scope = useWorkflowVariableOptions(); const mergedDisabled = disabled || form.disabled; @@ -69,7 +69,7 @@ export default observer(({ value, disabled, onChange }: any) => { {fields.length ? ( {fields - .filter((field) => field.name in value) + .filter((field) => value && field.name in value) .map((field) => { // constant for associations to use Input, others to use CollectionField // dynamic values only support belongsTo/hasOne association, other association type should disable diff --git a/packages/plugins/workflow/src/client/locale/zh-CN.ts b/packages/plugins/workflow/src/client/locale/zh-CN.ts index d0066cddf..f3cb6f0c4 100644 --- a/packages/plugins/workflow/src/client/locale/zh-CN.ts +++ b/packages/plugins/workflow/src/client/locale/zh-CN.ts @@ -116,6 +116,15 @@ export default { 'Continue after all branches succeeded': '全部分支都成功后才能继续', 'Continue after any branch succeeded': '任意分支成功后就继续', 'Continue after any branch succeeded, or exit after any branch failed': '任意分支成功继续,或失败后退出', + Loop: '循环', + 'Loop target': '循环对象', + 'Loop index': '当前索引', + 'Loop length': '循环长度', + 'Loop will cause performance issue based on the quantity, please use with caution.': + '循环次数过高可能引起性能问题,请谨慎使用。', + 'Scope variables': '局域变量', + 'Single number will be treated as times, single string will be treated as chars, other non-array value will be turned into a single item array.': + '单一数字值将被视为次数,单一字符串值将被视为字符数组,其他非数组值将被转换为值数组。', Delay: '延时', Duration: '时长', 'End Status': '到时状态', diff --git a/packages/plugins/workflow/src/client/nodes/calculation.tsx b/packages/plugins/workflow/src/client/nodes/calculation.tsx index af12743a5..787d3ca18 100644 --- a/packages/plugins/workflow/src/client/nodes/calculation.tsx +++ b/packages/plugins/workflow/src/client/nodes/calculation.tsx @@ -170,7 +170,7 @@ export default { RadioWithTooltip, DynamicConfig, }, - getOptions(config, types) { + useVariables(current, types) { if ( types && !types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type))) diff --git a/packages/plugins/workflow/src/client/nodes/create.tsx b/packages/plugins/workflow/src/client/nodes/create.tsx index 68b476259..b67694693 100644 --- a/packages/plugins/workflow/src/client/nodes/create.tsx +++ b/packages/plugins/workflow/src/client/nodes/create.tsx @@ -40,7 +40,7 @@ export default { CollectionFieldset, FieldsSelect, }, - getOptions(config, types) { + useVariables({ config }, types) { return useCollectionFieldOptions({ collection: config.collection, types, diff --git a/packages/plugins/workflow/src/client/nodes/index.tsx b/packages/plugins/workflow/src/client/nodes/index.tsx index f4ebf46f6..185030202 100644 --- a/packages/plugins/workflow/src/client/nodes/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/index.tsx @@ -2,10 +2,10 @@ import React, { useState, useContext } from 'react'; import { CloseOutlined, DeleteOutlined } from '@ant-design/icons'; import { css, cx } from '@emotion/css'; import { ISchema, useForm } from '@formily/react'; -import { Button, message, Modal, Tag, Alert, Input } from 'antd'; +import { Button, message, Modal, Tag, Alert, Input, Dropdown } from 'antd'; import { useTranslation } from 'react-i18next'; -import { Registry, parse } from '@nocobase/utils/client'; +import { Registry, parse, str2moment } from '@nocobase/utils/client'; import { ActionContext, SchemaComponent, @@ -17,13 +17,14 @@ import { useResourceActionContext, } from '@nocobase/client'; -import { nodeBlockClass, nodeCardClass, nodeClass, nodeMetaClass, nodeTitleClass } from '../style'; +import { nodeBlockClass, nodeCardClass, nodeClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style'; import { AddButton } from '../AddButton'; import { useFlowContext } from '../FlowContext'; import calculation from './calculation'; import condition from './condition'; import parallel from './parallel'; +import loop from './loop'; import delay from './delay'; import manual from './manual'; @@ -32,8 +33,8 @@ import query from './query'; import create from './create'; import update from './update'; import destroy from './destroy'; -import { JobStatusOptions, JobStatusOptionsMap } from '../constants'; -import { lang, NAMESPACE } from '../locale'; +import { JobStatusOptionsMap } from '../constants'; +import { NAMESPACE, lang } from '../locale'; import request from './request'; import { VariableOptions } from '../variable'; @@ -48,16 +49,18 @@ export interface Instruction { components?: { [key: string]: any }; render?(props): React.ReactNode; endding?: boolean; - getOptions?(config, types?): VariableOptions; + useVariables?(node, types?): VariableOptions; + useScopeVariables?(node, types?): VariableOptions; useInitializers?(node): SchemaInitializerItemOptions | null; initializers?: { [key: string]: any }; } export const instructions = new Registry(); +instructions.register('calculation', calculation); instructions.register('condition', condition); instructions.register('parallel', parallel); -instructions.register('calculation', calculation); +instructions.register('loop', loop); instructions.register('delay', delay); instructions.register('manual', manual); @@ -112,6 +115,21 @@ export function useAvailableUpstreams(node) { return stack; } +export function useUpstreamScopes(node) { + const stack: any[] = []; + if (!node) { + return []; + } + + for (let current = node; current; current = current.upstream) { + if (current.upstream && current.branchIndex != null) { + stack.push(current.upstream); + } + } + + return stack; +} + export function Node({ data }) { const instruction = instructions.get(data.type); @@ -212,18 +230,28 @@ export function RemoveButton() { ); } +function InnerJobButton({ job, ...props }) { + const { icon, color } = JobStatusOptionsMap[job.status]; + + return ( + + ); +} + export function JobButton() { - const compile = useCompile(); - const { execution } = useFlowContext(); - const { id, type, title, job } = useNodeContext() ?? {}; + const { execution, setViewJob } = useFlowContext(); + const { jobs } = useNodeContext() ?? {}; if (!execution) { return null; } - if (!job) { + if (!jobs.length) { return ( item.id == key); + setViewJob(job); + } - return ( - {icon}, - shape: 'circle', - className: [ - 'workflow-node-job-button', - css` - .ant-tag { - padding: 0; - width: 100%; - line-height: 18px; - margin-right: 0; - border-radius: 50%; + return jobs.length > 1 ? ( + { + const { icon, color } = JobStatusOptionsMap[job.status]; + return { + key: job.id, + label: ( +
- {compile(instruction.title)} - {title} - #{id} -
- ), - properties: { - status: { - type: 'number', - title: `{{t("Status", { ns: "${NAMESPACE}" })}}`, - 'x-decorator': 'FormItem', - 'x-component': 'Select', - enum: JobStatusOptions, - 'x-read-pretty': true, - }, - updatedAt: { - type: 'string', - title: `{{t("Executed at", { ns: "${NAMESPACE}" })}}`, - 'x-decorator': 'FormItem', - 'x-component': 'DatePicker', - 'x-component-props': { - showTime: true, - }, - 'x-read-pretty': true, - }, - result: { - type: 'object', - title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, - 'x-decorator': 'FormItem', - 'x-component': 'Input.JSON', - 'x-component-props': { - className: css` - padding: 1em; - background-color: #eee; - `, - }, - 'x-read-pretty': true, - }, - }, - }, - }, - }, - }, + `} + > + + {icon} + + + + ), + }; + }), + onClick: onOpenJob, }} - /> + > + +
+ ) : ( + setViewJob(jobs[0])} /> ); } @@ -353,7 +339,7 @@ export function NodeDefaultView(props) { return; } const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']); - for (let el = ev.target; el && el !== ev.currentTarget; el = el.parentNode) { + for (let el = ev.target; el && el !== ev.currentTarget && el !== document.documentElement; el = el.parentNode) { if ((Array.from(el.classList) as string[]).some((name: string) => whiteSet.has(name))) { setEditingConfig(true); ev.stopPropagation(); @@ -445,7 +431,6 @@ export function NodeDefaultView(props) { min-width: 6em; } } - .ant-input-affix-wrapper { &:not(.full-width) { .ant-input { diff --git a/packages/plugins/workflow/src/client/nodes/loop.tsx b/packages/plugins/workflow/src/client/nodes/loop.tsx new file mode 100644 index 000000000..e1890d811 --- /dev/null +++ b/packages/plugins/workflow/src/client/nodes/loop.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { Alert } from 'antd'; +import { ArrowUpOutlined } from '@ant-design/icons'; +import { cx, css } from '@emotion/css'; + +import { useCompile } from '@nocobase/client'; + +import { NodeDefaultView } from '.'; +import { useFlowContext } from '../FlowContext'; +import { lang, NAMESPACE } from '../locale'; +import { useWorkflowVariableOptions, VariableOption, VariableTypes } from '../variable'; +import { addButtonClass, branchBlockClass, branchClass, nodeSubtreeClass } from '../style'; +import { Branch } from '../Branch'; + +function findOption(options: VariableOption[], paths: string[]) { + let current = options; + for (let i = 0; i < paths.length; i++) { + const path = paths[i]; + const option = current.find((item) => item.value === path); + if (!option) { + return null; + } + if (option.children) { + current = option.children; + } + } + return current; +} + +export default { + title: `{{t("Loop", { ns: "${NAMESPACE}" })}}`, + type: 'loop', + group: 'control', + fieldset: { + warning: { + type: 'void', + 'x-component': Alert, + 'x-component-props': { + type: 'warning', + showIcon: true, + message: `{{t("Loop will cause performance issue based on the quantity, please use with caution.", { ns: "${NAMESPACE}" })}}`, + className: css` + width: 100%; + font-size: 85%; + margin-bottom: 2em; + `, + }, + }, + target: { + type: 'string', + title: `{{t("Loop target", { ns: "${NAMESPACE}" })}}`, + description: `{{t("Single number will be treated as times, single string will be treated as chars, other non-array value will be turned into a single item array.", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Variable.Input', + 'x-component-props': { + scope: '{{useWorkflowVariableOptions}}', + useTypedConstant: ['string', 'number', 'null'], + }, + required: true, + }, + }, + view: {}, + render: function Renderer(data) { + const { nodes } = useFlowContext(); + const entry = nodes.find((node) => node.upstreamId === data.id && node.branchIndex != null); + + return ( + +
+
+ + +
+
+
+ +
+
+
+
+
+ + ); + }, + scope: { + useWorkflowVariableOptions, + }, + components: {}, + useScopeVariables(node, types) { + const compile = useCompile(); + const { target } = node.config; + if (!target) { + return null; + } + + // const { workflow } = useFlowContext(); + // const current = useNodeContext(); + // const upstreams = useAvailableUpstreams(current); + // find target data model by path described in `config.target` + // 1. get options from $context/$jobsMapByNodeId + // 2. route to sub-options and use as loop target options + const targetOption: VariableOption = { key: 'item', value: 'item', label: lang('Loop target') }; + + if (typeof target === 'string' && target.startsWith('{{') && target.endsWith('}}')) { + const paths = target + .slice(2, -2) + .split('.') + .map((path) => path.trim()); + + const options = VariableTypes.filter((item) => ['$context', '$jobsMapByNodeId'].includes(item.value)).map( + (item: any) => { + const opts = typeof item.useOptions === 'function' ? item.useOptions(node, types).filter(Boolean) : null; + return { + label: compile(item.title), + value: item.value, + key: item.value, + children: compile(opts), + disabled: opts && !opts.length, + }; + }, + ); + + targetOption.children = findOption(options, paths); + } + + return [ + targetOption, + { key: 'index', value: 'index', label: lang('Loop index') }, + { key: 'length', value: 'length', label: lang('Loop length') }, + ]; + }, +}; diff --git a/packages/plugins/workflow/src/client/nodes/manual/index.tsx b/packages/plugins/workflow/src/client/nodes/manual/index.tsx index 3b86c4018..7e139f2cc 100644 --- a/packages/plugins/workflow/src/client/nodes/manual/index.tsx +++ b/packages/plugins/workflow/src/client/nodes/manual/index.tsx @@ -86,7 +86,7 @@ export default { ModeConfig, AssigneesSelect, }, - getOptions(config, types) { + useVariables({ config }, types) { const formKeys = Object.keys(config.forms ?? {}); if (!formKeys.length) { return null; diff --git a/packages/plugins/workflow/src/client/nodes/query.tsx b/packages/plugins/workflow/src/client/nodes/query.tsx index 5d045c8d8..8a85c78f2 100644 --- a/packages/plugins/workflow/src/client/nodes/query.tsx +++ b/packages/plugins/workflow/src/client/nodes/query.tsx @@ -14,15 +14,12 @@ export default { group: 'collection', fieldset: { collection, - // multiple: { - // type: 'boolean', - // title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`, - // 'x-decorator': 'FormItem', - // 'x-component': 'Checkbox', - // 'x-component-props': { - // disabled: true - // } - // }, + multiple: { + type: 'boolean', + title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'Checkbox', + }, params: { type: 'object', properties: { @@ -45,7 +42,7 @@ export default { FilterDynamicComponent, FieldsSelect, }, - getOptions(config, types) { + useVariables({ config }, types) { return useCollectionFieldOptions({ collection: config.collection, types, diff --git a/packages/plugins/workflow/src/client/style.tsx b/packages/plugins/workflow/src/client/style.tsx index 7f2ad267e..825f94b4b 100644 --- a/packages/plugins/workflow/src/client/style.tsx +++ b/packages/plugins/workflow/src/client/style.tsx @@ -1,12 +1,19 @@ import { css } from '@emotion/css'; export const workflowPageClass = css` + flex-grow: 1; + overflow: hidden; + display: flex; + flex-direction: column; + .workflow-toolbar { display: flex; align-items: center; justify-content: space-between; + position: relative; padding: 0.5rem 1rem; background: #fff; + border-bottom: 1px solid #e7e7e7; header { display: flex; @@ -28,8 +35,9 @@ export const workflowPageClass = css` } .workflow-canvas { - width: min-content; - min-width: 100%; + flex-grow: 1; + overflow: auto; + width: 100%; display: flex; flex-direction: column; align-items: center; @@ -95,11 +103,11 @@ export const branchClass = css` flex-direction: column; align-items: center; position: relative; + min-width: 20em; padding: 0 2em; .workflow-node-list { flex-grow: 1; - min-width: 20em; } .workflow-branch-lines { @@ -181,12 +189,8 @@ export const nodeCardClass = css` box-shadow: 0 0.25em 1em rgba(0, 100, 200, 0.25); } - .workflow-node-remove-button, - .workflow-node-job-button { - position: absolute; - } - .workflow-node-remove-button { + position: absolute; right: 0.5em; top: 0.5em; color: #999; @@ -202,21 +206,10 @@ export const nodeCardClass = css` } } - .workflow-node-job-button { - display: flex; - top: 1em; - right: 1em; - width: 1.25rem; - height: 1.25rem; - min-width: 1.25rem; - justify-content: center; - align-items: center; - font-size: 0.8em; - color: #fff; - - &[type='button'] { - border: none; - } + > .workflow-node-job-button { + position: absolute; + top: 1rem; + right: 1rem; } .ant-input { @@ -251,6 +244,30 @@ export const nodeCardClass = css` } `; +export const nodeJobButtonClass = css` + display: flex; + width: 1.25rem; + height: 1.25rem; + min-width: 1.25rem; + justify-content: center; + align-items: center; + font-size: 0.8em; + color: #fff; + + &[type='button'] { + border: none; + } + + .ant-tag { + padding: 0; + width: 100%; + line-height: 18px; + margin-right: 0; + border-radius: 50%; + text-align: center; + } +`; + export const nodeHeaderClass = css` position: relative; `; diff --git a/packages/plugins/workflow/src/client/triggers/index.tsx b/packages/plugins/workflow/src/client/triggers/index.tsx index 116694de2..0c2cea152 100644 --- a/packages/plugins/workflow/src/client/triggers/index.tsx +++ b/packages/plugins/workflow/src/client/triggers/index.tsx @@ -17,7 +17,7 @@ import { useResourceActionContext, } from '@nocobase/client'; -import { nodeCardClass, nodeMetaClass, nodeTitleClass } from '../style'; +import { nodeCardClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style'; import { useFlowContext } from '../FlowContext'; import collection from './collection'; import schedule from './schedule/'; @@ -86,7 +86,7 @@ function TriggerExecution() { 'x-component-props': { title: , shape: 'circle', - className: 'workflow-node-job-button', + className: cx(nodeJobButtonClass, 'workflow-node-job-button'), type: 'primary', }, properties: { diff --git a/packages/plugins/workflow/src/client/variable.tsx b/packages/plugins/workflow/src/client/variable.tsx index 4f26c2a1c..cae3e7a13 100644 --- a/packages/plugins/workflow/src/client/variable.tsx +++ b/packages/plugins/workflow/src/client/variable.tsx @@ -1,7 +1,7 @@ import { useCollectionManager, useCompile } from '@nocobase/client'; import { useFlowContext } from './FlowContext'; import { NAMESPACE } from './locale'; -import { instructions, useAvailableUpstreams, useNodeContext } from './nodes'; +import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes'; import { triggers } from './triggers'; export type VariableOption = { @@ -13,17 +13,37 @@ export type VariableOption = { export type VariableOptions = VariableOption[] | null; -const VariableTypes = [ +export const VariableTypes = [ + { + title: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`, + value: '$scopes', + useOptions(current, types) { + const scopes = useUpstreamScopes(current); + const options: VariableOption[] = []; + scopes.forEach((node) => { + const instruction = instructions.get(node.type); + const subOptions = instruction.useScopeVariables?.(node, types); + if (subOptions) { + options.push({ + key: node.id.toString(), + value: node.id.toString(), + label: node.title ?? `#${node.id}`, + children: subOptions, + }); + } + }); + return options; + }, + }, { title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, value: '$jobsMapByNodeId', - options(types) { - const current = useNodeContext(); + useOptions(current, types) { const upstreams = useAvailableUpstreams(current); const options: VariableOption[] = []; upstreams.forEach((node) => { const instruction = instructions.get(node.type); - const subOptions = instruction.getOptions?.(node.config, types); + const subOptions = instruction.useVariables?.(node, types); if (subOptions) { options.push({ key: node.id.toString(), @@ -39,7 +59,7 @@ const VariableTypes = [ { title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`, value: '$context', - options(types) { + useOptions(current, types) { const { workflow } = useFlowContext(); const trigger = triggers.get(workflow.type); return trigger?.getOptions?.(workflow.config, types) ?? null; @@ -48,7 +68,7 @@ const VariableTypes = [ { title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`, value: '$system', - options(types) { + useOptions(current, types) { return [ ...(!types || types.includes('date') ? [ @@ -137,8 +157,9 @@ export function filterTypedFields(fields, types, depth = 1) { export function useWorkflowVariableOptions(types?) { const compile = useCompile(); + const current = useNodeContext(); const options = VariableTypes.map((item: any) => { - const opts = typeof item.options === 'function' ? item.options(types).filter(Boolean) : item.options; + const opts = typeof item.useOptions === 'function' ? item.useOptions(current, types).filter(Boolean) : null; return { label: compile(item.title), value: item.value, @@ -214,8 +235,9 @@ function useNormalizedFields(collectionName) { export function useCollectionFieldOptions(options): VariableOption[] { const { fields, collection, types, depth = 1 } = options; const compile = useCompile(); - const normalizedFields = fields ?? useNormalizedFields(collection); - const result: VariableOption[] = filterTypedFields(normalizedFields, types, depth) + const normalizedFields = useNormalizedFields(collection); + const computedFields = fields ?? normalizedFields; + const result: VariableOption[] = filterTypedFields(computedFields, types, depth) .filter((field) => !isAssociationField(field) || depth) .map((field) => { const label = compile(field.uiSchema?.title || field.name); diff --git a/packages/plugins/workflow/src/server/Processor.ts b/packages/plugins/workflow/src/server/Processor.ts index de2368e48..5a14c34a3 100644 --- a/packages/plugins/workflow/src/server/Processor.ts +++ b/packages/plugins/workflow/src/server/Processor.ts @@ -5,6 +5,7 @@ import { parse } from '@nocobase/utils'; import { Transaction, Transactionable } from 'sequelize'; import Plugin from '.'; import { EXECUTION_STATUS, JOB_STATUS } from './constants'; +import { Runner } from './instructions'; import ExecutionModel from './models/Execution'; import FlowNodeModel from './models/FlowNode'; import JobModel from './models/Job'; @@ -131,7 +132,7 @@ export default class Processor { } } - private async exec(instruction: Function, node: FlowNodeModel, prevJob) { + private async exec(instruction: Runner, node: FlowNodeModel, prevJob) { let job; try { // call instruction to get result and status @@ -174,7 +175,7 @@ export default class Processor { if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) { // run next node - this.logger.debug(`run next node (${node.id})`); + this.logger.debug(`run next node (${node.downstreamId})`); return this.run(node.downstream, savedJob); } @@ -194,7 +195,7 @@ export default class Processor { // parent node should take over the control public async end(node, job) { - this.logger.debug(`branch ended at node (${node.id})})`); + this.logger.debug(`branch ended at node (${node.id})`); const parentNode = this.findBranchParentNode(node); // no parent, means on main flow if (parentNode) { @@ -289,6 +290,15 @@ export default class Processor { return null; } + findBranchEndNode(node: FlowNodeModel): FlowNodeModel | null { + for (let n = node; n; n = n.downstream) { + if (!n.downstream) { + return n; + } + } + return null; + } + findBranchParentJob(job: JobModel, node: FlowNodeModel): JobModel | null { for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) { if (j.nodeId === node.id) { @@ -298,6 +308,18 @@ export default class Processor { return null; } + findBranchLastJob(node: FlowNodeModel): JobModel | null { + for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) { + const jobs = Array.from(this.jobsMap.values()) + .filter((item) => item.nodeId === n.id) + .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + if (jobs.length) { + return jobs[jobs.length - 1]; + } + } + return null; + } + public getScope(node?) { const systemFns = {}; const scope = { @@ -308,10 +330,21 @@ export default class Processor { systemFns[name] = fn.bind(scope); } + const $scopes = {}; + if (node) { + for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) { + const instruction = this.options.plugin.instructions.get(n.type); + if (typeof instruction.getScope === 'function') { + $scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this); + } + } + } + return { $context: this.execution.context, $jobsMapByNodeId: this.jobsMapByNodeId, $system: systemFns, + $scopes, }; } diff --git a/packages/plugins/workflow/src/server/__tests__/collections/comments.ts b/packages/plugins/workflow/src/server/__tests__/collections/comments.ts index a56fd5ef3..30494fae0 100644 --- a/packages/plugins/workflow/src/server/__tests__/collections/comments.ts +++ b/packages/plugins/workflow/src/server/__tests__/collections/comments.ts @@ -7,6 +7,10 @@ export default { type: 'belongsTo', name: 'post', }, + { + type: 'text', + name: 'content', + }, { type: 'integer', name: 'status', diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts new file mode 100644 index 000000000..d87bb101d --- /dev/null +++ b/packages/plugins/workflow/src/server/__tests__/instructions/loop.test.ts @@ -0,0 +1,331 @@ +import Database from '@nocobase/database'; +import { Application } from '@nocobase/server'; +import { getApp, sleep } from '..'; +import { EXECUTION_STATUS, JOB_STATUS } from '../../constants'; + +describe('workflow > instructions > loop', () => { + let app: Application; + let db: Database; + let PostRepo; + let WorkflowModel; + let workflow; + let plugin; + + beforeEach(async () => { + app = await getApp(); + plugin = app.pm.get('workflow'); + + db = app.db; + WorkflowModel = db.getCollection('workflows').model; + PostRepo = db.getCollection('posts').repository; + + workflow = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + }); + + afterEach(() => app.stop()); + + describe('branch', () => { + it('no branch just pass', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n2); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(2); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(0); + }); + + it('should exit when branch meets error', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: 2, + }, + }); + + const n2 = await workflow.createNode({ + type: 'error', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.ERROR); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(2); + expect(jobs[0].status).toBe(JOB_STATUS.ERROR); + expect(jobs[0].result).toBe(0); + expect(jobs[1].status).toBe(JOB_STATUS.ERROR); + }); + }); + + describe('config', () => { + it('no target just pass', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(2); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(0); + }); + + it('null target just pass', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: null, + }, + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(2); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(0); + }); + + it('empty array just pass', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: [], + }, + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(2); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(0); + }); + + it('target is number, cycle number times', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: 2.5, + }, + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + + expect(jobs.length).toBe(4); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(2); + }); + + it('target is no array, set as an array', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: {}, + }, + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + + expect(jobs.length).toBe(3); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(1); + }); + + it('multiple targets', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: [1, 2], + }, + }); + + const n2 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + branchIndex: 0, + }); + + const n3 = await workflow.createNode({ + type: 'echo', + upstreamId: n1.id, + }); + + await n1.setDownstream(n3); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + + expect(jobs.length).toBe(4); + expect(jobs[0].status).toBe(JOB_STATUS.RESOLVED); + expect(jobs[0].result).toBe(2); + expect(jobs.filter((j) => j.nodeId === n2.id).length).toBe(2); + }); + }); + + describe('scope variable', () => { + it('item.key', async () => { + const n1 = await workflow.createNode({ + type: 'loop', + config: { + target: '{{$context.data.comments}}', + }, + }); + + const n2 = await workflow.createNode({ + type: 'calculation', + config: { + engine: 'formula.js', + expression: `{{$scopes.${n1.id}.item.content}}`, + }, + upstreamId: n1.id, + branchIndex: 0, + }); + + const post = await PostRepo.create({ + values: { + title: 't1', + comments: [{ content: 'c1' }, { content: 'c2' }], + }, + }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const jobs = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(jobs.length).toBe(3); + expect(jobs[1].result).toBe('c1'); + expect(jobs[2].result).toBe('c2'); + }); + }); +}); diff --git a/packages/plugins/workflow/src/server/actions/workflows.ts b/packages/plugins/workflow/src/server/actions/workflows.ts index edc1925f1..2ab0d5631 100644 --- a/packages/plugins/workflow/src/server/actions/workflows.ts +++ b/packages/plugins/workflow/src/server/actions/workflows.ts @@ -80,12 +80,12 @@ function migrateConfig(config, oldToNew) { case 'array': return value.map((item) => migrate(item)); case 'string': - return value.replace(/(\{\{\$jobsMapByNodeId\.)([\w-]+)/g, (_, jobVar, oldNodeId) => { + return value.replace(/({{\$jobsMapByNodeId|{{\$scopes)\.([\w-]+)/g, (_, jobVar, oldNodeId) => { const newNode = oldToNew.get(Number.parseInt(oldNodeId, 10)); if (!newNode) { throw new Error('node configurated for result is not existed'); } - return `{{$jobsMapByNodeId.${newNode.id}`; + return `${jobVar}.${newNode.id}`; }); default: return value; diff --git a/packages/plugins/workflow/src/server/instructions/calculation.ts b/packages/plugins/workflow/src/server/instructions/calculation.ts index 6e3523770..4464e6b84 100644 --- a/packages/plugins/workflow/src/server/instructions/calculation.ts +++ b/packages/plugins/workflow/src/server/instructions/calculation.ts @@ -15,7 +15,7 @@ export default { async run(node: FlowNodeModel, prevJob, processor: Processor) { const { dynamic = false } = node.config || {}; let { engine = 'math.js', expression = '' } = node.config; - let scope = processor.getScope(); + let scope = processor.getScope(node); if (dynamic) { const parsed = parse(dynamic)(scope) ?? {}; engine = parsed.engine; diff --git a/packages/plugins/workflow/src/server/instructions/condition.ts b/packages/plugins/workflow/src/server/instructions/condition.ts index 1f1793f48..cb00b0184 100644 --- a/packages/plugins/workflow/src/server/instructions/condition.ts +++ b/packages/plugins/workflow/src/server/instructions/condition.ts @@ -122,7 +122,7 @@ export default { try { result = evaluator ? evaluator(expression, processor.getScope()) - : logicCalculate(processor.getParsedValue(calculation)); + : logicCalculate(processor.getParsedValue(calculation, node)); } catch (e) { return { result: e.toString(), diff --git a/packages/plugins/workflow/src/server/instructions/create.ts b/packages/plugins/workflow/src/server/instructions/create.ts index e517a289a..39d5ea7ff 100644 --- a/packages/plugins/workflow/src/server/instructions/create.ts +++ b/packages/plugins/workflow/src/server/instructions/create.ts @@ -6,7 +6,7 @@ export default { const { collection, params: { appends = [], ...params } = {} } = node.config; const { repository, model } = (node.constructor).database.getCollection(collection); - const options = processor.getParsedValue(params); + const options = processor.getParsedValue(params, node); const result = await repository.create({ ...options, context: { diff --git a/packages/plugins/workflow/src/server/instructions/destroy.ts b/packages/plugins/workflow/src/server/instructions/destroy.ts index 23da9aed3..ccb2510c6 100644 --- a/packages/plugins/workflow/src/server/instructions/destroy.ts +++ b/packages/plugins/workflow/src/server/instructions/destroy.ts @@ -6,7 +6,7 @@ export default { const { collection, params = {} } = node.config; const repo = (node.constructor).database.getRepository(collection); - const options = processor.getParsedValue(params); + const options = processor.getParsedValue(params, node); const result = await repo.destroy({ ...options, context: { diff --git a/packages/plugins/workflow/src/server/instructions/index.ts b/packages/plugins/workflow/src/server/instructions/index.ts index a4b3dc904..6afa01323 100644 --- a/packages/plugins/workflow/src/server/instructions/index.ts +++ b/packages/plugins/workflow/src/server/instructions/index.ts @@ -24,6 +24,8 @@ export interface Instruction { // for start node in main flow (or branch) to resume when manual sub branch triggered resume?: Runner; + + getScope?: (node: FlowNodeModel, job: any, processor: Processor) => any; } type InstructionConstructor = { new (p: Plugin): T }; @@ -35,6 +37,7 @@ export default function (plugin, more: { [key: string]: T 'calculation', 'condition', 'parallel', + 'loop', 'delay', 'manual', 'query', diff --git a/packages/plugins/workflow/src/server/instructions/loop.ts b/packages/plugins/workflow/src/server/instructions/loop.ts new file mode 100644 index 000000000..9c703b079 --- /dev/null +++ b/packages/plugins/workflow/src/server/instructions/loop.ts @@ -0,0 +1,100 @@ +import FlowNodeModel from '../models/FlowNode'; +import JobModel from '../models/Job'; +import Processor from '../Processor'; +import { JOB_STATUS } from '../constants'; + +function getTargetLength(target) { + let length = 0; + if (typeof target === 'number') { + if (target < 0) { + throw new Error('Loop target in number type must be greater than 0'); + } + length = Math.floor(target); + } else { + const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null); + length = targets.length; + } + return length; +} + +export default { + async run(node: FlowNodeModel, prevJob: JobModel, processor: Processor) { + const [branch] = processor.getBranches(node); + const target = processor.getParsedValue(node.config.target, node); + const length = getTargetLength(target); + + if (!branch || !length) { + return { + status: JOB_STATUS.RESOLVED, + result: 0, + }; + } + + const job = await processor.saveJob({ + status: JOB_STATUS.PENDING, + // save loop index + result: 0, + nodeId: node.id, + upstreamId: prevJob?.id ?? null, + }); + + // TODO: add loop scope to stack + // processor.stack.push({ + // label: node.title, + // value: node.id + // }); + + await processor.run(branch, job); + + return null; + }, + + async resume(node: FlowNodeModel, branchJob, processor: Processor) { + const job = processor.findBranchParentJob(branchJob, node) as JobModel; + const loop = processor.nodesMap.get(job.nodeId); + const [branch] = processor.getBranches(node); + + const { result, status } = job; + // if loop has been done (resolved / rejected), do not care newly executed branch jobs. + if (status !== JOB_STATUS.PENDING) { + return null; + } + + const nextIndex = result + 1; + + const target = processor.getParsedValue(loop.config.target, node); + // branchJob.status === JOB_STATUS.RESOLVED means branchJob is done, try next loop or exit as resolved + if (branchJob.status > JOB_STATUS.PENDING) { + job.set({ result: nextIndex }); + + const length = getTargetLength(target); + if (nextIndex < length) { + await processor.saveJob(job); + await processor.run(branch, job); + return null; + } + } + + // branchJob.status < JOB_STATUS.PENDING means branchJob is rejected, any rejection should cause loop rejected + job.set({ + status: branchJob.status, + }); + + return job; + }, + + getScope(node, index, processor) { + const target = processor.getParsedValue(node.config.target, node); + const targets = (Array.isArray(target) ? target : [target]).filter((t) => t != null); + const length = getTargetLength(target); + const item = typeof target === 'number' ? index : targets[index]; + + const result = { + item, + index, + length, + }; + + return result; + }, +}; diff --git a/packages/plugins/workflow/src/server/instructions/parallel.ts b/packages/plugins/workflow/src/server/instructions/parallel.ts index cf24c0356..4018c4444 100644 --- a/packages/plugins/workflow/src/server/instructions/parallel.ts +++ b/packages/plugins/workflow/src/server/instructions/parallel.ts @@ -75,16 +75,19 @@ export default { const { mode = PARALLEL_MODE.ALL } = node.config; await branches.reduce( (promise: Promise, branch, i) => - promise.then((previous) => { + promise.then(async (previous) => { if (i && !Modes[mode].next(previous)) { - return Promise.resolve(previous); + return previous; } - return processor.run(branch, job); + await processor.run(branch, job); + + // find last job of the branch + return processor.findBranchLastJob(branch); }), Promise.resolve(), ); - return processor.end(node, job); + return null; }, async resume(node: FlowNodeModel, branchJob, processor: Processor) { diff --git a/packages/plugins/workflow/src/server/instructions/query.ts b/packages/plugins/workflow/src/server/instructions/query.ts index 8ecefe693..e4f92ad44 100644 --- a/packages/plugins/workflow/src/server/instructions/query.ts +++ b/packages/plugins/workflow/src/server/instructions/query.ts @@ -7,7 +7,7 @@ export default { const { collection, multiple, params = {}, failOnEmpty = false } = node.config; const repo = (node.constructor).database.getRepository(collection); - const options = processor.getParsedValue(params); + const options = processor.getParsedValue(params, node); const result = await (multiple ? repo.find : repo.findOne).call(repo, { ...options, context: { diff --git a/packages/plugins/workflow/src/server/instructions/request.ts b/packages/plugins/workflow/src/server/instructions/request.ts index 294c08ce5..135e1eb93 100644 --- a/packages/plugins/workflow/src/server/instructions/request.ts +++ b/packages/plugins/workflow/src/server/instructions/request.ts @@ -51,7 +51,7 @@ export default class implements Instruction { nodeId: node.id, }); - const config = processor.getParsedValue(node.config) as RequestConfig; + const config = processor.getParsedValue(node.config, node) as RequestConfig; request(config) .then((response) => { diff --git a/packages/plugins/workflow/src/server/instructions/update.ts b/packages/plugins/workflow/src/server/instructions/update.ts index ba82cd1c3..6ac3ee371 100644 --- a/packages/plugins/workflow/src/server/instructions/update.ts +++ b/packages/plugins/workflow/src/server/instructions/update.ts @@ -7,7 +7,7 @@ export default { const { collection, multiple = false, params = {} } = node.config; const repo = (node.constructor).database.getRepository(collection); - const options = processor.getParsedValue(params); + const options = processor.getParsedValue(params, node); const result = await repo.update({ ...options, context: { diff --git a/yarn.lock b/yarn.lock index ff6caa1eb..53d212e97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6680,56 +6680,6 @@ ansi-wrap@0.1.0, ansi-wrap@^0.1.0: resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw== -antd@4.22.8: - version "4.22.8" - resolved "https://registry.yarnpkg.com/antd/-/antd-4.22.8.tgz#e2f446932815a522a8aa3d285a8a9bdcb3d0fa9f" - integrity sha512-mqHuCg9itZX+z6wk+mvRBcfz/U9iiIXS4LoNkyo8X/UBgdN8CoetFmrdvA1UQy1BuWa0/n62LiS1LatdvoTuHw== - dependencies: - "@ant-design/colors" "^6.0.0" - "@ant-design/icons" "^4.7.0" - "@ant-design/react-slick" "~0.29.1" - "@babel/runtime" "^7.18.3" - "@ctrl/tinycolor" "^3.4.0" - classnames "^2.2.6" - copy-to-clipboard "^3.2.0" - lodash "^4.17.21" - memoize-one "^6.0.0" - moment "^2.29.2" - rc-cascader "~3.6.0" - rc-checkbox "~2.3.0" - rc-collapse "~3.3.0" - rc-dialog "~8.9.0" - rc-drawer "~5.1.0" - rc-dropdown "~4.0.0" - rc-field-form "~1.27.0" - rc-image "~5.7.0" - rc-input "~0.0.1-alpha.5" - rc-input-number "~7.3.5" - rc-mentions "~1.9.1" - rc-menu "~9.6.3" - rc-motion "^2.6.1" - rc-notification "~4.6.0" - rc-pagination "~3.1.17" - rc-picker "~2.6.10" - rc-progress "~3.3.2" - rc-rate "~2.9.0" - rc-resize-observer "^1.2.0" - rc-segmented "~2.1.0" - rc-select "~14.1.1" - rc-slider "~10.0.0" - rc-steps "~4.1.0" - rc-switch "~3.2.0" - rc-table "~7.25.3" - rc-tabs "~11.16.0" - rc-textarea "~0.3.0" - rc-tooltip "~5.2.0" - rc-tree "~5.6.5" - rc-tree-select "~5.4.0" - rc-trigger "^5.2.10" - rc-upload "~4.3.0" - rc-util "^5.22.5" - scroll-into-view-if-needed "^2.2.25" - antd@^4.24.8: version "4.24.8" resolved "https://registry.yarnpkg.com/antd/-/antd-4.24.8.tgz#22f34de6857556868780dfa5fe7b374b0b71b517" @@ -16681,11 +16631,6 @@ memoize-one@^5.1.1: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== -memoize-one@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" - integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== - memory-fs@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" @@ -20256,18 +20201,6 @@ rc-align@^4.0.0: rc-util "^5.26.0" resize-observer-polyfill "^1.5.1" -rc-cascader@~3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.6.2.tgz#2b5c108807234898cd9a0366d0626f786b7b5622" - integrity sha512-sf2otpazlROTzkD3nZVfIzXmfBLiEOBTXA5wxozGXBpS902McDpvF0bdcYBu5hN+rviEAm6Mh9cLXNQ1Ty8wKQ== - dependencies: - "@babel/runtime" "^7.12.5" - array-tree-filter "^2.1.0" - classnames "^2.3.1" - rc-select "~14.1.0" - rc-tree "~5.6.3" - rc-util "^5.6.1" - rc-cascader@~3.7.0: version "3.7.2" resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.7.2.tgz#447f2725add7953dee205d1cf59f58a8317bf5f7" @@ -20288,17 +20221,6 @@ rc-checkbox@~2.3.0: "@babel/runtime" "^7.10.1" classnames "^2.2.1" -rc-collapse@~3.3.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.3.1.tgz#fc66d4c9cfeaf41e932b2de6da2d454874aee55a" - integrity sha512-cOJfcSe3R8vocrF8T+PgaHDrgeA1tX+lwfhwSj60NX9QVRidsILIbRNDLD6nAzmcvVC5PWiIRiR4S1OobxdhCg== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "2.x" - rc-motion "^2.3.4" - rc-util "^5.2.1" - shallowequal "^1.1.0" - rc-collapse@~3.4.2: version "3.4.2" resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.4.2.tgz#1310be7ad4cd0dcfc622c45f6c3b5ffdee403ad7" @@ -20310,16 +20232,6 @@ rc-collapse@~3.4.2: rc-util "^5.2.1" shallowequal "^1.1.0" -rc-dialog@~8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.9.0.tgz#04dc39522f0321ed2e06018d4a7e02a4c32bd3ea" - integrity sha512-Cp0tbJnrvPchJfnwIvOMWmJ4yjX3HWFatO6oBFD1jx8QkgsQCR0p8nUWAKdd3seLJhEC39/v56kZaEjwp9muoQ== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.6" - rc-motion "^2.3.0" - rc-util "^5.21.0" - rc-dialog@~9.0.0, rc-dialog@~9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-9.0.2.tgz#aadfebdeba145f256c1fac9b9f509f893cdbb5b8" @@ -20331,16 +20243,6 @@ rc-dialog@~9.0.0, rc-dialog@~9.0.2: rc-motion "^2.3.0" rc-util "^5.21.0" -rc-drawer@~5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-5.1.0.tgz#c1b8a46e5c064ba46a16233fbcfb1ccec6a73c10" - integrity sha512-pU3Tsn99pxGdYowXehzZbdDVE+4lDXSGb7p8vA9mSmr569oc2Izh4Zw5vLKSe/Xxn2p5MSNbLVqD4tz+pK6SOw== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.6" - rc-motion "^2.6.1" - rc-util "^5.21.2" - rc-drawer@~6.1.0: version "6.1.5" resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.5.tgz#c4137b944c16b7c179d0dba6f06ebe54f9311ec8" @@ -20383,17 +20285,7 @@ rc-image@~5.13.0: rc-motion "^2.6.2" rc-util "^5.0.6" -rc-image@~5.7.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-5.7.1.tgz#678dc014845954c30237808c00c7b12e5f2a0b07" - integrity sha512-QyMfdhoUfb5W14plqXSisaYwpdstcLYnB0MjX5ccIK2rydQM9sDPuekQWu500DDGR2dBaIF5vx9XbWkNFK17Fg== - dependencies: - "@babel/runtime" "^7.11.2" - classnames "^2.2.6" - rc-dialog "~8.9.0" - rc-util "^5.0.6" - -rc-input-number@~7.3.5, rc-input-number@~7.3.9: +rc-input-number@~7.3.9: version "7.3.11" resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.3.11.tgz#c7089705a220e1a59ba974fabf89693e00dd2442" integrity sha512-aMWPEjFeles6PQnMqP5eWpxzsvHm9rh1jQOWXExUEIxhX62Fyl/ptifLHOn17+waDG1T/YUb6flfJbvwRhHrbA== @@ -20402,15 +20294,6 @@ rc-input-number@~7.3.5, rc-input-number@~7.3.9: classnames "^2.2.5" rc-util "^5.23.0" -rc-input@~0.0.1-alpha.5: - version "0.0.1-alpha.7" - resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.0.1-alpha.7.tgz#53e3f13871275c21d92b51f80b698f389ad45dd3" - integrity sha512-eozaqpCYWSY5LBMwlHgC01GArkVEP+XlJ84OMvdkwUnJBSv83Yxa15pZpn7vACAj84uDC4xOA2CoFdbLuqB08Q== - dependencies: - "@babel/runtime" "^7.11.1" - classnames "^2.2.1" - rc-util "^5.18.1" - rc-input@~0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.1.4.tgz#45cb4ba209ae6cc835a2acb8629d4f8f0cb347e0" @@ -20432,19 +20315,7 @@ rc-mentions@~1.13.1: rc-trigger "^5.0.4" rc-util "^5.22.5" -rc-mentions@~1.9.1: - version "1.9.2" - resolved "https://registry.yarnpkg.com/rc-mentions/-/rc-mentions-1.9.2.tgz#f264ebc4ec734dad9edc8e078b65ab3586d94a7b" - integrity sha512-uxb/lzNnEGmvraKWNGE6KXMVXvt8RQv9XW8R0Dqi3hYsyPiAZeHRCHQKdLARuk5YBhFhZ6ga55D/8XuY367g3g== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.6" - rc-menu "~9.6.0" - rc-textarea "^0.3.0" - rc-trigger "^5.0.4" - rc-util "^5.22.5" - -rc-menu@~9.6.0, rc-menu@~9.6.3: +rc-menu@~9.6.0: version "9.6.4" resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-9.6.4.tgz#033e7b8848c17a09a81b68b8d4c3fa457605f4f6" integrity sha512-6DiNAjxjVIPLZXHffXxxcyE15d4isRL7iQ1ru4MqYDH2Cqc5bW96wZOdMydFtGLyDdnmEQ9jVvdCE9yliGvzkw== @@ -20498,14 +20369,6 @@ rc-overflow@^1.0.0, rc-overflow@^1.2.0, rc-overflow@^1.2.8: rc-resize-observer "^1.0.0" rc-util "^5.19.2" -rc-pagination@~3.1.17: - version "3.1.17" - resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.17.tgz#91e690aa894806e344cea88ea4a16d244194a1bd" - integrity sha512-/BQ5UxcBnW28vFAcP2hfh+Xg15W0QZn8TWYwdCApchMH1H0CxiaUUcULP8uXcFM1TygcdKWdt3JqsL9cTAfdkQ== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.1" - rc-pagination@~3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.2.0.tgz#4f2fdba9fdac0f48e5c9fb1141973818138af7e1" @@ -20514,20 +20377,6 @@ rc-pagination@~3.2.0: "@babel/runtime" "^7.10.1" classnames "^2.2.1" -rc-picker@~2.6.10: - version "2.6.11" - resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.6.11.tgz#d4a55e46480517cd1bfea5f5acd28b1d6be232d2" - integrity sha512-INJ7ULu+Kj4UgqbcqE8Q+QpMw55xFf9kkyLBHJFk0ihjJpAV4glialRfqHE7k4KX2BWYPQfpILwhwR14x2EiRQ== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.1" - date-fns "2.x" - dayjs "1.x" - moment "^2.24.0" - rc-trigger "^5.0.4" - rc-util "^5.4.0" - shallowequal "^1.1.0" - rc-picker@~2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.7.0.tgz#3c19881da27a0c5ee4c7e7504e21b552bd43a94c" @@ -20542,15 +20391,6 @@ rc-picker@~2.7.0: rc-util "^5.4.0" shallowequal "^1.1.0" -rc-progress@~3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.3.3.tgz#eb9bffbacab1534f2542f9f6861ce772254362b1" - integrity sha512-MDVNVHzGanYtRy2KKraEaWeZLri2ZHWIRyaE1a9MQ2MuJ09m+Wxj5cfcaoaR6z5iRpHpA59YeUxAlpML8N4PJw== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.6" - rc-util "^5.16.1" - rc-progress@~3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.4.1.tgz#a9ffe099e88a4fc03afb09d8603162bf0760d743" @@ -20589,7 +20429,7 @@ rc-segmented@~2.1.0: rc-motion "^2.4.4" rc-util "^5.17.0" -rc-select@~14.1.0, rc-select@~14.1.1, rc-select@~14.1.13: +rc-select@~14.1.0, rc-select@~14.1.13: version "14.1.17" resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.1.17.tgz#e623eabeaa0dd117d5a63354e6ddaaa118abc5ee" integrity sha512-6qQhMqtoUkkboRqXKKFRR5Nu1mrnw2mC1uxIBIczg7aiJ94qCZBg4Ww8OLT9f4xdyCgbFSGh6r3yB9EBsjoHGA== @@ -20612,15 +20452,6 @@ rc-slider@~10.0.0: rc-util "^5.18.1" shallowequal "^1.1.0" -rc-steps@~4.1.0: - version "4.1.4" - resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-4.1.4.tgz#0ba82db202d59ca52d0693dc9880dd145b19dc23" - integrity sha512-qoCqKZWSpkh/b03ASGx1WhpKnuZcRWmvuW+ZUu4mvMdfvFzVxblTwUM+9aBd0mlEUFmt6GW8FXhMpHkK3Uzp3w== - dependencies: - "@babel/runtime" "^7.10.2" - classnames "^2.2.3" - rc-util "^5.0.1" - rc-steps@~5.0.0-alpha.2: version "5.0.0" resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-5.0.0.tgz#2e2403f2dd69eb3966d65f461f7e3a8ee1ef69fe" @@ -20639,17 +20470,6 @@ rc-switch@~3.2.0: classnames "^2.2.1" rc-util "^5.0.1" -rc-table@~7.25.3: - version "7.25.3" - resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.25.3.tgz#a2941d4fde4c181e687e97a294faca8e4122e26d" - integrity sha512-McsLJ2rg8EEpRBRYN4Pf9gT7ZNYnjvF9zrBpUBBbUX/fxk+eGi5ff1iPIhMyiHsH71/BmTUzX9nc9XqupD0nMg== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.5" - rc-resize-observer "^1.1.0" - rc-util "^5.22.5" - shallowequal "^1.1.0" - rc-table@~7.26.0: version "7.26.0" resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.26.0.tgz#9d517e7fa512e7571fdcc453eb1bf19edfac6fbc" @@ -20661,7 +20481,7 @@ rc-table@~7.26.0: rc-util "^5.22.5" shallowequal "^1.1.0" -rc-tabs@^11.7.1, rc-tabs@~11.16.0: +rc-tabs@^11.7.1: version "11.16.1" resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-11.16.1.tgz#7c57b6a092d9d0e2df54413b0319f195c27214a9" integrity sha512-bR7Dap23YyfzZQwtKomhiFEFzZuE7WaKWo+ypNRSGB9PDKSc6tM12VP8LWYkvmmQHthgwP0WRN8nFbSJWuqLYw== @@ -20686,17 +20506,6 @@ rc-tabs@~12.5.6: rc-resize-observer "^1.0.0" rc-util "^5.16.0" -rc-textarea@^0.3.0, rc-textarea@~0.3.0: - version "0.3.7" - resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.3.7.tgz#987142891efdedb774883c07e2f51b318fde5a11" - integrity sha512-yCdZ6binKmAQB13hc/oehh0E/QRwoPP1pjF21aHBxlgXO3RzPF6dUu4LG2R4FZ1zx/fQd2L1faktulrXOM/2rw== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "^2.2.1" - rc-resize-observer "^1.0.0" - rc-util "^5.7.0" - shallowequal "^1.1.0" - rc-textarea@^0.4.0, rc-textarea@~0.4.5: version "0.4.7" resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.4.7.tgz#627f662d46f99e0059d1c1ebc8db40c65339fe90" @@ -20717,17 +20526,6 @@ rc-tooltip@~5.2.0: classnames "^2.3.1" rc-trigger "^5.0.0" -rc-tree-select@~5.4.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.4.1.tgz#b97b9c6adcabc7415d25cfd40d18058b0c57bec2" - integrity sha512-xhXnKP8Stu2Q7wTcjJaSzSOLd4wmFtUZOwmy1cioaWyPbpiKlYdnALXA/9U49HOaV3KFXdRHE9Yi0KYED7yOAQ== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "2.x" - rc-select "~14.1.0" - rc-tree "~5.6.1" - rc-util "^5.16.1" - rc-tree-select@~5.5.0: version "5.5.5" resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.5.5.tgz#d28b3b45da1e820cd21762ba0ee93c19429bb369" @@ -20750,17 +20548,6 @@ rc-tree@^5.2.0, rc-tree@~5.7.0: rc-util "^5.16.1" rc-virtual-list "^3.4.8" -rc-tree@~5.6.1, rc-tree@~5.6.3, rc-tree@~5.6.5: - version "5.6.9" - resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-5.6.9.tgz#b73290a6dcad65e4ed5d8dc21cb198b30316404b" - integrity sha512-si8aGuWQ2/sh2Ibk+WdUdDeAxoviT/+kDY+NLtJ+RhqfySqPFqWM5uHTwgFRrWUvKCqEeE/PjCYuuhHrK7Y7+A== - dependencies: - "@babel/runtime" "^7.10.1" - classnames "2.x" - rc-motion "^2.0.1" - rc-util "^5.16.1" - rc-virtual-list "^3.4.8" - rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2, rc-trigger@^5.2.10, rc-trigger@^5.3.1: version "5.3.4" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.3.4.tgz#6b4b26e32825677c837d1eb4d7085035eecf9a61" @@ -20781,7 +20568,7 @@ rc-upload@~4.3.0: classnames "^2.2.5" rc-util "^5.2.0" -rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4: +rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.12.0, rc-util@^5.15.0, rc-util@^5.16.0, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.21.2, rc-util@^5.22.5, rc-util@^5.23.0, rc-util@^5.24.4, rc-util@^5.26.0, rc-util@^5.27.0, rc-util@^5.4.0, rc-util@^5.5.0, rc-util@^5.6.1, rc-util@^5.8.0, rc-util@^5.9.4: version "5.29.3" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37" integrity sha512-wX6ZwQTzY2v7phJBquN4mSEIFR0E0qumlENx0zjENtDvoVSq2s7cR95UidKRO1hOHfDsecsfM9D1gO4Kebs7fA==