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