import { CloseCircleOutlined } from '@ant-design/icons'; import { css, cx } from '@emotion/css'; import { Variable, useCompile } from '@nocobase/client'; import { evaluators } from '@nocobase/evaluators/client'; import { Registry } from '@nocobase/utils/client'; import { Button, Select } from 'antd'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { NodeDefaultView } from '.'; import { Branch } from '../Branch'; import { useFlowContext } from '../FlowContext'; import { RadioWithTooltip, RadioWithTooltipOption } from '../components/RadioWithTooltip'; import { renderEngineReference } from '../components/renderEngineReference'; import { NAMESPACE, lang } from '../locale'; import { branchBlockClass, nodeSubtreeClass } from '../style'; import { useWorkflowVariableOptions } from '../variable'; interface Calculator { name: string; type: 'boolean' | 'number' | 'string' | 'date' | 'unknown' | 'null' | 'array'; group: string; } export const calculators = new Registry(); calculators.register('equal', { name: '=', type: 'boolean', group: 'boolean', }); calculators.register('notEqual', { name: '≠', type: 'boolean', group: 'boolean', }); calculators.register('gt', { name: '>', type: 'boolean', group: 'boolean', }); calculators.register('gte', { name: '≥', type: 'boolean', group: 'boolean', }); calculators.register('lt', { name: '<', type: 'boolean', group: 'boolean', }); calculators.register('lte', { name: '≤', type: 'boolean', group: 'boolean', }); calculators.register('add', { name: '+', type: 'number', group: 'number', }); calculators.register('minus', { name: '-', type: 'number', group: 'number', }); calculators.register('multiple', { name: '*', type: 'number', group: 'number', }); calculators.register('divide', { name: '/', type: 'number', group: 'number', }); calculators.register('mod', { name: '%', type: 'number', group: 'number', }); calculators.register('includes', { name: '{{t("contains")}}', type: 'boolean', group: 'string', }); calculators.register('notIncludes', { name: '{{t("does not contain")}}', type: 'boolean', group: 'string', }); calculators.register('startsWith', { name: '{{t("starts with")}}', type: 'boolean', group: 'string', }); calculators.register('notStartsWith', { name: '{{t("not starts with")}}', type: 'boolean', group: 'string', }); calculators.register('endsWith', { name: '{{t("ends with")}}', type: 'boolean', group: 'string', }); calculators.register('notEndsWith', { name: '{{t("not ends with")}}', type: 'boolean', group: 'string', }); calculators.register('concat', { name: `{{t("concat", { ns: "${NAMESPACE}" })}}`, type: 'string', group: 'string', }); const calculatorGroups = [ { value: 'boolean', title: '{{t("Comparision")}}', }, { value: 'number', title: `{{t("Arithmetic calculation", { ns: "${NAMESPACE}" })}}`, }, { value: 'string', title: `{{t("String operation", { ns: "${NAMESPACE}" })}}`, }, { value: 'date', title: `{{t("Date", { ns: "${NAMESPACE}" })}}`, }, ]; function getGroupCalculators(group) { return Array.from(calculators.getEntities()).filter(([key, value]) => value.group === group); } function Calculation({ calculator, operands = [], onChange }) { const compile = useCompile(); const options = useWorkflowVariableOptions(); return (
onChange({ calculator, operands: [v, operands[1]] })} scope={options} useTypedConstant /> onChange({ calculator, operands: [operands[0], v] })} scope={options} useTypedConstant />
); } function CalculationItem({ value, onChange, onRemove }) { if (!value) { return null; } const { calculator, operands = [] } = value; return (
{value.group ? ( onChange({ ...value, group })} /> ) : ( )}
); } function CalculationGroup({ value, onChange }) { const { t } = useTranslation(); const { type = 'and', calculations = [] } = value; function onAddSingle() { onChange({ ...value, calculations: [...calculations, { not: false, calculator: 'equal' }], }); } function onAddGroup() { onChange({ ...value, calculations: [...calculations, { not: false, group: { type: 'and', calculations: [] } }], }); } function onRemove(i: number) { calculations.splice(i, 1); onChange({ ...value, calculations: [...calculations] }); } function onItemChange(i: number, v) { calculations.splice(i, 1, v); onChange({ ...value, calculations: [...calculations] }); } return (
{'Meet '} {' conditions in the group'}
{calculations.map((calculation, i) => ( onRemove(i)} /> ))}
); } function CalculationConfig({ value, onChange }) { const rule = value && Object.keys(value).length ? value : { group: { type: 'and', calculations: [] } }; return onChange({ ...rule, group })} />; } export default { title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`, type: 'condition', group: 'control', description: `{{t('Based on boolean result of the calculation to determine whether to "continue" or "exit" the process, or continue on different branches of "yes" and "no".', { ns: "${NAMESPACE}" })}}`, fieldset: { rejectOnFalse: { type: 'boolean', title: `{{t("Mode", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'Radio.Group', 'x-component-props': { disabled: true, }, enum: [ { value: true, label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`, }, { value: false, label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`, }, ], }, engine: { type: 'string', title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'RadioWithTooltip', 'x-component-props': { options: [ ['basic', { label: `{{t("Basic", { ns: "${NAMESPACE}" })}}` }], ...Array.from(evaluators.getEntities()), ].reduce((result: RadioWithTooltipOption[], [value, options]: any) => result.concat({ value, ...options }), []), }, required: true, default: 'basic', }, calculation: { type: 'string', title: `{{t("Condition", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'CalculationConfig', 'x-reactions': { dependencies: ['engine'], fulfill: { state: { visible: '{{$deps[0] === "basic"}}', }, }, }, required: true, }, expression: { type: 'string', title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`, 'x-decorator': 'FormItem', 'x-component': 'Variable.TextArea', 'x-component-props': { scope: '{{useWorkflowVariableOptions}}', }, ['x-validator'](value, rules, { form }) { const { values } = form; const { evaluate } = evaluators.get(values.engine); const exp = value.trim().replace(/{{([^{}]+)}}/g, ' 1 '); try { evaluate(exp); return ''; } catch (e) { return lang('Expression syntax error'); } }, 'x-reactions': { dependencies: ['engine'], fulfill: { state: { visible: '{{$deps[0] !== "basic"}}', }, schema: { description: '{{renderEngineReference($deps[0])}}', }, }, }, required: true, }, }, view: {}, options: [ { label: `{{t('Continue when "Yes"', { ns: "${NAMESPACE}" })}}`, key: 'rejectOnFalse', value: { rejectOnFalse: true }, }, { label: `{{t('Branch into "Yes" and "No"', { ns: "${NAMESPACE}" })}}`, key: 'branch', value: { rejectOnFalse: false }, }, ], render: function Renderer(data) { const { t } = useTranslation(); const { nodes } = useFlowContext(); const { id, config: { rejectOnFalse }, } = data; const trueEntry = nodes.find((item) => item.upstreamId === id && item.branchIndex === 1); const falseEntry = nodes.find((item) => item.upstreamId === id && item.branchIndex === 0); return ( {rejectOnFalse ? null : (
* > .workflow-branch-lines { > button { display: none; } } `, )} >
span { position: absolute; top: calc(1.5em - 1px); line-height: 1em; color: #999; background-color: #f0f2f5; padding: 1px; } `} > {t('No')} {t('Yes')}
)}
); }, scope: { renderEngineReference, useWorkflowVariableOptions, }, components: { CalculationConfig, RadioWithTooltip, }, };