useContextMenuAction.ts 12.5 KB
import { GraphEdge, GraphNode, VueFlowStore, pointToRendererPoint } from '@vue-flow/core';
import { getRuleNodeCache, setRuleNodeCache } from './useRuleChainCache';
import { RuleContextMenuEnum } from './useRuleChainContextMenu';
import { useAddNodes } from './useAddNodes';
import { buildUUID } from '/@/utils/uuid';
import { Ref, toRaw, unref } from 'vue';
import { useSaveAndRedo } from './useSaveAndRedo';
import { isNullOrUnDef, isUnDef } from '/@/utils/is';
import { useAddEdges } from './useAddEdges';
import { EdgeData } from '../types/node';
import { CreateNodeModal } from '../src/components/CreateNodeModal';
import { CreateEdgeModal } from '../src/components/CreateEdgeModal';
import { UpdateNodeDrawer } from '../src/components/UpdateNodeDrawer';
import { UpdateEdgeDrawer } from '../src/components/UpdateEdgeDrawer';
import { CreateRuleChainModal } from '../src/components/CreateRuleChainModal';
import { SOURCE_HANDLE, TARGET_HANDLE, useBasicDataTransform } from './useBasicDataTransform';
import { cloneDeep } from 'lodash-es';
import { useNewNode } from './useNewNode';
import { RuleChainDetail } from '../types/ruleNode';
import { saveRuleChainData, saveRuleChainDetail } from '/@/api/ruleDesigner';

interface HandleContextMenuActionParamsType {
  menuType: RuleContextMenuEnum;
  event?: Event;
  flowActionType?: VueFlowStore;
  node?: GraphNode;
  edge?: GraphEdge;
  useSaveAndRedoActionType?: ReturnType<typeof useSaveAndRedo>;
  modalActionType: {
    createNodeModalActionType: Ref<Nullable<InstanceType<typeof CreateNodeModal>>>;
    createEdgeModalActionType: Ref<Nullable<InstanceType<typeof CreateEdgeModal>>>;
    updateNodeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateNodeDrawer>>>;
    updateEdgeDrawerActionType: Ref<Nullable<InstanceType<typeof UpdateEdgeDrawer>>>;
    createRuleChainModalActionType: Ref<Nullable<InstanceType<typeof CreateRuleChainModal>>>;
  };
}

export function transformToRuleChain(
  nodesRef: Ref<GraphNode[]> | GraphNode[] = [],
  edgesRef: Ref<GraphEdge[]> | GraphEdge[] = []
) {
  const nodeMap = new Map<string, GraphNode>();
  const { combineData, validateCanCreateRuleChain } = useBasicDataTransform();

  nodesRef = cloneDeep(unref(nodesRef));
  edgesRef = cloneDeep(unref(edgesRef));

  unref(nodesRef).forEach((node) => nodeMap.set(node.id, node));
  const outputEdges = unref(edgesRef).filter((edge) => !nodeMap.has(edge.target));
  const outputEdgesId = outputEdges.map((edge) => edge.id);

  const inputEdges = unref(edgesRef).filter((edge) => !nodeMap.has(edge.source));
  const inputEdgesId = inputEdges.map((edge) => edge.id);

  const { getOutputNodeConfig } = useNewNode();
  const outputNode = outputEdges.map((edge) => {
    const id = buildUUID();
    const name = (edge.data as EdgeData).data?.type?.join(' / ') || '';
    edge.target = id;
    edge.targetHandle = `${id}${TARGET_HANDLE}`;
    return getOutputNodeConfig(name, edge.targetNode.position, id);
  });

  nodesRef = [...nodesRef, ...(outputNode as GraphNode[])];

  const { connections, nodes } = combineData(nodesRef, edgesRef);

  const { firstNode } = validateCanCreateRuleChain(nodesRef, edgesRef);

  const firstNodeIndex = nodesRef.findIndex((node) => node.id === firstNode?.id);

  return { connections, nodes, firstNodeIndex, outputEdgesId, inputEdgesId };
}

export const NODE_WIDTH = 176;
export const NODE_HEIGHT = 48;

function getElementsCenter(nodes: Ref<GraphNode[]> | GraphNode[] = []) {
  let leftTopX: number | undefined;
  let leftTopY: number | undefined;
  let rightBottomX: number | undefined;
  let rightBottomY: number | undefined;

  for (const node of unref(nodes)) {
    const { position } = node;
    const { x, y } = position;
    if (isUnDef(leftTopX)) {
      leftTopX = x;
      leftTopY = y;
      rightBottomX = x + NODE_WIDTH;
      rightBottomY = y + NODE_HEIGHT;

      continue;
    }

    if (x < leftTopX!) leftTopX = x;

    if (y < leftTopY!) leftTopY = y;

    if (x + NODE_WIDTH > rightBottomX!) rightBottomX = x + NODE_WIDTH;

    if (y + NODE_HEIGHT > rightBottomY!) rightBottomY = y + NODE_HEIGHT;
  }

  return {
    originX: (rightBottomX! - leftTopX!) / 2 + leftTopX!,
    originY: (rightBottomY! - leftTopY!) / 2 + leftTopY!,
  };
}

function createRuleChainNode({
  originX,
  originY,
  outputEdges,
  inputEdges,
  ruleChainDetail,
}: {
  originX: number;
  originY: number;
  ruleChainDetail: RuleChainDetail;
  outputEdges: Ref<(GraphEdge | undefined)[]> | (GraphEdge | undefined)[];
  inputEdges: Ref<(GraphEdge | undefined)[]> | (GraphEdge | undefined)[];
}) {
  const { getRuleChainNodeConfig } = useNewNode();
  const { getAddedgesParams } = useAddEdges();
  const nodeId = buildUUID();

  const ruleChainNode = getRuleChainNodeConfig({
    name: ruleChainDetail.name,
    position: { x: originX - NODE_WIDTH / 2, y: originY - NODE_HEIGHT / 2 },
    ruleChainId: ruleChainDetail.id.id,
    id: nodeId,
  });

  const newInputEdges = unref(inputEdges).map((edge) =>
    getAddedgesParams(
      {
        source: edge!.source,
        target: nodeId,
        sourceHandle: `${edge?.source}${SOURCE_HANDLE}`,
        targetHandle: `${nodeId}${TARGET_HANDLE}`,
      },
      toRaw(unref(edge!.data as EdgeData).data)
    )
  );

  const newOutputEdges = unref(outputEdges).map((edge) =>
    getAddedgesParams(
      {
        source: nodeId,
        target: edge!.target,
        sourceHandle: `${nodeId}${SOURCE_HANDLE}`,
        targetHandle: `${edge?.target}${TARGET_HANDLE}`,
      },
      toRaw(unref(edge!.data as EdgeData).data)
    )
  );

  return {
    nodes: [ruleChainNode],
    edges: [...newInputEdges, ...newOutputEdges],
  };
}

export function useContextMenuAction() {
  const copy = (params: HandleContextMenuActionParamsType) => {
    const { node } = params;
    if (!node) return;
    const { position, data, id } = node;
    const { getAddNodesParams } = useAddNodes();
    const { x, y } = position;

    const value = getAddNodesParams(position, { ...data, isSaved: false }, { id });

    setRuleNodeCache({
      nodes: [value],
      originX: x + NODE_WIDTH / 2,
      originY: y + NODE_HEIGHT / 2,
    });
  };

  const paste = (params: HandleContextMenuActionParamsType) => {
    const { event, flowActionType, useSaveAndRedoActionType } = params;
    const { triggerChange } = useSaveAndRedoActionType || {};

    const { getAddNodesParams } = useAddNodes();
    const { getAddedgesParams } = useAddEdges();

    const { edges = [], nodes = [], originX, originY } = getRuleNodeCache();

    const { x: clientX, y: clientY } = pointToRendererPoint(
      { x: (event as MouseEvent).offsetX, y: (event as MouseEvent).offsetY },
      flowActionType!.getViewport(),
      false,
      [25, 25]
    );

    const xAxisMoveDistance = clientX - originX!;
    const yAxisMoveDistance = clientY - originY!;

    const newEdges = edges.map((edge) =>
      getAddedgesParams({ ...edge, id: buildUUID() }, edge.data)
    );

    const newNode = nodes.map((node) => {
      const { position, data, id: oldId } = node;
      const { x, y } = position;
      const newId = buildUUID();

      const newX = xAxisMoveDistance + x;
      const newY = yAxisMoveDistance + y;

      for (const connection of newEdges || []) {
        if (connection.source.includes(oldId)) {
          connection.source = newId;
          connection.sourceHandle = connection.sourceHandle?.replaceAll(oldId, newId);
          continue;
        }

        if (connection.target.includes(oldId)) {
          connection.target = newId;
          connection.targetHandle = connection.targetHandle?.replaceAll(oldId, newId);
        }
      }

      return getAddNodesParams(
        { x: newX, y: newY },
        { ...data, created: false, isSaved: false },
        { id: newId }
      );
    });

    flowActionType?.addNodes(newNode);
    flowActionType?.addEdges(newEdges);

    triggerChange?.();

    flowActionType?.removeSelectedElements();
  };

  const selectAll = (params: HandleContextMenuActionParamsType) => {
    const { flowActionType } = params;
    flowActionType?.addSelectedElements(unref(flowActionType.getElements));
  };

  const unselect = (params: HandleContextMenuActionParamsType) => {
    const { flowActionType } = params;
    flowActionType?.removeSelectedElements();
  };

  const deleteElement = (parmas: HandleContextMenuActionParamsType) => {
    const { useSaveAndRedoActionType, flowActionType, node, edge } = parmas;
    const { triggerChange } = useSaveAndRedoActionType || {};

    node && flowActionType?.removeNodes(node);
    edge && flowActionType?.removeEdges(edge);

    triggerChange?.();
  };

  const deleteElements = (params: HandleContextMenuActionParamsType) => {
    const { flowActionType, useSaveAndRedoActionType } = params;
    flowActionType?.removeNodes(unref(flowActionType.getSelectedNodes));

    useSaveAndRedoActionType?.triggerChange?.();
  };

  const selectCopy = (params: HandleContextMenuActionParamsType) => {
    const { flowActionType } = params;
    const { getAddNodesParams } = useAddNodes();
    const { getAddedgesParams } = useAddEdges();

    const edges = unref(flowActionType?.getSelectedEdges)?.map((edge) =>
      getAddedgesParams(
        {
          source: edge.source,
          target: edge.target,
          sourceHandle: edge.sourceHandle,
          targetHandle: edge.targetHandle,
        },
        toRaw(unref(edge.data as EdgeData)?.data)
      )
    );

    const nodes = unref(flowActionType?.getSelectedNodes)?.map((node) => {
      const { id } = node;

      return getAddNodesParams(node.position, toRaw(unref(node.data)), { id });
    });

    const originRect = getElementsCenter(unref(flowActionType?.getSelectedNodes) || []);

    setRuleNodeCache({ nodes, edges, ...originRect });
  };

  const applyChange = (params: HandleContextMenuActionParamsType) => {
    const { useSaveAndRedoActionType, flowActionType } = params;

    useSaveAndRedoActionType?.handleApplyChange(flowActionType!);
  };

  const undoChange = (params: HandleContextMenuActionParamsType) => {
    const { useSaveAndRedoActionType, flowActionType } = params;

    useSaveAndRedoActionType?.handleRedoChange(flowActionType!);
  };

  const createRuleChain = async (params: HandleContextMenuActionParamsType) => {
    const { useSaveAndRedoActionType, modalActionType, flowActionType } = params;
    const { createRuleChainModalActionType } = modalActionType;
    const result = (await unref(createRuleChainModalActionType)?.openCreateRuleChainModal()) as {
      name: string;
      additionalInfo: { description: string };
    };

    const ruleChainDetail = await saveRuleChainDetail(
      Object.assign(result, { debugger: false, type: 'CORE' }) as Partial<RuleChainDetail>
    );

    const selectedNodes = unref(flowActionType?.getSelectedNodes);
    const selectedEdges = unref(flowActionType?.getSelectedEdges);
    const { firstNodeIndex, connections, nodes, outputEdgesId, inputEdgesId } =
      transformToRuleChain(selectedNodes, selectedEdges);
    await saveRuleChainData({
      firstNodeIndex,
      connections: connections.filter(
        (connection) => !isNullOrUnDef(connection.fromIndex) && !isNullOrUnDef(connection.toIndex)
      ),
      nodes,
      ruleChainId: ruleChainDetail.id,
    });

    const outputEdges = outputEdgesId.map((id) => flowActionType?.findEdge(id));
    const inputEdges = inputEdgesId.map((id) => flowActionType?.findEdge(id));

    const { originX, originY } = getElementsCenter(unref(selectedNodes));

    flowActionType?.removeNodes(selectedNodes || []);
    flowActionType?.removeEdges(selectedEdges || []);

    const { nodes: newNode, edges: newEdges } = createRuleChainNode({
      originX,
      originY,
      outputEdges,
      inputEdges,
      ruleChainDetail,
    });

    flowActionType?.addNodes(newNode);
    flowActionType?.addEdges(newEdges);
    useSaveAndRedoActionType?.triggerChange();
  };

  const handleContextMenuAction = (params: HandleContextMenuActionParamsType) => {
    const { menuType } = params;

    const handlerMapping = {
      [RuleContextMenuEnum.COPY]: copy,
      [RuleContextMenuEnum.PASTE]: paste,
      [RuleContextMenuEnum.SELECT_ALL]: selectAll,
      [RuleContextMenuEnum.UNSELECTED]: unselect,
      [RuleContextMenuEnum.DELETE]: deleteElement,
      [RuleContextMenuEnum.DELETE_SELECT]: deleteElements,
      [RuleContextMenuEnum.SELECT_COPY]: selectCopy,
      [RuleContextMenuEnum.APPLY_CHANGE]: applyChange,
      [RuleContextMenuEnum.UNDO_CHANGE]: undoChange,
      [RuleContextMenuEnum.CREATE_RULE_CHAIN]: createRuleChain,
    };

    if (handlerMapping[menuType]) {
      handlerMapping[menuType]?.(params);
    }
  };

  return {
    handleContextMenuAction,
  };
}