useRuleFlow.ts 11.2 KB
import type {
  Connection,
  EdgeComponent,
  NodeComponent,
  ValidConnectionFunc,
  GraphNode,
  NodeMouseEvent,
  GraphEdge,
  EdgeMouseEvent,
} from '@vue-flow/core';
import type { Ref } from 'vue';
import type { CreateNodeModal } from '../src/components/CreateNodeModal';
import type { EdgeData, NodeData } from '../types/node';
import type { CreateEdgeModal } from '../src/components/CreateEdgeModal';
import { BasicEdge, BasicNode } from '../src/components';
import { EdgeTypeEnum, ElementsTypeEnum, NodeTypeEnum } from '../enum';
import { markRaw, toRaw, unref } from 'vue';
import { isFunction } from 'lodash-es';
import { ConnectionLineType, SelectionMode, useVueFlow } from '@vue-flow/core';
import { isInputHandle, isOutputHandle } from '../utils';
import { useAddEdges } from './useAddEdges';
import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
import { isNumber } from '/@/utils/is';
import { RuleContextMenuEnum, useCreateRuleChainContextMenu } from './useRuleChainContextMenu';
import { RuleChainDetail } from '../types/ruleNode';
import { useContextMenuAction } from './useContextMenuAction';
import { useSaveAndRedo } from './useSaveAndRedo';
import { EntryCategoryComponentEnum } from '../enum/category';

interface UseRuleFlowOptionsType {
  id: string;
  ruleChainDetail: Ref<RuleChainDetail | undefined>;
  createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
  createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
  updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
  updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
  useSaveAndRedoActionType: ReturnType<typeof useSaveAndRedo>;
}

const validateInputAndOutput: ValidConnectionFunc = (connection: Connection) => {
  const { sourceHandle, targetHandle, source, target } = connection;

  return (
    isOutputHandle(sourceHandle || '') && isInputHandle(targetHandle || '') && source !== target
  );
};

export function useRuleFlow(options: UseRuleFlowOptionsType) {
  const {
    id,
    ruleChainDetail,
    createEdgeModalActionType,
    updateEdgeDrawerActionType,
    updateNodeDrawerActionType,
    useSaveAndRedoActionType,
  } = options;

  const { triggerChange } = useSaveAndRedoActionType;

  const flowActionType = useVueFlow({
    id,
    maxZoom: 1,
    minZoom: 1,
    panOnScroll: true,
    selectionMode: SelectionMode.Partial,
    nodeTypes: {
      [NodeTypeEnum.CUSTOM]: markRaw(BasicNode) as NodeComponent,
    },
    edgeTypes: {
      [EdgeTypeEnum.CUSTOM]: markRaw(BasicEdge) as EdgeComponent,
    },
    connectionLineOptions: {
      type: ConnectionLineType.Bezier,
    },
    defaultViewport: {
      x: 0,
      y: 0,
    },
    isValidConnection(connection, elements) {
      const validateList = [validateInputAndOutput];
      const targetData = elements.targetNode.data as NodeData;

      if (
        targetData.category?.validateConnection &&
        isFunction(targetData.category.validateConnection)
      )
        validateList.push(targetData.category?.validateConnection);

      if (targetData.config?.validateConnection && isFunction(targetData.config.validateConnection))
        validateList.push(targetData.config.validateConnection);

      if (!validateList.every((item) => item(connection, elements))) return false;

      if (validateCircularreference(elements.sourceNode!, elements.targetNode, elements.edges))
        return false;

      return true;
    },
  });

  const {
    getEdges,
    addEdges,
    findEdge,
    findNode,
    setViewport,
    removeEdges,
    onConnect,
    onPaneReady,
    onNodeDoubleClick,
    onEdgeDoubleClick,
    onNodeDragStop,
    onNodeContextMenu,
    onEdgeContextMenu,
    onPaneContextMenu,
  } = flowActionType;

  const { getAddedgesParams } = useAddEdges();

  onConnect(async (params) => {
    const { source } = params;
    const sourceNode = findNode(source);
    const sourceData = sourceNode?.data as NodeData;

    let types: string[] = [];

    if (sourceData && validateHasLabelConnection(sourceData)) {
      const { flag, data } = (await unref(createEdgeModalActionType)?.open(sourceData)) || {};
      if (!flag) return;
      types = toRaw(unref(data));
    }

    handleMaxConnectionPoint(sourceNode);

    addEdges(getAddedgesParams(params, types));

    triggerChange();
  });
  onPaneReady(async () => {
    setViewport({ x: 0, y: 0, zoom: 1 });
  });

  onNodeDoubleClick(async ({ node }) => {
    handleUpdateNode(node);
  });

  onEdgeDoubleClick(async ({ edge }) => {
    handleUpdateEdge(edge);
  });

  onNodeDragStop(() => {
    triggerChange();
  });

  const {
    createNodeContextMenu,
    createElementsSelectedContextMenu,
    createEdgeContextMenu,
    createPanelContextMenu,
  } = useCreateRuleChainContextMenu();

  const { handleContextMenuAction } = useContextMenuAction();

  onNodeContextMenu(async (params) => {
    const menuType = params.node.selected
      ? await createElementsSelectedContextMenu(
          params,
          unref(useSaveAndRedoActionType.changeMarker),
          validateSelectionElementsCanCreateRuleChain()
        )
      : await createNodeContextMenu(params);

    if (menuType) {
      if (menuType === RuleContextMenuEnum.DETAIL) {
        handleUpdateNode(params.node);
        return;
      }

      handleContextMenuAction({
        menuType,
        flowActionType,
        event: params.event,
        node: params.node,
        useSaveAndRedoActionType,
      });
    }
  });

  onEdgeContextMenu(async (params) => {
    const isInputNode =
      (params.edge.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT;
    const menuType = await createEdgeContextMenu(
      getCreateEdgeContextMenuParams(params),
      isInputNode
    );

    if (menuType) {
      if (menuType === RuleContextMenuEnum.DETAIL) {
        handleUpdateEdge(params.edge);
        return;
      }

      handleContextMenuAction({
        menuType,
        flowActionType,
        event: params.event,
        useSaveAndRedoActionType,
        edge: params.edge,
      });
    }
  });

  onPaneContextMenu(async (params) => {
    const menuType = unref(flowActionType.getSelectedElements).length
      ? await createElementsSelectedContextMenu(
          getCreatePanelContextMenuParams(params),
          unref(useSaveAndRedoActionType.changeMarker),
          validateSelectionElementsCanCreateRuleChain()
        )
      : await createPanelContextMenu(
          getCreatePanelContextMenuParams(params),
          unref(useSaveAndRedoActionType.changeMarker)
        );

    if (menuType) {
      handleContextMenuAction({
        menuType,
        flowActionType,
        event: params,
        useSaveAndRedoActionType,
      });
    }
  });

  /**
   * @description 验证是否有连接label
   * @param sourceData
   * @returns
   */
  function validateHasLabelConnection(sourceData: NodeData) {
    return !!sourceData?.config?.configurationDescriptor.nodeDefinition?.relationTypes?.length;
  }

  /**
   * @description 验证是否循环引用
   */
  function validateCircularreference(
    sourceNode: GraphNode,
    targetNode: GraphNode,
    edges: GraphEdge[]
  ) {
    const sourceId = sourceNode.id;
    const targetId = targetNode.id;

    const getAllIndex = (
      edges: GraphEdge[],
      validate: (item: GraphEdge, index: number) => boolean
    ) => {
      const indexList: number[] = [];

      for (let i = 0; i < edges.length; i++) {
        const item = edges[i];
        if (validate(item, i)) {
          indexList.push(i);
        }
      }

      return indexList;
    };

    const validate = (source: string, startSource: string) => {
      const nextNodesIndex = getAllIndex(edges, (item) => item.source === source);

      if (!nextNodesIndex.length) return false;

      for (const nextNodeIndex of nextNodesIndex) {
        const nextNode = edges[nextNodeIndex];
        if (nextNode && nextNode.target === startSource) {
          return true;
        }
        return validate(nextNode.target, startSource);
      }
    };

    return validate(targetId, sourceId);
  }

  /**
   * @description 处理最大链接点
   * @param sourceNode
   * @returns
   */
  function handleMaxConnectionPoint(sourceNode?: GraphNode) {
    if (!sourceNode) return;

    const maxConnectionPoint = unref(sourceNode).data?.config?.maxConnectionPoint;

    if (!maxConnectionPoint || !isNumber(maxConnectionPoint)) return;

    const sourceId = sourceNode.id;
    const connectionPool = unref(getEdges).filter((item) => item.source === sourceId);
    if (connectionPool.length >= maxConnectionPoint && connectionPool[0]) {
      removeEdges(connectionPool[0].id);
    }
  }

  async function handleUpdateNode(node: GraphNode) {
    if ((node.data as NodeData).config?.disableAction) return;
    const { flag, data } =
      (await unref(updateNodeDrawerActionType)?.open(
        toRaw((node as NodeData)?.data as unknown as NodeData),
        void 0,
        { id: node.id, type: ElementsTypeEnum.NODE }
      )) || {};

    if (!flag) return;

    const currentNode = findNode(node.id);
    (currentNode!.data as NodeData).data = data;
    triggerChange();
  }

  async function handleUpdateEdge(edge: GraphEdge) {
    if (!validateHasLabelConnection(edge.sourceNode.data)) return;

    if ((edge.sourceNode.data as NodeData).config?.disableAction) return;

    const { flag, data } =
      (await unref(updateEdgeDrawerActionType)?.open(
        toRaw(unref(edge.sourceNode?.data as unknown as NodeData)),
        toRaw(unref(edge.data as EdgeData)),
        { id: edge.id, type: ElementsTypeEnum.EDGE }
      )) || {};

    if (!flag) return;

    const currentEdge = findEdge(edge.id);

    (currentEdge!.data as EdgeData).data = toRaw(unref(data));

    triggerChange?.();
  }

  function getCreatePanelContextMenuParams(params: Event) {
    return {
      event: params as MouseEvent,
      node: {
        data: {
          data: { name: '规则链' },
          config: {
            name: unref(ruleChainDetail)?.name,
            backgroundColor: '#aac7e4',
            configurationDescriptor: {
              nodeDefinition: {
                icon: 'material-symbols:settings-ethernet',
              },
            },
          },
        },
      },
    } as NodeMouseEvent;
  }

  function getCreateEdgeContextMenuParams(params: EdgeMouseEvent) {
    return {
      event: params.event as MouseEvent,
      node: {
        data: {
          data: { name: '链接' },
          config: {
            name: unref(params.edge.data as EdgeData)?.data?.type?.join(' / '),
            backgroundColor: '#aac7e4',
            configurationDescriptor: {
              nodeDefinition: {
                icon: 'material-symbols:trending-flat',
              },
            },
          },
        },
      },
    } as NodeMouseEvent;
  }

  function validateSelectionElementsCanCreateRuleChain() {
    const nodes = unref(flowActionType.getSelectedNodes);
    const edges = unref(flowActionType.getSelectedEdges);

    for (const node of nodes) {
      const index = edges.findIndex((edge) => edge.target === node.id || edge.source === node.id);
      if (!~index) return false;
    }

    return true;
  }

  return { flowActionType };
}