Fix(plugin-workflow): client refactor (#1163)
* fix(plugin-workflow): avoid to delete using node * fix(plugin-workflow): refactor operand * fix(plugin-workflow): simplify code * fix(plugin-workflow): fix condition calculator
This commit is contained in:
parent
ca0621b517
commit
a951d49f55
@ -141,7 +141,38 @@ function getGroupCalculators(group) {
|
||||
|
||||
const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
||||
|
||||
export function parseStringValue(value: string, Types) {
|
||||
function getType(value) {
|
||||
if (value == null) {
|
||||
return 'null';
|
||||
}
|
||||
const type = typeof value;
|
||||
switch (type) {
|
||||
case 'object':
|
||||
break;
|
||||
default:
|
||||
// 'boolean'
|
||||
// 'number'
|
||||
// 'bigint'
|
||||
// 'string'
|
||||
// 'symbol'
|
||||
return type;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return 'date';
|
||||
}
|
||||
return 'object';
|
||||
}
|
||||
|
||||
export function parseValue(value: any, Types): {
|
||||
type: string;
|
||||
value?: any;
|
||||
options?: any;
|
||||
} {
|
||||
const valueType = getType(value);
|
||||
if (valueType !== 'string') {
|
||||
return { type: 'constant', value, options: { type: valueType } }
|
||||
}
|
||||
|
||||
const matcher = value.match(JT_VALUE_RE);
|
||||
if (!matcher) {
|
||||
return { type: 'constant', value, options: { type: 'string' } };
|
||||
@ -158,14 +189,20 @@ export function parseStringValue(value: string, Types) {
|
||||
export const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']);
|
||||
|
||||
const ConstantTypes = {
|
||||
null: {
|
||||
title: `{{t("Null", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 'null',
|
||||
default: null,
|
||||
component: NullRender,
|
||||
},
|
||||
string: {
|
||||
title: `{{t("String", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 'string',
|
||||
component({ onChange, type, options, value }) {
|
||||
component({ onChange, value }) {
|
||||
return (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={ev => onChange({ value: ev.target.value, type, options })}
|
||||
onChange={ev => onChange(ev.target.value)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@ -174,11 +211,11 @@ const ConstantTypes = {
|
||||
number: {
|
||||
title: '{{t("Number")}}',
|
||||
value: 'number',
|
||||
component({ onChange, type, options, value }) {
|
||||
component({ onChange, value }) {
|
||||
return (
|
||||
<InputNumber
|
||||
value={value}
|
||||
onChange={v => onChange({ value: v, type, options })}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
},
|
||||
@ -187,12 +224,12 @@ const ConstantTypes = {
|
||||
boolean: {
|
||||
title: `{{t("Boolean", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 'boolean',
|
||||
component({ onChange, type, options, value }) {
|
||||
component({ onChange, value }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
onChange={v => onChange({ value: v, type, options })}
|
||||
onChange={onChange}
|
||||
placeholder={t('Select')}
|
||||
>
|
||||
<Select.Option value={true}>{lang('True')}</Select.Option>
|
||||
@ -220,22 +257,16 @@ export const VariableTypes = {
|
||||
value: item.value,
|
||||
label: item.title
|
||||
})),
|
||||
component({ options = { type: 'string' } }) {
|
||||
return ConstantTypes[options.type]?.component ?? NullRender;
|
||||
component(props) {
|
||||
const { options = { type: 'string' } } = useOperandContext();
|
||||
return ConstantTypes[options.type]?.component(props);
|
||||
},
|
||||
appendTypeValue({ options = { type: 'string' } }) {
|
||||
return options?.type ? [options.type] : [];
|
||||
},
|
||||
onTypeChange(old, [type, optionsType], onChange) {
|
||||
if (old?.options?.type === optionsType) {
|
||||
return;
|
||||
}
|
||||
onTypeChange([type, optionsType], onChange) {
|
||||
const { default: value } = ConstantTypes[optionsType];
|
||||
onChange({
|
||||
value,
|
||||
type,
|
||||
options: { ...old.options, type: optionsType }
|
||||
});
|
||||
onChange(value);
|
||||
},
|
||||
parse(path) {
|
||||
return { path };
|
||||
@ -260,40 +291,30 @@ export const VariableTypes = {
|
||||
|
||||
return stack;
|
||||
},
|
||||
component({ options }) {
|
||||
component(props) {
|
||||
const { nodes } = useFlowContext();
|
||||
const { options } = useOperandContext();
|
||||
if (!options?.nodeId) {
|
||||
return NullRender;
|
||||
return null;
|
||||
}
|
||||
const node = nodes.find(n => n.id == options.nodeId);
|
||||
if (!node) {
|
||||
return NullRender;
|
||||
return null;
|
||||
}
|
||||
const instruction = instructions.get(node.type);
|
||||
return instruction?.getter ?? NullRender;
|
||||
return instruction?.getter(props);
|
||||
},
|
||||
appendTypeValue({ options = {} }: { type: string, options: any }) {
|
||||
return options.nodeId ? [Number.parseInt(options.nodeId, 10)] : [];
|
||||
},
|
||||
onTypeChange(old, [type, nodeId], onChange) {
|
||||
onChange({
|
||||
// ...old,
|
||||
type,
|
||||
options: { nodeId }
|
||||
});
|
||||
onTypeChange([type, nodeId], onChange) {
|
||||
onChange(`{{${type}.${nodeId}}}`);
|
||||
},
|
||||
parse([nodeId, ...path]) {
|
||||
return { nodeId, path: path.join('.') };
|
||||
},
|
||||
stringify({ options }) {
|
||||
const stack = ['$jobsMapByNodeId'];
|
||||
if (options.nodeId) {
|
||||
stack.push(options.nodeId);
|
||||
if (options.path) {
|
||||
stack.push(options.path);
|
||||
}
|
||||
}
|
||||
return `{{${stack.join('.')}}}`;
|
||||
stringify(next) {
|
||||
return `{{${next.join('.')}}}`;
|
||||
}
|
||||
},
|
||||
$context: {
|
||||
@ -304,29 +325,22 @@ export const VariableTypes = {
|
||||
const trigger = triggers.get(workflow.type);
|
||||
return trigger?.getOptions?.(workflow.config) ?? null;
|
||||
},
|
||||
component() {
|
||||
component(props) {
|
||||
const { workflow } = useFlowContext();
|
||||
const trigger = triggers.get(workflow.type);
|
||||
return trigger?.getter ?? NullRender;
|
||||
return trigger?.getter(props);
|
||||
},
|
||||
appendTypeValue({ options }) {
|
||||
return options.type ? [options.type] : [];
|
||||
},
|
||||
onTypeChange(old, [type, optionType], onChange) {
|
||||
onChange({ type, options: { type: optionType } });
|
||||
onTypeChange([type, optionType], onChange) {
|
||||
onChange(`{{${type}.${optionType}}}`);
|
||||
},
|
||||
parse([type, ...path]) {
|
||||
return { type, ...( path?.length ? { path: path.join('.') } : {}) };
|
||||
},
|
||||
stringify({ options }) {
|
||||
const stack = ['$context'];
|
||||
if (options?.type) {
|
||||
stack.push(options.type);
|
||||
}
|
||||
if (options?.path) {
|
||||
stack.push(options.path);
|
||||
}
|
||||
return `{{${stack.join('.')}}}`;
|
||||
stringify(next) {
|
||||
return `{{${next.join('.')}}}`;
|
||||
}
|
||||
},
|
||||
// calculation: Calculation
|
||||
@ -339,27 +353,30 @@ export function useVariableTypes() {
|
||||
}
|
||||
|
||||
interface OperandProps {
|
||||
value: {
|
||||
type: string;
|
||||
value?: any;
|
||||
options?: any;
|
||||
};
|
||||
value: any;
|
||||
onChange(v: any): void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const OperandContext = React.createContext(null);
|
||||
|
||||
export function useOperandContext() {
|
||||
return React.useContext(OperandContext);
|
||||
}
|
||||
|
||||
export function Operand({
|
||||
value: operand = { type: 'constant', value: '', options: { type: 'string' } },
|
||||
value = null,
|
||||
onChange,
|
||||
children
|
||||
}: OperandProps) {
|
||||
const compile = useCompile();
|
||||
const Types = useVariableTypes();
|
||||
|
||||
const operand = parseValue(value, Types);
|
||||
|
||||
const { type } = operand;
|
||||
|
||||
const { component, appendTypeValue } = Types[type] || {};
|
||||
const Variable = typeof component === 'function' ? component(operand) : NullRender;
|
||||
const { component: Variable = NullRender, appendTypeValue } = Types[type] || {};
|
||||
|
||||
return (
|
||||
<div className={css`
|
||||
@ -380,23 +397,32 @@ export function Operand({
|
||||
isLeaf: !options
|
||||
};
|
||||
})}
|
||||
onChange={(next: Array<string | number>) => {
|
||||
const { onTypeChange } = Types[next[0]];
|
||||
onChange={(next: string[]) => {
|
||||
// 类型变化,包括主类型和子类型
|
||||
const { onTypeChange, stringify } = Types[next[0]];
|
||||
// 自定义处理
|
||||
if (typeof onTypeChange === 'function') {
|
||||
onTypeChange(operand, next, onChange);
|
||||
} else {
|
||||
if (next[0] !== type) {
|
||||
onChange({ type: next[0], value: null });
|
||||
return onTypeChange(next, onChange);
|
||||
}
|
||||
// 主类型变化
|
||||
if (next[0] !== type) {
|
||||
if (next[0] === 'constant') {
|
||||
return onChange(null);
|
||||
} else if (stringify) {
|
||||
return onChange(stringify(next));
|
||||
}
|
||||
return onChange({ type: next[0], value: null });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{children ?? <Variable {...operand} onChange={op => onChange({ ...op })} />}
|
||||
<OperandContext.Provider value={operand}>
|
||||
{children ?? <Variable value={operand.value} onChange={onChange} />}
|
||||
</OperandContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Calculation({ calculator, operands = [], onChange }) {
|
||||
export function Calculation({ calculator, operands = [null], onChange }) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const compile = useCompile();
|
||||
return (
|
||||
@ -407,7 +433,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
align-items: center;
|
||||
`}>
|
||||
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
|
||||
{operands[0]
|
||||
{typeof operands[0] !== 'undefined'
|
||||
? (
|
||||
<>
|
||||
<Select
|
||||
@ -434,32 +460,20 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
}
|
||||
|
||||
export function VariableComponent({ value, onChange, renderSchemaComponent }) {
|
||||
const VTypes = { ...VariableTypes,
|
||||
const VTypes = {
|
||||
...VariableTypes,
|
||||
constant: {
|
||||
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 'constant',
|
||||
options: undefined
|
||||
}
|
||||
};
|
||||
|
||||
const operand = typeof value === 'string'
|
||||
? parseStringValue(value, VTypes)
|
||||
: { type: 'constant', value };
|
||||
const { type } = parseValue(value, VTypes);
|
||||
|
||||
return (
|
||||
<VariableTypesContext.Provider value={VTypes}>
|
||||
<Operand
|
||||
value={operand}
|
||||
onChange={(next) => {
|
||||
if (next.type !== operand.type && next.type === 'constant') {
|
||||
onChange(null);
|
||||
} else {
|
||||
const { stringify } = VTypes[next.type];
|
||||
onChange(stringify(next));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{operand.type === 'constant' ? renderSchemaComponent() : null}
|
||||
<Operand value={value} onChange={onChange}>
|
||||
{type === 'constant' ? renderSchemaComponent() : null}
|
||||
</Operand>
|
||||
</VariableTypesContext.Provider>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { css } from "@emotion/css";
|
||||
|
||||
import { CollectionField, CollectionProvider, SchemaComponent, useCollectionManager, useCompile } from "@nocobase/client";
|
||||
import { Operand, parseStringValue, VariableTypes, VariableTypesContext } from "../calculators";
|
||||
import { Operand, parseValue, VariableTypes, VariableTypesContext } from "../calculators";
|
||||
import { lang, NAMESPACE } from "../locale";
|
||||
|
||||
function AssociationInput(props) {
|
||||
@ -66,14 +66,10 @@ export default observer(({ value, onChange }: any) => {
|
||||
constant: {
|
||||
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
||||
value: 'constant',
|
||||
options: undefined
|
||||
}
|
||||
};
|
||||
|
||||
const operand = typeof value[field.name] === 'string'
|
||||
? parseStringValue(value[field.name], VTypes)
|
||||
: { type: 'constant', value: value[field.name] };
|
||||
|
||||
const operand = parseValue(value[field.name], VTypes);
|
||||
// constant for associations to use Input, others to use CollectionField
|
||||
// dynamic values only support belongsTo/hasOne association, other association type should disable
|
||||
|
||||
@ -86,14 +82,9 @@ export default observer(({ value, onChange }: any) => {
|
||||
`}>
|
||||
<VariableTypesContext.Provider value={VTypes}>
|
||||
<Operand
|
||||
value={operand}
|
||||
value={value[field.name]}
|
||||
onChange={(next) => {
|
||||
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'
|
||||
|
@ -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() {
|
||||
|
@ -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': '地址',
|
||||
|
@ -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';
|
||||
|
@ -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 (
|
||||
<div className={css`
|
||||
|
@ -6,6 +6,7 @@ import { useFlowContext } from '../FlowContext';
|
||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useOperandContext } from '../calculators';
|
||||
|
||||
|
||||
|
||||
@ -39,8 +40,8 @@ export default {
|
||||
components: {
|
||||
CollectionFieldset
|
||||
},
|
||||
getter(props) {
|
||||
const { type, options, onChange } = props;
|
||||
getter({ onChange }) {
|
||||
const { options } = useOperandContext();
|
||||
const { nodes } = useFlowContext();
|
||||
const { config } = nodes.find(n => 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}}}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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?')
|
||||
|
@ -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}}}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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 (
|
||||
<CollectionFieldSelect
|
||||
collection={workflow.config.collection}
|
||||
value={options?.path}
|
||||
onChange={(path) => {
|
||||
onChange({ type, options: { ...options, type: 'data', path } });
|
||||
onChange(`{{$context.data.${path}}}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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);
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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<string>) {
|
||||
// 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
|
||||
|
@ -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 {
|
||||
|
@ -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() {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user