### What problem does this PR solve? feat: Extract the code for building categorize operator coordinates to hooks.ts #1739 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.10.0
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { pick } from 'lodash'; | |||||
| import get from 'lodash/get'; | |||||
| import intersectionWith from 'lodash/intersectionWith'; | |||||
| import isEqual from 'lodash/isEqual'; | |||||
| import lowerFirst from 'lodash/lowerFirst'; | import lowerFirst from 'lodash/lowerFirst'; | ||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow'; | |||||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||||
| import { Operator, operatorMap } from '../../constant'; | import { Operator, operatorMap } from '../../constant'; | ||||
| import { IPosition, NodeData } from '../../interface'; | |||||
| import { NodeData } from '../../interface'; | |||||
| import OperatorIcon from '../../operator-icon'; | import OperatorIcon from '../../operator-icon'; | ||||
| import { buildNewPositionMap } from '../../utils'; | |||||
| import CategorizeHandle from './categorize-handle'; | import CategorizeHandle from './categorize-handle'; | ||||
| import NodeDropdown from './dropdown'; | import NodeDropdown from './dropdown'; | ||||
| import { useBuildCategorizeHandlePositions } from './hooks'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodePopover from './popover'; | import NodePopover from './popover'; | ||||
| export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | ||||
| const updateNodeInternals = useUpdateNodeInternals(); | |||||
| const [postionMap, setPositionMap] = useState<Record<string, IPosition>>({}); | |||||
| const categoryData = useMemo( | |||||
| () => get(data, 'form.category_description') ?? {}, | |||||
| [data], | |||||
| ); | |||||
| const style = operatorMap[data.label as Operator]; | const style = operatorMap[data.label as Operator]; | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| useEffect(() => { | |||||
| // Cache used coordinates | |||||
| setPositionMap((state) => { | |||||
| // index in use | |||||
| const indexesInUse = Object.values(state).map((x) => x.idx); | |||||
| const categoryDataKeys = Object.keys(categoryData); | |||||
| const stateKeys = Object.keys(state); | |||||
| if (!isEqual(categoryDataKeys.sort(), stateKeys.sort())) { | |||||
| const intersectionKeys = intersectionWith( | |||||
| stateKeys, | |||||
| categoryDataKeys, | |||||
| (categoryDataKey, postionMapKey) => categoryDataKey === postionMapKey, | |||||
| ); | |||||
| const newPositionMap = buildNewPositionMap( | |||||
| categoryDataKeys.filter( | |||||
| (x) => !intersectionKeys.some((y) => y === x), | |||||
| ), | |||||
| indexesInUse, | |||||
| ); | |||||
| const nextPostionMap = { | |||||
| ...pick(state, intersectionKeys), | |||||
| ...newPositionMap, | |||||
| }; | |||||
| return nextPostionMap; | |||||
| } | |||||
| return state; | |||||
| }); | |||||
| }, [categoryData]); | |||||
| useEffect(() => { | |||||
| updateNodeInternals(id); | |||||
| }, [id, updateNodeInternals, postionMap]); | |||||
| const { positions } = useBuildCategorizeHandlePositions({ data, id }); | |||||
| return ( | return ( | ||||
| <NodePopover nodeId={id}> | <NodePopover nodeId={id}> | ||||
| className={styles.handle} | className={styles.handle} | ||||
| id={'c'} | id={'c'} | ||||
| ></Handle> | ></Handle> | ||||
| {Object.keys(categoryData).map((x, idx) => { | |||||
| const position = postionMap[x]; | |||||
| {positions.map((position, idx) => { | |||||
| return ( | return ( | ||||
| position && ( | |||||
| <CategorizeHandle | |||||
| top={position.top} | |||||
| right={position.right} | |||||
| key={idx} | |||||
| text={x} | |||||
| idx={idx} | |||||
| ></CategorizeHandle> | |||||
| ) | |||||
| <CategorizeHandle | |||||
| top={position.top} | |||||
| right={position.right} | |||||
| key={idx} | |||||
| text={position.text} | |||||
| idx={idx} | |||||
| ></CategorizeHandle> | |||||
| ); | ); | ||||
| })} | })} | ||||
| <Flex vertical align="center" justify="center" gap={6}> | <Flex vertical align="center" justify="center" gap={6}> |
| import get from 'lodash/get'; | |||||
| import intersectionWith from 'lodash/intersectionWith'; | |||||
| import isEqual from 'lodash/isEqual'; | |||||
| import pick from 'lodash/pick'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { useUpdateNodeInternals } from 'reactflow'; | |||||
| import { IPosition, NodeData } from '../../interface'; | |||||
| import { buildNewPositionMap } from '../../utils'; | |||||
| export const useBuildCategorizeHandlePositions = ({ | |||||
| data, | |||||
| id, | |||||
| }: { | |||||
| id: string; | |||||
| data: NodeData; | |||||
| }) => { | |||||
| const updateNodeInternals = useUpdateNodeInternals(); | |||||
| const [positionMap, setPositionMap] = useState<Record<string, IPosition>>({}); | |||||
| const categoryData = useMemo( | |||||
| () => get(data, 'form.category_description') ?? {}, | |||||
| [data], | |||||
| ); | |||||
| const positions = useMemo(() => { | |||||
| return Object.keys(categoryData) | |||||
| .map((x) => { | |||||
| const position = positionMap[x]; | |||||
| return { text: x, ...position }; | |||||
| }) | |||||
| .filter((x) => typeof x?.right === 'number'); | |||||
| }, [categoryData, positionMap]); | |||||
| useEffect(() => { | |||||
| // Cache used coordinates | |||||
| setPositionMap((state) => { | |||||
| // index in use | |||||
| const indexesInUse = Object.values(state).map((x) => x.idx); | |||||
| const categoryDataKeys = Object.keys(categoryData); | |||||
| const stateKeys = Object.keys(state); | |||||
| if (!isEqual(categoryDataKeys.sort(), stateKeys.sort())) { | |||||
| const intersectionKeys = intersectionWith( | |||||
| stateKeys, | |||||
| categoryDataKeys, | |||||
| (categoryDataKey, postionMapKey) => categoryDataKey === postionMapKey, | |||||
| ); | |||||
| const newPositionMap = buildNewPositionMap( | |||||
| categoryDataKeys.filter( | |||||
| (x) => !intersectionKeys.some((y) => y === x), | |||||
| ), | |||||
| indexesInUse, | |||||
| ); | |||||
| const nextPositionMap = { | |||||
| ...pick(state, intersectionKeys), | |||||
| ...newPositionMap, | |||||
| }; | |||||
| return nextPositionMap; | |||||
| } | |||||
| return state; | |||||
| }); | |||||
| }, [categoryData]); | |||||
| useEffect(() => { | |||||
| updateNodeInternals(id); | |||||
| }, [id, updateNodeInternals, positionMap]); | |||||
| return { positions }; | |||||
| }; | |||||
| export const useBuildSwitchHandlePositions = ({ | |||||
| data, | |||||
| id, | |||||
| }: { | |||||
| id: string; | |||||
| data: NodeData; | |||||
| }) => {}; |
| [Operator.BaiduFanyi]: 'ragNode', | [Operator.BaiduFanyi]: 'ragNode', | ||||
| [Operator.QWeather]: 'ragNode', | [Operator.QWeather]: 'ragNode', | ||||
| [Operator.ExeSQL]: 'ragNode', | [Operator.ExeSQL]: 'ragNode', | ||||
| [Operator.Switch]: 'logicNode', | |||||
| [Operator.Switch]: 'categorizeNode', | |||||
| }; | }; | ||||
| export const LanguageOptions = [ | export const LanguageOptions = [ |
| import { useTranslate } from '@/hooks/common-hooks'; | import { useTranslate } from '@/hooks/common-hooks'; | ||||
| import { useCallback, useMemo } from 'react'; | import { useCallback, useMemo } from 'react'; | ||||
| import { Operator } from './constant'; | |||||
| import { Operator, RestrictedUpstreamMap } from './constant'; | |||||
| import useGraphStore from './store'; | import useGraphStore from './store'; | ||||
| const ExcludedNodesMap = { | const ExcludedNodesMap = { | ||||
| ], | ], | ||||
| [Operator.Relevant]: [Operator.Begin, Operator.Answer, Operator.Relevant], | [Operator.Relevant]: [Operator.Begin, Operator.Answer, Operator.Relevant], | ||||
| [Operator.Generate]: [Operator.Begin], | [Operator.Generate]: [Operator.Begin], | ||||
| [Operator.Switch]: [Operator.Begin], | |||||
| }; | }; | ||||
| export const useBuildFormSelectOptions = ( | export const useBuildFormSelectOptions = ( | ||||
| const buildCategorizeToOptions = useCallback( | const buildCategorizeToOptions = useCallback( | ||||
| (toList: string[]) => { | (toList: string[]) => { | ||||
| const excludedNodes: Operator[] = ExcludedNodesMap[operatorName] ?? []; | |||||
| const excludedNodes: Operator[] = | |||||
| RestrictedUpstreamMap[operatorName] ?? []; | |||||
| return nodes | return nodes | ||||
| .filter( | .filter( | ||||
| (x) => | (x) => |
| ]); | ]); | ||||
| } | } | ||||
| }, | }, | ||||
| addNode: (node: Node) => { | addNode: (node: Node) => { | ||||
| set({ nodes: get().nodes.concat(node) }); | set({ nodes: get().nodes.concat(node) }); | ||||
| }, | }, |
| import { CloseOutlined } from '@ant-design/icons'; | import { CloseOutlined } from '@ant-design/icons'; | ||||
| import { Button, Card, Form, Input, Typography } from 'antd'; | |||||
| import { Button, Card, Form, Input, Select, Typography } from 'antd'; | |||||
| import React from 'react'; | import React from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { Operator } from '../constant'; | |||||
| import { useBuildFormSelectOptions } from '../form-hooks'; | |||||
| import { IOperatorForm } from '../interface'; | import { IOperatorForm } from '../interface'; | ||||
| const subLabelCol = { | const subLabelCol = { | ||||
| span: 17, | span: 17, | ||||
| }; | }; | ||||
| const SwitchForm: React.FC = ({ form, onValuesChange }: IOperatorForm) => { | |||||
| const SwitchForm: React.FC = ({ | |||||
| form, | |||||
| onValuesChange, | |||||
| nodeId, | |||||
| }: IOperatorForm) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | |||||
| Operator.Categorize, | |||||
| nodeId, | |||||
| ); | |||||
| return ( | return ( | ||||
| <Form | <Form | ||||
| onValuesChange={onValuesChange} | onValuesChange={onValuesChange} | ||||
| > | > | ||||
| <Form.Item label={t('flow.to')} name={['end_cpn_id']}> | <Form.Item label={t('flow.to')} name={['end_cpn_id']}> | ||||
| <Input /> | |||||
| <Select options={buildCategorizeToOptions([])} /> | |||||
| </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']}> | ||||
| <Input /> | |||||
| <Select options={buildCategorizeToOptions([])} /> | |||||
| </Form.Item> | </Form.Item> | ||||
| {/* Nest Form.List */} | |||||
| <Form.Item label="Items"> | <Form.Item label="Items"> | ||||
| <Form.List name={[field.name, 'items']}> | <Form.List name={[field.name, 'items']}> | ||||
| {(subFields, subOpt) => ( | {(subFields, subOpt) => ( |
| import { RequestMethod, extend } from 'umi-request'; | import { RequestMethod, extend } from 'umi-request'; | ||||
| import { convertTheKeysOfTheObjectToSnake } from './common-util'; | import { convertTheKeysOfTheObjectToSnake } from './common-util'; | ||||
| const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; | |||||
| const FAILED_TO_FETCH = 'Failed to fetch'; | |||||
| const RetcodeMessage = { | const RetcodeMessage = { | ||||
| 200: i18n.t('message.200'), | 200: i18n.t('message.200'), | ||||
| message: string; | message: string; | ||||
| }): Response => { | }): Response => { | ||||
| const { response } = error; | const { response } = error; | ||||
| if (error.message === ABORT_REQUEST_ERR_MESSAGE) { | |||||
| console.log('user abort request'); | |||||
| if (error.message === FAILED_TO_FETCH) { | |||||
| notification.error({ | |||||
| description: i18n.t('message.networkAnomalyDescription'), | |||||
| message: i18n.t('message.networkAnomaly'), | |||||
| }); | |||||
| } else { | } else { | ||||
| if (response && response.status) { | if (response && response.status) { | ||||
| const errorText = | const errorText = | ||||
| message: `${i18n.t('message.requestError')} ${status}: ${url}`, | message: `${i18n.t('message.requestError')} ${status}: ${url}`, | ||||
| description: errorText, | description: errorText, | ||||
| }); | }); | ||||
| } else if (!response) { | |||||
| notification.error({ | |||||
| description: i18n.t('message.networkAnomalyDescription'), | |||||
| message: i18n.t('message.networkAnomaly'), | |||||
| }); | |||||
| } | } | ||||
| } | } | ||||
| return response; | return response; | ||||
| if (options.responseType === 'blob') { | if (options.responseType === 'blob') { | ||||
| return response; | return response; | ||||
| } | } | ||||
| const data: ResponseType = await response.clone().json(); | const data: ResponseType = await response.clone().json(); | ||||
| if (data.retcode === 401 || data.retcode === 401) { | if (data.retcode === 401 || data.retcode === 401) { |