refactor(plugin-workflow): change to use node key for variables (#2909)
* refactor(plugin-workflow): change to use node key for variables * refactor(plugin-workflow): add node key to node panel * fix(plugin-workflow): fix locale * fix(plugin-workflow): fix nodes variables * fix(plugin-workflow): fix migration * fix(plugin-workflow): fix migration and manual detail block
This commit is contained in:
		
							parent
							
								
									b8bc32f421
								
							
						
					
					
						commit
						a845dc0937
					
				@ -32,6 +32,7 @@ function attachJobs(nodes, jobs: any[] = []): void {
 | 
				
			|||||||
    node.jobs.push(item);
 | 
					    node.jobs.push(item);
 | 
				
			||||||
    item.node = {
 | 
					    item.node = {
 | 
				
			||||||
      id: node.id,
 | 
					      id: node.id,
 | 
				
			||||||
 | 
					      key: node.key,
 | 
				
			||||||
      title: node.title,
 | 
					      title: node.title,
 | 
				
			||||||
      type: node.type,
 | 
					      type: node.type,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ function Initializer({ node, resultTitle, insert, ...props }) {
 | 
				
			|||||||
              'x-component': 'ValueBlock.Result',
 | 
					              'x-component': 'ValueBlock.Result',
 | 
				
			||||||
              'x-component-props': {
 | 
					              'x-component-props': {
 | 
				
			||||||
                // NOTE: as same format as other reference for migration of revision
 | 
					                // NOTE: as same format as other reference for migration of revision
 | 
				
			||||||
                dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
 | 
					                dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -49,7 +49,10 @@ function Result({ dataSource }) {
 | 
				
			|||||||
    return field.title;
 | 
					    return field.title;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const result = parse(dataSource)({
 | 
					  const result = parse(dataSource)({
 | 
				
			||||||
    $jobsMapByNodeId: (execution.jobs ?? []).reduce((map, job) => Object.assign(map, { [job.nodeId]: job.result }), {}),
 | 
					    $jobsMapByNodeKey: (execution.jobs ?? []).reduce(
 | 
				
			||||||
 | 
					      (map, job) => Object.assign(map, { [job.nodeId]: job.result }),
 | 
				
			||||||
 | 
					      {},
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
 | 
				
			|||||||
@ -346,7 +346,7 @@ export default {
 | 
				
			|||||||
    ValueBlock,
 | 
					    ValueBlock,
 | 
				
			||||||
    AssociatedConfig,
 | 
					    AssociatedConfig,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
 | 
					  useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) {
 | 
				
			||||||
    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)))
 | 
				
			||||||
@ -354,7 +354,7 @@ export default {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      [fieldNames.value]: `${id}`,
 | 
					      [fieldNames.value]: key,
 | 
				
			||||||
      [fieldNames.label]: title,
 | 
					      [fieldNames.label]: title,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,9 @@
 | 
				
			|||||||
import { FormItem, FormLayout } from '@formily/antd-v5';
 | 
					import { FormItem, FormLayout } from '@formily/antd-v5';
 | 
				
			||||||
import { SchemaInitializerItemOptions, Variable, css, defaultFieldNames, useCollectionManager } from '@nocobase/client';
 | 
					import { SchemaInitializerItemOptions, Variable, css, defaultFieldNames, useCollectionManager } from '@nocobase/client';
 | 
				
			||||||
import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client';
 | 
					import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client';
 | 
				
			||||||
import { parse } from '@nocobase/utils/client';
 | 
					 | 
				
			||||||
import { Radio } from 'antd';
 | 
					import { Radio } from 'antd';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { useFlowContext } from '../FlowContext';
 | 
					 | 
				
			||||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
 | 
					import { RadioWithTooltip } from '../components/RadioWithTooltip';
 | 
				
			||||||
import { ValueBlock } from '../components/ValueBlock';
 | 
					import { ValueBlock } from '../components/ValueBlock';
 | 
				
			||||||
import { renderEngineReference } from '../components/renderEngineReference';
 | 
					import { renderEngineReference } from '../components/renderEngineReference';
 | 
				
			||||||
@ -167,32 +165,11 @@ export default {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
      return <Variable.Input scope={scope} {...props} />;
 | 
					      return <Variable.Input scope={scope} {...props} />;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    CalculationResult({ dataSource }) {
 | 
					 | 
				
			||||||
      const { execution } = useFlowContext();
 | 
					 | 
				
			||||||
      if (!execution) {
 | 
					 | 
				
			||||||
        return lang('Calculation result');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const result = parse(dataSource)({
 | 
					 | 
				
			||||||
        $jobsMapByNodeId: (execution.jobs ?? []).reduce(
 | 
					 | 
				
			||||||
          (map, job) => Object.assign(map, { [job.nodeId]: job.result }),
 | 
					 | 
				
			||||||
          {},
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return (
 | 
					 | 
				
			||||||
        <pre
 | 
					 | 
				
			||||||
          className={css`
 | 
					 | 
				
			||||||
            margin: 0;
 | 
					 | 
				
			||||||
          `}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {JSON.stringify(result, null, 2)}
 | 
					 | 
				
			||||||
        </pre>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    RadioWithTooltip,
 | 
					    RadioWithTooltip,
 | 
				
			||||||
    DynamicConfig,
 | 
					    DynamicConfig,
 | 
				
			||||||
 | 
					    ValueBlock,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
 | 
					  useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) {
 | 
				
			||||||
    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)))
 | 
				
			||||||
@ -200,7 +177,7 @@ export default {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      [fieldNames.value]: `${id}`,
 | 
					      [fieldNames.value]: key,
 | 
				
			||||||
      [fieldNames.label]: title,
 | 
					      [fieldNames.label]: title,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
				
			|||||||
@ -44,13 +44,12 @@ export default {
 | 
				
			|||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    CollectionFieldset,
 | 
					    CollectionFieldset,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  useVariables({ id, title, config }, options) {
 | 
					  useVariables({ key: name, title, config }, options) {
 | 
				
			||||||
    const compile = useCompile();
 | 
					    const compile = useCompile();
 | 
				
			||||||
    const { getCollectionFields } = useCollectionManager();
 | 
					    const { getCollectionFields } = useCollectionManager();
 | 
				
			||||||
    // const depth = config?.params?.appends?.length
 | 
					    // const depth = config?.params?.appends?.length
 | 
				
			||||||
    //   ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
 | 
					    //   ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
 | 
				
			||||||
    //   : 0;
 | 
					    //   : 0;
 | 
				
			||||||
    const name = `${id}`;
 | 
					 | 
				
			||||||
    const [result] = getCollectionFieldOptions({
 | 
					    const [result] = getCollectionFieldOptions({
 | 
				
			||||||
      // collection: config.collection,
 | 
					      // collection: config.collection,
 | 
				
			||||||
      // depth: options?.depth ?? depth,
 | 
					      // depth: options?.depth ?? depth,
 | 
				
			||||||
@ -83,7 +82,7 @@ export default {
 | 
				
			|||||||
      title: node.title ?? `#${node.id}`,
 | 
					      title: node.title ?? `#${node.id}`,
 | 
				
			||||||
      component: CollectionBlockInitializer,
 | 
					      component: CollectionBlockInitializer,
 | 
				
			||||||
      collection: node.config.collection,
 | 
					      collection: node.config.collection,
 | 
				
			||||||
      dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
 | 
					      dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  initializers: {},
 | 
					  initializers: {},
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ import {
 | 
				
			|||||||
  useResourceActionContext,
 | 
					  useResourceActionContext,
 | 
				
			||||||
} from '@nocobase/client';
 | 
					} from '@nocobase/client';
 | 
				
			||||||
import { Registry, parse, str2moment } from '@nocobase/utils/client';
 | 
					import { Registry, parse, str2moment } from '@nocobase/utils/client';
 | 
				
			||||||
import { App, Button, Dropdown, Input, Tag, message } from 'antd';
 | 
					import { App, Button, Dropdown, Input, Tag, Tooltip, message } from 'antd';
 | 
				
			||||||
import React, { useContext, useState } from 'react';
 | 
					import React, { useContext, useState } from 'react';
 | 
				
			||||||
import { useTranslation } from 'react-i18next';
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
import { AddButton } from '../AddButton';
 | 
					import { AddButton } from '../AddButton';
 | 
				
			||||||
@ -199,7 +199,7 @@ export function RemoveButton() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const template = parse(node.config);
 | 
					      const template = parse(node.config);
 | 
				
			||||||
      const refs = template.parameters.filter(
 | 
					      const refs = template.parameters.filter(
 | 
				
			||||||
        ({ key }) => key.startsWith(`$jobsMapByNodeId.${current.id}.`) || key === `$jobsMapByNodeId.${current.id}`,
 | 
					        ({ key }) => key.startsWith(`$jobsMapByNodeKey.${current.key}.`) || key === `$jobsMapByNodeKey.${current.key}`,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return refs.length;
 | 
					      return refs.length;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -371,7 +371,33 @@ export function NodeDefaultView(props) {
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
                [`${instruction.type}_${data.id}`]: {
 | 
					                [`${instruction.type}_${data.id}`]: {
 | 
				
			||||||
                  type: 'void',
 | 
					                  type: 'void',
 | 
				
			||||||
                  title: data.title,
 | 
					                  title: (
 | 
				
			||||||
 | 
					                    <div
 | 
				
			||||||
 | 
					                      className={css`
 | 
				
			||||||
 | 
					                        display: flex;
 | 
				
			||||||
 | 
					                        justify-content: space-between;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        strong {
 | 
				
			||||||
 | 
					                          font-weight: bold;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        .ant-tag {
 | 
				
			||||||
 | 
					                          margin-inline-end: 0;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        code {
 | 
				
			||||||
 | 
					                          font-weight: normal;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                      `}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <strong>{data.title}</strong>
 | 
				
			||||||
 | 
					                      <Tooltip title={lang('Variable key of node')}>
 | 
				
			||||||
 | 
					                        <Tag>
 | 
				
			||||||
 | 
					                          <code>{data.key}</code>
 | 
				
			||||||
 | 
					                        </Tag>
 | 
				
			||||||
 | 
					                      </Tooltip>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
                  'x-component': 'Action.Drawer',
 | 
					                  'x-component': 'Action.Drawer',
 | 
				
			||||||
                  'x-decorator': 'Form',
 | 
					                  'x-decorator': 'Form',
 | 
				
			||||||
                  'x-decorator-props': {
 | 
					                  'x-decorator-props': {
 | 
				
			||||||
 | 
				
			|||||||
@ -118,7 +118,7 @@ export default {
 | 
				
			|||||||
    // const current = useNodeContext();
 | 
					    // const current = useNodeContext();
 | 
				
			||||||
    // const upstreams = useAvailableUpstreams(current);
 | 
					    // const upstreams = useAvailableUpstreams(current);
 | 
				
			||||||
    // find target data model by path described in `config.target`
 | 
					    // find target data model by path described in `config.target`
 | 
				
			||||||
    // 1. get options from $context/$jobsMapByNodeId
 | 
					    // 1. get options from $context/$jobsMapByNodeKey
 | 
				
			||||||
    // 2. route to sub-options and use as loop target options
 | 
					    // 2. route to sub-options and use as loop target options
 | 
				
			||||||
    let targetOption: VariableOption = {
 | 
					    let targetOption: VariableOption = {
 | 
				
			||||||
      key: 'item',
 | 
					      key: 'item',
 | 
				
			||||||
 | 
				
			|||||||
@ -13,12 +13,17 @@ import { useFlowContext } from '../../FlowContext';
 | 
				
			|||||||
import { parse } from '@nocobase/utils/client';
 | 
					import { parse } from '@nocobase/utils/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function useFlowContextData(dataSource) {
 | 
					function useFlowContextData(dataSource) {
 | 
				
			||||||
  const { execution } = useFlowContext();
 | 
					  const { execution, nodes } = useFlowContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const nodesKeyMap = useMemo(() => {
 | 
				
			||||||
 | 
					    return nodes.reduce((map, node) => Object.assign(map, { [node.id]: node.key }), {});
 | 
				
			||||||
 | 
					  }, [nodes]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const data = useMemo(
 | 
					  const data = useMemo(
 | 
				
			||||||
    () => ({
 | 
					    () => ({
 | 
				
			||||||
      $context: execution?.context,
 | 
					      $context: execution?.context,
 | 
				
			||||||
      $jobsMapByNodeId: (execution?.jobs ?? []).reduce(
 | 
					      $jobsMapByNodeKey: (execution?.jobs ?? []).reduce(
 | 
				
			||||||
        (map, job) => Object.assign(map, { [job.nodeId]: job.result }),
 | 
					        (map, job) => Object.assign(map, { [nodesKeyMap[job.nodeId]]: job.result }),
 | 
				
			||||||
        {},
 | 
					        {},
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 | 
				
			|||||||
@ -85,7 +85,7 @@ export default {
 | 
				
			|||||||
    ModeConfig,
 | 
					    ModeConfig,
 | 
				
			||||||
    AssigneesSelect,
 | 
					    AssigneesSelect,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  useVariables({ id, title, config }, { types, fieldNames = defaultFieldNames }) {
 | 
					  useVariables({ key, title, config }, { types, fieldNames = defaultFieldNames }) {
 | 
				
			||||||
    const compile = useCompile();
 | 
					    const compile = useCompile();
 | 
				
			||||||
    const { getCollectionFields } = useCollectionManager();
 | 
					    const { getCollectionFields } = useCollectionManager();
 | 
				
			||||||
    const formKeys = Object.keys(config.forms ?? {});
 | 
					    const formKeys = Object.keys(config.forms ?? {});
 | 
				
			||||||
@ -119,7 +119,7 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return options.length
 | 
					    return options.length
 | 
				
			||||||
      ? {
 | 
					      ? {
 | 
				
			||||||
          [fieldNames.value]: `${id}`,
 | 
					          [fieldNames.value]: key,
 | 
				
			||||||
          [fieldNames.label]: title,
 | 
					          [fieldNames.label]: title,
 | 
				
			||||||
          [fieldNames.children]: options,
 | 
					          [fieldNames.children]: options,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@ -143,7 +143,7 @@ export default {
 | 
				
			|||||||
              title: form.title ?? formKey,
 | 
					              title: form.title ?? formKey,
 | 
				
			||||||
              component: CollectionBlockInitializer,
 | 
					              component: CollectionBlockInitializer,
 | 
				
			||||||
              collection: form.collection,
 | 
					              collection: form.collection,
 | 
				
			||||||
              dataSource: `{{$jobsMapByNodeId.${node.id}.${formKey}}}`,
 | 
					              dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`,
 | 
				
			||||||
            } as SchemaInitializerItemOptions)
 | 
					            } as SchemaInitializerItemOptions)
 | 
				
			||||||
          : null;
 | 
					          : null;
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 | 
				
			|||||||
@ -89,13 +89,12 @@ export default {
 | 
				
			|||||||
    FilterDynamicComponent,
 | 
					    FilterDynamicComponent,
 | 
				
			||||||
    SchemaComponentContext,
 | 
					    SchemaComponentContext,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  useVariables({ id, title, config }, options) {
 | 
					  useVariables({ key: name, title, config }, options) {
 | 
				
			||||||
    const compile = useCompile();
 | 
					    const compile = useCompile();
 | 
				
			||||||
    const { getCollectionFields } = useCollectionManager();
 | 
					    const { getCollectionFields } = useCollectionManager();
 | 
				
			||||||
    // const depth = config?.params?.appends?.length
 | 
					    // const depth = config?.params?.appends?.length
 | 
				
			||||||
    //   ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
 | 
					    //   ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
 | 
				
			||||||
    //   : 0;
 | 
					    //   : 0;
 | 
				
			||||||
    const name = `${id}`;
 | 
					 | 
				
			||||||
    const [result] = getCollectionFieldOptions({
 | 
					    const [result] = getCollectionFieldOptions({
 | 
				
			||||||
      // collection: config.collection,
 | 
					      // collection: config.collection,
 | 
				
			||||||
      // depth: options?.depth ?? depth,
 | 
					      // depth: options?.depth ?? depth,
 | 
				
			||||||
@ -128,7 +127,7 @@ export default {
 | 
				
			|||||||
      title: node.title ?? `#${node.id}`,
 | 
					      title: node.title ?? `#${node.id}`,
 | 
				
			||||||
      component: CollectionBlockInitializer,
 | 
					      component: CollectionBlockInitializer,
 | 
				
			||||||
      collection: node.config.collection,
 | 
					      collection: node.config.collection,
 | 
				
			||||||
      dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
 | 
					      dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  initializers: {},
 | 
					  initializers: {},
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@ export const defaultFieldNames = { label: 'label', value: 'value', children: 'ch
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const nodesOptions = {
 | 
					export const nodesOptions = {
 | 
				
			||||||
  label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
 | 
					  label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
 | 
				
			||||||
  value: '$jobsMapByNodeId',
 | 
					  value: '$jobsMapByNodeKey',
 | 
				
			||||||
  useOptions(options: OptionsOfUseVariableOptions) {
 | 
					  useOptions(options: OptionsOfUseVariableOptions) {
 | 
				
			||||||
    const current = useNodeContext();
 | 
					    const current = useNodeContext();
 | 
				
			||||||
    const upstreams = useAvailableUpstreams(current);
 | 
					    const upstreams = useAvailableUpstreams(current);
 | 
				
			||||||
@ -76,8 +76,8 @@ export const scopeOptions = {
 | 
				
			|||||||
      const subOptions = instruction.useScopeVariables?.(node, options);
 | 
					      const subOptions = instruction.useScopeVariables?.(node, options);
 | 
				
			||||||
      if (subOptions) {
 | 
					      if (subOptions) {
 | 
				
			||||||
        result.push({
 | 
					        result.push({
 | 
				
			||||||
          key: node.id.toString(),
 | 
					          key: node.key,
 | 
				
			||||||
          [fieldNames.value]: node.id.toString(),
 | 
					          [fieldNames.value]: node.key,
 | 
				
			||||||
          [fieldNames.label]: node.title ?? `#${node.id}`,
 | 
					          [fieldNames.label]: node.title ?? `#${node.id}`,
 | 
				
			||||||
          [fieldNames.children]: subOptions,
 | 
					          [fieldNames.children]: subOptions,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
				
			|||||||
@ -89,6 +89,7 @@ export default {
 | 
				
			|||||||
  Advanced: '高级模式',
 | 
					  Advanced: '高级模式',
 | 
				
			||||||
  End: '结束',
 | 
					  End: '结束',
 | 
				
			||||||
  'Node result': '节点数据',
 | 
					  'Node result': '节点数据',
 | 
				
			||||||
 | 
					  'Variable key of node': '节点变量标识',
 | 
				
			||||||
  Calculator: '运算',
 | 
					  Calculator: '运算',
 | 
				
			||||||
  'Calculate an expression based on a calculation engine and obtain a value as the result. Variables in the upstream nodes can be used in the expression. The expression can be static or dynamic one from an expression collections.':
 | 
					  'Calculate an expression based on a calculation engine and obtain a value as the result. Variables in the upstream nodes can be used in the expression. The expression can be static or dynamic one from an expression collections.':
 | 
				
			||||||
    '基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。表达式可以是静态的,也可以是表达式表中的动态表达式。',
 | 
					    '基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。表达式可以是静态的,也可以是表达式表中的动态表达式。',
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,7 @@ export default class Processor {
 | 
				
			|||||||
  nodes: FlowNodeModel[] = [];
 | 
					  nodes: FlowNodeModel[] = [];
 | 
				
			||||||
  nodesMap = new Map<number, FlowNodeModel>();
 | 
					  nodesMap = new Map<number, FlowNodeModel>();
 | 
				
			||||||
  jobsMap = new Map<number, JobModel>();
 | 
					  jobsMap = new Map<number, JobModel>();
 | 
				
			||||||
  jobsMapByNodeId: { [key: number]: any } = {};
 | 
					  jobsMapByNodeKey: { [key: string]: any } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    public execution: ExecutionModel,
 | 
					    public execution: ExecutionModel,
 | 
				
			||||||
@ -61,8 +61,9 @@ export default class Processor {
 | 
				
			|||||||
  private makeJobs(jobs: Array<JobModel>) {
 | 
					  private makeJobs(jobs: Array<JobModel>) {
 | 
				
			||||||
    jobs.forEach((job) => {
 | 
					    jobs.forEach((job) => {
 | 
				
			||||||
      this.jobsMap.set(job.id, job);
 | 
					      this.jobsMap.set(job.id, job);
 | 
				
			||||||
      // TODO: should consider cycle, and from previous job
 | 
					
 | 
				
			||||||
      this.jobsMapByNodeId[job.nodeId] = job.result;
 | 
					      const node = this.nodesMap.get(job.nodeId);
 | 
				
			||||||
 | 
					      this.jobsMapByNodeKey[node.key] = job.result;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -252,7 +253,9 @@ export default class Processor {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.jobsMap.set(job.id, job);
 | 
					    this.jobsMap.set(job.id, job);
 | 
				
			||||||
    this.jobsMapByNodeId[job.nodeId] = job.result;
 | 
					
 | 
				
			||||||
 | 
					    const node = this.nodesMap.get(job.nodeId);
 | 
				
			||||||
 | 
					    this.jobsMapByNodeKey[node.key] = job.result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return job;
 | 
					    return job;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -339,13 +342,13 @@ export default class Processor {
 | 
				
			|||||||
    for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
 | 
					    for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
 | 
				
			||||||
      const instruction = this.options.plugin.instructions.get(n.type);
 | 
					      const instruction = this.options.plugin.instructions.get(n.type);
 | 
				
			||||||
      if (typeof instruction.getScope === 'function') {
 | 
					      if (typeof instruction.getScope === 'function') {
 | 
				
			||||||
        $scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this);
 | 
					        $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      $context: this.execution.context,
 | 
					      $context: this.execution.context,
 | 
				
			||||||
      $jobsMapByNodeId: this.jobsMapByNodeId,
 | 
					      $jobsMapByNodeKey: this.jobsMapByNodeKey,
 | 
				
			||||||
      $system: systemFns,
 | 
					      $system: systemFns,
 | 
				
			||||||
      $scopes,
 | 
					      $scopes,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
				
			|||||||
@ -260,7 +260,7 @@ describe('workflow > actions > workflows', () => {
 | 
				
			|||||||
        type: 'calculation',
 | 
					        type: 'calculation',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          engine: 'math.js',
 | 
					          engine: 'math.js',
 | 
				
			||||||
          expression: `{{$jobsMapByNodeId.${n1.id}.data.read}} + {{$jobsMapByNodeId.${n1.id}.data.read}}`,
 | 
					          expression: `{{$jobsMapByNodeKey.${n1.key}.data.read}} + {{$jobsMapByNodeKey.${n1.key}.data.read}}`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        upstreamId: n1.id,
 | 
					        upstreamId: n1.id,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -279,11 +279,12 @@ describe('workflow > actions > workflows', () => {
 | 
				
			|||||||
      const n1_2 = w2.nodes.find((n) => !n.upstreamId);
 | 
					      const n1_2 = w2.nodes.find((n) => !n.upstreamId);
 | 
				
			||||||
      const n2_2 = w2.nodes.find((n) => !n.downstreamId);
 | 
					      const n2_2 = w2.nodes.find((n) => !n.downstreamId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(n1_2.key).toBe(n1.key);
 | 
				
			||||||
      expect(n1_2.type).toBe('echo');
 | 
					      expect(n1_2.type).toBe('echo');
 | 
				
			||||||
      expect(n2_2.type).toBe('calculation');
 | 
					      expect(n2_2.type).toBe('calculation');
 | 
				
			||||||
      expect(n2_2.config).toMatchObject({
 | 
					      expect(n2_2.config).toMatchObject({
 | 
				
			||||||
        engine: 'math.js',
 | 
					        engine: 'math.js',
 | 
				
			||||||
        expression: `{{$jobsMapByNodeId.${n1_2.id}.data.read}} + {{$jobsMapByNodeId.${n1_2.id}.data.read}}`,
 | 
					        expression: `{{$jobsMapByNodeKey.${n1_2.key}.data.read}} + {{$jobsMapByNodeKey.${n1_2.key}.data.read}}`,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await w2.update({ enabled: true });
 | 
					      await w2.update({ enabled: true });
 | 
				
			||||||
@ -299,47 +300,6 @@ describe('workflow > actions > workflows', () => {
 | 
				
			|||||||
      expect(calculation.result).toBe(2);
 | 
					      expect(calculation.result).toBe(2);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('revision with using of deleted nodes', async () => {
 | 
					 | 
				
			||||||
      const w1 = await WorkflowModel.create({
 | 
					 | 
				
			||||||
        enabled: true,
 | 
					 | 
				
			||||||
        type: 'collection',
 | 
					 | 
				
			||||||
        config: {
 | 
					 | 
				
			||||||
          mode: 1,
 | 
					 | 
				
			||||||
          collection: 'posts',
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const n2 = await w1.createNode({
 | 
					 | 
				
			||||||
        type: 'calculation',
 | 
					 | 
				
			||||||
        config: {
 | 
					 | 
				
			||||||
          calculation: {
 | 
					 | 
				
			||||||
            calculator: 'add',
 | 
					 | 
				
			||||||
            operands: [
 | 
					 | 
				
			||||||
              {
 | 
					 | 
				
			||||||
                type: '$jobsMapByNodeId',
 | 
					 | 
				
			||||||
                options: {
 | 
					 | 
				
			||||||
                  nodeId: 0,
 | 
					 | 
				
			||||||
                  path: 'data.read',
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              {
 | 
					 | 
				
			||||||
                value: `{{$jobsMapByNodeId.0.data.read}}`,
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const { status } = await agent.resource(`workflows`).revision({
 | 
					 | 
				
			||||||
        filterByTk: w1.id,
 | 
					 | 
				
			||||||
        filter: {
 | 
					 | 
				
			||||||
          key: w1.key,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(status).toBe(400);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it('duplicate workflow', async () => {
 | 
					    it('duplicate workflow', async () => {
 | 
				
			||||||
      const w1 = await WorkflowModel.create({
 | 
					      const w1 = await WorkflowModel.create({
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
 | 
				
			|||||||
@ -248,7 +248,7 @@ describe('workflow > instructions > aggregate', () => {
 | 
				
			|||||||
          associated: true,
 | 
					          associated: true,
 | 
				
			||||||
          association: {
 | 
					          association: {
 | 
				
			||||||
            name: 'posts',
 | 
					            name: 'posts',
 | 
				
			||||||
            associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
 | 
					            associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
 | 
				
			||||||
            associatedCollection: 'tags',
 | 
					            associatedCollection: 'tags',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          params: {
 | 
					          params: {
 | 
				
			||||||
@ -266,7 +266,7 @@ describe('workflow > instructions > aggregate', () => {
 | 
				
			|||||||
          associated: true,
 | 
					          associated: true,
 | 
				
			||||||
          association: {
 | 
					          association: {
 | 
				
			||||||
            name: 'posts',
 | 
					            name: 'posts',
 | 
				
			||||||
            associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
 | 
					            associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
 | 
				
			||||||
            associatedCollection: 'tags',
 | 
					            associatedCollection: 'tags',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          params: {
 | 
					          params: {
 | 
				
			||||||
 | 
				
			|||||||
@ -88,7 +88,7 @@ describe('workflow > instructions > calculation', () => {
 | 
				
			|||||||
      expect(job.result).toBe(2);
 | 
					      expect(job.result).toBe(2);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('$jobsMapByNodeId', async () => {
 | 
					    it('$jobsMapByNodeKey', async () => {
 | 
				
			||||||
      const n1 = await workflow.createNode({
 | 
					      const n1 = await workflow.createNode({
 | 
				
			||||||
        type: 'echo',
 | 
					        type: 'echo',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -97,7 +97,7 @@ describe('workflow > instructions > calculation', () => {
 | 
				
			|||||||
        type: 'calculation',
 | 
					        type: 'calculation',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          engine: 'math.js',
 | 
					          engine: 'math.js',
 | 
				
			||||||
          expression: `{{$jobsMapByNodeId.${n1.id}.data.read}} + 1`,
 | 
					          expression: `{{$jobsMapByNodeKey.${n1.key}.data.read}} + 1`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        upstreamId: n1.id,
 | 
					        upstreamId: n1.id,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -231,7 +231,7 @@ describe('workflow > instructions > calculation', () => {
 | 
				
			|||||||
      const n2 = await workflow.createNode({
 | 
					      const n2 = await workflow.createNode({
 | 
				
			||||||
        type: 'calculation',
 | 
					        type: 'calculation',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          dynamic: `{{$jobsMapByNodeId.${n1.id}}}`,
 | 
					          dynamic: `{{$jobsMapByNodeKey.${n1.key}}}`,
 | 
				
			||||||
          scope: '{{$context.data}}',
 | 
					          scope: '{{$context.data}}',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        upstreamId: n1.id,
 | 
					        upstreamId: n1.id,
 | 
				
			||||||
 | 
				
			|||||||
@ -867,7 +867,7 @@ describe('workflow > instructions > manual', () => {
 | 
				
			|||||||
        type: 'calculation',
 | 
					        type: 'calculation',
 | 
				
			||||||
        config: {
 | 
					        config: {
 | 
				
			||||||
          engine: 'math.js',
 | 
					          engine: 'math.js',
 | 
				
			||||||
          expression: `{{$jobsMapByNodeId.${n1.id}.f1.number}} + 1`,
 | 
					          expression: `{{$jobsMapByNodeKey.${n1.key}.f1.number}} + 1`,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        upstreamId: n1.id,
 | 
					        upstreamId: n1.id,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
				
			|||||||
@ -130,7 +130,7 @@ describe('workflow > instructions > query', () => {
 | 
				
			|||||||
          collection: 'posts',
 | 
					          collection: 'posts',
 | 
				
			||||||
          params: {
 | 
					          params: {
 | 
				
			||||||
            filter: {
 | 
					            filter: {
 | 
				
			||||||
              title: `{{$jobsMapByNodeId.${n1.id}.data.title}}`,
 | 
					              title: `{{$jobsMapByNodeKey.${n1.key}.data.title}}`,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
@ -78,7 +78,7 @@ describe('workflow > instructions > update', () => {
 | 
				
			|||||||
          collection: 'posts',
 | 
					          collection: 'posts',
 | 
				
			||||||
          params: {
 | 
					          params: {
 | 
				
			||||||
            filter: {
 | 
					            filter: {
 | 
				
			||||||
              id: `{{$jobsMapByNodeId.${n1.id}.id}}`,
 | 
					              id: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            values: {
 | 
					            values: {
 | 
				
			||||||
              title: 'changed',
 | 
					              title: 'changed',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import actions, { Context, utils } from '@nocobase/actions';
 | 
					import actions, { Context, utils } from '@nocobase/actions';
 | 
				
			||||||
import { Op, Repository } from '@nocobase/database';
 | 
					import { Op, Repository } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Plugin from '../Plugin';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function update(context: Context, next) {
 | 
					export async function update(context: Context, next) {
 | 
				
			||||||
  const repository = utils.getRepositoryFromParams(context) as Repository;
 | 
					  const repository = utils.getRepositoryFromParams(context) as Repository;
 | 
				
			||||||
  const { filterByTk, values } = context.action.params;
 | 
					  const { filterByTk, values } = context.action.params;
 | 
				
			||||||
@ -51,51 +53,8 @@ export async function destroy(context: Context, next) {
 | 
				
			|||||||
  next();
 | 
					  next();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function typeOf(value) {
 | 
					 | 
				
			||||||
  if (Array.isArray(value)) {
 | 
					 | 
				
			||||||
    return 'array';
 | 
					 | 
				
			||||||
  } else if (value instanceof Date) {
 | 
					 | 
				
			||||||
    return 'date';
 | 
					 | 
				
			||||||
  } else if (value === null) {
 | 
					 | 
				
			||||||
    return 'null';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return typeof value;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function migrateConfig(config, oldToNew) {
 | 
					 | 
				
			||||||
  function migrate(value) {
 | 
					 | 
				
			||||||
    switch (typeOf(value)) {
 | 
					 | 
				
			||||||
      case 'object':
 | 
					 | 
				
			||||||
        if (value.type === '$jobsMapByNodeId') {
 | 
					 | 
				
			||||||
          return {
 | 
					 | 
				
			||||||
            ...value,
 | 
					 | 
				
			||||||
            options: {
 | 
					 | 
				
			||||||
              ...value.options,
 | 
					 | 
				
			||||||
              nodeId: oldToNew.get(value.options?.nodeId)?.id,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          };
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Object.keys(value).reduce((result, key) => ({ ...result, [key]: migrate(value[key]) }), {});
 | 
					 | 
				
			||||||
      case 'array':
 | 
					 | 
				
			||||||
        return value.map((item) => migrate(item));
 | 
					 | 
				
			||||||
      case 'string':
 | 
					 | 
				
			||||||
        return value.replace(/({{\$jobsMapByNodeId|{{\$scopes)\.([\w-]+)/g, (_, jobVar, oldNodeId) => {
 | 
					 | 
				
			||||||
          const newNode = oldToNew.get(Number.parseInt(oldNodeId, 10));
 | 
					 | 
				
			||||||
          if (!newNode) {
 | 
					 | 
				
			||||||
            throw new Error('node configurated for result is not existed');
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return `${jobVar}.${newNode.id}`;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        return value;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return migrate(config);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function revision(context: Context, next) {
 | 
					export async function revision(context: Context, next) {
 | 
				
			||||||
 | 
					  const plugin = context.app.getPlugin('workflow') as Plugin;
 | 
				
			||||||
  const { db } = context;
 | 
					  const { db } = context;
 | 
				
			||||||
  const repository = utils.getRepositoryFromParams(context);
 | 
					  const repository = utils.getRepositoryFromParams(context);
 | 
				
			||||||
  const { filterByTk, filter = {}, values = {} } = context.action.params;
 | 
					  const { filterByTk, filter = {}, values = {} } = context.action.params;
 | 
				
			||||||
@ -109,6 +68,8 @@ export async function revision(context: Context, next) {
 | 
				
			|||||||
      transaction,
 | 
					      transaction,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const trigger = plugin.triggers.get(origin.type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const revisionData = filter.key
 | 
					    const revisionData = filter.key
 | 
				
			||||||
      ? {
 | 
					      ? {
 | 
				
			||||||
          key: filter.key,
 | 
					          key: filter.key,
 | 
				
			||||||
@ -121,9 +82,12 @@ export async function revision(context: Context, next) {
 | 
				
			|||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
        title: `${origin.title} copy`,
 | 
					        title: `${origin.title} copy`,
 | 
				
			||||||
        description: origin.description,
 | 
					        description: origin.description,
 | 
				
			||||||
        type: origin.type,
 | 
					 | 
				
			||||||
        config: origin.config,
 | 
					 | 
				
			||||||
        ...revisionData,
 | 
					        ...revisionData,
 | 
				
			||||||
 | 
					        type: origin.type,
 | 
				
			||||||
 | 
					        config:
 | 
				
			||||||
 | 
					          typeof trigger.duplicateConfig === 'function'
 | 
				
			||||||
 | 
					            ? await trigger.duplicateConfig(origin, { transaction })
 | 
				
			||||||
 | 
					            : origin.config,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      transaction,
 | 
					      transaction,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -136,10 +100,15 @@ export async function revision(context: Context, next) {
 | 
				
			|||||||
    const oldToNew = new Map();
 | 
					    const oldToNew = new Map();
 | 
				
			||||||
    const newToOld = new Map();
 | 
					    const newToOld = new Map();
 | 
				
			||||||
    for await (const node of origin.nodes) {
 | 
					    for await (const node of origin.nodes) {
 | 
				
			||||||
 | 
					      const instruction = plugin.instructions.get(node.type);
 | 
				
			||||||
      const newNode = await instance.createNode(
 | 
					      const newNode = await instance.createNode(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          type: node.type,
 | 
					          type: node.type,
 | 
				
			||||||
          config: node.config,
 | 
					          key: node.key,
 | 
				
			||||||
 | 
					          config:
 | 
				
			||||||
 | 
					            typeof instruction.duplicateConfig === 'function'
 | 
				
			||||||
 | 
					              ? await instruction.duplicateConfig(node, { transaction })
 | 
				
			||||||
 | 
					              : node.config,
 | 
				
			||||||
          title: node.title,
 | 
					          title: node.title,
 | 
				
			||||||
          branchIndex: node.branchIndex,
 | 
					          branchIndex: node.branchIndex,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -154,18 +123,11 @@ export async function revision(context: Context, next) {
 | 
				
			|||||||
      const oldNode = originalNodesMap.get(oldId);
 | 
					      const oldNode = originalNodesMap.get(oldId);
 | 
				
			||||||
      const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : null;
 | 
					      const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : null;
 | 
				
			||||||
      const newDownstream = oldNode.downstreamId ? oldToNew.get(oldNode.downstreamId) : null;
 | 
					      const newDownstream = oldNode.downstreamId ? oldToNew.get(oldNode.downstreamId) : null;
 | 
				
			||||||
      let migratedConfig;
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        migratedConfig = migrateConfig(oldNode.config, oldToNew);
 | 
					 | 
				
			||||||
      } catch (err) {
 | 
					 | 
				
			||||||
        return context.throw(400, err.message);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await newNode.update(
 | 
					      await newNode.update(
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          upstreamId: newUpstream?.id ?? null,
 | 
					          upstreamId: newUpstream?.id ?? null,
 | 
				
			||||||
          downstreamId: newDownstream?.id ?? null,
 | 
					          downstreamId: newDownstream?.id ?? null,
 | 
				
			||||||
          config: migratedConfig,
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        { transaction },
 | 
					        { transaction },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,10 @@ export default {
 | 
				
			|||||||
  duplicator: 'required',
 | 
					  duplicator: 'required',
 | 
				
			||||||
  name: 'flow_nodes',
 | 
					  name: 'flow_nodes',
 | 
				
			||||||
  fields: [
 | 
					  fields: [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      type: 'uid',
 | 
				
			||||||
 | 
					      name: 'key',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      type: 'string',
 | 
					      type: 'string',
 | 
				
			||||||
      name: 'title',
 | 
					      name: 'title',
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { requireModule } from '@nocobase/utils';
 | 
					import { requireModule } from '@nocobase/utils';
 | 
				
			||||||
 | 
					import { Transactionable } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Plugin from '..';
 | 
					import Plugin from '..';
 | 
				
			||||||
import Processor from '../Processor';
 | 
					import Processor from '../Processor';
 | 
				
			||||||
@ -25,7 +26,9 @@ 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;
 | 
					  getScope?: (node: FlowNodeModel, data: any, processor: Processor) => any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  duplicateConfig?: (node: FlowNodeModel, options: Transactionable) => object | Promise<object>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type InstructionConstructor<T> = { new (p: Plugin): T };
 | 
					type InstructionConstructor<T> = { new (p: Plugin): T };
 | 
				
			||||||
 | 
				
			|||||||
@ -30,22 +30,6 @@ export default class extends Migration {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        { transaction },
 | 
					        { transaction },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      await queryInterface.changeColumn(
 | 
					 | 
				
			||||||
        db.getCollection('executions').model.getTableName(),
 | 
					 | 
				
			||||||
        'context',
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          type: DataTypes.JSON,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        { transaction },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      await queryInterface.changeColumn(
 | 
					 | 
				
			||||||
        db.getCollection('jobs').model.getTableName(),
 | 
					 | 
				
			||||||
        'result',
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          type: DataTypes.JSON,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        { transaction },
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,12 +2,12 @@ import { Migration } from '@nocobase/server';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export default class extends Migration {
 | 
					export default class extends Migration {
 | 
				
			||||||
  async up() {
 | 
					  async up() {
 | 
				
			||||||
    const match = await this.app.version.satisfies('<0.11.0-alpha.2');
 | 
					    const match = await this.app.version.satisfies('<0.14.0-alpha.8');
 | 
				
			||||||
    if (!match) {
 | 
					    if (!match) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const { db } = this.context;
 | 
					    const { db } = this.context;
 | 
				
			||||||
    const WorkflowRepo = db.getRepository('flow_nodes');
 | 
					    const WorkflowRepo = db.getRepository('workflows');
 | 
				
			||||||
    await db.sequelize.transaction(async (transaction) => {
 | 
					    await db.sequelize.transaction(async (transaction) => {
 | 
				
			||||||
      const workflows = await WorkflowRepo.find({
 | 
					      const workflows = await WorkflowRepo.find({
 | 
				
			||||||
        transaction,
 | 
					        transaction,
 | 
				
			||||||
@ -16,6 +16,9 @@ export default class extends Migration {
 | 
				
			|||||||
      await workflows.reduce(
 | 
					      await workflows.reduce(
 | 
				
			||||||
        (promise, workflow) =>
 | 
					        (promise, workflow) =>
 | 
				
			||||||
          promise.then(() => {
 | 
					          promise.then(() => {
 | 
				
			||||||
 | 
					            if (!workflow.useTransaction) {
 | 
				
			||||||
 | 
					              return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            workflow.set('options', {
 | 
					            workflow.set('options', {
 | 
				
			||||||
              useTransaction: workflow.get('useTransaction'),
 | 
					              useTransaction: workflow.get('useTransaction'),
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					import { DataTypes } from 'sequelize';
 | 
				
			||||||
 | 
					import { Migration } from '@nocobase/server';
 | 
				
			||||||
 | 
					import { uid } from '@nocobase/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function migrateNodeConfig(config = {}, nodesMap) {
 | 
				
			||||||
 | 
					  Object.keys(config).forEach((key) => {
 | 
				
			||||||
 | 
					    const valueType = typeof config[key];
 | 
				
			||||||
 | 
					    if (valueType === 'string') {
 | 
				
			||||||
 | 
					      config[key] = config[key]
 | 
				
			||||||
 | 
					        .replace(/{{\s*\$jobsMapByNodeId\.(\d+)(\.[^}]+)?\s*}}/g, (matched, id, path) => {
 | 
				
			||||||
 | 
					          return `{{$jobsMapByNodeKey.${nodesMap[id].key}${path || ''}}}`;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .replace(/{{\s*\$scopes\.(\d+)(\.[^}]+)?\s*}}/g, (matched, id, path) => {
 | 
				
			||||||
 | 
					          return `{{$scopes.${nodesMap[id].key}${path || ''}}}`;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    } else if (valueType === 'object' && config[key]) {
 | 
				
			||||||
 | 
					      migrateNodeConfig(config[key], nodesMap);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return config;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class extends Migration {
 | 
				
			||||||
 | 
					  async up() {
 | 
				
			||||||
 | 
					    const match = await this.app.version.satisfies('<0.14.0-alpha.8');
 | 
				
			||||||
 | 
					    if (!match) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const { db } = this.context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const NodeRepo = db.getRepository('flow_nodes');
 | 
				
			||||||
 | 
					    const { key } = await this.queryInterface.describeTable('flow_nodes');
 | 
				
			||||||
 | 
					    await db.sequelize.transaction(async (transaction) => {
 | 
				
			||||||
 | 
					      if (!key) {
 | 
				
			||||||
 | 
					        await this.queryInterface.addColumn('flow_nodes', 'key', DataTypes.STRING, {
 | 
				
			||||||
 | 
					          transaction,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const nodes = await NodeRepo.find({
 | 
				
			||||||
 | 
					        transaction,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const nodesMap = nodes.reduce((map, node) => {
 | 
				
			||||||
 | 
					        map[node.id] = node;
 | 
				
			||||||
 | 
					        if (!node.get('key')) {
 | 
				
			||||||
 | 
					          node.set('key', uid());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return map;
 | 
				
			||||||
 | 
					      }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await nodes.reduce(
 | 
				
			||||||
 | 
					        (promise, node) =>
 | 
				
			||||||
 | 
					          promise.then(() => {
 | 
				
			||||||
 | 
					            node.set('config', migrateNodeConfig(node.config, nodesMap));
 | 
				
			||||||
 | 
					            node.changed('config', true);
 | 
				
			||||||
 | 
					            return node.save({
 | 
				
			||||||
 | 
					              silent: true,
 | 
				
			||||||
 | 
					              transaction,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        Promise.resolve(),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import { requireModule } from '@nocobase/utils';
 | 
					 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { requireModule } from '@nocobase/utils';
 | 
				
			||||||
 | 
					import { Transactionable } from '@nocobase/database';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Plugin from '..';
 | 
					import Plugin from '..';
 | 
				
			||||||
import type { WorkflowModel } from '../types';
 | 
					import type { WorkflowModel } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -8,6 +10,7 @@ export abstract class Trigger {
 | 
				
			|||||||
  constructor(public readonly plugin: Plugin) {}
 | 
					  constructor(public readonly plugin: Plugin) {}
 | 
				
			||||||
  abstract on(workflow: WorkflowModel): void;
 | 
					  abstract on(workflow: WorkflowModel): void;
 | 
				
			||||||
  abstract off(workflow: WorkflowModel): void;
 | 
					  abstract off(workflow: WorkflowModel): void;
 | 
				
			||||||
 | 
					  duplicateConfig?(workflow: WorkflowModel, options: Transactionable): object | Promise<object>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function <T extends Trigger>(plugin, more: { [key: string]: { new (p: Plugin): T } } = {}) {
 | 
					export default function <T extends Trigger>(plugin, more: { [key: string]: { new (p: Plugin): T } } = {}) {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user