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 = [ 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,56 +225,64 @@ 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 (
<div className={css` <VariableTypesContext.Provider value={VariableTypes}>
display: flex; <div className={css`
gap: .5em; display: flex;
align-items: center; gap: .5em;
align-items: center;
.ant-select{ .ant-select{
width: auto; width: auto;
} min-width: 6em;
`}> }
<Operand value={operands[0]} onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))} /> `}>
{operands[0] <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 value={calculator} onChange={v => onChange({ operands, calculator: v })}>
<Select.Option key={item.value} value={item.value}>{item.name}</Select.Option> {calculators.map(group => (
))} <Select.OptGroup key={group.value} label={group.title}>
</Select> {group.children.map(item => (
<Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} /> <Select.Option key={item.value} value={item.value}>{item.name}</Select.Option>
</> ))}
) </Select.OptGroup>
: null ))}
} </Select>
</div> <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 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);

View File

@ -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 onChange(null);
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 { } else {
if (next[0] !== type) { const { stringify } = VTypes[next.type];
onChange(null); 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 => (

View File

@ -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