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);
|
||||
item.node = {
|
||||
id: node.id,
|
||||
key: node.key,
|
||||
title: node.title,
|
||||
type: node.type,
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ function Initializer({ node, resultTitle, insert, ...props }) {
|
||||
'x-component': 'ValueBlock.Result',
|
||||
'x-component-props': {
|
||||
// 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;
|
||||
}
|
||||
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 (
|
||||
|
@ -346,7 +346,7 @@ export default {
|
||||
ValueBlock,
|
||||
AssociatedConfig,
|
||||
},
|
||||
useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
|
||||
useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) {
|
||||
if (
|
||||
types &&
|
||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||
@ -354,7 +354,7 @@ export default {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
[fieldNames.value]: `${id}`,
|
||||
[fieldNames.value]: key,
|
||||
[fieldNames.label]: title,
|
||||
};
|
||||
},
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { FormItem, FormLayout } from '@formily/antd-v5';
|
||||
import { SchemaInitializerItemOptions, Variable, css, defaultFieldNames, useCollectionManager } from '@nocobase/client';
|
||||
import { Evaluator, evaluators, getOptions } from '@nocobase/evaluators/client';
|
||||
import { parse } from '@nocobase/utils/client';
|
||||
import { Radio } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
import { ValueBlock } from '../components/ValueBlock';
|
||||
import { renderEngineReference } from '../components/renderEngineReference';
|
||||
@ -167,32 +165,11 @@ export default {
|
||||
});
|
||||
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,
|
||||
DynamicConfig,
|
||||
ValueBlock,
|
||||
},
|
||||
useVariables({ id, title }, { types, fieldNames = defaultFieldNames }) {
|
||||
useVariables({ key, title }, { types, fieldNames = defaultFieldNames }) {
|
||||
if (
|
||||
types &&
|
||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||
@ -200,7 +177,7 @@ export default {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
[fieldNames.value]: `${id}`,
|
||||
[fieldNames.value]: key,
|
||||
[fieldNames.label]: title,
|
||||
};
|
||||
},
|
||||
|
@ -44,13 +44,12 @@ export default {
|
||||
components: {
|
||||
CollectionFieldset,
|
||||
},
|
||||
useVariables({ id, title, config }, options) {
|
||||
useVariables({ key: name, title, config }, options) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
// const depth = config?.params?.appends?.length
|
||||
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||
// : 0;
|
||||
const name = `${id}`;
|
||||
const [result] = getCollectionFieldOptions({
|
||||
// collection: config.collection,
|
||||
// depth: options?.depth ?? depth,
|
||||
@ -83,7 +82,7 @@ export default {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
component: CollectionBlockInitializer,
|
||||
collection: node.config.collection,
|
||||
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
|
||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
||||
};
|
||||
},
|
||||
initializers: {},
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
useResourceActionContext,
|
||||
} from '@nocobase/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 { useTranslation } from 'react-i18next';
|
||||
import { AddButton } from '../AddButton';
|
||||
@ -199,7 +199,7 @@ export function RemoveButton() {
|
||||
|
||||
const template = parse(node.config);
|
||||
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;
|
||||
});
|
||||
@ -371,7 +371,33 @@ export function NodeDefaultView(props) {
|
||||
},
|
||||
[`${instruction.type}_${data.id}`]: {
|
||||
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-decorator': 'Form',
|
||||
'x-decorator-props': {
|
||||
|
@ -118,7 +118,7 @@ export default {
|
||||
// const current = useNodeContext();
|
||||
// const upstreams = useAvailableUpstreams(current);
|
||||
// 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
|
||||
let targetOption: VariableOption = {
|
||||
key: 'item',
|
||||
|
@ -13,12 +13,17 @@ import { useFlowContext } from '../../FlowContext';
|
||||
import { parse } from '@nocobase/utils/client';
|
||||
|
||||
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(
|
||||
() => ({
|
||||
$context: execution?.context,
|
||||
$jobsMapByNodeId: (execution?.jobs ?? []).reduce(
|
||||
(map, job) => Object.assign(map, { [job.nodeId]: job.result }),
|
||||
$jobsMapByNodeKey: (execution?.jobs ?? []).reduce(
|
||||
(map, job) => Object.assign(map, { [nodesKeyMap[job.nodeId]]: job.result }),
|
||||
{},
|
||||
),
|
||||
}),
|
||||
|
@ -85,7 +85,7 @@ export default {
|
||||
ModeConfig,
|
||||
AssigneesSelect,
|
||||
},
|
||||
useVariables({ id, title, config }, { types, fieldNames = defaultFieldNames }) {
|
||||
useVariables({ key, title, config }, { types, fieldNames = defaultFieldNames }) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const formKeys = Object.keys(config.forms ?? {});
|
||||
@ -119,7 +119,7 @@ export default {
|
||||
|
||||
return options.length
|
||||
? {
|
||||
[fieldNames.value]: `${id}`,
|
||||
[fieldNames.value]: key,
|
||||
[fieldNames.label]: title,
|
||||
[fieldNames.children]: options,
|
||||
}
|
||||
@ -143,7 +143,7 @@ export default {
|
||||
title: form.title ?? formKey,
|
||||
component: CollectionBlockInitializer,
|
||||
collection: form.collection,
|
||||
dataSource: `{{$jobsMapByNodeId.${node.id}.${formKey}}}`,
|
||||
dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`,
|
||||
} as SchemaInitializerItemOptions)
|
||||
: null;
|
||||
})
|
||||
|
@ -89,13 +89,12 @@ export default {
|
||||
FilterDynamicComponent,
|
||||
SchemaComponentContext,
|
||||
},
|
||||
useVariables({ id, title, config }, options) {
|
||||
useVariables({ key: name, title, config }, options) {
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
// const depth = config?.params?.appends?.length
|
||||
// ? config?.params?.appends.reduce((max, item) => Math.max(max, item.split('.').length), 1)
|
||||
// : 0;
|
||||
const name = `${id}`;
|
||||
const [result] = getCollectionFieldOptions({
|
||||
// collection: config.collection,
|
||||
// depth: options?.depth ?? depth,
|
||||
@ -128,7 +127,7 @@ export default {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
component: CollectionBlockInitializer,
|
||||
collection: node.config.collection,
|
||||
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
|
||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
||||
};
|
||||
},
|
||||
initializers: {},
|
||||
|
@ -37,7 +37,7 @@ export const defaultFieldNames = { label: 'label', value: 'value', children: 'ch
|
||||
|
||||
export const nodesOptions = {
|
||||
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$jobsMapByNodeId',
|
||||
value: '$jobsMapByNodeKey',
|
||||
useOptions(options: OptionsOfUseVariableOptions) {
|
||||
const current = useNodeContext();
|
||||
const upstreams = useAvailableUpstreams(current);
|
||||
@ -76,8 +76,8 @@ export const scopeOptions = {
|
||||
const subOptions = instruction.useScopeVariables?.(node, options);
|
||||
if (subOptions) {
|
||||
result.push({
|
||||
key: node.id.toString(),
|
||||
[fieldNames.value]: node.id.toString(),
|
||||
key: node.key,
|
||||
[fieldNames.value]: node.key,
|
||||
[fieldNames.label]: node.title ?? `#${node.id}`,
|
||||
[fieldNames.children]: subOptions,
|
||||
});
|
||||
|
@ -89,6 +89,7 @@ export default {
|
||||
Advanced: '高级模式',
|
||||
End: '结束',
|
||||
'Node result': '节点数据',
|
||||
'Variable key of node': '节点变量标识',
|
||||
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.':
|
||||
'基于计算引擎对一个表达式进行计算,并获得一个值作为结果。表达式中可以使用上游节点里的变量。表达式可以是静态的,也可以是表达式表中的动态表达式。',
|
||||
|
@ -30,7 +30,7 @@ export default class Processor {
|
||||
nodes: FlowNodeModel[] = [];
|
||||
nodesMap = new Map<number, FlowNodeModel>();
|
||||
jobsMap = new Map<number, JobModel>();
|
||||
jobsMapByNodeId: { [key: number]: any } = {};
|
||||
jobsMapByNodeKey: { [key: string]: any } = {};
|
||||
|
||||
constructor(
|
||||
public execution: ExecutionModel,
|
||||
@ -61,8 +61,9 @@ export default class Processor {
|
||||
private makeJobs(jobs: Array<JobModel>) {
|
||||
jobs.forEach((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.jobsMapByNodeId[job.nodeId] = job.result;
|
||||
|
||||
const node = this.nodesMap.get(job.nodeId);
|
||||
this.jobsMapByNodeKey[node.key] = job.result;
|
||||
|
||||
return job;
|
||||
}
|
||||
@ -339,13 +342,13 @@ export default class Processor {
|
||||
for (let n = this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) {
|
||||
const instruction = this.options.plugin.instructions.get(n.type);
|
||||
if (typeof instruction.getScope === 'function') {
|
||||
$scopes[n.id] = instruction.getScope(n, this.jobsMapByNodeId[n.id], this);
|
||||
$scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$context: this.execution.context,
|
||||
$jobsMapByNodeId: this.jobsMapByNodeId,
|
||||
$jobsMapByNodeKey: this.jobsMapByNodeKey,
|
||||
$system: systemFns,
|
||||
$scopes,
|
||||
};
|
||||
|
@ -260,7 +260,7 @@ describe('workflow > actions > workflows', () => {
|
||||
type: 'calculation',
|
||||
config: {
|
||||
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,
|
||||
});
|
||||
@ -279,11 +279,12 @@ describe('workflow > actions > workflows', () => {
|
||||
const n1_2 = w2.nodes.find((n) => !n.upstreamId);
|
||||
const n2_2 = w2.nodes.find((n) => !n.downstreamId);
|
||||
|
||||
expect(n1_2.key).toBe(n1.key);
|
||||
expect(n1_2.type).toBe('echo');
|
||||
expect(n2_2.type).toBe('calculation');
|
||||
expect(n2_2.config).toMatchObject({
|
||||
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 });
|
||||
@ -299,47 +300,6 @@ describe('workflow > actions > workflows', () => {
|
||||
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 () => {
|
||||
const w1 = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
|
@ -248,7 +248,7 @@ describe('workflow > instructions > aggregate', () => {
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'posts',
|
||||
associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
|
||||
associatedCollection: 'tags',
|
||||
},
|
||||
params: {
|
||||
@ -266,7 +266,7 @@ describe('workflow > instructions > aggregate', () => {
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'posts',
|
||||
associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
associatedKey: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
|
||||
associatedCollection: 'tags',
|
||||
},
|
||||
params: {
|
||||
|
@ -88,7 +88,7 @@ describe('workflow > instructions > calculation', () => {
|
||||
expect(job.result).toBe(2);
|
||||
});
|
||||
|
||||
it('$jobsMapByNodeId', async () => {
|
||||
it('$jobsMapByNodeKey', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'echo',
|
||||
});
|
||||
@ -97,7 +97,7 @@ describe('workflow > instructions > calculation', () => {
|
||||
type: 'calculation',
|
||||
config: {
|
||||
engine: 'math.js',
|
||||
expression: `{{$jobsMapByNodeId.${n1.id}.data.read}} + 1`,
|
||||
expression: `{{$jobsMapByNodeKey.${n1.key}.data.read}} + 1`,
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
@ -231,7 +231,7 @@ describe('workflow > instructions > calculation', () => {
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'calculation',
|
||||
config: {
|
||||
dynamic: `{{$jobsMapByNodeId.${n1.id}}}`,
|
||||
dynamic: `{{$jobsMapByNodeKey.${n1.key}}}`,
|
||||
scope: '{{$context.data}}',
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
|
@ -867,7 +867,7 @@ describe('workflow > instructions > manual', () => {
|
||||
type: 'calculation',
|
||||
config: {
|
||||
engine: 'math.js',
|
||||
expression: `{{$jobsMapByNodeId.${n1.id}.f1.number}} + 1`,
|
||||
expression: `{{$jobsMapByNodeKey.${n1.key}.f1.number}} + 1`,
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
@ -130,7 +130,7 @@ describe('workflow > instructions > query', () => {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
title: `{{$jobsMapByNodeId.${n1.id}.data.title}}`,
|
||||
title: `{{$jobsMapByNodeKey.${n1.key}.data.title}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -78,7 +78,7 @@ describe('workflow > instructions > update', () => {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
id: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
id: `{{$jobsMapByNodeKey.${n1.key}.id}}`,
|
||||
},
|
||||
values: {
|
||||
title: 'changed',
|
||||
|
@ -1,6 +1,8 @@
|
||||
import actions, { Context, utils } from '@nocobase/actions';
|
||||
import { Op, Repository } from '@nocobase/database';
|
||||
|
||||
import Plugin from '../Plugin';
|
||||
|
||||
export async function update(context: Context, next) {
|
||||
const repository = utils.getRepositoryFromParams(context) as Repository;
|
||||
const { filterByTk, values } = context.action.params;
|
||||
@ -51,51 +53,8 @@ export async function destroy(context: Context, 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) {
|
||||
const plugin = context.app.getPlugin('workflow') as Plugin;
|
||||
const { db } = context;
|
||||
const repository = utils.getRepositoryFromParams(context);
|
||||
const { filterByTk, filter = {}, values = {} } = context.action.params;
|
||||
@ -109,6 +68,8 @@ export async function revision(context: Context, next) {
|
||||
transaction,
|
||||
});
|
||||
|
||||
const trigger = plugin.triggers.get(origin.type);
|
||||
|
||||
const revisionData = filter.key
|
||||
? {
|
||||
key: filter.key,
|
||||
@ -121,9 +82,12 @@ export async function revision(context: Context, next) {
|
||||
values: {
|
||||
title: `${origin.title} copy`,
|
||||
description: origin.description,
|
||||
type: origin.type,
|
||||
config: origin.config,
|
||||
...revisionData,
|
||||
type: origin.type,
|
||||
config:
|
||||
typeof trigger.duplicateConfig === 'function'
|
||||
? await trigger.duplicateConfig(origin, { transaction })
|
||||
: origin.config,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
@ -136,10 +100,15 @@ export async function revision(context: Context, next) {
|
||||
const oldToNew = new Map();
|
||||
const newToOld = new Map();
|
||||
for await (const node of origin.nodes) {
|
||||
const instruction = plugin.instructions.get(node.type);
|
||||
const newNode = await instance.createNode(
|
||||
{
|
||||
type: node.type,
|
||||
config: node.config,
|
||||
key: node.key,
|
||||
config:
|
||||
typeof instruction.duplicateConfig === 'function'
|
||||
? await instruction.duplicateConfig(node, { transaction })
|
||||
: node.config,
|
||||
title: node.title,
|
||||
branchIndex: node.branchIndex,
|
||||
},
|
||||
@ -154,18 +123,11 @@ export async function revision(context: Context, next) {
|
||||
const oldNode = originalNodesMap.get(oldId);
|
||||
const newUpstream = oldNode.upstreamId ? oldToNew.get(oldNode.upstreamId) : 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(
|
||||
{
|
||||
upstreamId: newUpstream?.id ?? null,
|
||||
downstreamId: newDownstream?.id ?? null,
|
||||
config: migratedConfig,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
@ -5,6 +5,10 @@ export default {
|
||||
duplicator: 'required',
|
||||
name: 'flow_nodes',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
|
||||
import { requireModule } from '@nocobase/utils';
|
||||
import { Transactionable } from '@nocobase/database';
|
||||
|
||||
import Plugin from '..';
|
||||
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
|
||||
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 };
|
||||
|
@ -30,22 +30,6 @@ export default class extends Migration {
|
||||
},
|
||||
{ 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 {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const { db } = this.context;
|
||||
const WorkflowRepo = db.getRepository('flow_nodes');
|
||||
const WorkflowRepo = db.getRepository('workflows');
|
||||
await db.sequelize.transaction(async (transaction) => {
|
||||
const workflows = await WorkflowRepo.find({
|
||||
transaction,
|
||||
@ -16,6 +16,9 @@ export default class extends Migration {
|
||||
await workflows.reduce(
|
||||
(promise, workflow) =>
|
||||
promise.then(() => {
|
||||
if (!workflow.useTransaction) {
|
||||
return;
|
||||
}
|
||||
workflow.set('options', {
|
||||
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 { requireModule } from '@nocobase/utils';
|
||||
import { Transactionable } from '@nocobase/database';
|
||||
|
||||
import Plugin from '..';
|
||||
import type { WorkflowModel } from '../types';
|
||||
|
||||
@ -8,6 +10,7 @@ export abstract class Trigger {
|
||||
constructor(public readonly plugin: Plugin) {}
|
||||
abstract on(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 } } = {}) {
|
||||
|
Loading…
Reference in New Issue
Block a user