Feat/plugin workflow (#278)
* fix(plugin-workflow): fix query node variable config * feat(plugin-workflow): add more calculators * refactor(plugin-workflow): simplify operand codes and fix variable component bugs
This commit is contained in:
parent
f791d43716
commit
c4afb7586c
@ -10,8 +10,46 @@ function NullRender() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const calculators = [
|
export const calculators = [
|
||||||
{ value: 'equal', name: '等于' },
|
{
|
||||||
{ value: 'notEqual', name: '不等于' }
|
value: 'boolean',
|
||||||
|
title: '值比较',
|
||||||
|
children: [
|
||||||
|
{ value: 'equal', name: '=' },
|
||||||
|
{ value: 'notEqual', name: '≠' },
|
||||||
|
{ value: 'gt', name: '>' },
|
||||||
|
{ value: 'gte', name: '≥' },
|
||||||
|
{ value: 'lt', name: '<' },
|
||||||
|
{ value: 'lte', name: '≤' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'number',
|
||||||
|
title: '算术运算',
|
||||||
|
children: [
|
||||||
|
{ value: 'add', name: '+' },
|
||||||
|
{ value: 'minus', name: '-' },
|
||||||
|
{ value: 'multipe', name: '*' },
|
||||||
|
{ value: 'divide', name: '/' },
|
||||||
|
{ value: 'mod', name: '%' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'string',
|
||||||
|
title: '字符串',
|
||||||
|
children: [
|
||||||
|
{ value: 'includes', name: '包含' },
|
||||||
|
{ value: 'notIncludes', name: '不包含' },
|
||||||
|
{ value: 'startsWith', name: '开头是' },
|
||||||
|
{ value: 'notStartsWith', name: '开头不是' },
|
||||||
|
{ value: 'endsWith', name: '结尾是' },
|
||||||
|
{ value: 'notEndsWith', name: '结尾不是' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'date',
|
||||||
|
title: '日期',
|
||||||
|
children: []
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
||||||
@ -79,17 +117,17 @@ export const VariableTypes = {
|
|||||||
label: item.title
|
label: item.title
|
||||||
})),
|
})),
|
||||||
component({ options: { type } = { type: 'string' } }) {
|
component({ options: { type } = { type: 'string' } }) {
|
||||||
return type ? ConstantTypes[type].component : NullRender;
|
return ConstantTypes[type]?.component ?? NullRender;
|
||||||
},
|
},
|
||||||
appendTypeValue({ options = { type: 'string' } }) {
|
appendTypeValue({ options = { type: 'string' } }) {
|
||||||
return options?.type ? [options.type] : [];
|
return options?.type ? [options.type] : [];
|
||||||
},
|
},
|
||||||
onTypeChange(props, [type, optionsType], onChange) {
|
onTypeChange(old, [type, optionsType], onChange) {
|
||||||
const { default: value } = ConstantTypes[optionsType];
|
const { default: value } = ConstantTypes[optionsType];
|
||||||
onChange({
|
onChange({
|
||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
options: { ...props.options, type: optionsType }
|
options: { ...old.options, type: optionsType }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
parse(path) {
|
parse(path) {
|
||||||
@ -104,7 +142,7 @@ export const VariableTypes = {
|
|||||||
const stack = [];
|
const stack = [];
|
||||||
for (let current = node.upstream; current; current = current.upstream) {
|
for (let current = node.upstream; current; current = current.upstream) {
|
||||||
const { getter } = instructions.get(current.type);
|
const { getter } = instructions.get(current.type);
|
||||||
// consider `getter` as the key of a value available node
|
// Note: consider `getter` as the key of a value available node
|
||||||
if (getter) {
|
if (getter) {
|
||||||
stack.push({
|
stack.push({
|
||||||
value: current.id,
|
value: current.id,
|
||||||
@ -130,11 +168,11 @@ export const VariableTypes = {
|
|||||||
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(props, [type, nodeId], onChange) {
|
onTypeChange(old, [type, nodeId], onChange) {
|
||||||
onChange({
|
onChange({
|
||||||
...props,
|
// ...old,
|
||||||
type,
|
type,
|
||||||
options: { ...props.options, nodeId }
|
options: { nodeId }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
parse([nodeId, ...path]) {
|
parse([nodeId, ...path]) {
|
||||||
@ -155,6 +193,12 @@ export const VariableTypes = {
|
|||||||
// calculation: Calculation
|
// calculation: Calculation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const VariableTypesContext = React.createContext(null);
|
||||||
|
|
||||||
|
export function useVariableTypes() {
|
||||||
|
return React.useContext(VariableTypesContext);
|
||||||
|
}
|
||||||
|
|
||||||
interface OperandProps {
|
interface OperandProps {
|
||||||
value: {
|
value: {
|
||||||
type: string;
|
type: string;
|
||||||
@ -165,10 +209,12 @@ interface OperandProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Operand({ onChange, value: operand = { type: 'constant', value: '', options: { type: 'string' } } }: OperandProps) {
|
export function Operand({ onChange, value: operand = { type: 'constant', value: '', options: { type: 'string' } } }: OperandProps) {
|
||||||
|
const Types = useVariableTypes();
|
||||||
|
|
||||||
const { type } = operand;
|
const { type } = operand;
|
||||||
|
|
||||||
const { component, appendTypeValue } = VariableTypes[type];
|
const { component, appendTypeValue } = Types[type];
|
||||||
const VariableComponent = typeof component === 'function' ? component(operand) : component;
|
const VariableComponent = typeof component === 'function' ? component(operand) : NullRender;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css`
|
<div className={css`
|
||||||
@ -179,33 +225,35 @@ export function Operand({ onChange, value: operand = { type: 'constant', value:
|
|||||||
<Cascader
|
<Cascader
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
value={[type, ...(appendTypeValue ? appendTypeValue(operand) : [])]}
|
value={[type, ...(appendTypeValue ? appendTypeValue(operand) : [])]}
|
||||||
options={Object.values(VariableTypes).map(item => {
|
options={Object.values(Types).map((item: any) => {
|
||||||
const children = typeof item.options === 'function' ? item.options() : item.options;
|
const children = typeof item.options === 'function' ? item.options() : item.options;
|
||||||
return {
|
return {
|
||||||
label: item.title,
|
label: item.title,
|
||||||
value: item.value,
|
value: item.value,
|
||||||
children,
|
children,
|
||||||
disabled: children && !children.length
|
disabled: children && !children.length,
|
||||||
|
isLeaf: !children
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
onChange={(t: Array<string | number>) => {
|
onChange={(next: Array<string | number>) => {
|
||||||
const { onTypeChange } = VariableTypes[t[0]];
|
const { onTypeChange } = Types[next[0]];
|
||||||
if (typeof onTypeChange === 'function') {
|
if (typeof onTypeChange === 'function') {
|
||||||
onTypeChange(operand, t, onChange);
|
onTypeChange(operand, next, onChange);
|
||||||
} else {
|
} else {
|
||||||
if (t[0] !== type) {
|
if (next[0] !== type) {
|
||||||
onChange({ type: t[0], value: null });
|
onChange({ type: next[0], value: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<VariableComponent {...operand} onChange={v => onChange({ ...operand, value: v })} />
|
<VariableComponent {...operand} onChange={op => onChange({ ...op })} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Calculation({ calculator, operands, onChange }) {
|
export function Calculation({ calculator, operands = [], onChange }) {
|
||||||
return (
|
return (
|
||||||
|
<VariableTypesContext.Provider value={VariableTypes}>
|
||||||
<div className={css`
|
<div className={css`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
@ -213,6 +261,7 @@ export function Calculation({ calculator, operands, onChange }) {
|
|||||||
|
|
||||||
.ant-select{
|
.ant-select{
|
||||||
width: auto;
|
width: auto;
|
||||||
|
min-width: 6em;
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
|
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
|
||||||
@ -220,9 +269,13 @@ export function Calculation({ calculator, operands, onChange }) {
|
|||||||
? (
|
? (
|
||||||
<>
|
<>
|
||||||
<Select value={calculator} onChange={v => onChange({ operands, calculator: v })}>
|
<Select value={calculator} onChange={v => onChange({ operands, calculator: v })}>
|
||||||
{calculators.map(item => (
|
{calculators.map(group => (
|
||||||
|
<Select.OptGroup key={group.value} label={group.title}>
|
||||||
|
{group.children.map(item => (
|
||||||
<Select.Option key={item.value} value={item.value}>{item.name}</Select.Option>
|
<Select.Option key={item.value} value={item.value}>{item.name}</Select.Option>
|
||||||
))}
|
))}
|
||||||
|
</Select.OptGroup>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} />
|
<Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} />
|
||||||
</>
|
</>
|
||||||
@ -230,5 +283,6 @@ export function Calculation({ calculator, operands, onChange }) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</VariableTypesContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
31
packages/client/src/workflow/nodes/calculation.tsx
Normal file
31
packages/client/src/workflow/nodes/calculation.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Calculation } from '../calculators';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: '运算',
|
||||||
|
type: 'calculation',
|
||||||
|
group: 'control',
|
||||||
|
fieldset: {
|
||||||
|
calculation: {
|
||||||
|
type: 'object',
|
||||||
|
title: '配置计算',
|
||||||
|
name: 'calculation',
|
||||||
|
required: true,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CalculationConfig',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CalculationConfig({ value, onChange }) {
|
||||||
|
return (
|
||||||
|
<Calculation {...value} onChange={onChange} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getter() {
|
||||||
|
return <div>计算值</div>;
|
||||||
|
}
|
||||||
|
};
|
@ -14,6 +14,7 @@ import { nodeClass, nodeCardClass, nodeHeaderClass, nodeTitleClass, nodeBlockCla
|
|||||||
import query from './query';
|
import query from './query';
|
||||||
import condition from './condition';
|
import condition from './condition';
|
||||||
import parallel from './parallel';
|
import parallel from './parallel';
|
||||||
|
import calculation from './calculation';
|
||||||
|
|
||||||
|
|
||||||
function useUpdateConfigAction() {
|
function useUpdateConfigAction() {
|
||||||
@ -60,6 +61,7 @@ export const instructions = new Registry<Instruction>();
|
|||||||
instructions.register('query', query);
|
instructions.register('query', query);
|
||||||
instructions.register('condition', condition);
|
instructions.register('condition', condition);
|
||||||
instructions.register('parallel', parallel);
|
instructions.register('parallel', parallel);
|
||||||
|
instructions.register('calculation', calculation);
|
||||||
|
|
||||||
const NodeContext = React.createContext(null);
|
const NodeContext = React.createContext(null);
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import { Cascader, Select } from 'antd';
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
import { useRequest, useCollectionManager } from '../..';
|
import { useCollectionManager } from '../..';
|
||||||
import { useCollectionFilterOptions } from '../../collection-manager/action-hooks';
|
import { useCollectionFilterOptions } from '../../collection-manager/action-hooks';
|
||||||
import { useFlowContext } from '../WorkflowCanvas';
|
import { useFlowContext } from '../WorkflowCanvas';
|
||||||
import { parseStringValue, VariableTypes } from '../calculators';
|
import { Operand, parseStringValue, VariableTypes, VariableTypesContext } from '../calculators';
|
||||||
|
|
||||||
const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']);
|
const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']);
|
||||||
|
|
||||||
@ -56,12 +56,10 @@ export default {
|
|||||||
},
|
},
|
||||||
'x-component': 'Filter',
|
'x-component': 'Filter',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
useDataSource(options) {
|
useProps() {
|
||||||
const { values } = useForm();
|
const { values } = useForm();
|
||||||
const data = useCollectionFilterOptions(values.collection);
|
const options = useCollectionFilterOptions(values.collection);
|
||||||
return useRequest(() => Promise.resolve({
|
return { options };
|
||||||
data
|
|
||||||
}), options)
|
|
||||||
},
|
},
|
||||||
dynamicComponent: 'VariableComponent'
|
dynamicComponent: 'VariableComponent'
|
||||||
}
|
}
|
||||||
@ -91,7 +89,10 @@ export default {
|
|||||||
constant: {
|
constant: {
|
||||||
title: '常量',
|
title: '常量',
|
||||||
value: 'constant',
|
value: 'constant',
|
||||||
options: undefined
|
options: undefined,
|
||||||
|
component() {
|
||||||
|
return renderSchemaComponent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,59 +100,33 @@ export default {
|
|||||||
? parseStringValue(value, VTypes)
|
? parseStringValue(value, VTypes)
|
||||||
: { type: 'constant', value };
|
: { type: 'constant', value };
|
||||||
|
|
||||||
const { component, appendTypeValue } = VTypes[operand.type];
|
|
||||||
const [types, setTypes] = useState([operand.type, ...(appendTypeValue ? appendTypeValue(operand) : [])]);
|
|
||||||
const [type] = types;
|
|
||||||
|
|
||||||
const VariableComponent = typeof component === 'function' ? component(operand) : component;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css`
|
<VariableTypesContext.Provider value={VTypes}>
|
||||||
display: flex;
|
<Operand
|
||||||
gap: .5em;
|
value={operand}
|
||||||
align-items: center;
|
onChange={(next) => {
|
||||||
`}>
|
if (next.type !== operand.type && next.type === 'constant') {
|
||||||
<Cascader
|
|
||||||
allowClear={false}
|
|
||||||
value={types}
|
|
||||||
options={Object.values(VTypes).map(item => ({
|
|
||||||
label: item.title,
|
|
||||||
value: item.value,
|
|
||||||
children: typeof item.options === 'function' ? item.options() : item.options
|
|
||||||
}))}
|
|
||||||
onChange={(next: Array<any>) => {
|
|
||||||
const { onTypeChange, stringify } = VTypes[next[0]];
|
|
||||||
setTypes(next);
|
|
||||||
if (typeof onTypeChange === 'function') {
|
|
||||||
onTypeChange(operand, next, (op) => {
|
|
||||||
onChange(stringify(op));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (next[0] !== type) {
|
|
||||||
onChange(null);
|
onChange(null);
|
||||||
}
|
} else {
|
||||||
|
const { stringify } = VTypes[next.type];
|
||||||
|
onChange(stringify(next));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{type === 'constant'
|
</VariableTypesContext.Provider>
|
||||||
? renderSchemaComponent()
|
|
||||||
: <VariableComponent {...operand} onChange={(v) => {
|
|
||||||
const { stringify } = VTypes[type];
|
|
||||||
onChange(stringify(v));
|
|
||||||
}} />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getter({ options, onChange }) {
|
getter({ type, options, onChange }) {
|
||||||
const { collections = [] } = useCollectionManager();
|
const { collections = [] } = useCollectionManager();
|
||||||
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 collection = collections.find(item => item.name === config.collection) ?? { fields: [] };
|
const collection = collections.find(item => item.name === config.collection) ?? { fields: [] };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={options.path} placeholder="选择字段" onChange={path => onChange({ options: { ...options, path } })}>
|
<Select value={options.path} placeholder="选择字段" onChange={path => {
|
||||||
|
onChange({ type, options: { ...options, path } });
|
||||||
|
}}>
|
||||||
{collection.fields
|
{collection.fields
|
||||||
.filter(field => BaseTypeSet.has(field.uiSchema.type))
|
.filter(field => BaseTypeSet.has(field.uiSchema.type))
|
||||||
.map(field => (
|
.map(field => (
|
||||||
|
@ -172,6 +172,41 @@ calculators.register('*', multipe);
|
|||||||
calculators.register('/', divide);
|
calculators.register('/', divide);
|
||||||
calculators.register('%', mod);
|
calculators.register('%', mod);
|
||||||
|
|
||||||
|
function includes(a, b) {
|
||||||
|
return a.includes(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notIncludes(a, b) {
|
||||||
|
return !a.includes(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWith(a: string, b: string) {
|
||||||
|
return a.startsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notStartsWith(a: string, b: string) {
|
||||||
|
return !a.startsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endsWith(a: string, b: string) {
|
||||||
|
return a.endsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notEndsWith(a: string, b: string) {
|
||||||
|
return !a.endsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculators.register('includes', includes);
|
||||||
|
calculators.register('notIncludes', notIncludes);
|
||||||
|
calculators.register('startsWith', startsWith);
|
||||||
|
calculators.register('notStartsWith', notStartsWith);
|
||||||
|
calculators.register('endsWith', endsWith);
|
||||||
|
calculators.register('notEndsWith', notEndsWith);
|
||||||
|
|
||||||
|
function before(a: string, b: string) {
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
|
||||||
calculators.register('now', () => new Date());
|
calculators.register('now', () => new Date());
|
||||||
|
|
||||||
// TODO: add more common calculators
|
// TODO: add more common calculators
|
||||||
|
Loading…
Reference in New Issue
Block a user