### 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
| @@ -1,66 +1,21 @@ | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { Flex } from 'antd'; | |||
| 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 { useEffect, useMemo, useState } from 'react'; | |||
| import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow'; | |||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||
| import { Operator, operatorMap } from '../../constant'; | |||
| import { IPosition, NodeData } from '../../interface'; | |||
| import { NodeData } from '../../interface'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import { buildNewPositionMap } from '../../utils'; | |||
| import CategorizeHandle from './categorize-handle'; | |||
| import NodeDropdown from './dropdown'; | |||
| import { useBuildCategorizeHandlePositions } from './hooks'; | |||
| import styles from './index.less'; | |||
| import NodePopover from './popover'; | |||
| 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 { 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 ( | |||
| <NodePopover nodeId={id}> | |||
| @@ -94,18 +49,15 @@ export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| {Object.keys(categoryData).map((x, idx) => { | |||
| const position = postionMap[x]; | |||
| {positions.map((position, idx) => { | |||
| 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}> | |||
| @@ -0,0 +1,77 @@ | |||
| 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; | |||
| }) => {}; | |||
| @@ -485,7 +485,7 @@ export const NodeMap = { | |||
| [Operator.BaiduFanyi]: 'ragNode', | |||
| [Operator.QWeather]: 'ragNode', | |||
| [Operator.ExeSQL]: 'ragNode', | |||
| [Operator.Switch]: 'logicNode', | |||
| [Operator.Switch]: 'categorizeNode', | |||
| }; | |||
| export const LanguageOptions = [ | |||
| @@ -1,6 +1,6 @@ | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { Operator } from './constant'; | |||
| import { Operator, RestrictedUpstreamMap } from './constant'; | |||
| import useGraphStore from './store'; | |||
| const ExcludedNodesMap = { | |||
| @@ -13,6 +13,7 @@ const ExcludedNodesMap = { | |||
| ], | |||
| [Operator.Relevant]: [Operator.Begin, Operator.Answer, Operator.Relevant], | |||
| [Operator.Generate]: [Operator.Begin], | |||
| [Operator.Switch]: [Operator.Begin], | |||
| }; | |||
| export const useBuildFormSelectOptions = ( | |||
| @@ -23,7 +24,8 @@ export const useBuildFormSelectOptions = ( | |||
| const buildCategorizeToOptions = useCallback( | |||
| (toList: string[]) => { | |||
| const excludedNodes: Operator[] = ExcludedNodesMap[operatorName] ?? []; | |||
| const excludedNodes: Operator[] = | |||
| RestrictedUpstreamMap[operatorName] ?? []; | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| @@ -147,7 +147,6 @@ const useGraphStore = create<RFState>()( | |||
| ]); | |||
| } | |||
| }, | |||
| addNode: (node: Node) => { | |||
| set({ nodes: get().nodes.concat(node) }); | |||
| }, | |||
| @@ -1,7 +1,9 @@ | |||
| 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 { useTranslation } from 'react-i18next'; | |||
| import { Operator } from '../constant'; | |||
| import { useBuildFormSelectOptions } from '../form-hooks'; | |||
| import { IOperatorForm } from '../interface'; | |||
| const subLabelCol = { | |||
| @@ -12,8 +14,16 @@ const subWrapperCol = { | |||
| span: 17, | |||
| }; | |||
| const SwitchForm: React.FC = ({ form, onValuesChange }: IOperatorForm) => { | |||
| const SwitchForm: React.FC = ({ | |||
| form, | |||
| onValuesChange, | |||
| nodeId, | |||
| }: IOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | |||
| Operator.Categorize, | |||
| nodeId, | |||
| ); | |||
| return ( | |||
| <Form | |||
| @@ -26,7 +36,7 @@ const SwitchForm: React.FC = ({ form, onValuesChange }: IOperatorForm) => { | |||
| onValuesChange={onValuesChange} | |||
| > | |||
| <Form.Item label={t('flow.to')} name={['end_cpn_id']}> | |||
| <Input /> | |||
| <Select options={buildCategorizeToOptions([])} /> | |||
| </Form.Item> | |||
| <Form.Item label={t('flow.no')} name={['no']}> | |||
| <Input /> | |||
| @@ -55,9 +65,8 @@ const SwitchForm: React.FC = ({ form, onValuesChange }: IOperatorForm) => { | |||
| </Form.Item> | |||
| <Form.Item label={t('flow.to')} name={[field.name, 'to']}> | |||
| <Input /> | |||
| <Select options={buildCategorizeToOptions([])} /> | |||
| </Form.Item> | |||
| {/* Nest Form.List */} | |||
| <Form.Item label="Items"> | |||
| <Form.List name={[field.name, 'items']}> | |||
| {(subFields, subOpt) => ( | |||
| @@ -9,7 +9,7 @@ import { history } from 'umi'; | |||
| import { RequestMethod, extend } from 'umi-request'; | |||
| import { convertTheKeysOfTheObjectToSnake } from './common-util'; | |||
| const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; | |||
| const FAILED_TO_FETCH = 'Failed to fetch'; | |||
| const RetcodeMessage = { | |||
| 200: i18n.t('message.200'), | |||
| @@ -50,8 +50,11 @@ const errorHandler = (error: { | |||
| message: string; | |||
| }): Response => { | |||
| 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 { | |||
| if (response && response.status) { | |||
| const errorText = | |||
| @@ -61,11 +64,6 @@ const errorHandler = (error: { | |||
| message: `${i18n.t('message.requestError')} ${status}: ${url}`, | |||
| description: errorText, | |||
| }); | |||
| } else if (!response) { | |||
| notification.error({ | |||
| description: i18n.t('message.networkAnomalyDescription'), | |||
| message: i18n.t('message.networkAnomaly'), | |||
| }); | |||
| } | |||
| } | |||
| return response; | |||
| @@ -102,6 +100,7 @@ request.interceptors.response.use(async (response: any, options) => { | |||
| if (options.responseType === 'blob') { | |||
| return response; | |||
| } | |||
| const data: ResponseType = await response.clone().json(); | |||
| if (data.retcode === 401 || data.retcode === 401) { | |||