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
This commit is contained in:
		
							parent
							
								
									8cf3c40ad4
								
							
						
					
					
						commit
						238af440e3
					
				@ -4,9 +4,9 @@ import { useCollectionManager } from '.';
 | 
				
			|||||||
import { useCompile } from '../../schema-component';
 | 
					import { useCompile } from '../../schema-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function useCollectionDataSource(filter?: Function) {
 | 
					export function useCollectionDataSource(filter?: Function) {
 | 
				
			||||||
 | 
					  const compile = useCompile();
 | 
				
			||||||
 | 
					  const { collections = [] } = useCollectionManager();
 | 
				
			||||||
  return (field: any) => {
 | 
					  return (field: any) => {
 | 
				
			||||||
    const compile = useCompile();
 | 
					 | 
				
			||||||
    const { collections = [] } = useCollectionManager();
 | 
					 | 
				
			||||||
    action.bound((data: any) => {
 | 
					    action.bound((data: any) => {
 | 
				
			||||||
      const filtered = typeof filter === 'function' ? data.filter(filter) : data;
 | 
					      const filtered = typeof filter === 'function' ? data.filter(filter) : data;
 | 
				
			||||||
      field.dataSource = filtered.map((item) => ({
 | 
					      field.dataSource = filtered.map((item) => ({
 | 
				
			||||||
 | 
				
			|||||||
@ -97,11 +97,14 @@ const ConstantTypes = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getTypedConstantOption(type) {
 | 
					function getTypedConstantOption(type: string, types?: true | string[]) {
 | 
				
			||||||
 | 
					  const allTypes = Object.values(ConstantTypes);
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    value: '',
 | 
					    value: '',
 | 
				
			||||||
    label: '{{t("Constant")}}',
 | 
					    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,
 | 
					    component: ConstantTypes[type]?.component,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -129,7 +132,7 @@ export function Input(props) {
 | 
				
			|||||||
        label: '{{t("Constant")}}',
 | 
					        label: '{{t("Constant")}}',
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    : useTypedConstant
 | 
					    : useTypedConstant
 | 
				
			||||||
    ? getTypedConstantOption(type)
 | 
					    ? getTypedConstantOption(type, useTypedConstant)
 | 
				
			||||||
    : {
 | 
					    : {
 | 
				
			||||||
        value: '',
 | 
					        value: '',
 | 
				
			||||||
        label: '{{t("Null")}}',
 | 
					        label: '{{t("Null")}}',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										24
									
								
								packages/plugins/workflow/src/client/CanvasContent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packages/plugins/workflow/src/client/CanvasContent.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
				
			||||||
 | 
					    <div className="workflow-canvas">
 | 
				
			||||||
 | 
					      <TriggerConfig />
 | 
				
			||||||
 | 
					      <div className={branchBlockClass}>
 | 
				
			||||||
 | 
					        <Branch entry={entry} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div className={cx('end', nodeCardClass)}>
 | 
				
			||||||
 | 
					        <div className={cx(nodeMetaClass)}>
 | 
				
			||||||
 | 
					          <Tag color="#333">{lang('End')}</Tag>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,35 +1,117 @@
 | 
				
			|||||||
import React, { useEffect } from 'react';
 | 
					import React, { useEffect, useState } from 'react';
 | 
				
			||||||
import { Tag, Breadcrumb } from 'antd';
 | 
					import { Tag, Breadcrumb } from 'antd';
 | 
				
			||||||
import { cx } from '@emotion/css';
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					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 { str2moment } from '@nocobase/utils/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FlowContext } from './FlowContext';
 | 
					import { FlowContext, useFlowContext } from './FlowContext';
 | 
				
			||||||
import { branchBlockClass, nodeCardClass, nodeMetaClass } from './style';
 | 
					import { nodeTitleClass } from './style';
 | 
				
			||||||
import { TriggerConfig } from './triggers';
 | 
					import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
 | 
				
			||||||
import { Branch } from './Branch';
 | 
					import { lang, NAMESPACE } from './locale';
 | 
				
			||||||
import { ExecutionStatusOptionsMap } from './constants';
 | 
					 | 
				
			||||||
import { lang } from './locale';
 | 
					 | 
				
			||||||
import { linkNodes } from './utils';
 | 
					import { linkNodes } from './utils';
 | 
				
			||||||
 | 
					import { instructions } from './nodes';
 | 
				
			||||||
 | 
					import { CanvasContent } from './CanvasContent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function attachJobs(nodes, jobs: any[] = []): void {
 | 
					function attachJobs(nodes, jobs: any[] = []): void {
 | 
				
			||||||
  const nodesMap = new Map();
 | 
					  const nodesMap = new Map();
 | 
				
			||||||
  nodes.forEach((item) => nodesMap.set(item.id, item));
 | 
					  nodes.forEach((item) => {
 | 
				
			||||||
  const jobsMap = new Map();
 | 
					    item.jobs = [];
 | 
				
			||||||
  jobs.forEach((item) => jobsMap.set(item.nodeId, item));
 | 
					    nodesMap.set(item.id, item);
 | 
				
			||||||
  for (const node of nodesMap.values()) {
 | 
					  });
 | 
				
			||||||
    if (jobsMap.has(node.id)) {
 | 
					  jobs.forEach((item) => {
 | 
				
			||||||
      node.job = jobsMap.get(node.id);
 | 
					    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 (
 | 
				
			||||||
 | 
					    <ActionContext.Provider value={{ visible: Boolean(job), setVisible: setViewJob }}>
 | 
				
			||||||
 | 
					      <SchemaComponent
 | 
				
			||||||
 | 
					        schema={{
 | 
				
			||||||
 | 
					          type: 'void',
 | 
				
			||||||
 | 
					          properties: {
 | 
				
			||||||
 | 
					            [`${job?.id}-modal`]: {
 | 
				
			||||||
 | 
					              type: 'void',
 | 
				
			||||||
 | 
					              'x-decorator': 'Form',
 | 
				
			||||||
 | 
					              'x-decorator-props': {
 | 
				
			||||||
 | 
					                initialValue: job,
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              'x-component': 'Action.Modal',
 | 
				
			||||||
 | 
					              title: (
 | 
				
			||||||
 | 
					                <div className={nodeTitleClass}>
 | 
				
			||||||
 | 
					                  <Tag>{compile(instruction?.title)}</Tag>
 | 
				
			||||||
 | 
					                  <strong>{node.title}</strong>
 | 
				
			||||||
 | 
					                  <span className="workflow-node-id">#{node.id}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              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,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </ActionContext.Provider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ExecutionCanvas() {
 | 
					export function ExecutionCanvas() {
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
  const { data, loading } = useResourceActionContext();
 | 
					  const { data, loading } = useResourceActionContext();
 | 
				
			||||||
  const { setTitle } = useDocumentTitle();
 | 
					  const { setTitle } = useDocumentTitle();
 | 
				
			||||||
 | 
					  const [viewJob, setViewJob] = useState(null);
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const { workflow } = data?.data ?? {};
 | 
					    const { workflow } = data?.data ?? {};
 | 
				
			||||||
    setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
 | 
					    setTitle?.(`${workflow?.title ? `${workflow.title} - ` : ''}${lang('Execution history')}`);
 | 
				
			||||||
@ -58,6 +140,8 @@ export function ExecutionCanvas() {
 | 
				
			|||||||
        workflow: workflow.type ? workflow : null,
 | 
					        workflow: workflow.type ? workflow : null,
 | 
				
			||||||
        nodes,
 | 
					        nodes,
 | 
				
			||||||
        execution,
 | 
					        execution,
 | 
				
			||||||
 | 
					        viewJob,
 | 
				
			||||||
 | 
					        setViewJob
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div className="workflow-toolbar">
 | 
					      <div className="workflow-toolbar">
 | 
				
			||||||
@ -79,17 +163,8 @@ export function ExecutionCanvas() {
 | 
				
			|||||||
          <time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
 | 
					          <time>{str2moment(execution.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
 | 
				
			||||||
        </aside>
 | 
					        </aside>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="workflow-canvas">
 | 
					      <CanvasContent entry={entry} />
 | 
				
			||||||
        <TriggerConfig />
 | 
					      <JobModal />
 | 
				
			||||||
        <div className={branchBlockClass}>
 | 
					 | 
				
			||||||
          <Branch entry={entry} />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div className={cx(nodeCardClass)}>
 | 
					 | 
				
			||||||
          <div className={cx(nodeMetaClass)}>
 | 
					 | 
				
			||||||
            <Tag color="#333">{lang('End')}</Tag>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </FlowContext.Provider>
 | 
					    </FlowContext.Provider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -16,13 +16,12 @@ import {
 | 
				
			|||||||
} from '@nocobase/client';
 | 
					} from '@nocobase/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FlowContext, useFlowContext } from './FlowContext';
 | 
					import { FlowContext, useFlowContext } from './FlowContext';
 | 
				
			||||||
import { branchBlockClass, nodeCardClass, nodeMetaClass, workflowVersionDropdownClass } from './style';
 | 
					import { workflowVersionDropdownClass } from './style';
 | 
				
			||||||
import { TriggerConfig } from './triggers';
 | 
					 | 
				
			||||||
import { Branch } from './Branch';
 | 
					 | 
				
			||||||
import { executionSchema } from './schemas/executions';
 | 
					import { executionSchema } from './schemas/executions';
 | 
				
			||||||
import { ExecutionLink } from './ExecutionLink';
 | 
					import { ExecutionLink } from './ExecutionLink';
 | 
				
			||||||
import { lang } from './locale';
 | 
					import { lang } from './locale';
 | 
				
			||||||
import { linkNodes } from './utils';
 | 
					import { linkNodes } from './utils';
 | 
				
			||||||
 | 
					import { CanvasContent } from './CanvasContent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
 | 
					function ExecutionResourceProvider({ request, filter = {}, ...others }) {
 | 
				
			||||||
  const { workflow } = useFlowContext();
 | 
					  const { workflow } = useFlowContext();
 | 
				
			||||||
@ -214,17 +213,7 @@ export function WorkflowCanvas() {
 | 
				
			|||||||
          </ActionContext.Provider>
 | 
					          </ActionContext.Provider>
 | 
				
			||||||
        </aside>
 | 
					        </aside>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div className="workflow-canvas">
 | 
					      <CanvasContent entry={entry} />
 | 
				
			||||||
        <TriggerConfig />
 | 
					 | 
				
			||||||
        <div className={branchBlockClass}>
 | 
					 | 
				
			||||||
          <Branch entry={entry} />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div className={cx('end', nodeCardClass)}>
 | 
					 | 
				
			||||||
          <div className={cx(nodeMetaClass)}>
 | 
					 | 
				
			||||||
            <Tag color="#333">{lang('End')}</Tag>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </FlowContext.Provider>
 | 
					    </FlowContext.Provider>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -48,7 +48,7 @@ export default observer(({ value, disabled, onChange }: any) => {
 | 
				
			|||||||
      !['formula'].includes(field.type),
 | 
					      !['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 scope = useWorkflowVariableOptions();
 | 
				
			||||||
  const mergedDisabled = disabled || form.disabled;
 | 
					  const mergedDisabled = disabled || form.disabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -69,7 +69,7 @@ export default observer(({ value, disabled, onChange }: any) => {
 | 
				
			|||||||
      {fields.length ? (
 | 
					      {fields.length ? (
 | 
				
			||||||
        <CollectionProvider collection={getCollection(collectionName)}>
 | 
					        <CollectionProvider collection={getCollection(collectionName)}>
 | 
				
			||||||
          {fields
 | 
					          {fields
 | 
				
			||||||
            .filter((field) => field.name in value)
 | 
					            .filter((field) => value && field.name in value)
 | 
				
			||||||
            .map((field) => {
 | 
					            .map((field) => {
 | 
				
			||||||
              // constant for associations to use Input, others to use CollectionField
 | 
					              // constant for associations to use Input, others to use CollectionField
 | 
				
			||||||
              // dynamic values only support belongsTo/hasOne association, other association type should disable
 | 
					              // dynamic values only support belongsTo/hasOne association, other association type should disable
 | 
				
			||||||
 | 
				
			|||||||
@ -116,6 +116,15 @@ export default {
 | 
				
			|||||||
  'Continue after all branches succeeded': '全部分支都成功后才能继续',
 | 
					  'Continue after all branches succeeded': '全部分支都成功后才能继续',
 | 
				
			||||||
  'Continue after any branch succeeded': '任意分支成功后就继续',
 | 
					  'Continue after any branch succeeded': '任意分支成功后就继续',
 | 
				
			||||||
  'Continue after any branch succeeded, or exit after any branch failed': '任意分支成功继续,或失败后退出',
 | 
					  '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: '延时',
 | 
					  Delay: '延时',
 | 
				
			||||||
  Duration: '时长',
 | 
					  Duration: '时长',
 | 
				
			||||||
  'End Status': '到时状态',
 | 
					  'End Status': '到时状态',
 | 
				
			||||||
 | 
				
			|||||||
@ -170,7 +170,7 @@ export default {
 | 
				
			|||||||
    RadioWithTooltip,
 | 
					    RadioWithTooltip,
 | 
				
			||||||
    DynamicConfig,
 | 
					    DynamicConfig,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getOptions(config, types) {
 | 
					  useVariables(current, types) {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      types &&
 | 
					      types &&
 | 
				
			||||||
      !types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
 | 
					      !types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ export default {
 | 
				
			|||||||
    CollectionFieldset,
 | 
					    CollectionFieldset,
 | 
				
			||||||
    FieldsSelect,
 | 
					    FieldsSelect,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getOptions(config, types) {
 | 
					  useVariables({ config }, types) {
 | 
				
			||||||
    return useCollectionFieldOptions({
 | 
					    return useCollectionFieldOptions({
 | 
				
			||||||
      collection: config.collection,
 | 
					      collection: config.collection,
 | 
				
			||||||
      types,
 | 
					      types,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,10 +2,10 @@ import React, { useState, useContext } from 'react';
 | 
				
			|||||||
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
 | 
					import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
 | 
				
			||||||
import { css, cx } from '@emotion/css';
 | 
					import { css, cx } from '@emotion/css';
 | 
				
			||||||
import { ISchema, useForm } from '@formily/react';
 | 
					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 { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Registry, parse } from '@nocobase/utils/client';
 | 
					import { Registry, parse, str2moment } from '@nocobase/utils/client';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ActionContext,
 | 
					  ActionContext,
 | 
				
			||||||
  SchemaComponent,
 | 
					  SchemaComponent,
 | 
				
			||||||
@ -17,13 +17,14 @@ import {
 | 
				
			|||||||
  useResourceActionContext,
 | 
					  useResourceActionContext,
 | 
				
			||||||
} from '@nocobase/client';
 | 
					} 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 { AddButton } from '../AddButton';
 | 
				
			||||||
import { useFlowContext } from '../FlowContext';
 | 
					import { useFlowContext } from '../FlowContext';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import calculation from './calculation';
 | 
					import calculation from './calculation';
 | 
				
			||||||
import condition from './condition';
 | 
					import condition from './condition';
 | 
				
			||||||
import parallel from './parallel';
 | 
					import parallel from './parallel';
 | 
				
			||||||
 | 
					import loop from './loop';
 | 
				
			||||||
import delay from './delay';
 | 
					import delay from './delay';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import manual from './manual';
 | 
					import manual from './manual';
 | 
				
			||||||
@ -32,8 +33,8 @@ import query from './query';
 | 
				
			|||||||
import create from './create';
 | 
					import create from './create';
 | 
				
			||||||
import update from './update';
 | 
					import update from './update';
 | 
				
			||||||
import destroy from './destroy';
 | 
					import destroy from './destroy';
 | 
				
			||||||
import { JobStatusOptions, JobStatusOptionsMap } from '../constants';
 | 
					import { JobStatusOptionsMap } from '../constants';
 | 
				
			||||||
import { lang, NAMESPACE } from '../locale';
 | 
					import { NAMESPACE, lang } from '../locale';
 | 
				
			||||||
import request from './request';
 | 
					import request from './request';
 | 
				
			||||||
import { VariableOptions } from '../variable';
 | 
					import { VariableOptions } from '../variable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -48,16 +49,18 @@ export interface Instruction {
 | 
				
			|||||||
  components?: { [key: string]: any };
 | 
					  components?: { [key: string]: any };
 | 
				
			||||||
  render?(props): React.ReactNode;
 | 
					  render?(props): React.ReactNode;
 | 
				
			||||||
  endding?: boolean;
 | 
					  endding?: boolean;
 | 
				
			||||||
  getOptions?(config, types?): VariableOptions;
 | 
					  useVariables?(node, types?): VariableOptions;
 | 
				
			||||||
 | 
					  useScopeVariables?(node, types?): VariableOptions;
 | 
				
			||||||
  useInitializers?(node): SchemaInitializerItemOptions | null;
 | 
					  useInitializers?(node): SchemaInitializerItemOptions | null;
 | 
				
			||||||
  initializers?: { [key: string]: any };
 | 
					  initializers?: { [key: string]: any };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const instructions = new Registry<Instruction>();
 | 
					export const instructions = new Registry<Instruction>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					instructions.register('calculation', calculation);
 | 
				
			||||||
instructions.register('condition', condition);
 | 
					instructions.register('condition', condition);
 | 
				
			||||||
instructions.register('parallel', parallel);
 | 
					instructions.register('parallel', parallel);
 | 
				
			||||||
instructions.register('calculation', calculation);
 | 
					instructions.register('loop', loop);
 | 
				
			||||||
instructions.register('delay', delay);
 | 
					instructions.register('delay', delay);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
instructions.register('manual', manual);
 | 
					instructions.register('manual', manual);
 | 
				
			||||||
@ -112,6 +115,21 @@ export function useAvailableUpstreams(node) {
 | 
				
			|||||||
  return stack;
 | 
					  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 }) {
 | 
					export function Node({ data }) {
 | 
				
			||||||
  const instruction = instructions.get(data.type);
 | 
					  const instruction = instructions.get(data.type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -212,18 +230,28 @@ export function RemoveButton() {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function InnerJobButton({ job, ...props }) {
 | 
				
			||||||
 | 
					  const { icon, color } = JobStatusOptionsMap[job.status];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Button {...props} shape="circle" className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
 | 
				
			||||||
 | 
					      <Tag color={color}>{icon}</Tag>
 | 
				
			||||||
 | 
					    </Button>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function JobButton() {
 | 
					export function JobButton() {
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const { execution, setViewJob } = useFlowContext();
 | 
				
			||||||
  const { execution } = useFlowContext();
 | 
					  const { jobs } = useNodeContext() ?? {};
 | 
				
			||||||
  const { id, type, title, job } = useNodeContext() ?? {};
 | 
					 | 
				
			||||||
  if (!execution) {
 | 
					  if (!execution) {
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!job) {
 | 
					  if (!jobs.length) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <span
 | 
					      <span
 | 
				
			||||||
        className={cx(
 | 
					        className={cx(
 | 
				
			||||||
 | 
					          nodeJobButtonClass,
 | 
				
			||||||
          'workflow-node-job-button',
 | 
					          'workflow-node-job-button',
 | 
				
			||||||
          css`
 | 
					          css`
 | 
				
			||||||
            border: 2px solid #d9d9d9;
 | 
					            border: 2px solid #d9d9d9;
 | 
				
			||||||
@ -235,87 +263,45 @@ export function JobButton() {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const instruction = instructions.get(type);
 | 
					  function onOpenJob({ key }) {
 | 
				
			||||||
  const { value, icon, color } = JobStatusOptionsMap[job.status];
 | 
					    const job = jobs.find((item) => item.id == key);
 | 
				
			||||||
 | 
					    setViewJob(job);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return jobs.length > 1 ? (
 | 
				
			||||||
    <SchemaComponent
 | 
					    <Dropdown
 | 
				
			||||||
      schema={{
 | 
					      menu={{
 | 
				
			||||||
        type: 'void',
 | 
					        items: jobs.map((job) => {
 | 
				
			||||||
        properties: {
 | 
					          const { icon, color } = JobStatusOptionsMap[job.status];
 | 
				
			||||||
          [`${job.id}-button`]: {
 | 
					          return {
 | 
				
			||||||
            type: 'void',
 | 
					            key: job.id,
 | 
				
			||||||
            'x-component': 'Action',
 | 
					            label: (
 | 
				
			||||||
            'x-component-props': {
 | 
					              <div
 | 
				
			||||||
              title: <Tag color={color}>{icon}</Tag>,
 | 
					                className={css`
 | 
				
			||||||
              shape: 'circle',
 | 
					                  display: flex;
 | 
				
			||||||
              className: [
 | 
					                  gap: 0.5em;
 | 
				
			||||||
                'workflow-node-job-button',
 | 
					
 | 
				
			||||||
                css`
 | 
					                  time {
 | 
				
			||||||
                  .ant-tag {
 | 
					                    color: #999;
 | 
				
			||||||
                    padding: 0;
 | 
					                    font-size: 0.8em;
 | 
				
			||||||
                    width: 100%;
 | 
					 | 
				
			||||||
                    line-height: 18px;
 | 
					 | 
				
			||||||
                    margin-right: 0;
 | 
					 | 
				
			||||||
                    border-radius: 50%;
 | 
					 | 
				
			||||||
                  }
 | 
					                  }
 | 
				
			||||||
                `,
 | 
					                `}
 | 
				
			||||||
              ],
 | 
					              >
 | 
				
			||||||
            },
 | 
					                <span className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
 | 
				
			||||||
            properties: {
 | 
					                  <Tag color={color}>{icon}</Tag>
 | 
				
			||||||
              [`${job.id}-modal`]: {
 | 
					                </span>
 | 
				
			||||||
                type: 'void',
 | 
					                <time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
 | 
				
			||||||
                'x-decorator': 'Form',
 | 
					              </div>
 | 
				
			||||||
                'x-decorator-props': {
 | 
					            ),
 | 
				
			||||||
                  initialValue: job,
 | 
					          };
 | 
				
			||||||
                },
 | 
					        }),
 | 
				
			||||||
                'x-component': 'Action.Modal',
 | 
					        onClick: onOpenJob,
 | 
				
			||||||
                title: (
 | 
					 | 
				
			||||||
                  <div className={cx(nodeTitleClass)}>
 | 
					 | 
				
			||||||
                    <Tag>{compile(instruction.title)}</Tag>
 | 
					 | 
				
			||||||
                    <strong>{title}</strong>
 | 
					 | 
				
			||||||
                    <span className="workflow-node-id">#{id}</span>
 | 
					 | 
				
			||||||
                  </div>
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                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,
 | 
					 | 
				
			||||||
                  },
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    />
 | 
					    >
 | 
				
			||||||
 | 
					      <InnerJobButton job={jobs[jobs.length - 1]} />
 | 
				
			||||||
 | 
					    </Dropdown>
 | 
				
			||||||
 | 
					  ) : (
 | 
				
			||||||
 | 
					    <InnerJobButton job={jobs[0]} onClick={() => setViewJob(jobs[0])} />
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -353,7 +339,7 @@ export function NodeDefaultView(props) {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']);
 | 
					    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))) {
 | 
					      if ((Array.from(el.classList) as string[]).some((name: string) => whiteSet.has(name))) {
 | 
				
			||||||
        setEditingConfig(true);
 | 
					        setEditingConfig(true);
 | 
				
			||||||
        ev.stopPropagation();
 | 
					        ev.stopPropagation();
 | 
				
			||||||
@ -445,7 +431,6 @@ export function NodeDefaultView(props) {
 | 
				
			|||||||
                              min-width: 6em;
 | 
					                              min-width: 6em;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                          }
 | 
					                          }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                          .ant-input-affix-wrapper {
 | 
					                          .ant-input-affix-wrapper {
 | 
				
			||||||
                            &:not(.full-width) {
 | 
					                            &:not(.full-width) {
 | 
				
			||||||
                              .ant-input {
 | 
					                              .ant-input {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										162
									
								
								packages/plugins/workflow/src/client/nodes/loop.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								packages/plugins/workflow/src/client/nodes/loop.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 (
 | 
				
			||||||
 | 
					      <NodeDefaultView data={data}>
 | 
				
			||||||
 | 
					        <div className={cx(nodeSubtreeClass)}>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={cx(
 | 
				
			||||||
 | 
					              branchBlockClass,
 | 
				
			||||||
 | 
					              css`
 | 
				
			||||||
 | 
					                padding-left: 20em;
 | 
				
			||||||
 | 
					              `,
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <Branch from={data} entry={entry} branchIndex={entry?.branchIndex ?? 0} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className={cx(branchClass)}>
 | 
				
			||||||
 | 
					              <div className="workflow-branch-lines" />
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                className={cx(
 | 
				
			||||||
 | 
					                  addButtonClass,
 | 
				
			||||||
 | 
					                  css`
 | 
				
			||||||
 | 
					                    display: flex;
 | 
				
			||||||
 | 
					                    justify-content: center;
 | 
				
			||||||
 | 
					                    align-items: center;
 | 
				
			||||||
 | 
					                    position: absolute;
 | 
				
			||||||
 | 
					                    top: 50%;
 | 
				
			||||||
 | 
					                    transform: translateY(-50%);
 | 
				
			||||||
 | 
					                    width: 2em;
 | 
				
			||||||
 | 
					                    height: 6em;
 | 
				
			||||||
 | 
					                  `,
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <ArrowUpOutlined
 | 
				
			||||||
 | 
					                  className={css`
 | 
				
			||||||
 | 
					                    background-color: #f0f2f5;
 | 
				
			||||||
 | 
					                  `}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div
 | 
				
			||||||
 | 
					            className={css`
 | 
				
			||||||
 | 
					              position: relative;
 | 
				
			||||||
 | 
					              height: 2em;
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </NodeDefaultView>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  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') },
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -86,7 +86,7 @@ export default {
 | 
				
			|||||||
    ModeConfig,
 | 
					    ModeConfig,
 | 
				
			||||||
    AssigneesSelect,
 | 
					    AssigneesSelect,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getOptions(config, types) {
 | 
					  useVariables({ config }, types) {
 | 
				
			||||||
    const formKeys = Object.keys(config.forms ?? {});
 | 
					    const formKeys = Object.keys(config.forms ?? {});
 | 
				
			||||||
    if (!formKeys.length) {
 | 
					    if (!formKeys.length) {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
 | 
				
			|||||||
@ -14,15 +14,12 @@ export default {
 | 
				
			|||||||
  group: 'collection',
 | 
					  group: 'collection',
 | 
				
			||||||
  fieldset: {
 | 
					  fieldset: {
 | 
				
			||||||
    collection,
 | 
					    collection,
 | 
				
			||||||
    // multiple: {
 | 
					    multiple: {
 | 
				
			||||||
    //   type: 'boolean',
 | 
					      type: 'boolean',
 | 
				
			||||||
    //   title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`,
 | 
					      title: `{{t("Multiple records", { ns: "${NAMESPACE}" })}}`,
 | 
				
			||||||
    //   'x-decorator': 'FormItem',
 | 
					      'x-decorator': 'FormItem',
 | 
				
			||||||
    //   'x-component': 'Checkbox',
 | 
					      'x-component': 'Checkbox',
 | 
				
			||||||
    //   'x-component-props': {
 | 
					    },
 | 
				
			||||||
    //     disabled: true
 | 
					 | 
				
			||||||
    //   }
 | 
					 | 
				
			||||||
    // },
 | 
					 | 
				
			||||||
    params: {
 | 
					    params: {
 | 
				
			||||||
      type: 'object',
 | 
					      type: 'object',
 | 
				
			||||||
      properties: {
 | 
					      properties: {
 | 
				
			||||||
@ -45,7 +42,7 @@ export default {
 | 
				
			|||||||
    FilterDynamicComponent,
 | 
					    FilterDynamicComponent,
 | 
				
			||||||
    FieldsSelect,
 | 
					    FieldsSelect,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  getOptions(config, types) {
 | 
					  useVariables({ config }, types) {
 | 
				
			||||||
    return useCollectionFieldOptions({
 | 
					    return useCollectionFieldOptions({
 | 
				
			||||||
      collection: config.collection,
 | 
					      collection: config.collection,
 | 
				
			||||||
      types,
 | 
					      types,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,19 @@
 | 
				
			|||||||
import { css } from '@emotion/css';
 | 
					import { css } from '@emotion/css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const workflowPageClass = css`
 | 
					export const workflowPageClass = css`
 | 
				
			||||||
 | 
					  flex-grow: 1;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .workflow-toolbar {
 | 
					  .workflow-toolbar {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
    justify-content: space-between;
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
    padding: 0.5rem 1rem;
 | 
					    padding: 0.5rem 1rem;
 | 
				
			||||||
    background: #fff;
 | 
					    background: #fff;
 | 
				
			||||||
 | 
					    border-bottom: 1px solid #e7e7e7;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    header {
 | 
					    header {
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
@ -28,8 +35,9 @@ export const workflowPageClass = css`
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .workflow-canvas {
 | 
					  .workflow-canvas {
 | 
				
			||||||
    width: min-content;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    min-width: 100%;
 | 
					    overflow: auto;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-direction: column;
 | 
					    flex-direction: column;
 | 
				
			||||||
    align-items: center;
 | 
					    align-items: center;
 | 
				
			||||||
@ -95,11 +103,11 @@ export const branchClass = css`
 | 
				
			|||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  min-width: 20em;
 | 
				
			||||||
  padding: 0 2em;
 | 
					  padding: 0 2em;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .workflow-node-list {
 | 
					  .workflow-node-list {
 | 
				
			||||||
    flex-grow: 1;
 | 
					    flex-grow: 1;
 | 
				
			||||||
    min-width: 20em;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .workflow-branch-lines {
 | 
					  .workflow-branch-lines {
 | 
				
			||||||
@ -181,12 +189,8 @@ export const nodeCardClass = css`
 | 
				
			|||||||
    box-shadow: 0 0.25em 1em rgba(0, 100, 200, 0.25);
 | 
					    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 {
 | 
					  .workflow-node-remove-button {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
    right: 0.5em;
 | 
					    right: 0.5em;
 | 
				
			||||||
    top: 0.5em;
 | 
					    top: 0.5em;
 | 
				
			||||||
    color: #999;
 | 
					    color: #999;
 | 
				
			||||||
@ -202,21 +206,10 @@ export const nodeCardClass = css`
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .workflow-node-job-button {
 | 
					  > .workflow-node-job-button {
 | 
				
			||||||
    display: flex;
 | 
					    position: absolute;
 | 
				
			||||||
    top: 1em;
 | 
					    top: 1rem;
 | 
				
			||||||
    right: 1em;
 | 
					    right: 1rem;
 | 
				
			||||||
    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-input {
 | 
					  .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`
 | 
					export const nodeHeaderClass = css`
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ import {
 | 
				
			|||||||
  useResourceActionContext,
 | 
					  useResourceActionContext,
 | 
				
			||||||
} from '@nocobase/client';
 | 
					} from '@nocobase/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { nodeCardClass, nodeMetaClass, nodeTitleClass } from '../style';
 | 
					import { nodeCardClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style';
 | 
				
			||||||
import { useFlowContext } from '../FlowContext';
 | 
					import { useFlowContext } from '../FlowContext';
 | 
				
			||||||
import collection from './collection';
 | 
					import collection from './collection';
 | 
				
			||||||
import schedule from './schedule/';
 | 
					import schedule from './schedule/';
 | 
				
			||||||
@ -86,7 +86,7 @@ function TriggerExecution() {
 | 
				
			|||||||
        'x-component-props': {
 | 
					        'x-component-props': {
 | 
				
			||||||
          title: <InfoOutlined />,
 | 
					          title: <InfoOutlined />,
 | 
				
			||||||
          shape: 'circle',
 | 
					          shape: 'circle',
 | 
				
			||||||
          className: 'workflow-node-job-button',
 | 
					          className: cx(nodeJobButtonClass, 'workflow-node-job-button'),
 | 
				
			||||||
          type: 'primary',
 | 
					          type: 'primary',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        properties: {
 | 
					        properties: {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { useCollectionManager, useCompile } from '@nocobase/client';
 | 
					import { useCollectionManager, useCompile } from '@nocobase/client';
 | 
				
			||||||
import { useFlowContext } from './FlowContext';
 | 
					import { useFlowContext } from './FlowContext';
 | 
				
			||||||
import { NAMESPACE } from './locale';
 | 
					import { NAMESPACE } from './locale';
 | 
				
			||||||
import { instructions, useAvailableUpstreams, useNodeContext } from './nodes';
 | 
					import { instructions, useAvailableUpstreams, useNodeContext, useUpstreamScopes } from './nodes';
 | 
				
			||||||
import { triggers } from './triggers';
 | 
					import { triggers } from './triggers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type VariableOption = {
 | 
					export type VariableOption = {
 | 
				
			||||||
@ -13,17 +13,37 @@ export type VariableOption = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type VariableOptions = VariableOption[] | null;
 | 
					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}" })}}`,
 | 
					    title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
 | 
				
			||||||
    value: '$jobsMapByNodeId',
 | 
					    value: '$jobsMapByNodeId',
 | 
				
			||||||
    options(types) {
 | 
					    useOptions(current, types) {
 | 
				
			||||||
      const current = useNodeContext();
 | 
					 | 
				
			||||||
      const upstreams = useAvailableUpstreams(current);
 | 
					      const upstreams = useAvailableUpstreams(current);
 | 
				
			||||||
      const options: VariableOption[] = [];
 | 
					      const options: VariableOption[] = [];
 | 
				
			||||||
      upstreams.forEach((node) => {
 | 
					      upstreams.forEach((node) => {
 | 
				
			||||||
        const instruction = instructions.get(node.type);
 | 
					        const instruction = instructions.get(node.type);
 | 
				
			||||||
        const subOptions = instruction.getOptions?.(node.config, types);
 | 
					        const subOptions = instruction.useVariables?.(node, types);
 | 
				
			||||||
        if (subOptions) {
 | 
					        if (subOptions) {
 | 
				
			||||||
          options.push({
 | 
					          options.push({
 | 
				
			||||||
            key: node.id.toString(),
 | 
					            key: node.id.toString(),
 | 
				
			||||||
@ -39,7 +59,7 @@ const VariableTypes = [
 | 
				
			|||||||
  {
 | 
					  {
 | 
				
			||||||
    title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
 | 
					    title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
 | 
				
			||||||
    value: '$context',
 | 
					    value: '$context',
 | 
				
			||||||
    options(types) {
 | 
					    useOptions(current, types) {
 | 
				
			||||||
      const { workflow } = useFlowContext();
 | 
					      const { workflow } = useFlowContext();
 | 
				
			||||||
      const trigger = triggers.get(workflow.type);
 | 
					      const trigger = triggers.get(workflow.type);
 | 
				
			||||||
      return trigger?.getOptions?.(workflow.config, types) ?? null;
 | 
					      return trigger?.getOptions?.(workflow.config, types) ?? null;
 | 
				
			||||||
@ -48,7 +68,7 @@ const VariableTypes = [
 | 
				
			|||||||
  {
 | 
					  {
 | 
				
			||||||
    title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
 | 
					    title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
 | 
				
			||||||
    value: '$system',
 | 
					    value: '$system',
 | 
				
			||||||
    options(types) {
 | 
					    useOptions(current, types) {
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        ...(!types || types.includes('date')
 | 
					        ...(!types || types.includes('date')
 | 
				
			||||||
          ? [
 | 
					          ? [
 | 
				
			||||||
@ -137,8 +157,9 @@ export function filterTypedFields(fields, types, depth = 1) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function useWorkflowVariableOptions(types?) {
 | 
					export function useWorkflowVariableOptions(types?) {
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
 | 
					  const current = useNodeContext();
 | 
				
			||||||
  const options = VariableTypes.map((item: any) => {
 | 
					  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 {
 | 
					    return {
 | 
				
			||||||
      label: compile(item.title),
 | 
					      label: compile(item.title),
 | 
				
			||||||
      value: item.value,
 | 
					      value: item.value,
 | 
				
			||||||
@ -214,8 +235,9 @@ function useNormalizedFields(collectionName) {
 | 
				
			|||||||
export function useCollectionFieldOptions(options): VariableOption[] {
 | 
					export function useCollectionFieldOptions(options): VariableOption[] {
 | 
				
			||||||
  const { fields, collection, types, depth = 1 } = options;
 | 
					  const { fields, collection, types, depth = 1 } = options;
 | 
				
			||||||
  const compile = useCompile();
 | 
					  const compile = useCompile();
 | 
				
			||||||
  const normalizedFields = fields ?? useNormalizedFields(collection);
 | 
					  const normalizedFields = useNormalizedFields(collection);
 | 
				
			||||||
  const result: VariableOption[] = filterTypedFields(normalizedFields, types, depth)
 | 
					  const computedFields = fields ?? normalizedFields;
 | 
				
			||||||
 | 
					  const result: VariableOption[] = filterTypedFields(computedFields, types, depth)
 | 
				
			||||||
    .filter((field) => !isAssociationField(field) || depth)
 | 
					    .filter((field) => !isAssociationField(field) || depth)
 | 
				
			||||||
    .map((field) => {
 | 
					    .map((field) => {
 | 
				
			||||||
      const label = compile(field.uiSchema?.title || field.name);
 | 
					      const label = compile(field.uiSchema?.title || field.name);
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { parse } from '@nocobase/utils';
 | 
				
			|||||||
import { Transaction, Transactionable } from 'sequelize';
 | 
					import { Transaction, Transactionable } from 'sequelize';
 | 
				
			||||||
import Plugin from '.';
 | 
					import Plugin from '.';
 | 
				
			||||||
import { EXECUTION_STATUS, JOB_STATUS } from './constants';
 | 
					import { EXECUTION_STATUS, JOB_STATUS } from './constants';
 | 
				
			||||||
 | 
					import { Runner } from './instructions';
 | 
				
			||||||
import ExecutionModel from './models/Execution';
 | 
					import ExecutionModel from './models/Execution';
 | 
				
			||||||
import FlowNodeModel from './models/FlowNode';
 | 
					import FlowNodeModel from './models/FlowNode';
 | 
				
			||||||
import JobModel from './models/Job';
 | 
					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;
 | 
					    let job;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // call instruction to get result and status
 | 
					      // call instruction to get result and status
 | 
				
			||||||
@ -174,7 +175,7 @@ export default class Processor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) {
 | 
					    if (savedJob.status === JOB_STATUS.RESOLVED && node.downstream) {
 | 
				
			||||||
      // run next node
 | 
					      // 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);
 | 
					      return this.run(node.downstream, savedJob);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -194,7 +195,7 @@ export default class Processor {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // parent node should take over the control
 | 
					  // parent node should take over the control
 | 
				
			||||||
  public async end(node, job) {
 | 
					  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);
 | 
					    const parentNode = this.findBranchParentNode(node);
 | 
				
			||||||
    // no parent, means on main flow
 | 
					    // no parent, means on main flow
 | 
				
			||||||
    if (parentNode) {
 | 
					    if (parentNode) {
 | 
				
			||||||
@ -289,6 +290,15 @@ export default class Processor {
 | 
				
			|||||||
    return null;
 | 
					    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 {
 | 
					  findBranchParentJob(job: JobModel, node: FlowNodeModel): JobModel | null {
 | 
				
			||||||
    for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) {
 | 
					    for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) {
 | 
				
			||||||
      if (j.nodeId === node.id) {
 | 
					      if (j.nodeId === node.id) {
 | 
				
			||||||
@ -298,6 +308,18 @@ export default class Processor {
 | 
				
			|||||||
    return null;
 | 
					    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?) {
 | 
					  public getScope(node?) {
 | 
				
			||||||
    const systemFns = {};
 | 
					    const systemFns = {};
 | 
				
			||||||
    const scope = {
 | 
					    const scope = {
 | 
				
			||||||
@ -308,10 +330,21 @@ export default class Processor {
 | 
				
			|||||||
      systemFns[name] = fn.bind(scope);
 | 
					      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 {
 | 
					    return {
 | 
				
			||||||
      $context: this.execution.context,
 | 
					      $context: this.execution.context,
 | 
				
			||||||
      $jobsMapByNodeId: this.jobsMapByNodeId,
 | 
					      $jobsMapByNodeId: this.jobsMapByNodeId,
 | 
				
			||||||
      $system: systemFns,
 | 
					      $system: systemFns,
 | 
				
			||||||
 | 
					      $scopes,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,10 @@ export default {
 | 
				
			|||||||
      type: 'belongsTo',
 | 
					      type: 'belongsTo',
 | 
				
			||||||
      name: 'post',
 | 
					      name: 'post',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'text',
 | 
				
			||||||
 | 
					      name: 'content',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      type: 'integer',
 | 
					      type: 'integer',
 | 
				
			||||||
      name: 'status',
 | 
					      name: 'status',
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -80,12 +80,12 @@ function migrateConfig(config, oldToNew) {
 | 
				
			|||||||
      case 'array':
 | 
					      case 'array':
 | 
				
			||||||
        return value.map((item) => migrate(item));
 | 
					        return value.map((item) => migrate(item));
 | 
				
			||||||
      case 'string':
 | 
					      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));
 | 
					          const newNode = oldToNew.get(Number.parseInt(oldNodeId, 10));
 | 
				
			||||||
          if (!newNode) {
 | 
					          if (!newNode) {
 | 
				
			||||||
            throw new Error('node configurated for result is not existed');
 | 
					            throw new Error('node configurated for result is not existed');
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          return `{{$jobsMapByNodeId.${newNode.id}`;
 | 
					          return `${jobVar}.${newNode.id}`;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return value;
 | 
					        return value;
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ export default {
 | 
				
			|||||||
  async run(node: FlowNodeModel, prevJob, processor: Processor) {
 | 
					  async run(node: FlowNodeModel, prevJob, processor: Processor) {
 | 
				
			||||||
    const { dynamic = false } = <CalculationConfig>node.config || {};
 | 
					    const { dynamic = false } = <CalculationConfig>node.config || {};
 | 
				
			||||||
    let { engine = 'math.js', expression = '' } = node.config;
 | 
					    let { engine = 'math.js', expression = '' } = node.config;
 | 
				
			||||||
    let scope = processor.getScope();
 | 
					    let scope = processor.getScope(node);
 | 
				
			||||||
    if (dynamic) {
 | 
					    if (dynamic) {
 | 
				
			||||||
      const parsed = parse(dynamic)(scope) ?? {};
 | 
					      const parsed = parse(dynamic)(scope) ?? {};
 | 
				
			||||||
      engine = parsed.engine;
 | 
					      engine = parsed.engine;
 | 
				
			||||||
 | 
				
			|||||||
@ -122,7 +122,7 @@ export default {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      result = evaluator
 | 
					      result = evaluator
 | 
				
			||||||
        ? evaluator(expression, processor.getScope())
 | 
					        ? evaluator(expression, processor.getScope())
 | 
				
			||||||
        : logicCalculate(processor.getParsedValue(calculation));
 | 
					        : logicCalculate(processor.getParsedValue(calculation, node));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        result: e.toString(),
 | 
					        result: e.toString(),
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ export default {
 | 
				
			|||||||
    const { collection, params: { appends = [], ...params } = {} } = node.config;
 | 
					    const { collection, params: { appends = [], ...params } = {} } = node.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
 | 
					    const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
 | 
				
			||||||
    const options = processor.getParsedValue(params);
 | 
					    const options = processor.getParsedValue(params, node);
 | 
				
			||||||
    const result = await repository.create({
 | 
					    const result = await repository.create({
 | 
				
			||||||
      ...options,
 | 
					      ...options,
 | 
				
			||||||
      context: {
 | 
					      context: {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ export default {
 | 
				
			|||||||
    const { collection, params = {} } = node.config;
 | 
					    const { collection, params = {} } = node.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
 | 
					    const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
 | 
				
			||||||
    const options = processor.getParsedValue(params);
 | 
					    const options = processor.getParsedValue(params, node);
 | 
				
			||||||
    const result = await repo.destroy({
 | 
					    const result = await repo.destroy({
 | 
				
			||||||
      ...options,
 | 
					      ...options,
 | 
				
			||||||
      context: {
 | 
					      context: {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,6 +24,8 @@ export interface Instruction {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // for start node in main flow (or branch) to resume when manual sub branch triggered
 | 
					  // for start node in main flow (or branch) to resume when manual sub branch triggered
 | 
				
			||||||
  resume?: Runner;
 | 
					  resume?: Runner;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getScope?: (node: FlowNodeModel, job: any, processor: Processor) => any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type InstructionConstructor<T> = { new (p: Plugin): T };
 | 
					type InstructionConstructor<T> = { new (p: Plugin): T };
 | 
				
			||||||
@ -35,6 +37,7 @@ export default function <T extends Instruction>(plugin, more: { [key: string]: T
 | 
				
			|||||||
    'calculation',
 | 
					    'calculation',
 | 
				
			||||||
    'condition',
 | 
					    'condition',
 | 
				
			||||||
    'parallel',
 | 
					    'parallel',
 | 
				
			||||||
 | 
					    'loop',
 | 
				
			||||||
    'delay',
 | 
					    'delay',
 | 
				
			||||||
    'manual',
 | 
					    'manual',
 | 
				
			||||||
    'query',
 | 
					    'query',
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										100
									
								
								packages/plugins/workflow/src/server/instructions/loop.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								packages/plugins/workflow/src/server/instructions/loop.ts
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -75,16 +75,19 @@ export default {
 | 
				
			|||||||
    const { mode = PARALLEL_MODE.ALL } = node.config;
 | 
					    const { mode = PARALLEL_MODE.ALL } = node.config;
 | 
				
			||||||
    await branches.reduce(
 | 
					    await branches.reduce(
 | 
				
			||||||
      (promise: Promise<any>, branch, i) =>
 | 
					      (promise: Promise<any>, branch, i) =>
 | 
				
			||||||
        promise.then((previous) => {
 | 
					        promise.then(async (previous) => {
 | 
				
			||||||
          if (i && !Modes[mode].next(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(),
 | 
					      Promise.resolve(),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return processor.end(node, job);
 | 
					    return null;
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async resume(node: FlowNodeModel, branchJob, processor: Processor) {
 | 
					  async resume(node: FlowNodeModel, branchJob, processor: Processor) {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ export default {
 | 
				
			|||||||
    const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
 | 
					    const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
 | 
					    const repo = (<typeof FlowNodeModel>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, {
 | 
					    const result = await (multiple ? repo.find : repo.findOne).call(repo, {
 | 
				
			||||||
      ...options,
 | 
					      ...options,
 | 
				
			||||||
      context: {
 | 
					      context: {
 | 
				
			||||||
 | 
				
			|||||||
@ -51,7 +51,7 @@ export default class implements Instruction {
 | 
				
			|||||||
      nodeId: node.id,
 | 
					      nodeId: node.id,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const config = processor.getParsedValue(node.config) as RequestConfig;
 | 
					    const config = processor.getParsedValue(node.config, node) as RequestConfig;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    request(config)
 | 
					    request(config)
 | 
				
			||||||
      .then((response) => {
 | 
					      .then((response) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ export default {
 | 
				
			|||||||
    const { collection, multiple = false, params = {} } = node.config;
 | 
					    const { collection, multiple = false, params = {} } = node.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
 | 
					    const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
 | 
				
			||||||
    const options = processor.getParsedValue(params);
 | 
					    const options = processor.getParsedValue(params, node);
 | 
				
			||||||
    const result = await repo.update({
 | 
					    const result = await repo.update({
 | 
				
			||||||
      ...options,
 | 
					      ...options,
 | 
				
			||||||
      context: {
 | 
					      context: {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										223
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										223
									
								
								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"
 | 
					  resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
 | 
				
			||||||
  integrity sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==
 | 
					  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:
 | 
					antd@^4.24.8:
 | 
				
			||||||
  version "4.24.8"
 | 
					  version "4.24.8"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/antd/-/antd-4.24.8.tgz#22f34de6857556868780dfa5fe7b374b0b71b517"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
 | 
				
			||||||
  integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
 | 
					  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:
 | 
					memory-fs@^0.5.0:
 | 
				
			||||||
  version "0.5.0"
 | 
					  version "0.5.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c"
 | 
					  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"
 | 
					    rc-util "^5.26.0"
 | 
				
			||||||
    resize-observer-polyfill "^1.5.1"
 | 
					    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:
 | 
					rc-cascader@~3.7.0:
 | 
				
			||||||
  version "3.7.2"
 | 
					  version "3.7.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.7.2.tgz#447f2725add7953dee205d1cf59f58a8317bf5f7"
 | 
					  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"
 | 
					    "@babel/runtime" "^7.10.1"
 | 
				
			||||||
    classnames "^2.2.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:
 | 
					rc-collapse@~3.4.2:
 | 
				
			||||||
  version "3.4.2"
 | 
					  version "3.4.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-collapse/-/rc-collapse-3.4.2.tgz#1310be7ad4cd0dcfc622c45f6c3b5ffdee403ad7"
 | 
					  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"
 | 
					    rc-util "^5.2.1"
 | 
				
			||||||
    shallowequal "^1.1.0"
 | 
					    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:
 | 
					rc-dialog@~9.0.0, rc-dialog@~9.0.2:
 | 
				
			||||||
  version "9.0.2"
 | 
					  version "9.0.2"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-9.0.2.tgz#aadfebdeba145f256c1fac9b9f509f893cdbb5b8"
 | 
					  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-motion "^2.3.0"
 | 
				
			||||||
    rc-util "^5.21.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:
 | 
					rc-drawer@~6.1.0:
 | 
				
			||||||
  version "6.1.5"
 | 
					  version "6.1.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-6.1.5.tgz#c4137b944c16b7c179d0dba6f06ebe54f9311ec8"
 | 
					  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-motion "^2.6.2"
 | 
				
			||||||
    rc-util "^5.0.6"
 | 
					    rc-util "^5.0.6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rc-image@~5.7.0:
 | 
					rc-input-number@~7.3.9:
 | 
				
			||||||
  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:
 | 
					 | 
				
			||||||
  version "7.3.11"
 | 
					  version "7.3.11"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.3.11.tgz#c7089705a220e1a59ba974fabf89693e00dd2442"
 | 
					  resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.3.11.tgz#c7089705a220e1a59ba974fabf89693e00dd2442"
 | 
				
			||||||
  integrity sha512-aMWPEjFeles6PQnMqP5eWpxzsvHm9rh1jQOWXExUEIxhX62Fyl/ptifLHOn17+waDG1T/YUb6flfJbvwRhHrbA==
 | 
					  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"
 | 
					    classnames "^2.2.5"
 | 
				
			||||||
    rc-util "^5.23.0"
 | 
					    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:
 | 
					rc-input@~0.1.4:
 | 
				
			||||||
  version "0.1.4"
 | 
					  version "0.1.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-input/-/rc-input-0.1.4.tgz#45cb4ba209ae6cc835a2acb8629d4f8f0cb347e0"
 | 
					  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-trigger "^5.0.4"
 | 
				
			||||||
    rc-util "^5.22.5"
 | 
					    rc-util "^5.22.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rc-mentions@~1.9.1:
 | 
					rc-menu@~9.6.0:
 | 
				
			||||||
  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:
 | 
					 | 
				
			||||||
  version "9.6.4"
 | 
					  version "9.6.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-9.6.4.tgz#033e7b8848c17a09a81b68b8d4c3fa457605f4f6"
 | 
					  resolved "https://registry.yarnpkg.com/rc-menu/-/rc-menu-9.6.4.tgz#033e7b8848c17a09a81b68b8d4c3fa457605f4f6"
 | 
				
			||||||
  integrity sha512-6DiNAjxjVIPLZXHffXxxcyE15d4isRL7iQ1ru4MqYDH2Cqc5bW96wZOdMydFtGLyDdnmEQ9jVvdCE9yliGvzkw==
 | 
					  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-resize-observer "^1.0.0"
 | 
				
			||||||
    rc-util "^5.19.2"
 | 
					    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:
 | 
					rc-pagination@~3.2.0:
 | 
				
			||||||
  version "3.2.0"
 | 
					  version "3.2.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.2.0.tgz#4f2fdba9fdac0f48e5c9fb1141973818138af7e1"
 | 
					  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"
 | 
					    "@babel/runtime" "^7.10.1"
 | 
				
			||||||
    classnames "^2.2.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:
 | 
					rc-picker@~2.7.0:
 | 
				
			||||||
  version "2.7.0"
 | 
					  version "2.7.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.7.0.tgz#3c19881da27a0c5ee4c7e7504e21b552bd43a94c"
 | 
					  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"
 | 
					    rc-util "^5.4.0"
 | 
				
			||||||
    shallowequal "^1.1.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:
 | 
					rc-progress@~3.4.1:
 | 
				
			||||||
  version "3.4.1"
 | 
					  version "3.4.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-progress/-/rc-progress-3.4.1.tgz#a9ffe099e88a4fc03afb09d8603162bf0760d743"
 | 
					  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-motion "^2.4.4"
 | 
				
			||||||
    rc-util "^5.17.0"
 | 
					    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"
 | 
					  version "14.1.17"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.1.17.tgz#e623eabeaa0dd117d5a63354e6ddaaa118abc5ee"
 | 
					  resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-14.1.17.tgz#e623eabeaa0dd117d5a63354e6ddaaa118abc5ee"
 | 
				
			||||||
  integrity sha512-6qQhMqtoUkkboRqXKKFRR5Nu1mrnw2mC1uxIBIczg7aiJ94qCZBg4Ww8OLT9f4xdyCgbFSGh6r3yB9EBsjoHGA==
 | 
					  integrity sha512-6qQhMqtoUkkboRqXKKFRR5Nu1mrnw2mC1uxIBIczg7aiJ94qCZBg4Ww8OLT9f4xdyCgbFSGh6r3yB9EBsjoHGA==
 | 
				
			||||||
@ -20612,15 +20452,6 @@ rc-slider@~10.0.0:
 | 
				
			|||||||
    rc-util "^5.18.1"
 | 
					    rc-util "^5.18.1"
 | 
				
			||||||
    shallowequal "^1.1.0"
 | 
					    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:
 | 
					rc-steps@~5.0.0-alpha.2:
 | 
				
			||||||
  version "5.0.0"
 | 
					  version "5.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-steps/-/rc-steps-5.0.0.tgz#2e2403f2dd69eb3966d65f461f7e3a8ee1ef69fe"
 | 
					  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"
 | 
					    classnames "^2.2.1"
 | 
				
			||||||
    rc-util "^5.0.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:
 | 
					rc-table@~7.26.0:
 | 
				
			||||||
  version "7.26.0"
 | 
					  version "7.26.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.26.0.tgz#9d517e7fa512e7571fdcc453eb1bf19edfac6fbc"
 | 
					  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"
 | 
					    rc-util "^5.22.5"
 | 
				
			||||||
    shallowequal "^1.1.0"
 | 
					    shallowequal "^1.1.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rc-tabs@^11.7.1, rc-tabs@~11.16.0:
 | 
					rc-tabs@^11.7.1:
 | 
				
			||||||
  version "11.16.1"
 | 
					  version "11.16.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-11.16.1.tgz#7c57b6a092d9d0e2df54413b0319f195c27214a9"
 | 
					  resolved "https://registry.yarnpkg.com/rc-tabs/-/rc-tabs-11.16.1.tgz#7c57b6a092d9d0e2df54413b0319f195c27214a9"
 | 
				
			||||||
  integrity sha512-bR7Dap23YyfzZQwtKomhiFEFzZuE7WaKWo+ypNRSGB9PDKSc6tM12VP8LWYkvmmQHthgwP0WRN8nFbSJWuqLYw==
 | 
					  integrity sha512-bR7Dap23YyfzZQwtKomhiFEFzZuE7WaKWo+ypNRSGB9PDKSc6tM12VP8LWYkvmmQHthgwP0WRN8nFbSJWuqLYw==
 | 
				
			||||||
@ -20686,17 +20506,6 @@ rc-tabs@~12.5.6:
 | 
				
			|||||||
    rc-resize-observer "^1.0.0"
 | 
					    rc-resize-observer "^1.0.0"
 | 
				
			||||||
    rc-util "^5.16.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:
 | 
					rc-textarea@^0.4.0, rc-textarea@~0.4.5:
 | 
				
			||||||
  version "0.4.7"
 | 
					  version "0.4.7"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-textarea/-/rc-textarea-0.4.7.tgz#627f662d46f99e0059d1c1ebc8db40c65339fe90"
 | 
					  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"
 | 
					    classnames "^2.3.1"
 | 
				
			||||||
    rc-trigger "^5.0.0"
 | 
					    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:
 | 
					rc-tree-select@~5.5.0:
 | 
				
			||||||
  version "5.5.5"
 | 
					  version "5.5.5"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-5.5.5.tgz#d28b3b45da1e820cd21762ba0ee93c19429bb369"
 | 
					  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-util "^5.16.1"
 | 
				
			||||||
    rc-virtual-list "^3.4.8"
 | 
					    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:
 | 
					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"
 | 
					  version "5.3.4"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.3.4.tgz#6b4b26e32825677c837d1eb4d7085035eecf9a61"
 | 
					  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"
 | 
					    classnames "^2.2.5"
 | 
				
			||||||
    rc-util "^5.2.0"
 | 
					    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"
 | 
					  version "5.29.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37"
 | 
					  resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.29.3.tgz#dc02b7b2103468e9fdf14e0daa58584f47898e37"
 | 
				
			||||||
  integrity sha512-wX6ZwQTzY2v7phJBquN4mSEIFR0E0qumlENx0zjENtDvoVSq2s7cR95UidKRO1hOHfDsecsfM9D1gO4Kebs7fA==
 | 
					  integrity sha512-wX6ZwQTzY2v7phJBquN4mSEIFR0E0qumlENx0zjENtDvoVSq2s7cR95UidKRO1hOHfDsecsfM9D1gO4Kebs7fA==
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user