### What problem does this PR solve? feat: Build the edges of Switch by form data #1739 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| import { useUpdateNodeInternals } from 'reactflow'; | import { useUpdateNodeInternals } from 'reactflow'; | ||||
| import { Operator } from '../../constant'; | import { Operator } from '../../constant'; | ||||
| import { IPosition, NodeData } from '../../interface'; | import { IPosition, NodeData } from '../../interface'; | ||||
| import { buildNewPositionMap, isKeysEqual } from '../../utils'; | |||||
| import { | |||||
| buildNewPositionMap, | |||||
| generateSwitchHandleText, | |||||
| isKeysEqual, | |||||
| } from '../../utils'; | |||||
| export const useBuildCategorizeHandlePositions = ({ | export const useBuildCategorizeHandlePositions = ({ | ||||
| data, | data, | ||||
| const position = positionMap[x]; | const position = positionMap[x]; | ||||
| let text = x; | let text = x; | ||||
| if (operatorName === Operator.Switch) { | if (operatorName === Operator.Switch) { | ||||
| text = `Item ${idx + 1}`; | |||||
| text = generateSwitchHandleText(idx); | |||||
| } | } | ||||
| return { text, ...position }; | return { text, ...position }; | ||||
| }) | }) |
| const getOtherFieldValues = ( | const getOtherFieldValues = ( | ||||
| form: FormInstance, | form: FormInstance, | ||||
| formListName: string = 'items', | |||||
| field: FormListFieldData, | field: FormListFieldData, | ||||
| latestField: string, | latestField: string, | ||||
| ) => | ) => | ||||
| (form.getFieldValue(['items']) ?? []) | |||||
| (form.getFieldValue([formListName]) ?? []) | |||||
| .map((x: any) => x[latestField]) | .map((x: any) => x[latestField]) | ||||
| .filter( | .filter( | ||||
| (x: string) => | (x: string) => | ||||
| x !== form.getFieldValue(['items', field.name, latestField]), | |||||
| x !== form.getFieldValue([formListName, field.name, latestField]), | |||||
| ); | ); | ||||
| const NameInput = ({ | const NameInput = ({ | ||||
| ]} | ]} | ||||
| > | > | ||||
| <NameInput | <NameInput | ||||
| otherNames={getOtherFieldValues(form, field, 'name')} | |||||
| otherNames={getOtherFieldValues( | |||||
| form, | |||||
| 'items', | |||||
| field, | |||||
| 'name', | |||||
| )} | |||||
| validate={(errors: string[]) => | validate={(errors: string[]) => | ||||
| form.setFields([ | form.setFields([ | ||||
| { | { | ||||
| <Select | <Select | ||||
| allowClear | allowClear | ||||
| options={buildCategorizeToOptions( | options={buildCategorizeToOptions( | ||||
| getOtherFieldValues(form, field, 'to'), | |||||
| getOtherFieldValues(form, 'items', field, 'to'), | |||||
| )} | )} | ||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| ); | ); | ||||
| }} | }} | ||||
| </Form.List> | </Form.List> | ||||
| {/* <Form.Item noStyle shouldUpdate> | |||||
| {() => ( | |||||
| <Typography> | |||||
| <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre> | |||||
| </Typography> | |||||
| )} | |||||
| </Form.Item> */} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; |
| initialSwitchValues, | initialSwitchValues, | ||||
| initialWikipediaValues, | initialWikipediaValues, | ||||
| } from './constant'; | } from './constant'; | ||||
| import { ICategorizeForm, IRelevantForm } from './interface'; | |||||
| import { ICategorizeForm, IRelevantForm, ISwitchForm } from './interface'; | |||||
| import useGraphStore, { RFState } from './store'; | import useGraphStore, { RFState } from './store'; | ||||
| import { | import { | ||||
| buildDslComponentsByGraph, | buildDslComponentsByGraph, | ||||
| generateSwitchHandleText, | |||||
| receiveMessageError, | receiveMessageError, | ||||
| replaceIdWithText, | replaceIdWithText, | ||||
| } from './utils'; | } from './utils'; | ||||
| [setEdgesByNodeId], | [setEdgesByNodeId], | ||||
| ); | ); | ||||
| const buildSwitchEdgesByFormData = useCallback( | |||||
| (nodeId: string, form: ISwitchForm) => { | |||||
| // add | |||||
| // delete | |||||
| // edit | |||||
| const conditions = form.conditions; | |||||
| const downstreamEdges = conditions.reduce<Edge[]>((pre, _, idx) => { | |||||
| const target = conditions[idx]?.to; | |||||
| if (target) { | |||||
| pre.push({ | |||||
| id: uuid(), | |||||
| source: nodeId, | |||||
| target, | |||||
| sourceHandle: generateSwitchHandleText(idx), | |||||
| }); | |||||
| } | |||||
| return pre; | |||||
| }, []); | |||||
| setEdgesByNodeId(nodeId, downstreamEdges); | |||||
| }, | |||||
| [setEdgesByNodeId], | |||||
| ); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| nodes.forEach((node) => { | nodes.forEach((node) => { | ||||
| const currentNode = getNode(node.id); | const currentNode = getNode(node.id); | ||||
| case Operator.Categorize: | case Operator.Categorize: | ||||
| buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm); | buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm); | ||||
| break; | break; | ||||
| case Operator.Switch: | |||||
| buildSwitchEdgesByFormData(node.id, form as ISwitchForm); | |||||
| break; | |||||
| default: | default: | ||||
| break; | break; | ||||
| } | } | ||||
| buildCategorizeEdgesByFormData, | buildCategorizeEdgesByFormData, | ||||
| getNode, | getNode, | ||||
| buildRelevantEdgesByFormData, | buildRelevantEdgesByFormData, | ||||
| buildSwitchEdgesByFormData, | |||||
| ]); | ]); | ||||
| }; | }; |
| import { immer } from 'zustand/middleware/immer'; | import { immer } from 'zustand/middleware/immer'; | ||||
| import { Operator } from './constant'; | import { Operator } from './constant'; | ||||
| import { NodeData } from './interface'; | import { NodeData } from './interface'; | ||||
| import { isEdgeEqual } from './utils'; | |||||
| import { getOperatorIndex, isEdgeEqual } from './utils'; | |||||
| export type RFState = { | export type RFState = { | ||||
| nodes: Node<NodeData>[]; | nodes: Node<NodeData>[]; | ||||
| 'to', | 'to', | ||||
| ]); | ]); | ||||
| break; | break; | ||||
| // case Operator.Switch: | |||||
| // if (sourceHandle) | |||||
| // updateNodeForm(source, target, [ | |||||
| // 'conditions', | |||||
| // sourceHandle, | |||||
| // 'to', | |||||
| // ]); | |||||
| // break; | |||||
| case Operator.Switch: { | |||||
| if (sourceHandle) { | |||||
| const operatorIndex = getOperatorIndex(sourceHandle); | |||||
| if (operatorIndex) { | |||||
| updateNodeForm(source, target, [ | |||||
| 'conditions', | |||||
| operatorIndex, | |||||
| 'to', | |||||
| ]); | |||||
| } | |||||
| } | |||||
| break; | |||||
| } | |||||
| default: | default: | ||||
| break; | break; | ||||
| } | } | ||||
| // Delete the edge on the classification node or relevant node anchor when the anchor is connected to other nodes | // Delete the edge on the classification node or relevant node anchor when the anchor is connected to other nodes | ||||
| const { edges, getOperatorTypeFromId, deleteEdgeById } = get(); | const { edges, getOperatorTypeFromId, deleteEdgeById } = get(); | ||||
| // the node containing the anchor | // the node containing the anchor | ||||
| const anchoredNodes = [Operator.Categorize, Operator.Relevant]; | |||||
| const anchoredNodes = [ | |||||
| Operator.Categorize, | |||||
| Operator.Relevant, | |||||
| Operator.Switch, | |||||
| ]; | |||||
| if ( | if ( | ||||
| anchoredNodes.some( | anchoredNodes.some( | ||||
| (x) => x === getOperatorTypeFromId(connection.source), | (x) => x === getOperatorTypeFromId(connection.source), | ||||
| 'to', | 'to', | ||||
| ]); | ]); | ||||
| break; | break; | ||||
| case Operator.Switch: { | |||||
| if (sourceHandle) { | |||||
| const operatorIndex = getOperatorIndex(sourceHandle); | |||||
| if (operatorIndex) { | |||||
| updateNodeForm(source, undefined, [ | |||||
| 'conditions', | |||||
| operatorIndex, | |||||
| 'to', | |||||
| ]); | |||||
| } | |||||
| } | |||||
| break; | |||||
| } | |||||
| default: | default: | ||||
| break; | break; | ||||
| } | } |
| import { CloseOutlined } from '@ant-design/icons'; | import { CloseOutlined } from '@ant-design/icons'; | ||||
| import { Button, Card, Form, Input, Select, Typography } from 'antd'; | import { Button, Card, Form, Input, Select, Typography } from 'antd'; | ||||
| import React from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { Operator } from '../constant'; | import { Operator } from '../constant'; | ||||
| import { useBuildFormSelectOptions } from '../form-hooks'; | import { useBuildFormSelectOptions } from '../form-hooks'; | ||||
| import { IOperatorForm } from '../interface'; | |||||
| import { IOperatorForm, ISwitchForm } from '../interface'; | |||||
| import { getOtherFieldValues } from '../utils'; | |||||
| const subLabelCol = { | const subLabelCol = { | ||||
| span: 7, | span: 7, | ||||
| span: 17, | span: 17, | ||||
| }; | }; | ||||
| const SwitchForm: React.FC = ({ | |||||
| form, | |||||
| onValuesChange, | |||||
| nodeId, | |||||
| }: IOperatorForm) => { | |||||
| const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | const buildCategorizeToOptions = useBuildFormSelectOptions( | ||||
| Operator.Categorize, | |||||
| nodeId, | |||||
| Operator.Switch, | |||||
| node?.id, | |||||
| ); | ); | ||||
| const getSelectedConditionTos = () => { | |||||
| const conditions: ISwitchForm['conditions'] = | |||||
| form?.getFieldValue('conditions'); | |||||
| return conditions?.filter((x) => !!x).map((x) => x?.to) ?? []; | |||||
| }; | |||||
| return ( | return ( | ||||
| <Form | <Form | ||||
| labelCol={{ span: 8 }} | labelCol={{ span: 8 }} | ||||
| onValuesChange={onValuesChange} | onValuesChange={onValuesChange} | ||||
| > | > | ||||
| <Form.Item label={t('flow.to')} name={['end_cpn_id']}> | <Form.Item label={t('flow.to')} name={['end_cpn_id']}> | ||||
| <Select options={buildCategorizeToOptions([])} /> | |||||
| <Select | |||||
| allowClear | |||||
| options={buildCategorizeToOptions(getSelectedConditionTos())} | |||||
| /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label={t('flow.no')} name={['no']}> | <Form.Item label={t('flow.no')} name={['no']}> | ||||
| <Input /> | <Input /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label={t('flow.to')} name={[field.name, 'to']}> | <Form.Item label={t('flow.to')} name={[field.name, 'to']}> | ||||
| <Select options={buildCategorizeToOptions([])} /> | |||||
| <Select | |||||
| allowClear | |||||
| options={buildCategorizeToOptions([ | |||||
| form?.getFieldValue('end_cpn_id'), | |||||
| ...getOtherFieldValues(form!, 'conditions', field, 'to'), | |||||
| ])} | |||||
| /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label="Items"> | <Form.Item label="Items"> | ||||
| <Form.List name={[field.name, 'items']}> | <Form.List name={[field.name, 'items']}> |
| import { DSLComponents } from '@/interfaces/database/flow'; | import { DSLComponents } from '@/interfaces/database/flow'; | ||||
| import { removeUselessFieldsFromValues } from '@/utils/form'; | import { removeUselessFieldsFromValues } from '@/utils/form'; | ||||
| import { FormInstance, FormListFieldData } from 'antd'; | |||||
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import { curry, intersectionWith, isEqual, sample } from 'lodash'; | |||||
| import { curry, get, intersectionWith, isEqual, sample } from 'lodash'; | |||||
| import pipe from 'lodash/fp/pipe'; | import pipe from 'lodash/fp/pipe'; | ||||
| import isObject from 'lodash/isObject'; | import isObject from 'lodash/isObject'; | ||||
| import { Edge, Node, Position } from 'reactflow'; | import { Edge, Node, Position } from 'reactflow'; | ||||
| export const isKeysEqual = (currentKeys: string[], previousKeys: string[]) => { | export const isKeysEqual = (currentKeys: string[], previousKeys: string[]) => { | ||||
| return isEqual(currentKeys.sort(), previousKeys.sort()); | return isEqual(currentKeys.sort(), previousKeys.sort()); | ||||
| }; | }; | ||||
| export const getOperatorIndex = (handleTitle: string) => { | |||||
| return handleTitle.split(' ').at(-1); | |||||
| }; | |||||
| // Get the value of other forms except itself | |||||
| export const getOtherFieldValues = ( | |||||
| form: FormInstance, | |||||
| formListName: string = 'items', | |||||
| field: FormListFieldData, | |||||
| latestField: string, | |||||
| ) => | |||||
| (form.getFieldValue([formListName]) ?? []) | |||||
| .map((x: any) => { | |||||
| return get(x, latestField); | |||||
| }) | |||||
| .filter( | |||||
| (x: string) => | |||||
| x !== form.getFieldValue([formListName, field.name, latestField]), | |||||
| ); | |||||
| export const generateSwitchHandleText = (idx: number) => { | |||||
| return `Item ${idx + 1}`; | |||||
| }; |