useBasicDataTransform.ts 9.12 KB
import { Ref, toRaw, unref } from 'vue';
import { BasicNodeBindData, EdgeData, NodeData } from '../types/node';
import { Elements, GraphEdge, GraphNode } from '@vue-flow/core';
import { ConnectionItemType, RuleChainType } from '../types/ruleNode';
import { allComponents } from '../packages';
import { RuleNodeTypeEnum } from '../packages/index.type';
import { buildUUID } from '/@/utils/uuid';
import { isNullOrUnDef } from '/@/utils/is';
import { useAddNodes } from './useAddNodes';
import { useAddEdges } from './useAddEdges';
import { RuleChainEntityType } from '../enum/entity';
import { EntryCategoryComponentEnum } from '../enum/category';

const ignoreNodeKeys: string[] = [EntryCategoryComponentEnum.INPUT];

export const SOURCE_HANDLE = '__handle-right';
export const TARGET_HANDLE = '__handle-left';

export function useBasicDataTransform() {
  const nodeConfigMap = new Map<string, NodeData>();

  function initNodeConfigMap() {
    for (const key of Object.keys(allComponents)) {
      const category = allComponents[key as RuleNodeTypeEnum];
      for (const nodeConfig of category.components) {
        const { clazz } = nodeConfig;
        nodeConfigMap.set(clazz, { config: nodeConfig, categoryConfig: category.category });
      }
    }
  }

  function mergeData(data: NodeData['data'], nodeData: NodeData, node: GraphNode) {
    const { x: layoutX, y: layoutY } = node.position;

    return {
      debugMode: !!data?.debugMode,
      name: data?.name,
      type: nodeData.config?.clazz,
      configuration: toRaw(unref(data?.configuration)),
      additionalInfo: {
        description: data?.description,
        layoutX,
        layoutY,
      },
    };
  }

  function deconstructionConnection(
    ruleChain: Ref<RuleChainType> | RuleChainType,
    inputNodeId: string
  ) {
    const { connections, nodes, firstNodeIndex } = unref(ruleChain);
    const indexMap = new Map<number, BasicNodeBindData>();
    const groupByConnections = new Map<string, string[]>();

    const SEPARATOR = ',';
    const edges: Elements = [];
    const { getAddedgesParams } = useAddEdges();

    nodes.forEach((item, index) => indexMap.set(index, item));

    (connections || []).forEach((item) => {
      const { fromIndex, toIndex, type } = item;
      const key = `${fromIndex}${SEPARATOR}${toIndex}`;
      if (!groupByConnections.has(key)) groupByConnections.set(key, []);

      const types = groupByConnections.get(key)!;
      types.push(type);
    });

    for (const [key, types] of Array.from(groupByConnections.entries())) {
      const [fromIndex, toIndex] = key.split(SEPARATOR);

      const sourceNode = indexMap.get(Number(fromIndex));
      const targetNode = indexMap.get(Number(toIndex));
      const source = sourceNode?.id?.id || buildUUID();
      const target = targetNode?.id?.id || buildUUID();
      const sourceHandle = `${source}${SOURCE_HANDLE}`;
      const targetHandle = `${target}${TARGET_HANDLE}`;

      edges.push(
        getAddedgesParams(
          {
            source,
            target,
            sourceHandle,
            targetHandle,
          },
          { type: types }
        )
      );
    }

    if (!isNullOrUnDef(firstNodeIndex)) {
      const targetNode = indexMap.get(firstNodeIndex);
      const source = inputNodeId;
      const target = targetNode?.id?.id || buildUUID();
      const sourceHandle = `${source}${SOURCE_HANDLE}`;
      const targetHandle = `${target}${TARGET_HANDLE}`;
      edges.push(
        getAddedgesParams(
          {
            source,
            target,
            sourceHandle,
            targetHandle,
          },
          {}
        )
      );
    }

    return edges;
  }

  function genNewNodeByData(node: BasicNodeBindData, config: NodeData) {
    const { additionalInfo, configuration, debugMode, name, id } = node;
    const { layoutX, layoutY, description } = additionalInfo || {};
    const { getAddNodesParams } = useAddNodes();

    const value = getAddNodesParams(
      { x: layoutX!, y: layoutY! },
      {
        ...config,
        data: {
          configuration,
          debugMode,
          description,
          name,
        },
        created: !!id?.id,
      },
      {
        id: id?.id || buildUUID(),
      }
    );

    if (!id?.id) node.id = { id: value.id, entityType: RuleChainEntityType.RULE_NODE };

    return value;
  }

  function deconstructionNode(nodes: RuleChainType['nodes']) {
    const addNodes: Elements = [];
    for (const node of unref(nodes)) {
      const { type } = node;
      if (!type) continue;
      const nodeConfig = nodeConfigMap.get(type);

      if (!nodeConfig) {
        throw `No component configuration of type '${type}' was found`;
      }

      const newNode = genNewNodeByData(node, nodeConfig);
      newNode.data.isSaved = true;

      addNodes.push(newNode);
    }
    return addNodes;
  }

  function deconstructionData(
    ruleChain: RuleChainType | Ref<RuleChainType | undefined>,
    inputNodeId: string
  ) {
    if (!ruleChain || !unref(ruleChain)) return;
    ruleChain = toRaw(unref(ruleChain))!;

    const nodes = deconstructionNode(ruleChain?.nodes || []);

    const edges = deconstructionConnection(ruleChain!, inputNodeId);

    return {
      nodes,
      edges,
    };
  }

  initNodeConfigMap();

  /**
   * @description 保存连接信息
   */
  function getConnections(
    nodesRef: Ref<GraphNode[]> | GraphNode[],
    edges: Ref<GraphEdge[]> | GraphEdge[]
  ) {
    const nodeIndexMap = new Map();

    const connections: ConnectionItemType[] = [];

    unref(nodesRef).forEach((item, index) => {
      nodeIndexMap.set(item.id, index);
    });

    for (const item of unref(edges)) {
      const { data, target, source } = item;
      const { data: bindData } = data as EdgeData;
      const { type } = bindData || {};
      const fromIndex = nodeIndexMap.get(source);
      const toIndex = nodeIndexMap.get(target);
      type?.forEach((key) => {
        connections.push({ fromIndex, toIndex, type: key });
      });
    }

    return connections;
  }

  function getNodes(nodesRef: Ref<GraphNode[]> | GraphNode[]) {
    const nodes: BasicNodeBindData[] = [];

    let offsetX = 0;
    let offsetY = 0;
    for (const node of unref(nodesRef)) {
      const nodeData = node.data as NodeData;

      if (ignoreNodeKeys.includes(nodeData.config?.key as string)) continue;

      const data = nodeData.data;

      const resultNode = Object.assign(
        mergeData(data, nodeData, node),
        nodeData?.isSaved
          ? ({
              id: { id: node.id, entityType: RuleChainEntityType.RULE_NODE },
            } as BasicNodeBindData)
          : {}
      );

      if (resultNode.additionalInfo.layoutX < offsetX) offsetX = resultNode.additionalInfo.layoutX;
      if (resultNode.additionalInfo.layoutY < offsetY) offsetY = resultNode.additionalInfo.layoutY;
      nodes.push(resultNode);
    }

    /**
     * compatible thingsboard rule chain designer.
     * thingsboard rule chain designer does not have negative coordinated.
     */
    if (offsetX < 0 || offsetY < 0) {
      nodes.forEach((node) => {
        const { layoutX = 0, layoutY = 0 } = node.additionalInfo || {};
        node.additionalInfo!.layoutX = layoutX + Math.abs(offsetX);
        node.additionalInfo!.layoutY = layoutY + Math.abs(offsetY);
      });
    }

    return nodes;
  }

  function getFirsetNodeIndex(
    nodesRef: Ref<GraphNode[]> | GraphNode[],
    edges: Ref<GraphEdge[]> | GraphEdge[]
  ) {
    const inputNode = unref(edges).find(
      (item) => (item.sourceNode.data as NodeData).config?.key === EntryCategoryComponentEnum.INPUT
    );

    if (inputNode) {
      const targetId = inputNode.target;
      const index = unref(nodesRef).findIndex((item) => item.id === targetId);
      return index;
    }
  }

  function combineData(
    nodesRef: Ref<GraphNode[]> | GraphNode[] = [],
    edgesRef: Ref<GraphEdge[]> | GraphEdge[] = []
  ) {
    const extraIgnoreNodeRef = unref(nodesRef).filter(
      (item) => !ignoreNodeKeys.includes((item.data as NodeData).config?.key as string)
    );

    const connections = getConnections(extraIgnoreNodeRef, edgesRef);

    const nodes = getNodes(extraIgnoreNodeRef);

    const firstNodeIndex = getFirsetNodeIndex(extraIgnoreNodeRef, edgesRef);

    return { connections, nodes, firstNodeIndex };
  }

  function validateCanCreateRuleChain(
    nodes: Ref<GraphNode[]> | GraphNode[] = [],
    edges: Ref<GraphEdge[]> | GraphEdge[] = []
  ) {
    let flag = true;

    if (unref(nodes).length <= 1) flag = false;

    const rootNode: GraphNode[] = [];

    for (const edge of unref(edges)) {
      if (!unref(nodes).find((node) => node.id === edge.source)) rootNode.push(edge.targetNode);
    }

    for (const node of unref(nodes)) {
      if (!unref(edges).some((edge) => edge.target === node.id)) rootNode.push(node);
    }

    if (rootNode.length > 1) return { flag: false, firstNode: null };

    for (const node of unref(nodes)) {
      const list = unref(edges).filter(
        (edge) => edge.source === node.id || edge.target === node.id
      );

      if (!list.length) {
        flag = false;
        break;
      }
    }

    return { flag, firstNode: rootNode[0] || null };
  }

  return {
    mergeData,
    combineData,
    deconstructionData,
    validateCanCreateRuleChain,
  };
}