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,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> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										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 |                 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 => ( | ||||||
|  | |||||||
| @ -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