diff --git a/packages/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/client/src/collection-manager/Configuration/AddFieldAction.tsx index 63151e0a9..43b59a504 100644 --- a/packages/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -127,7 +127,7 @@ export const AddFieldAction = () => { {options.map((option) => { return ( option.children.length > 0 && ( - + {option.children.map((child) => { return {compile(child.title)}; })} diff --git a/packages/client/src/workflow/WorkflowCanvas.tsx b/packages/client/src/workflow/WorkflowCanvas.tsx index 86c091910..9afca0990 100644 --- a/packages/client/src/workflow/WorkflowCanvas.tsx +++ b/packages/client/src/workflow/WorkflowCanvas.tsx @@ -77,14 +77,7 @@ export function Branch({ {controller}
- {list.map(item => { - return ( -
- - -
- ); - })} + {list.map(item => )}
); @@ -98,7 +91,7 @@ interface AddButtonProps { branchIndex?: number; }; -function AddButton({ upstream, branchIndex = null }: AddButtonProps) { +export function AddButton({ upstream, branchIndex = null }: AddButtonProps) { const { resource } = useCollection(); const { data } = useResourceActionContext(); const { onNodeAdded } = useFlowContext(); @@ -125,20 +118,30 @@ function AddButton({ upstream, branchIndex = null }: AddButtonProps) { onNodeAdded(node); } + const groups = [ + { value: 'control', name: '流程控制' }, + { value: 'model', name: '数据表操作' }, + ]; + const instructionList = (Array.from(instructions.getValues()) as Instruction[]); + return (
onCreate(ev)}> - {(Array.from(instructions.getValues()) as Instruction[]).map(item => item.options - ? ( - - {item.options.map(option => ( - {option.label} + {groups.map(group => ( + + {instructionList.filter(item => item.group === group.value).map(item => item.options + ? ( + + {item.options.map(option => ( + {option.label} + ))} + + ) + : ( + {item.title} ))} - - ) - : ( - {item.title} + ))} }> diff --git a/packages/client/src/workflow/calculators.tsx b/packages/client/src/workflow/calculators.tsx index a4096948d..45ebee120 100644 --- a/packages/client/src/workflow/calculators.tsx +++ b/packages/client/src/workflow/calculators.tsx @@ -1,10 +1,13 @@ import React from "react"; -import { Input, Select } from "antd"; +import { Cascader, DatePicker, Input, InputNumber, Select } from "antd"; import { css } from "@emotion/css"; -import { useNodeContext } from "./nodes"; - +import { instructions, useNodeContext } from "./nodes"; +import { useFlowContext } from "./WorkflowCanvas"; +function NullRender() { + return null; +} export const calculators = [ { value: 'equal', name: '等于' }, @@ -13,67 +16,219 @@ export const calculators = [ const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/; -function JobSelect({ value, onChange }) { - const node = useNodeContext(); - const stack = []; - for (let current = node.upstream; current; current = current.upstream) { - stack.push(current); +export function parseStringValue(value: string, Types) { + const matcher = value.match(JT_VALUE_RE); + if (!matcher) { + return { type: 'constant', value, options: { type: 'string' } }; } - return ( - - ); + + const [type, ...paths] = matcher[1].split('.'); + + return { + type, + options: paths.length ? (Types || VariableTypes)[type].parse(paths) : {} + }; } -function ContextSelect({ value, onChange }) { - return ( - - ); -} - -const VariableTypeComponent = { - constant({ onChange, ...props }) { - return onChange(ev.target.value)} /> +const ConstantTypes = { + string: { + title: '字符串', + value: 'string', + component({ onChange, type, options, ...props }) { + return onChange(ev.target.value)} />; + }, + default: '' }, - job: JobSelect, - context: ContextSelect, + number: { + title: '数字', + value: 'number', + component({ type, options, ...props }) { + return ; + }, + default: 0 + }, + boolean: { + title: '逻辑值', + value: 'boolean', + component({ type, options, ...props }) { + return ( + + ); + }, + default: false + }, + // date: { + // title: '日期', + // value: 'date', + // component({ type, options, ...props }) { + // return ; + // }, + // default: new Date() + // } +}; + +export const VariableTypes = { + constant: { + title: '常量', + value: 'constant', + options: Object.values(ConstantTypes).map(item => ({ + value: item.value, + label: item.title + })), + component({ options: { type } = { type: 'string' } }) { + return type ? ConstantTypes[type].component : NullRender; + }, + appendTypeValue({ options = { type: 'string' } }) { + return options?.type ? [options.type] : []; + }, + onTypeChange(props, [type, optionsType], onChange) { + const { default: value } = ConstantTypes[optionsType]; + onChange({ + value, + type, + options: { ...props.options, type: optionsType } + }); + }, + parse(path) { + return { path }; + } + }, + job: { + title: '节点数据', + value: 'job', + options() { + const node = useNodeContext(); + 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 + if (getter) { + stack.push({ + value: current.id, + label: current.title ?? `#${current.id}` + }); + } + } + + return stack; + }, + component({ options }) { + const { nodes } = useFlowContext(); + if (!options?.nodeId) { + return NullRender; + } + const node = nodes.find(n => n.id == options.nodeId); + if (!node) { + return NullRender; + } + const instruction = instructions.get(node.type); + return instruction?.getter ?? NullRender; + }, + appendTypeValue({ options = {} }: { type: string, options: any }) { + return options.nodeId ? [Number.parseInt(options.nodeId, 10)] : []; + }, + onTypeChange(props, [type, nodeId], onChange) { + onChange({ + ...props, + type, + options: { ...props.options, nodeId } + }); + }, + parse([nodeId, ...path]) { + return { nodeId, path: path.join('.') }; + }, + stringify({ options }) { + const stack = ['job']; + if (options.nodeId) { + stack.push(options.nodeId); + if (options.path) { + stack.push(options.path); + } + } + return `{{${stack.join('.')}}}`; + } + }, + // context: ContextSelect, // calculation: Calculation }; -function OperandTypeSelect({ value, onChange }) { - return ( - - ); +interface OperandProps { + value: { + type: string; + value?: any; + options?: any; + }; + onChange(v: any): void } -export function Operand({ value: operand = { type: 'constant', value: '' }, onChange }) { - const { value } = operand; - let { type = 'constant' } = operand; - if (typeof value === 'string') { - const matcher = value.match(JT_VALUE_RE); +export function Operand({ onChange, value: operand = { type: 'constant', value: '', options: { type: 'string' } } }: OperandProps) { + const { type } = operand; - if (matcher) { - console.log(matcher); - } - } - - const VariableComponent = VariableTypeComponent[type]; + const { component, appendTypeValue } = VariableTypes[type]; + const VariableComponent = typeof component === 'function' ? component(operand) : component; return (
- onChange({ ...operand, type: v })} /> - onChange({ ...operand, value: v })} /> + { + const children = typeof item.options === 'function' ? item.options() : item.options; + return { + label: item.title, + value: item.value, + children, + disabled: children && !children.length + }; + })} + onChange={(t: Array) => { + const { onTypeChange } = VariableTypes[t[0]]; + if (typeof onTypeChange === 'function') { + onTypeChange(operand, t, onChange); + } else { + if (t[0] !== type) { + onChange({ type: t[0], value: null }); + } + } + }} + /> + onChange({ ...operand, value: v })} /> +
+ ); +} + +export function Calculation({ calculator, operands, onChange }) { + return ( +
+ onChange({ calculator, operands: [v, operands[1]] }))} /> + {operands[0] + ? ( + <> + + onChange({ calculator, operands: [operands[0], v] }))} /> + + ) + : null + }
); } diff --git a/packages/client/src/workflow/nodes/condition.tsx b/packages/client/src/workflow/nodes/condition.tsx index f540a0058..a438af99d 100644 --- a/packages/client/src/workflow/nodes/condition.tsx +++ b/packages/client/src/workflow/nodes/condition.tsx @@ -7,7 +7,7 @@ import { Trans } from "react-i18next"; import { NodeDefaultView } from "."; import { Branch, useFlowContext } from "../WorkflowCanvas"; import { branchBlockClass, nodeSubtreeClass } from "../style"; -import { calculators, Operand } from "../calculators"; +import { Calculation } from "../calculators"; // import { SchemaComponent } from "../../schema-component"; function CalculationItem({ value, onChange, onRemove }) { @@ -30,24 +30,7 @@ function CalculationItem({ value, onChange, onRemove }) { onChange={group => onChange({ ...value, group })} /> ) - : ( -
- onChange({ ...value, operands: [v, operands[1]] })} /> - - onChange({ ...value, operands: [operands[0], v] })} /> -
- ) + : }
@@ -146,6 +129,7 @@ function CalculationConfig({ value, onChange }) { export default { title: '条件判断', type: 'condition', + group: 'control', fieldset: { rejectOnFalse: { type: 'boolean', diff --git a/packages/client/src/workflow/nodes/index.tsx b/packages/client/src/workflow/nodes/index.tsx index 0778150fa..3c93aabcf 100644 --- a/packages/client/src/workflow/nodes/index.tsx +++ b/packages/client/src/workflow/nodes/index.tsx @@ -1,15 +1,15 @@ import React, { useContext } from 'react'; -import { cx } from '@emotion/css'; +import { css, cx } from '@emotion/css'; import { Button, Modal, Tag } from 'antd'; -import { DeleteOutlined } from '@ant-design/icons'; +import { DeleteOutlined, CloseOutlined } from '@ant-design/icons'; import { ISchema, useForm } from '@formily/react'; import { Registry } from '@nocobase/utils'; import { SchemaComponent, useActionContext, useAPIClient, useCollection, useRequest, useResourceActionContext } from '../..'; -import { useFlowContext } from '../WorkflowCanvas'; +import { AddButton, useFlowContext } from '../WorkflowCanvas'; -import { nodeClass, nodeCardClass, nodeHeaderClass, nodeTitleClass } from '../style'; +import { nodeClass, nodeCardClass, nodeHeaderClass, nodeTitleClass, nodeBlockClass } from '../style'; import query from './query'; import condition from './condition'; @@ -44,12 +44,15 @@ function useUpdateConfigAction() { export interface Instruction { title: string; type: string; + group: string; options?: { label: string; value: any; key: string }[]; fieldset: { [key: string]: ISchema }; - view: ISchema; + view?: ISchema; scope?: { [key: string]: any }; - components?: { [key: string]: any } - render?(props): React.ReactElement + components?: { [key: string]: any }; + render?(props): React.ReactElement; + endding?: boolean; + getter?(node: any): React.ReactElement; }; export const instructions = new Registry(); @@ -67,12 +70,38 @@ export function useNodeContext() { export function Node({ data }) { const instruction = instructions.get(data.type); - if (instruction.render) { - return instruction.render(data); - } - return ( - +
+ {instruction.render + ? instruction.render(data) + : + } + {!instruction.endding + ? + : ( +
+ +
+ ) + } +
); } @@ -95,7 +124,7 @@ export function RemoveButton() { Modal.confirm({ title: '删除分支', - content: '节点包含分支,将删除其所有分支下的子节点,确定继续?', + content: '节点包含分支,将同时删除其所有分支下的子节点,确定继续?', onOk }); } @@ -123,7 +152,7 @@ export function NodeDefaultView(props) { a.branchIndex - b.branchIndex); const [branchCount, setBranchCount] = useState(Math.max(2, branches.length)); const tempBranches = Array(Math.max(0, branchCount - branches.length)).fill(null); diff --git a/packages/client/src/workflow/nodes/query.tsx b/packages/client/src/workflow/nodes/query.tsx index ac122ec8d..e362e7924 100644 --- a/packages/client/src/workflow/nodes/query.tsx +++ b/packages/client/src/workflow/nodes/query.tsx @@ -1,10 +1,21 @@ +import React, { useState } from 'react'; +import { useForm } from '@formily/react'; import { action } from '@formily/reactive'; +import { Cascader, Select } from 'antd'; import { t } from 'i18next'; -import { useCollectionManager } from '../../collection-manager'; +import { css } from '@emotion/css'; + +import { useRequest, useCollectionManager } from '../..'; +import { useCollectionFilterOptions } from '../../collection-manager/action-hooks'; +import { useFlowContext } from '../WorkflowCanvas'; +import { parseStringValue, VariableTypes } from '../calculators'; + +const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']); export default { title: '数据查询', type: 'query', + group: 'model', fieldset: { collection: { type: 'string', @@ -20,7 +31,42 @@ export default { title: '多条数据', name: 'multiple', 'x-decorator': 'FormItem', - 'x-component': 'Checkbox' + 'x-component': 'Checkbox', + 'x-component-props': { + disabled: true + } + }, + params: { + type: 'object', + name: 'params', + title: '查询参数', + 'x-decorator': 'FormItem', + properties: { + filter: { + type: 'object', + title: '筛选条件', + name: 'filter', + 'x-decorator': 'div', + 'x-decorator-props': { + className: css` + .ant-select{ + width: auto; + } + ` + }, + 'x-component': 'Filter', + 'x-component-props': { + useDataSource(options) { + const { values } = useForm(); + const data = useCollectionFilterOptions(values.collection); + return useRequest(() => Promise.resolve({ + data + }), options) + }, + dynamicComponent: 'VariableComponent' + } + } + } } }, view: { @@ -38,5 +84,80 @@ export default { })(collections); } } + }, + components: { + VariableComponent({ value, onChange, renderSchemaComponent }) { + const VTypes = { ...VariableTypes, + constant: { + title: '常量', + value: 'constant', + options: undefined + } + }; + + const operand = typeof value === 'string' + ? 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 ( +
+ ({ + label: item.title, + value: item.value, + children: typeof item.options === 'function' ? item.options() : item.options + }))} + onChange={(next: Array) => { + const { onTypeChange, stringify } = VTypes[next[0]]; + setTypes(next); + if (typeof onTypeChange === 'function') { + onTypeChange(operand, next, (op) => { + onChange(stringify(op)); + }); + } else { + if (next[0] !== type) { + onChange(null); + } + } + }} + /> + {type === 'constant' + ? renderSchemaComponent() + : { + const { stringify } = VTypes[type]; + onChange(stringify(v)); + }} /> + } +
+ ); + } + }, + getter({ 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 ( + + ); } }; diff --git a/packages/client/src/workflow/style.tsx b/packages/client/src/workflow/style.tsx index fcdb2fce1..2890240ab 100644 --- a/packages/client/src/workflow/style.tsx +++ b/packages/client/src/workflow/style.tsx @@ -88,6 +88,8 @@ export const branchClass = css` `; export const nodeBlockClass = css` + flex-grow: 1; + flex-shrink: 0; display: flex; flex-direction: column; align-items: center; @@ -95,6 +97,7 @@ export const nodeBlockClass = css` `; export const nodeClass = css` + flex-shrink: 0; display: flex; flex-direction: column; align-items: center;