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:
Junyi 2022-04-10 20:06:40 +08:00 committed by GitHub
parent f791d43716
commit c4afb7586c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 187 additions and 90 deletions

View File

@ -10,8 +10,46 @@ function NullRender() {
}
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*$/;
@ -79,17 +117,17 @@ export const VariableTypes = {
label: item.title
})),
component({ options: { type } = { type: 'string' } }) {
return type ? ConstantTypes[type].component : NullRender;
return ConstantTypes[type]?.component ?? NullRender;
},
appendTypeValue({ options = { type: 'string' } }) {
return options?.type ? [options.type] : [];
},
onTypeChange(props, [type, optionsType], onChange) {
onTypeChange(old, [type, optionsType], onChange) {
const { default: value } = ConstantTypes[optionsType];
onChange({
value,
type,
options: { ...props.options, type: optionsType }
options: { ...old.options, type: optionsType }
});
},
parse(path) {
@ -104,7 +142,7 @@ export const VariableTypes = {
const stack = [];
for (let current = node.upstream; current; current = current.upstream) {
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) {
stack.push({
value: current.id,
@ -130,11 +168,11 @@ export const VariableTypes = {
appendTypeValue({ options = {} }: { type: string, options: any }) {
return options.nodeId ? [Number.parseInt(options.nodeId, 10)] : [];
},
onTypeChange(props, [type, nodeId], onChange) {
onTypeChange(old, [type, nodeId], onChange) {
onChange({
...props,
// ...old,
type,
options: { ...props.options, nodeId }
options: { nodeId }
});
},
parse([nodeId, ...path]) {
@ -155,6 +193,12 @@ export const VariableTypes = {
// calculation: Calculation
};
export const VariableTypesContext = React.createContext(null);
export function useVariableTypes() {
return React.useContext(VariableTypesContext);
}
interface OperandProps {
value: {
type: string;
@ -165,10 +209,12 @@ interface OperandProps {
}
export function Operand({ onChange, value: operand = { type: 'constant', value: '', options: { type: 'string' } } }: OperandProps) {
const Types = useVariableTypes();
const { type } = operand;
const { component, appendTypeValue } = VariableTypes[type];
const VariableComponent = typeof component === 'function' ? component(operand) : component;
const { component, appendTypeValue } = Types[type];
const VariableComponent = typeof component === 'function' ? component(operand) : NullRender;
return (
<div className={css`
@ -179,56 +225,64 @@ export function Operand({ onChange, value: operand = { type: 'constant', value:
<Cascader
allowClear={false}
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;
return {
label: item.title,
value: item.value,
children,
disabled: children && !children.length
disabled: children && !children.length,
isLeaf: !children
};
})}
onChange={(t: Array<string | number>) => {
const { onTypeChange } = VariableTypes[t[0]];
onChange={(next: Array<string | number>) => {
const { onTypeChange } = Types[next[0]];
if (typeof onTypeChange === 'function') {
onTypeChange(operand, t, onChange);
onTypeChange(operand, next, onChange);
} else {
if (t[0] !== type) {
onChange({ type: t[0], value: null });
if (next[0] !== type) {
onChange({ type: next[0], value: null });
}
}
}}
/>
<VariableComponent {...operand} onChange={v => onChange({ ...operand, value: v })} />
<VariableComponent {...operand} onChange={op => onChange({ ...op })} />
</div>
);
}
export function Calculation({ calculator, operands, onChange }) {
export function Calculation({ calculator, operands = [], onChange }) {
return (
<div className={css`
display: flex;
gap: .5em;
align-items: center;
<VariableTypesContext.Provider value={VariableTypes}>
<div className={css`
display: flex;
gap: .5em;
align-items: center;
.ant-select{
width: auto;
}
`}>
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
{operands[0]
? (
<>
<Select value={calculator} onChange={v => onChange({ operands, calculator: v })}>
{calculators.map(item => (
<Select.Option key={item.value} value={item.value}>{item.name}</Select.Option>
))}
</Select>
<Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} />
</>
)
: null
}
</div>
.ant-select{
width: auto;
min-width: 6em;
}
`}>
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} />
{operands[0]
? (
<>
<Select value={calculator} onChange={v => onChange({ operands, calculator: v })}>
{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.OptGroup>
))}
</Select>
<Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} />
</>
)
: null
}
</div>
</VariableTypesContext.Provider>
);
}

View 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>;
}
};

View File

@ -14,6 +14,7 @@ import { nodeClass, nodeCardClass, nodeHeaderClass, nodeTitleClass, nodeBlockCla
import query from './query';
import condition from './condition';
import parallel from './parallel';
import calculation from './calculation';
function useUpdateConfigAction() {
@ -60,6 +61,7 @@ export const instructions = new Registry<Instruction>();
instructions.register('query', query);
instructions.register('condition', condition);
instructions.register('parallel', parallel);
instructions.register('calculation', calculation);
const NodeContext = React.createContext(null);

View File

@ -5,10 +5,10 @@ import { Cascader, Select } from 'antd';
import { t } from 'i18next';
import { css } from '@emotion/css';
import { useRequest, useCollectionManager } from '../..';
import { useCollectionManager } from '../..';
import { useCollectionFilterOptions } from '../../collection-manager/action-hooks';
import { useFlowContext } from '../WorkflowCanvas';
import { parseStringValue, VariableTypes } from '../calculators';
import { Operand, parseStringValue, VariableTypes, VariableTypesContext } from '../calculators';
const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']);
@ -56,12 +56,10 @@ export default {
},
'x-component': 'Filter',
'x-component-props': {
useDataSource(options) {
useProps() {
const { values } = useForm();
const data = useCollectionFilterOptions(values.collection);
return useRequest(() => Promise.resolve({
data
}), options)
const options = useCollectionFilterOptions(values.collection);
return { options };
},
dynamicComponent: 'VariableComponent'
}
@ -91,7 +89,10 @@ export default {
constant: {
title: '常量',
value: 'constant',
options: undefined
options: undefined,
component() {
return renderSchemaComponent;
}
}
};
@ -99,59 +100,33 @@ export default {
? parseStringValue(value, VTypes)
: { 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 (
<div className={css`
display: flex;
gap: .5em;
align-items: center;
`}>
<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));
});
<VariableTypesContext.Provider value={VTypes}>
<Operand
value={operand}
onChange={(next) => {
if (next.type !== operand.type && next.type === 'constant') {
onChange(null);
} else {
if (next[0] !== type) {
onChange(null);
}
const { stringify } = VTypes[next.type];
onChange(stringify(next));
}
}}
/>
{type === 'constant'
? renderSchemaComponent()
: <VariableComponent {...operand} onChange={(v) => {
const { stringify } = VTypes[type];
onChange(stringify(v));
}} />
}
</div>
</VariableTypesContext.Provider>
);
}
},
getter({ options, onChange }) {
getter({ type, options, onChange }) {
const { collections = [] } = useCollectionManager();
const { nodes } = useFlowContext();
const { config } = nodes.find(n => n.id == options.nodeId);
const collection = collections.find(item => item.name === config.collection) ?? { fields: [] };
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
.filter(field => BaseTypeSet.has(field.uiSchema.type))
.map(field => (

View File

@ -172,6 +172,41 @@ calculators.register('*', multipe);
calculators.register('/', divide);
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());
// TODO: add more common calculators