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 = [ | ||||
|   { 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,33 +225,35 @@ 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 ( | ||||
|     <VariableTypesContext.Provider value={VariableTypes}> | ||||
|       <div className={css` | ||||
|         display: flex; | ||||
|         gap: .5em; | ||||
| @ -213,6 +261,7 @@ export function Calculation({ calculator, operands, onChange }) { | ||||
| 
 | ||||
|         .ant-select{ | ||||
|           width: auto; | ||||
|           min-width: 6em; | ||||
|         } | ||||
|       `}>
 | ||||
|         <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 })}> | ||||
|               {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.OptGroup> | ||||
|                 ))} | ||||
|               </Select> | ||||
|               <Operand value={operands[1]} onChange={(v => onChange({ calculator, operands: [operands[0], v] }))} /> | ||||
|             </> | ||||
| @ -230,5 +283,6 @@ export function Calculation({ calculator, operands, onChange }) { | ||||
|           : null | ||||
|         } | ||||
|       </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 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); | ||||
| 
 | ||||
|  | ||||
| @ -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)); | ||||
|                 }); | ||||
|               } else { | ||||
|                 if (next[0] !== type) { | ||||
|         <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)); | ||||
|               } | ||||
|             }} | ||||
|           /> | ||||
|           {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 => ( | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user