import React from "react"; import { observer, useForm } from "@formily/react"; import { Button, Cascader, Dropdown, Input, InputNumber, Menu, Select, Form } from "antd"; import { css } from "@emotion/css"; import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { instructions, useNodeContext } from "./nodes"; import { useFlowContext } from "./WorkflowCanvas"; import { triggers } from "./triggers"; import { SchemaComponent, useCollectionManager, useCompile } from ".."; import { useTranslation } from "react-i18next"; import { t } from "i18next"; function NullRender() { return null; } export const calculators = [ { value: 'boolean', title: '{{t("Comparison")}}', children: [ { value: 'equal', name: '=' }, { value: 'notEqual', name: '≠' }, { value: 'gt', name: '>' }, { value: 'gte', name: '≥' }, { value: 'lt', name: '<' }, { value: 'lte', name: '≤' } ] }, { value: 'number', title: '{{t("Arithmetic calculation")}}', children: [ { value: 'add', name: '+' }, { value: 'minus', name: '-' }, { value: 'multiple', name: '*' }, { value: 'divide', name: '/' }, { value: 'mod', name: '%' }, ] }, { value: 'string', title: '{{t("String operation")}}', children: [ { value: 'includes', name: '{{t("contains")}}' }, { value: 'notIncludes', name: '{{t("does not contain")}}' }, { value: 'startsWith', name: '{{t("starts with")}}' }, { value: 'notStartsWith', name: '{{t("not starts with")}}' }, { value: 'endsWith', name: '{{t("ends with")}}' }, { value: 'notEndsWith', name: '{{t("not ends with")}}' } ] }, { value: 'date', title: '{{t("Date")}}', children: [] } ]; const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/; export function parseStringValue(value: string, Types) { const matcher = value.match(JT_VALUE_RE); if (!matcher) { return { type: 'constant', value, options: { type: 'string' } }; } const [type, ...paths] = matcher[1].split('.'); return { type, options: paths.length ? (Types || VariableTypes)[type]?.parse(paths) : {} }; } export const BaseTypeSet = new Set(['boolean', 'number', 'string', 'date']); const ConstantTypes = { string: { title: '{{t("String")}}', value: 'string', component({ onChange, type, options, value }) { return ( onChange({ value: ev.target.value, type, options })} /> ); }, default: '' }, number: { title: '{{t("Number")}}', value: 'number', component({ onChange, type, options, value }) { return ( onChange({ value: v, type, options })} /> ); }, default: 0 }, boolean: { title: '{{t("Boolean")}}', value: 'boolean', component({ onChange, type, options, value }) { const { t } = useTranslation(); return ( ); }, default: false }, // date: { // title: '日期', // value: 'date', // component({ onChange, type, options, value }) { // return onChange({ value: v, type, options })}/>; // }, // default: new Date() // } }; export const VariableTypes = { constant: { title: '{{t("Constant")}}', value: 'constant', options: Object.values(ConstantTypes).map(item => ({ value: item.value, label: item.title })), component({ options = { type: 'string' } }) { return ConstantTypes[options.type]?.component ?? NullRender; }, appendTypeValue({ options = { type: 'string' } }) { return options?.type ? [options.type] : []; }, onTypeChange(old, [type, optionsType], onChange) { if (old?.options?.type === optionsType) { return; } const { default: value } = ConstantTypes[optionsType]; onChange({ value, type, options: { ...old.options, type: optionsType } }); }, parse(path) { return { path }; } }, $jobsMapByNodeId: { title: '{{t("Node result")}}', value: '$jobsMapByNodeId', options() { const node = useNodeContext(); const stack = []; for (let current = node.upstream; current; current = current.upstream) { const { getter } = instructions.get(current.type); // Note: 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(old, [type, nodeId], onChange) { onChange({ // ...old, type, options: { nodeId } }); }, parse([nodeId, ...path]) { return { nodeId, path: path.join('.') }; }, stringify({ options }) { const stack = ['$jobsMapByNodeId']; if (options.nodeId) { stack.push(options.nodeId); if (options.path) { stack.push(options.path); } } return `{{${stack.join('.')}}}`; } }, $context: { title: '{{t("Trigger context")}}', value: '$context', component() { const { workflow } = useFlowContext(); const trigger = triggers.get(workflow.type); return trigger?.getter ?? NullRender; }, parse([prefix, ...path]) { return { path: path.join('.') }; }, stringify({ options }) { const stack = ['$context']; if (options?.path) { stack.push(options.path); } return `{{${stack.join('.')}}}`; } }, // calculation: Calculation }; export const VariableTypesContext = React.createContext(null); export function useVariableTypes() { return React.useContext(VariableTypesContext); } interface OperandProps { value: { type: string; value?: any; options?: any; }; onChange(v: any): void; children?: React.ReactNode; } export function Operand({ value: operand = { type: 'constant', value: '', options: { type: 'string' } }, onChange, children }: OperandProps) { const compile = useCompile(); const Types = useVariableTypes(); const { type } = operand; const { component, appendTypeValue } = Types[type] || {}; const VariableComponent = typeof component === 'function' ? component(operand) : NullRender; return (
{ const options = typeof item.options === 'function' ? item.options() : item.options; return { label: compile(item.title), value: item.value, children: compile(options), disabled: options && !options.length, isLeaf: !options }; })} onChange={(next: Array) => { const { onTypeChange } = Types[next[0]]; if (typeof onTypeChange === 'function') { onTypeChange(operand, next, onChange); } else { if (next[0] !== type) { onChange({ type: next[0], value: null }); } } }} /> {children ?? onChange({ ...op })} />}
); } export function Calculation({ calculator, operands = [], onChange }) { const compile = useCompile(); return (
onChange({ calculator, operands: [v, operands[1]] }))} /> {operands[0] ? ( <> onChange({ calculator, operands: [operands[0], v] }))} /> ) : null }
); } export function VariableComponent({ value, onChange, renderSchemaComponent }) { const VTypes = { ...VariableTypes, constant: { title: '{{t("Constant")}}', value: 'constant', options: undefined } }; const operand = typeof value === 'string' ? parseStringValue(value, VTypes) : { type: 'constant', value }; return ( { if (next.type !== operand.type && next.type === 'constant') { onChange(null); } else { const { stringify } = VTypes[next.type]; onChange(stringify(next)); } }} > {operand.type === 'constant' ? renderSchemaComponent() : null} ); } // NOTE: observer for watching useProps export const CollectionFieldset = observer(({ value, onChange }: any) => { const { t } = useTranslation(); const compile = useCompile(); const { getCollectionFields } = useCollectionManager(); const { values: data } = useForm(); const fields = getCollectionFields(data?.config?.collection) .filter(field => ( !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false) )); const VTypes = { ...VariableTypes, constant: { title: '{{t("Constant")}}', value: 'constant', options: undefined } }; return (
.ant-formily-item{ flex-direction: column; > .ant-formily-item-label{ line-height: 32px; } } `}> {fields.length ? ( <> {fields .filter(field => field.name in value) .map(field => { const operand = typeof value[field.name] === 'string' ? parseStringValue(value[field.name], VTypes) : { type: 'constant', value: value[field.name] }; // TODO: try to use to replace this map return ( { if (next.type !== operand.type && next.type === 'constant') { onChange({ ...value, [field.name]: null }); } else { const { stringify } = VTypes[next.type]; onChange({ ...value, [field.name]: stringify(next) }); } }} > {operand.type === 'constant' ? : null } ) : null } ) :

{t('Please select collection first')}

}
); });