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 (
+
+ );
+}
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() {
-
+
+
);
}
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() {
-
+
);
}
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==