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*$/;
|
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);
|
const matcher = value.match(JT_VALUE_RE);
|
||||||
if (!matcher) {
|
if (!matcher) {
|
||||||
return { type: 'constant', value, options: { type: 'string' } };
|
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']);
|
export const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']);
|
||||||
|
|
||||||
const ConstantTypes = {
|
const ConstantTypes = {
|
||||||
|
null: {
|
||||||
|
title: `{{t("Null", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
value: 'null',
|
||||||
|
default: null,
|
||||||
|
component: NullRender,
|
||||||
|
},
|
||||||
string: {
|
string: {
|
||||||
title: `{{t("String", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("String", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: 'string',
|
value: 'string',
|
||||||
component({ onChange, type, options, value }) {
|
component({ onChange, value }) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={ev => onChange({ value: ev.target.value, type, options })}
|
onChange={ev => onChange(ev.target.value)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -174,11 +211,11 @@ const ConstantTypes = {
|
|||||||
number: {
|
number: {
|
||||||
title: '{{t("Number")}}',
|
title: '{{t("Number")}}',
|
||||||
value: 'number',
|
value: 'number',
|
||||||
component({ onChange, type, options, value }) {
|
component({ onChange, value }) {
|
||||||
return (
|
return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
value={value}
|
value={value}
|
||||||
onChange={v => onChange({ value: v, type, options })}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -187,12 +224,12 @@ const ConstantTypes = {
|
|||||||
boolean: {
|
boolean: {
|
||||||
title: `{{t("Boolean", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Boolean", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: 'boolean',
|
value: 'boolean',
|
||||||
component({ onChange, type, options, value }) {
|
component({ onChange, value }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
value={value}
|
||||||
onChange={v => onChange({ value: v, type, options })}
|
onChange={onChange}
|
||||||
placeholder={t('Select')}
|
placeholder={t('Select')}
|
||||||
>
|
>
|
||||||
<Select.Option value={true}>{lang('True')}</Select.Option>
|
<Select.Option value={true}>{lang('True')}</Select.Option>
|
||||||
@ -220,22 +257,16 @@ export const VariableTypes = {
|
|||||||
value: item.value,
|
value: item.value,
|
||||||
label: item.title
|
label: item.title
|
||||||
})),
|
})),
|
||||||
component({ options = { type: 'string' } }) {
|
component(props) {
|
||||||
return ConstantTypes[options.type]?.component ?? NullRender;
|
const { options = { type: 'string' } } = useOperandContext();
|
||||||
|
return ConstantTypes[options.type]?.component(props);
|
||||||
},
|
},
|
||||||
appendTypeValue({ options = { type: 'string' } }) {
|
appendTypeValue({ options = { type: 'string' } }) {
|
||||||
return options?.type ? [options.type] : [];
|
return options?.type ? [options.type] : [];
|
||||||
},
|
},
|
||||||
onTypeChange(old, [type, optionsType], onChange) {
|
onTypeChange([type, optionsType], onChange) {
|
||||||
if (old?.options?.type === optionsType) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { default: value } = ConstantTypes[optionsType];
|
const { default: value } = ConstantTypes[optionsType];
|
||||||
onChange({
|
onChange(value);
|
||||||
value,
|
|
||||||
type,
|
|
||||||
options: { ...old.options, type: optionsType }
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
parse(path) {
|
parse(path) {
|
||||||
return { path };
|
return { path };
|
||||||
@ -260,40 +291,30 @@ export const VariableTypes = {
|
|||||||
|
|
||||||
return stack;
|
return stack;
|
||||||
},
|
},
|
||||||
component({ options }) {
|
component(props) {
|
||||||
const { nodes } = useFlowContext();
|
const { nodes } = useFlowContext();
|
||||||
|
const { options } = useOperandContext();
|
||||||
if (!options?.nodeId) {
|
if (!options?.nodeId) {
|
||||||
return NullRender;
|
return null;
|
||||||
}
|
}
|
||||||
const node = nodes.find(n => n.id == options.nodeId);
|
const node = nodes.find(n => n.id == options.nodeId);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return NullRender;
|
return null;
|
||||||
}
|
}
|
||||||
const instruction = instructions.get(node.type);
|
const instruction = instructions.get(node.type);
|
||||||
return instruction?.getter ?? NullRender;
|
return instruction?.getter(props);
|
||||||
},
|
},
|
||||||
appendTypeValue({ options = {} }: { type: string, options: any }) {
|
appendTypeValue({ options = {} }: { type: string, options: any }) {
|
||||||
return options.nodeId ? [Number.parseInt(options.nodeId, 10)] : [];
|
return options.nodeId ? [Number.parseInt(options.nodeId, 10)] : [];
|
||||||
},
|
},
|
||||||
onTypeChange(old, [type, nodeId], onChange) {
|
onTypeChange([type, nodeId], onChange) {
|
||||||
onChange({
|
onChange(`{{${type}.${nodeId}}}`);
|
||||||
// ...old,
|
|
||||||
type,
|
|
||||||
options: { nodeId }
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
parse([nodeId, ...path]) {
|
parse([nodeId, ...path]) {
|
||||||
return { nodeId, path: path.join('.') };
|
return { nodeId, path: path.join('.') };
|
||||||
},
|
},
|
||||||
stringify({ options }) {
|
stringify(next) {
|
||||||
const stack = ['$jobsMapByNodeId'];
|
return `{{${next.join('.')}}}`;
|
||||||
if (options.nodeId) {
|
|
||||||
stack.push(options.nodeId);
|
|
||||||
if (options.path) {
|
|
||||||
stack.push(options.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `{{${stack.join('.')}}}`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
$context: {
|
$context: {
|
||||||
@ -304,29 +325,22 @@ export const VariableTypes = {
|
|||||||
const trigger = triggers.get(workflow.type);
|
const trigger = triggers.get(workflow.type);
|
||||||
return trigger?.getOptions?.(workflow.config) ?? null;
|
return trigger?.getOptions?.(workflow.config) ?? null;
|
||||||
},
|
},
|
||||||
component() {
|
component(props) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
const trigger = triggers.get(workflow.type);
|
const trigger = triggers.get(workflow.type);
|
||||||
return trigger?.getter ?? NullRender;
|
return trigger?.getter(props);
|
||||||
},
|
},
|
||||||
appendTypeValue({ options }) {
|
appendTypeValue({ options }) {
|
||||||
return options.type ? [options.type] : [];
|
return options.type ? [options.type] : [];
|
||||||
},
|
},
|
||||||
onTypeChange(old, [type, optionType], onChange) {
|
onTypeChange([type, optionType], onChange) {
|
||||||
onChange({ type, options: { type: optionType } });
|
onChange(`{{${type}.${optionType}}}`);
|
||||||
},
|
},
|
||||||
parse([type, ...path]) {
|
parse([type, ...path]) {
|
||||||
return { type, ...( path?.length ? { path: path.join('.') } : {}) };
|
return { type, ...( path?.length ? { path: path.join('.') } : {}) };
|
||||||
},
|
},
|
||||||
stringify({ options }) {
|
stringify(next) {
|
||||||
const stack = ['$context'];
|
return `{{${next.join('.')}}}`;
|
||||||
if (options?.type) {
|
|
||||||
stack.push(options.type);
|
|
||||||
}
|
|
||||||
if (options?.path) {
|
|
||||||
stack.push(options.path);
|
|
||||||
}
|
|
||||||
return `{{${stack.join('.')}}}`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// calculation: Calculation
|
// calculation: Calculation
|
||||||
@ -339,27 +353,30 @@ export function useVariableTypes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OperandProps {
|
interface OperandProps {
|
||||||
value: {
|
value: any;
|
||||||
type: string;
|
|
||||||
value?: any;
|
|
||||||
options?: any;
|
|
||||||
};
|
|
||||||
onChange(v: any): void;
|
onChange(v: any): void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OperandContext = React.createContext(null);
|
||||||
|
|
||||||
|
export function useOperandContext() {
|
||||||
|
return React.useContext(OperandContext);
|
||||||
|
}
|
||||||
|
|
||||||
export function Operand({
|
export function Operand({
|
||||||
value: operand = { type: 'constant', value: '', options: { type: 'string' } },
|
value = null,
|
||||||
onChange,
|
onChange,
|
||||||
children
|
children
|
||||||
}: OperandProps) {
|
}: OperandProps) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const Types = useVariableTypes();
|
const Types = useVariableTypes();
|
||||||
|
|
||||||
|
const operand = parseValue(value, Types);
|
||||||
|
|
||||||
const { type } = operand;
|
const { type } = operand;
|
||||||
|
|
||||||
const { component, appendTypeValue } = Types[type] || {};
|
const { component: Variable = NullRender, appendTypeValue } = Types[type] || {};
|
||||||
const Variable = typeof component === 'function' ? component(operand) : NullRender;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css`
|
<div className={css`
|
||||||
@ -380,23 +397,32 @@ export function Operand({
|
|||||||
isLeaf: !options
|
isLeaf: !options
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
onChange={(next: Array<string | number>) => {
|
onChange={(next: string[]) => {
|
||||||
const { onTypeChange } = Types[next[0]];
|
// 类型变化,包括主类型和子类型
|
||||||
|
const { onTypeChange, stringify } = Types[next[0]];
|
||||||
|
// 自定义处理
|
||||||
if (typeof onTypeChange === 'function') {
|
if (typeof onTypeChange === 'function') {
|
||||||
onTypeChange(operand, next, onChange);
|
return onTypeChange(next, onChange);
|
||||||
} else {
|
|
||||||
if (next[0] !== type) {
|
|
||||||
onChange({ type: next[0], value: null });
|
|
||||||
}
|
}
|
||||||
|
// 主类型变化
|
||||||
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Calculation({ calculator, operands = [], onChange }) {
|
export function Calculation({ calculator, operands = [null], onChange }) {
|
||||||
const { t } = useWorkflowTranslation();
|
const { t } = useWorkflowTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
return (
|
return (
|
||||||
@ -407,7 +433,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`}>
|
`}>
|
||||||
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
|
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
|
||||||
{operands[0]
|
{typeof operands[0] !== 'undefined'
|
||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<Select
|
<Select
|
||||||
@ -434,32 +460,20 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function VariableComponent({ value, onChange, renderSchemaComponent }) {
|
export function VariableComponent({ value, onChange, renderSchemaComponent }) {
|
||||||
const VTypes = { ...VariableTypes,
|
const VTypes = {
|
||||||
|
...VariableTypes,
|
||||||
constant: {
|
constant: {
|
||||||
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: 'constant',
|
value: 'constant',
|
||||||
options: undefined
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const operand = typeof value === 'string'
|
const { type } = parseValue(value, VTypes);
|
||||||
? parseStringValue(value, VTypes)
|
|
||||||
: { type: 'constant', value };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VariableTypesContext.Provider value={VTypes}>
|
<VariableTypesContext.Provider value={VTypes}>
|
||||||
<Operand
|
<Operand value={value} onChange={onChange}>
|
||||||
value={operand}
|
{type === 'constant' ? renderSchemaComponent() : null}
|
||||||
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>
|
</Operand>
|
||||||
</VariableTypesContext.Provider>
|
</VariableTypesContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
|
|
||||||
import { CollectionField, CollectionProvider, SchemaComponent, useCollectionManager, useCompile } from "@nocobase/client";
|
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";
|
import { lang, NAMESPACE } from "../locale";
|
||||||
|
|
||||||
function AssociationInput(props) {
|
function AssociationInput(props) {
|
||||||
@ -66,14 +66,10 @@ export default observer(({ value, onChange }: any) => {
|
|||||||
constant: {
|
constant: {
|
||||||
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Constant", { ns: "${NAMESPACE}" })}}`,
|
||||||
value: 'constant',
|
value: 'constant',
|
||||||
options: undefined
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const operand = typeof value[field.name] === 'string'
|
const operand = parseValue(value[field.name], VTypes);
|
||||||
? parseStringValue(value[field.name], VTypes)
|
|
||||||
: { type: 'constant', value: value[field.name] };
|
|
||||||
|
|
||||||
// constant for associations to use Input, others to use CollectionField
|
// constant for associations to use Input, others to use CollectionField
|
||||||
// dynamic values only support belongsTo/hasOne association, other association type should disable
|
// 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}>
|
<VariableTypesContext.Provider value={VTypes}>
|
||||||
<Operand
|
<Operand
|
||||||
value={operand}
|
value={value[field.name]}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
if (next.type !== operand.type && next.type === 'constant') {
|
onChange({ ...value, [field.name]: next });
|
||||||
onChange({ ...value, [field.name]: null });
|
|
||||||
} else {
|
|
||||||
const { stringify } = VTypes[next.type];
|
|
||||||
onChange({ ...value, [field.name]: stringify(next) });
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{operand.type === 'constant'
|
{operand.type === 'constant'
|
||||||
|
@ -15,8 +15,8 @@ i18n.addResources('ja-JP', NAMESPACE, jaJP);
|
|||||||
i18n.addResources('ru-RU', NAMESPACE, ruRU);
|
i18n.addResources('ru-RU', NAMESPACE, ruRU);
|
||||||
i18n.addResources('tr-TR', NAMESPACE, trTR);
|
i18n.addResources('tr-TR', NAMESPACE, trTR);
|
||||||
|
|
||||||
export function lang(key: string) {
|
export function lang(key: string, options = {}) {
|
||||||
return i18n.t(key, { ns: NAMESPACE });
|
return i18n.t(key, { ...options, ns: NAMESPACE });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkflowTranslation() {
|
export function useWorkflowTranslation() {
|
||||||
|
@ -65,6 +65,7 @@ export default {
|
|||||||
'Node result': '节点数据',
|
'Node result': '节点数据',
|
||||||
'Constant': '常量',
|
'Constant': '常量',
|
||||||
|
|
||||||
|
'Null': '空值',
|
||||||
'Boolean': '逻辑值',
|
'Boolean': '逻辑值',
|
||||||
'String': '字符串',
|
'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.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。',
|
'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': '已经执行过工作流的触发器不能被修改',
|
'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改',
|
||||||
'Node 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 请求',
|
'HTTP request': 'HTTP 请求',
|
||||||
'URL': '地址',
|
'URL': '地址',
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Calculation } from '../calculators';
|
import { Calculation } from '../calculators';
|
||||||
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
||||||
|
@ -4,8 +4,6 @@ import { Button, Select } from "antd";
|
|||||||
import { CloseCircleOutlined } from '@ant-design/icons';
|
import { CloseCircleOutlined } from '@ant-design/icons';
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { i18n } from "@nocobase/client";
|
|
||||||
|
|
||||||
import { NodeDefaultView } from ".";
|
import { NodeDefaultView } from ".";
|
||||||
import { Branch } from "../Branch";
|
import { Branch } from "../Branch";
|
||||||
import { useFlowContext } from '../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
@ -20,7 +18,7 @@ function CalculationItem({ value, onChange, onRemove }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { calculator, operands = [] } = value;
|
const { calculator, operands = [null] } = value;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css`
|
<div className={css`
|
||||||
|
@ -6,6 +6,7 @@ import { useFlowContext } from '../FlowContext';
|
|||||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||||
import CollectionFieldset from '../components/CollectionFieldset';
|
import CollectionFieldset from '../components/CollectionFieldset';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
|
import { useOperandContext } from '../calculators';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -39,8 +40,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
CollectionFieldset
|
CollectionFieldset
|
||||||
},
|
},
|
||||||
getter(props) {
|
getter({ onChange }) {
|
||||||
const { type, options, onChange } = props;
|
const { options } = useOperandContext();
|
||||||
const { nodes } = useFlowContext();
|
const { nodes } = useFlowContext();
|
||||||
const { config } = nodes.find(n => n.id == options.nodeId);
|
const { config } = nodes.find(n => n.id == options.nodeId);
|
||||||
const value = options?.path;
|
const value = options?.path;
|
||||||
@ -50,7 +51,7 @@ export default {
|
|||||||
collection={config.collection}
|
collection={config.collection}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(path) => {
|
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 { ISchema, useForm } from '@formily/react';
|
||||||
import { Button, message, Modal, Tag } from 'antd';
|
import { Button, message, Modal, Tag } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import parse from 'json-templates';
|
||||||
|
|
||||||
import { Registry } from '@nocobase/utils/client';
|
import { Registry } from '@nocobase/utils/client';
|
||||||
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
|
import { SchemaComponent, useActionContext, useAPIClient, useCompile, useRequest, useResourceActionContext } from '@nocobase/client';
|
||||||
|
|
||||||
@ -146,6 +148,24 @@ export function RemoveButton() {
|
|||||||
onNodeRemoved(node);
|
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 hasBranches = !nodes.find(item => item.upstream === current && item.branchIndex != null);
|
||||||
const message = hasBranches
|
const message = hasBranches
|
||||||
? t('Are you sure you want to delete it?')
|
? 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 { useCollectionDataSource, useCollectionManager, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from '../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { VariableComponent } from '../calculators';
|
import { useOperandContext, VariableComponent } from '../calculators';
|
||||||
import { collection, filter } from '../schemas/collection';
|
import { collection, filter } from '../schemas/collection';
|
||||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
@ -46,8 +46,9 @@ export default {
|
|||||||
VariableComponent
|
VariableComponent
|
||||||
},
|
},
|
||||||
getter(props) {
|
getter(props) {
|
||||||
const { type, options, onChange } = props;
|
const { onChange } = props;
|
||||||
const { nodes } = useFlowContext();
|
const { nodes } = useFlowContext();
|
||||||
|
const { options } = useOperandContext();
|
||||||
const { config } = nodes.find(n => n.id == options.nodeId);
|
const { config } = nodes.find(n => n.id == options.nodeId);
|
||||||
const value = options?.path;
|
const value = options?.path;
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export default {
|
|||||||
collection={config.collection}
|
collection={config.collection}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(path) => {
|
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 { onFieldValueChange } from '@formily/core';
|
||||||
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
import CollectionFieldSelect from '../components/CollectionFieldSelect';
|
||||||
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
||||||
|
import { useOperandContext } from '../calculators';
|
||||||
|
|
||||||
const FieldsSelect = observer((props) => {
|
const FieldsSelect = observer((props) => {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
@ -147,15 +148,16 @@ export default {
|
|||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
getter(props) {
|
getter(props) {
|
||||||
const { type, options, onChange } = props;
|
const { onChange } = props;
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
|
const { options } = useOperandContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionFieldSelect
|
<CollectionFieldSelect
|
||||||
collection={workflow.config.collection}
|
collection={workflow.config.collection}
|
||||||
value={options?.path}
|
value={options?.path}
|
||||||
onChange={(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 { ScheduleConfig } from './ScheduleConfig';
|
||||||
import { useFlowContext } from '../../FlowContext';
|
import { useFlowContext } from '../../FlowContext';
|
||||||
import { BaseTypeSet } from '../../calculators';
|
import { BaseTypeSet, useOperandContext } from '../../calculators';
|
||||||
import { SCHEDULE_MODE } from './constants';
|
import { SCHEDULE_MODE } from './constants';
|
||||||
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
|
import { NAMESPACE, useWorkflowTranslation } from '../../locale';
|
||||||
|
|
||||||
@ -40,11 +40,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
getter({ type, options, onChange }) {
|
getter({ onChange }) {
|
||||||
const { t } = useWorkflowTranslation();
|
const { t } = useWorkflowTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { collections = [] } = useCollectionManager();
|
const { collections = [] } = useCollectionManager();
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
|
const { options } = useOperandContext();
|
||||||
const path = options?.path ? options.path.split('.') : [];
|
const path = options?.path ? options.path.split('.') : [];
|
||||||
if (!options.type || options.type === 'date') {
|
if (!options.type || options.type === 'date') {
|
||||||
return null;
|
return null;
|
||||||
@ -61,7 +62,7 @@ export default {
|
|||||||
label: compile(field.uiSchema?.title),
|
label: compile(field.uiSchema?.title),
|
||||||
}))}
|
}))}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
onChange({ type, options: { ...options, path: next.join('.') } });
|
onChange(`{{$context.${next.join('.')}}}`);
|
||||||
}}
|
}}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
/>
|
/>
|
||||||
|
@ -76,6 +76,14 @@ export default class WorkflowPlugin extends Plugin {
|
|||||||
directory: path.resolve(__dirname, 'collections'),
|
directory: path.resolve(__dirname, 'collections'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.db.addMigrations({
|
||||||
|
namespace: 'workflow',
|
||||||
|
directory: path.resolve(__dirname, 'migrations'),
|
||||||
|
context: {
|
||||||
|
plugin: this,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
initActions(this);
|
initActions(this);
|
||||||
initTriggers(this, options.triggers);
|
initTriggers(this, options.triggers);
|
||||||
initInstructions(this, options.instructions);
|
initInstructions(this, options.instructions);
|
||||||
|
@ -33,6 +33,26 @@ describe('workflow > instructions > calculation', () => {
|
|||||||
|
|
||||||
describe('operand types', () => {
|
describe('operand types', () => {
|
||||||
it('constant', async () => {
|
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({
|
const n1 = await workflow.createNode({
|
||||||
type: 'calculation',
|
type: 'calculation',
|
||||||
config: {
|
config: {
|
||||||
@ -84,10 +104,7 @@ describe('workflow > instructions > calculation', () => {
|
|||||||
config: {
|
config: {
|
||||||
calculation: {
|
calculation: {
|
||||||
calculator: 'add',
|
calculator: 'add',
|
||||||
operands: [
|
operands: [1, '{{$context.data.read}}']
|
||||||
{ value: 1 },
|
|
||||||
{ value: '{{$context.data.read}}' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -141,10 +158,7 @@ describe('workflow > instructions > calculation', () => {
|
|||||||
config: {
|
config: {
|
||||||
calculation: {
|
calculation: {
|
||||||
calculator: 'add',
|
calculator: 'add',
|
||||||
operands: [
|
operands: [1, `{{$jobsMapByNodeId.${n1.id}.data.read}}`]
|
||||||
{ value: 1 },
|
|
||||||
{ value: `{{$jobsMapByNodeId.${n1.id}.data.read}}` }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
upstreamId: n1.id
|
upstreamId: n1.id
|
||||||
@ -198,10 +212,7 @@ describe('workflow > instructions > calculation', () => {
|
|||||||
type: '$calculation',
|
type: '$calculation',
|
||||||
options: {
|
options: {
|
||||||
calculator: 'minus',
|
calculator: 'minus',
|
||||||
operands: [
|
operands: ['{{$context.data.read}}', 2]
|
||||||
{ value: '{{$context.data.read}}' },
|
|
||||||
{ value: 2 }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -25,6 +25,8 @@ export type CalculationOptions = {
|
|||||||
operands: Operand[]
|
operands: Operand[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ValueOperand = string | number | boolean | null | Date;
|
||||||
|
|
||||||
export type ConstantOperand = {
|
export type ConstantOperand = {
|
||||||
type?: 'constant';
|
type?: 'constant';
|
||||||
value: any
|
value: any
|
||||||
@ -51,7 +53,7 @@ export type Calculation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// TODO(type): union type here is wrong
|
// 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
|
// @deprecated
|
||||||
// HACK: if no path provided, return self
|
// 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.
|
// this method could only be used in executing nodes.
|
||||||
// because type of 'job' need loaded jobs in runtime execution.
|
// because type of 'job' need loaded jobs in runtime execution.
|
||||||
// or the execution should be prepared first.
|
// or the execution should be prepared first.
|
||||||
export function calculate(operand: Operand, lastJob: JobModel, processor: Processor) {
|
export function calculate(operand, lastJob: JobModel, processor: Processor) {
|
||||||
switch (operand.type) {
|
if (typeof operand !== 'object' || operand == null) {
|
||||||
|
return operand;
|
||||||
|
}
|
||||||
// @Deprecated
|
// @Deprecated
|
||||||
|
switch (operand.type) {
|
||||||
// from execution context
|
// from execution context
|
||||||
case '$context':
|
case '$context':
|
||||||
return get(processor.execution.context, [operand.options.type, operand.options.path].filter(Boolean).join('.'));
|
return get(processor.execution.context, [operand.options.type, operand.options.path].filter(Boolean).join('.'));
|
||||||
|
|
||||||
// @Deprecated
|
|
||||||
// from last job (or input job)
|
// from last job (or input job)
|
||||||
case '$input':
|
case '$input':
|
||||||
return lastJob ?? get(lastJob.result, operand.options.path);
|
return lastJob ?? get(lastJob.result, operand.options.path);
|
||||||
|
|
||||||
// @Deprecated
|
|
||||||
// from job in execution
|
// from job in execution
|
||||||
case '$jobsMapByNodeId':
|
case '$jobsMapByNodeId':
|
||||||
// assume jobs have been fetched from execution before
|
// 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
|
// TODO(optimize): loading of jobs could be reduced and turned into incrementally in processor
|
||||||
// const jobs = await processor.getJobs();
|
// const jobs = await processor.getJobs();
|
||||||
const { calculation, rejectOnFalse } = node.config || {};
|
const { calculation, rejectOnFalse } = node.config || {};
|
||||||
const result = logicCalculate(calculation, prevJob, processor);
|
|
||||||
|
const result = logicCalculate(processor.getParsedValue(calculation), prevJob, processor);
|
||||||
|
|
||||||
if (!result && rejectOnFalse) {
|
if (!result && rejectOnFalse) {
|
||||||
return {
|
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