mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 12:40:50 +00:00
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,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>
|
||||
);
|
||||
}
|
||||
|
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));
|
||||
});
|
||||
<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 => (
|
||||
|
@ -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