{
- if (next.type !== operand.type && next.type === 'constant') {
- onChange({ ...value, [field.name]: null });
- } else {
- const { stringify } = VTypes[next.type];
- onChange({ ...value, [field.name]: stringify(next) });
- }
+ onChange({ ...value, [field.name]: next });
}}
>
{operand.type === 'constant'
diff --git a/packages/plugins/workflow/src/client/locale/index.ts b/packages/plugins/workflow/src/client/locale/index.ts
index 2cc84f046..eafdadf59 100644
--- a/packages/plugins/workflow/src/client/locale/index.ts
+++ b/packages/plugins/workflow/src/client/locale/index.ts
@@ -15,8 +15,8 @@ i18n.addResources('ja-JP', NAMESPACE, jaJP);
i18n.addResources('ru-RU', NAMESPACE, ruRU);
i18n.addResources('tr-TR', NAMESPACE, trTR);
-export function lang(key: string) {
- return i18n.t(key, { ns: NAMESPACE });
+export function lang(key: string, options = {}) {
+ return i18n.t(key, { ...options, ns: NAMESPACE });
}
export function useWorkflowTranslation() {
diff --git a/packages/plugins/workflow/src/client/locale/zh-CN.ts b/packages/plugins/workflow/src/client/locale/zh-CN.ts
index c7ee2e623..573b8e4b3 100644
--- a/packages/plugins/workflow/src/client/locale/zh-CN.ts
+++ b/packages/plugins/workflow/src/client/locale/zh-CN.ts
@@ -65,6 +65,7 @@ export default {
'Node result': '节点数据',
'Constant': '常量',
+ 'Null': '空值',
'Boolean': '逻辑值',
'String': '字符串',
@@ -127,6 +128,8 @@ export default {
'Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。',
'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改',
'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改',
+ 'Can not delete': '无法删除',
+ 'The result of this node has been referenced by other nodes ({{nodes}}), please remove the usage before deleting.': '该节点的执行结果已被其他节点({{nodes}})引用,删除前请先移除引用。',
'HTTP request': 'HTTP 请求',
'URL': '地址',
diff --git a/packages/plugins/workflow/src/client/nodes/calculation.tsx b/packages/plugins/workflow/src/client/nodes/calculation.tsx
index e682398e9..402ce1044 100644
--- a/packages/plugins/workflow/src/client/nodes/calculation.tsx
+++ b/packages/plugins/workflow/src/client/nodes/calculation.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { css } from '@emotion/css';
-import { useTranslation } from 'react-i18next';
import { Calculation } from '../calculators';
import { NAMESPACE, useWorkflowTranslation } from '../locale';
diff --git a/packages/plugins/workflow/src/client/nodes/condition.tsx b/packages/plugins/workflow/src/client/nodes/condition.tsx
index 026b25250..14a189737 100644
--- a/packages/plugins/workflow/src/client/nodes/condition.tsx
+++ b/packages/plugins/workflow/src/client/nodes/condition.tsx
@@ -4,8 +4,6 @@ import { Button, Select } from "antd";
import { CloseCircleOutlined } from '@ant-design/icons';
import { Trans, useTranslation } from "react-i18next";
-import { i18n } from "@nocobase/client";
-
import { NodeDefaultView } from ".";
import { Branch } from "../Branch";
import { useFlowContext } from '../FlowContext';
@@ -20,7 +18,7 @@ function CalculationItem({ value, onChange, onRemove }) {
return null;
}
- const { calculator, operands = [] } = value;
+ const { calculator, operands = [null] } = value;
return (
n.id == options.nodeId);
const value = options?.path;
@@ -50,7 +51,7 @@ export default {
collection={config.collection}
value={value}
onChange={(path) => {
- onChange({ type, options: { ...options, path } });
+ onChange(`{{$jobsMapByNodeId.${options.nodeId}.${path}}}`);
}}
/>
);
diff --git a/packages/plugins/workflow/src/client/nodes/index.tsx b/packages/plugins/workflow/src/client/nodes/index.tsx
index a956751a0..1018b4b50 100644
--- a/packages/plugins/workflow/src/client/nodes/index.tsx
+++ b/packages/plugins/workflow/src/client/nodes/index.tsx
@@ -7,6 +7,8 @@ import { css, cx } from '@emotion/css';
import { ISchema, useForm } from '@formily/react';
import { Button, message, Modal, Tag } from 'antd';
import { useTranslation } from 'react-i18next';
+import parse from 'json-templates';
+
import { Registry } from '@nocobase/utils/client';
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
@@ -146,6 +148,24 @@ export function RemoveButton() {
onNodeRemoved(node);
}
+ const usingNodes = nodes.filter(node => {
+ if (node === current) {
+ return false;
+ }
+
+ const template = parse(node.config);
+ const refs = template.parameters.filter(({ key }) => key.startsWith(`$jobsMapByNodeId.${current.id}.`) || key === `$jobsMapByNodeId.${current.id}`);
+ return refs.length;
+ });
+
+ if (usingNodes.length) {
+ Modal.error({
+ title: lang('Can not delete'),
+ content: lang('The result of this node has been referenced by other nodes ({{nodes}}), please remove the usage before deleting.', { nodes: usingNodes.map(item => `#${item.id}`).join(', ') }),
+ });
+ return;
+ }
+
const hasBranches = !nodes.find(item => item.upstream === current && item.branchIndex != null);
const message = hasBranches
? t('Are you sure you want to delete it?')
diff --git a/packages/plugins/workflow/src/client/nodes/query.tsx b/packages/plugins/workflow/src/client/nodes/query.tsx
index 8f2dd5897..f22e4b821 100644
--- a/packages/plugins/workflow/src/client/nodes/query.tsx
+++ b/packages/plugins/workflow/src/client/nodes/query.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
import { useFlowContext } from '../FlowContext';
-import { VariableComponent } from '../calculators';
+import { useOperandContext, VariableComponent } from '../calculators';
import { collection, filter } from '../schemas/collection';
import CollectionFieldSelect from '../components/CollectionFieldSelect';
import { NAMESPACE } from '../locale';
@@ -46,8 +46,9 @@ export default {
VariableComponent
},
getter(props) {
- const { type, options, onChange } = props;
+ const { onChange } = props;
const { nodes } = useFlowContext();
+ const { options } = useOperandContext();
const { config } = nodes.find(n => n.id == options.nodeId);
const value = options?.path;
@@ -56,7 +57,7 @@ export default {
collection={config.collection}
value={value}
onChange={(path) => {
- onChange({ type, options: { ...options, path } });
+ onChange(`{{$jobsMapByNodeId.${options.nodeId}.${path}}}`);
}}
/>
);
diff --git a/packages/plugins/workflow/src/client/triggers/collection.tsx b/packages/plugins/workflow/src/client/triggers/collection.tsx
index 130993253..eff78ccef 100644
--- a/packages/plugins/workflow/src/client/triggers/collection.tsx
+++ b/packages/plugins/workflow/src/client/triggers/collection.tsx
@@ -10,6 +10,7 @@ import { css } from '@emotion/css';
import { onFieldValueChange } from '@formily/core';
import CollectionFieldSelect from '../components/CollectionFieldSelect';
import { NAMESPACE, useWorkflowTranslation } from '../locale';
+import { useOperandContext } from '../calculators';
const FieldsSelect = observer((props) => {
const compile = useCompile();
@@ -147,15 +148,16 @@ export default {
return options;
},
getter(props) {
- const { type, options, onChange } = props;
+ const { onChange } = props;
const { workflow } = useFlowContext();
+ const { options } = useOperandContext();
return (
{
- onChange({ type, options: { ...options, type: 'data', path } });
+ onChange(`{{$context.data.${path}}}`);
}}
/>
);
diff --git a/packages/plugins/workflow/src/client/triggers/schedule/index.tsx b/packages/plugins/workflow/src/client/triggers/schedule/index.tsx
index 379844049..2cb35106e 100644
--- a/packages/plugins/workflow/src/client/triggers/schedule/index.tsx
+++ b/packages/plugins/workflow/src/client/triggers/schedule/index.tsx
@@ -5,7 +5,7 @@ import { useCompile, useCollectionDataSource, useCollectionManager } from '@noco
import { ScheduleConfig } from './ScheduleConfig';
import { useFlowContext } from '../../FlowContext';
-import { BaseTypeSet } from '../../calculators';
+import { BaseTypeSet, useOperandContext } from '../../calculators';
import { SCHEDULE_MODE } from './constants';
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
@@ -40,11 +40,12 @@ export default {
}
return options;
},
- getter({ type, options, onChange }) {
+ getter({ onChange }) {
const { t } = useWorkflowTranslation();
const compile = useCompile();
const { collections = [] } = useCollectionManager();
const { workflow } = useFlowContext();
+ const { options } = useOperandContext();
const path = options?.path ? options.path.split('.') : [];
if (!options.type || options.type === 'date') {
return null;
@@ -61,7 +62,7 @@ export default {
label: compile(field.uiSchema?.title),
}))}
onChange={(next) => {
- onChange({ type, options: { ...options, path: next.join('.') } });
+ onChange(`{{$context.${next.join('.')}}}`);
}}
allowClear={false}
/>
diff --git a/packages/plugins/workflow/src/server/Plugin.ts b/packages/plugins/workflow/src/server/Plugin.ts
index b5f6bf4e6..55d4b0fac 100644
--- a/packages/plugins/workflow/src/server/Plugin.ts
+++ b/packages/plugins/workflow/src/server/Plugin.ts
@@ -76,6 +76,14 @@ export default class WorkflowPlugin extends Plugin {
directory: path.resolve(__dirname, 'collections'),
});
+ this.db.addMigrations({
+ namespace: 'workflow',
+ directory: path.resolve(__dirname, 'migrations'),
+ context: {
+ plugin: this,
+ },
+ });
+
initActions(this);
initTriggers(this, options.triggers);
initInstructions(this, options.instructions);
diff --git a/packages/plugins/workflow/src/server/__tests__/instructions/calculation.test.ts b/packages/plugins/workflow/src/server/__tests__/instructions/calculation.test.ts
index 09978be96..28ce689ac 100644
--- a/packages/plugins/workflow/src/server/__tests__/instructions/calculation.test.ts
+++ b/packages/plugins/workflow/src/server/__tests__/instructions/calculation.test.ts
@@ -33,6 +33,26 @@ describe('workflow > instructions > calculation', () => {
describe('operand types', () => {
it('constant', async () => {
+ const n1 = await workflow.createNode({
+ type: 'calculation',
+ config: {
+ calculation: {
+ calculator: 'add',
+ operands: [1, 1]
+ }
+ }
+ });
+
+ const post = await PostRepo.create({ values: { title: 't1' } });
+
+ await sleep(500);
+
+ const [execution] = await workflow.getExecutions();
+ const [job] = await execution.getJobs();
+ expect(job.result).toBe(2);
+ });
+
+ it('constant (legacy)', async () => {
const n1 = await workflow.createNode({
type: 'calculation',
config: {
@@ -84,10 +104,7 @@ describe('workflow > instructions > calculation', () => {
config: {
calculation: {
calculator: 'add',
- operands: [
- { value: 1 },
- { value: '{{$context.data.read}}' }
- ]
+ operands: [1, '{{$context.data.read}}']
}
}
});
@@ -141,10 +158,7 @@ describe('workflow > instructions > calculation', () => {
config: {
calculation: {
calculator: 'add',
- operands: [
- { value: 1 },
- { value: `{{$jobsMapByNodeId.${n1.id}.data.read}}` }
- ]
+ operands: [1, `{{$jobsMapByNodeId.${n1.id}.data.read}}`]
}
},
upstreamId: n1.id
@@ -198,10 +212,7 @@ describe('workflow > instructions > calculation', () => {
type: '$calculation',
options: {
calculator: 'minus',
- operands: [
- { value: '{{$context.data.read}}' },
- { value: 2 }
- ]
+ operands: ['{{$context.data.read}}', 2]
}
}
]
diff --git a/packages/plugins/workflow/src/server/calculators/index.ts b/packages/plugins/workflow/src/server/calculators/index.ts
index da3a9ac7b..14d627d4d 100644
--- a/packages/plugins/workflow/src/server/calculators/index.ts
+++ b/packages/plugins/workflow/src/server/calculators/index.ts
@@ -25,6 +25,8 @@ export type CalculationOptions = {
operands: Operand[]
};
+export type ValueOperand = string | number | boolean | null | Date;
+
export type ConstantOperand = {
type?: 'constant';
value: any
@@ -51,7 +53,7 @@ export type Calculation = {
};
// TODO(type): union type here is wrong
-export type Operand = ContextOperand | InputOperand | JobOperand | ConstantOperand | Calculation;
+export type Operand = ValueOperand | ContextOperand | InputOperand | JobOperand | ConstantOperand | Calculation;
// @deprecated
// HACK: if no path provided, return self
@@ -65,19 +67,20 @@ function get(object, path?: string | Array) {
// this method could only be used in executing nodes.
// because type of 'job' need loaded jobs in runtime execution.
// or the execution should be prepared first.
-export function calculate(operand: Operand, lastJob: JobModel, processor: Processor) {
+export function calculate(operand, lastJob: JobModel, processor: Processor) {
+ if (typeof operand !== 'object' || operand == null) {
+ return operand;
+ }
+ // @Deprecated
switch (operand.type) {
- // @Deprecated
// from execution context
case '$context':
return get(processor.execution.context, [operand.options.type, operand.options.path].filter(Boolean).join('.'));
- // @Deprecated
// from last job (or input job)
case '$input':
return lastJob ?? get(lastJob.result, operand.options.path);
- // @Deprecated
// from job in execution
case '$jobsMapByNodeId':
// assume jobs have been fetched from execution before
diff --git a/packages/plugins/workflow/src/server/instructions/condition.ts b/packages/plugins/workflow/src/server/instructions/condition.ts
index af1072ba9..ef3501064 100644
--- a/packages/plugins/workflow/src/server/instructions/condition.ts
+++ b/packages/plugins/workflow/src/server/instructions/condition.ts
@@ -77,7 +77,8 @@ export default {
// TODO(optimize): loading of jobs could be reduced and turned into incrementally in processor
// const jobs = await processor.getJobs();
const { calculation, rejectOnFalse } = node.config || {};
- const result = logicCalculate(calculation, prevJob, processor);
+
+ const result = logicCalculate(processor.getParsedValue(calculation), prevJob, processor);
if (!result && rejectOnFalse) {
return {
diff --git a/packages/plugins/workflow/src/server/migrations/20221129153547-calculation-variables.ts b/packages/plugins/workflow/src/server/migrations/20221129153547-calculation-variables.ts
new file mode 100644
index 000000000..d68de4f90
--- /dev/null
+++ b/packages/plugins/workflow/src/server/migrations/20221129153547-calculation-variables.ts
@@ -0,0 +1,62 @@
+import { Migration } from '@nocobase/server';
+
+const VTypes = {
+ constant(operand) {
+ return operand.value;
+ },
+ $jobsMapByNodeId({ options }) {
+ const paths = [options.nodeId, options.path].filter(Boolean);
+ return paths ? `{{$jobsMapByNodeId.${paths.join('.')}}}` : null;
+ },
+ $context({ options }) {
+ return `{{$context.${options.path}}}`;
+ }
+};
+
+function migrateConfig(config) {
+ if (Array.isArray(config)) {
+ return config.map(item => migrateConfig(item));
+ }
+ if (typeof config !== 'object') {
+ return config;
+ }
+ if (!config) {
+ return config;
+ }
+ if (config.type && VTypes[config.type] && (config.options || config.value)) {
+ return VTypes[config.type](config);
+ }
+ return Object.keys(config).reduce((memo, key) => ({ ...memo, [key]: migrateConfig(config[key]) }), {});
+}
+
+export default class AddUsersPhoneMigration extends Migration {
+ async up() {
+ const match = await this.app.version.satisfies('<=0.8.0-alpha.13');
+ if (!match) {
+ return;
+ }
+ const NodeRepo = this.context.db.getRepository('flow_nodes');
+ await this.context.db.sequelize.transaction(async (transaction) => {
+ const nodes = await NodeRepo.find({
+ filter: {
+ type: {
+ $or: ['calculation', 'condition']
+ }
+ },
+ transaction
+ });
+ console.log('%d nodes need to be migrated.', nodes.length);
+
+ await nodes.reduce((promise, node) => {
+ return node.update({
+ config: migrateConfig(node.config)
+ }, {
+ transaction
+ });
+
+ }, Promise.resolve());
+ });
+ }
+
+ async down() {}
+}