feat(graph-collection-manager): display collections on demand (#2583)
* refactor: graph collection support on demand rendering collection * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: code improve * refactor: support connect parent or child * refactor: code improve * refactor: code improve * style: style improve * style: style improve * refactor: code improve * refactor: code improve * refactor: code improve
This commit is contained in:
		
							parent
							
								
									d6a2ab4b61
								
							
						
					
					
						commit
						eeb3adf928
					
				@ -282,7 +282,6 @@ export const AddFieldAction = (props) => {
 | 
			
		||||
      items,
 | 
			
		||||
    };
 | 
			
		||||
  }, [getInterface, items, record]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    record.template !== 'view' && (
 | 
			
		||||
      <RecordProvider record={record}>
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,13 @@
 | 
			
		||||
import {
 | 
			
		||||
  ApartmentOutlined,
 | 
			
		||||
  FullscreenExitOutlined,
 | 
			
		||||
  FullscreenOutlined,
 | 
			
		||||
  LineHeightOutlined,
 | 
			
		||||
  MenuOutlined,
 | 
			
		||||
  ShareAltOutlined,
 | 
			
		||||
} from '@ant-design/icons';
 | 
			
		||||
import { ApartmentOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Graph } from '@antv/x6';
 | 
			
		||||
import { MiniMap } from '@antv/x6-plugin-minimap';
 | 
			
		||||
import { Scroller } from '@antv/x6-plugin-scroller';
 | 
			
		||||
import { Selection } from '@antv/x6-plugin-selection';
 | 
			
		||||
import { Snapline } from '@antv/x6-plugin-snapline';
 | 
			
		||||
import { register } from '@antv/x6-react-shape';
 | 
			
		||||
import { css, cx } from '@emotion/css';
 | 
			
		||||
import { cx } from '@emotion/css';
 | 
			
		||||
import { SchemaOptionsContext } from '@formily/react';
 | 
			
		||||
import { useSearchParams } from 'react-router-dom';
 | 
			
		||||
import {
 | 
			
		||||
  APIClientProvider,
 | 
			
		||||
  CollectionCategroriesContext,
 | 
			
		||||
@ -21,7 +15,6 @@ import {
 | 
			
		||||
  CollectionManagerContext,
 | 
			
		||||
  CollectionManagerProvider,
 | 
			
		||||
  CurrentAppInfoContext,
 | 
			
		||||
  Popover,
 | 
			
		||||
  SchemaComponent,
 | 
			
		||||
  SchemaComponentOptions,
 | 
			
		||||
  Select,
 | 
			
		||||
@ -32,10 +25,9 @@ import {
 | 
			
		||||
  useCurrentAppInfo,
 | 
			
		||||
  useGlobalTheme,
 | 
			
		||||
} from '@nocobase/client';
 | 
			
		||||
import { useFullscreen } from 'ahooks';
 | 
			
		||||
import { App, Button, ConfigProvider, Input, Layout, Menu, Spin, Switch, Tooltip } from 'antd';
 | 
			
		||||
import dagre from 'dagre';
 | 
			
		||||
import lodash from 'lodash';
 | 
			
		||||
import { Button, ConfigProvider, Layout, Switch, Tooltip, App, Spin } from 'antd';
 | 
			
		||||
import dagre from 'dagre';
 | 
			
		||||
import React, { createContext, forwardRef, useContext, useEffect, useLayoutEffect, useState } from 'react';
 | 
			
		||||
import { useAsyncDataSource, useCreateActionAndRefreshCM } from './action-hooks';
 | 
			
		||||
import { AddCollectionAction } from './components/AddCollectionAction';
 | 
			
		||||
@ -51,7 +43,12 @@ import {
 | 
			
		||||
  getPopupContainer,
 | 
			
		||||
  useGCMTranslation,
 | 
			
		||||
} from './utils';
 | 
			
		||||
const { drop, groupBy, last, maxBy, minBy, take } = lodash;
 | 
			
		||||
import { LocateCollectionAction } from './components/LocateCollectionAction';
 | 
			
		||||
import { SelectCollectionsAction } from './components/SelectCollectionsAction';
 | 
			
		||||
import { DirectionAction } from './components/DirectionAction';
 | 
			
		||||
import { ConnectorAction } from './components/ConnectorAction';
 | 
			
		||||
import { FullscreenAction } from './components/FullScreenAction';
 | 
			
		||||
const { drop, groupBy, last, maxBy, minBy, take, uniq } = lodash;
 | 
			
		||||
 | 
			
		||||
const LINE_HEIGHT = 40;
 | 
			
		||||
const NODE_WIDTH = 250;
 | 
			
		||||
@ -72,7 +69,7 @@ export enum ConnectionType {
 | 
			
		||||
}
 | 
			
		||||
const getGridData = (num, arr) => {
 | 
			
		||||
  const newArr = [];
 | 
			
		||||
  while (arr.length > 0 && num) {
 | 
			
		||||
  while (arr?.length > 0 && num) {
 | 
			
		||||
    newArr.push(arr.splice(0, num));
 | 
			
		||||
  }
 | 
			
		||||
  return newArr;
 | 
			
		||||
@ -105,7 +102,7 @@ async function layout(createPositions) {
 | 
			
		||||
              return v.collectionName === node.store.data.name;
 | 
			
		||||
            })) ||
 | 
			
		||||
          {};
 | 
			
		||||
        const calculatedPosition = { x: col * 325 + 50, y: row * 400 + 60 };
 | 
			
		||||
        const calculatedPosition = { x: col * 325 + 50, y: row * 400 + 100 };
 | 
			
		||||
        node.position(targetPosition.x || calculatedPosition.x, targetPosition.y || calculatedPosition.y);
 | 
			
		||||
        if (positions && !positions.find((v) => v.collectionName === node.store.data.name)) {
 | 
			
		||||
          // 位置表中没有的表都自动保存
 | 
			
		||||
@ -142,8 +139,8 @@ function optimizeEdge(edge) {
 | 
			
		||||
  } = edge;
 | 
			
		||||
  const source = edge.getSource();
 | 
			
		||||
  const target = edge.getTarget();
 | 
			
		||||
  const sorceNodeX = targetGraph.getCellById(source.cell).position().x;
 | 
			
		||||
  const targeNodeX = targetGraph.getCellById(target.cell).position().x;
 | 
			
		||||
  const sorceNodeX = targetGraph.getCellById(source.cell)?.position().x;
 | 
			
		||||
  const targeNodeX = targetGraph.getCellById(target.cell)?.position().x;
 | 
			
		||||
  const leftAnchor = connectionType
 | 
			
		||||
    ? {
 | 
			
		||||
        name: 'topLeft',
 | 
			
		||||
@ -220,7 +217,7 @@ function optimizeEdge(edge) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CollapsedContext = createContext<any>({});
 | 
			
		||||
export const CollapsedContext = createContext<any>({});
 | 
			
		||||
const formatNodeData = () => {
 | 
			
		||||
  const layoutNodes = [];
 | 
			
		||||
  const edges = targetGraph.getEdges();
 | 
			
		||||
@ -239,10 +236,10 @@ const formatNodeData = () => {
 | 
			
		||||
  return nodeGroup;
 | 
			
		||||
};
 | 
			
		||||
//自动布局
 | 
			
		||||
const handelResetLayout = () => {
 | 
			
		||||
  const { linkNodes = [], rawNodes } = formatNodeData();
 | 
			
		||||
const handelResetLayout = (isTemporaryLayout?) => {
 | 
			
		||||
  const { linkNodes = [], rawNodes = [] } = formatNodeData();
 | 
			
		||||
  const { positions } = targetGraph;
 | 
			
		||||
  const nodes = linkNodes.concat(rawNodes);
 | 
			
		||||
  const nodes = linkNodes.concat(rawNodes || []);
 | 
			
		||||
  const edges = targetGraph.getEdges();
 | 
			
		||||
  const g = new dagre.graphlib.Graph();
 | 
			
		||||
  let alternateNum;
 | 
			
		||||
@ -342,12 +339,16 @@ const handelResetLayout = () => {
 | 
			
		||||
    optimizeEdge(edge);
 | 
			
		||||
  });
 | 
			
		||||
  targetGraph.positionCell(nodes[0], 'top-left', { padding: 100 });
 | 
			
		||||
  if (!isTemporaryLayout) {
 | 
			
		||||
    targetGraph.updatePositionAction(updatePositionData, true);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
  const { theme } = useGlobalTheme();
 | 
			
		||||
  const { styles } = useStyles();
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  const selectedCollections = searchParams.get('collections');
 | 
			
		||||
  const options = useContext(SchemaOptionsContext);
 | 
			
		||||
  const ctx = useContext(CollectionManagerContext);
 | 
			
		||||
  const api = useAPIClient();
 | 
			
		||||
@ -364,11 +365,13 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
  const categoryCtx = useContext(CollectionCategroriesContext);
 | 
			
		||||
  const scope = { ...options?.scope };
 | 
			
		||||
  const components = { ...options?.components };
 | 
			
		||||
 | 
			
		||||
  const saveGraphPositionAction = async (data) => {
 | 
			
		||||
    await api.resource('graphPositions').create({ values: data });
 | 
			
		||||
    await refreshPositions();
 | 
			
		||||
  };
 | 
			
		||||
  const updatePositionAction = async (data, isbatch = false) => {
 | 
			
		||||
    if (!selectedCollections) {
 | 
			
		||||
      if (isbatch) {
 | 
			
		||||
        await api.resource('graphPositions').update({
 | 
			
		||||
          values: data,
 | 
			
		||||
@ -380,6 +383,7 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      await refreshPositions();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const refreshPositions = async () => {
 | 
			
		||||
    const { data } = await api.resource('graphPositions').list({ paginate: false });
 | 
			
		||||
@ -400,7 +404,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    setCollectionData(data);
 | 
			
		||||
    setCollectionList(data);
 | 
			
		||||
    if (!currentNodes.length) {
 | 
			
		||||
      if (!selectedCollections) {
 | 
			
		||||
        renderInitGraphCollection(data);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      renderDiffGraphCollection(data);
 | 
			
		||||
    }
 | 
			
		||||
@ -428,6 +434,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    targetGraph.connectionType = ConnectionType.Both;
 | 
			
		||||
    targetGraph.direction = DirectionType.Target;
 | 
			
		||||
    targetGraph.cacheCollection = {};
 | 
			
		||||
    targetGraph.onConnectionAssociation = handleConnectionAssociation;
 | 
			
		||||
    targetGraph.onConnectionChilds = handleConnectionChilds;
 | 
			
		||||
    targetGraph.onConnectionParents = handleConnectionParents;
 | 
			
		||||
    Graph.registerPortLayout(
 | 
			
		||||
      'erPortPosition',
 | 
			
		||||
      (portsPositionArgs) => {
 | 
			
		||||
@ -503,16 +512,19 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    });
 | 
			
		||||
    targetGraph.use(
 | 
			
		||||
      new Scroller({
 | 
			
		||||
        autoResize: true,
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        pannable: true,
 | 
			
		||||
        padding: { top: 0, left: 500, right: 300, bottom: 400 },
 | 
			
		||||
        pageVisible: true,
 | 
			
		||||
        pageBreak: false,
 | 
			
		||||
        padding: { top: 10, left: 500, right: 300, bottom: 300 },
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    targetGraph.use(
 | 
			
		||||
      new MiniMap({
 | 
			
		||||
        container: document.getElementById('graph-minimap'),
 | 
			
		||||
        width: 300,
 | 
			
		||||
        height: 250,
 | 
			
		||||
        height: 200,
 | 
			
		||||
        padding: 10,
 | 
			
		||||
        graphOptions: {
 | 
			
		||||
          async: true,
 | 
			
		||||
@ -546,6 +558,10 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      handleEdgeUnActive(targetEdge);
 | 
			
		||||
    });
 | 
			
		||||
    targetGraph.on('node:mouseleave', ({ e, node }) => {
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      node.setProp({ actived: false });
 | 
			
		||||
    });
 | 
			
		||||
    targetGraph.on('node:moved', ({ e, node }) => {
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      const connectEdges = targetGraph.getConnectedEdges(node);
 | 
			
		||||
@ -567,9 +583,13 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
        optimizeEdge(edge);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    targetGraph.on('cell:mouseenter', ({ e, cell, edge }) => {
 | 
			
		||||
    targetGraph.on('cell:mouseenter', ({ e, cell, edge, node }) => {
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      cell.toFront();
 | 
			
		||||
      if (node) {
 | 
			
		||||
        cell.setProp({ actived: true });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (edge) {
 | 
			
		||||
        handleEdgeActive(edge);
 | 
			
		||||
      }
 | 
			
		||||
@ -580,7 +600,7 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
      }
 | 
			
		||||
      targetGraph.collapseNodes?.map((v) => {
 | 
			
		||||
        const node = targetGraph.getCellById(Object.keys(v)[0]);
 | 
			
		||||
        Object.values(v)[0] && node.setData({ collapse: false });
 | 
			
		||||
        Object.values(v)[0] && node?.setData({ collapse: false });
 | 
			
		||||
      });
 | 
			
		||||
      targetGraph.cleanSelection();
 | 
			
		||||
    });
 | 
			
		||||
@ -591,6 +611,30 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
      node.setProp({ select: false });
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  const handleConnectionAssociation = ({ target, through }) => {
 | 
			
		||||
    const data = targetGraph.selectedCollections.split(',') || [];
 | 
			
		||||
    data.push(target);
 | 
			
		||||
    through && data.push(through);
 | 
			
		||||
    const queryString = uniq(data).toString();
 | 
			
		||||
    setSearchParams([['collections', queryString]]);
 | 
			
		||||
    targetGraph.selectedCollections = queryString;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleConnectionChilds = (collections) => {
 | 
			
		||||
    let data = targetGraph.selectedCollections.split(',') || [];
 | 
			
		||||
    data = data.concat(collections);
 | 
			
		||||
    const queryString = uniq(data).toString();
 | 
			
		||||
    setSearchParams([['collections', queryString]]);
 | 
			
		||||
    targetGraph.selectedCollections = queryString;
 | 
			
		||||
  };
 | 
			
		||||
  const handleConnectionParents = (collections) => {
 | 
			
		||||
    console.log(collections);
 | 
			
		||||
    let data = targetGraph.selectedCollections.split(',') || [];
 | 
			
		||||
    data = data.concat(collections);
 | 
			
		||||
    const queryString = uniq(data).toString();
 | 
			
		||||
    setSearchParams([['collections', queryString]]);
 | 
			
		||||
    targetGraph.selectedCollections = queryString;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleEdgeUnActive = (targetEdge) => {
 | 
			
		||||
    targetGraph.activeEdge = null;
 | 
			
		||||
@ -680,11 +724,14 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    lightUp(targetEdge);
 | 
			
		||||
    m2mEdge && lightUp(m2mEdge);
 | 
			
		||||
  };
 | 
			
		||||
  // 首次渲染
 | 
			
		||||
  // 全量渲染
 | 
			
		||||
  const renderInitGraphCollection = (rawData) => {
 | 
			
		||||
    targetGraph.clearCells();
 | 
			
		||||
    const { nodesData, edgesData, inheritEdges } = formatData(rawData);
 | 
			
		||||
    targetGraph.data = { nodes: nodesData, edges: edgesData };
 | 
			
		||||
    targetGraph.fromJSON({ nodes: nodesData, edges: inheritEdges.concat(edgesData) });
 | 
			
		||||
    targetGraph.fromJSON({ nodes: nodesData });
 | 
			
		||||
    targetGraph.addEdges(edgesData);
 | 
			
		||||
    targetGraph.addEdges(inheritEdges);
 | 
			
		||||
    layout(saveGraphPositionAction);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -704,8 +751,8 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    const diffNodes = getDiffNode(nodesData, currentNodes);
 | 
			
		||||
    const diffEdges = getDiffEdge(edgesData, currentEdgesGroup.currentRelateEdges || []);
 | 
			
		||||
    const diffInheritEdge = getDiffEdge(inheritEdges, currentEdgesGroup.currentInheritEdges || []);
 | 
			
		||||
    const maxY = maxBy(positions, 'y').y;
 | 
			
		||||
    const minX = minBy(positions, 'x').x;
 | 
			
		||||
    const maxY = maxBy(positions, 'y')?.y;
 | 
			
		||||
    const minX = minBy(positions, 'x')?.x;
 | 
			
		||||
    const yNodes = positions.filter((v) => {
 | 
			
		||||
      return Math.abs(v.y - maxY) < 100;
 | 
			
		||||
    });
 | 
			
		||||
@ -726,10 +773,12 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
            ...node,
 | 
			
		||||
            position,
 | 
			
		||||
          });
 | 
			
		||||
          if (!selectedCollections) {
 | 
			
		||||
            saveGraphPositionAction({
 | 
			
		||||
              collectionName: node.name,
 | 
			
		||||
              ...position,
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
          targetGraph && targetGraph.positionCell(targetNode, 'top', { padding: 200 });
 | 
			
		||||
          break;
 | 
			
		||||
        case 'insertPort':
 | 
			
		||||
@ -947,11 +996,16 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
        };
 | 
			
		||||
      }, 0);
 | 
			
		||||
    } else {
 | 
			
		||||
      targetGraph.filterConfig = null;
 | 
			
		||||
      handleCleanHighlight();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  //切换连线类型
 | 
			
		||||
  const handleSetRelationshipType = (type) => {
 | 
			
		||||
    targetGraph.connectionType = type;
 | 
			
		||||
    const { filterConfig } = targetGraph;
 | 
			
		||||
    filterConfig && handleFiterCollections(filterConfig.key);
 | 
			
		||||
    handleSetEdgeVisible(type);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -982,6 +1036,14 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleDirectionChange = (direction) => {
 | 
			
		||||
    targetGraph.direction = direction;
 | 
			
		||||
    const { filterConfig } = targetGraph;
 | 
			
		||||
    if (filterConfig) {
 | 
			
		||||
      handleFiterCollections(filterConfig.key);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useLayoutEffect(() => {
 | 
			
		||||
    initGraphCollections();
 | 
			
		||||
    return () => {
 | 
			
		||||
@ -1007,6 +1069,18 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (selectedCollections && collectionList.length) {
 | 
			
		||||
      const selectKeys = selectedCollections?.split(',');
 | 
			
		||||
      const data = collectionList.filter((v) => selectKeys.includes(v.name));
 | 
			
		||||
      renderInitGraphCollection(data);
 | 
			
		||||
      handelResetLayout(true);
 | 
			
		||||
      targetGraph.selectedCollections = selectedCollections;
 | 
			
		||||
    } else {
 | 
			
		||||
      !selectedCollections && renderInitGraphCollection(collections);
 | 
			
		||||
    }
 | 
			
		||||
  }, [searchParams, collectionList]);
 | 
			
		||||
 | 
			
		||||
  const loadCollections = async () => {
 | 
			
		||||
    return targetGraph.collections?.map((collection: any) => ({
 | 
			
		||||
      label: compile(collection.title),
 | 
			
		||||
@ -1026,6 +1100,11 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
                      <Select popupMatchSelectWidth={false} {...props} getPopupContainer={getPopupContainer} />
 | 
			
		||||
                    ),
 | 
			
		||||
                    AddCollectionAction,
 | 
			
		||||
                    LocateCollectionAction,
 | 
			
		||||
                    SelectCollectionsAction,
 | 
			
		||||
                    DirectionAction,
 | 
			
		||||
                    ConnectorAction,
 | 
			
		||||
                    FullscreenAction,
 | 
			
		||||
                  }}
 | 
			
		||||
                  schema={{
 | 
			
		||||
                    type: 'void',
 | 
			
		||||
@ -1050,6 +1129,10 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
                          },
 | 
			
		||||
                        },
 | 
			
		||||
                        properties: {
 | 
			
		||||
                          selectCollections: {
 | 
			
		||||
                            type: 'array',
 | 
			
		||||
                            'x-component': 'SelectCollectionsAction',
 | 
			
		||||
                          },
 | 
			
		||||
                          actions: {
 | 
			
		||||
                            type: 'void',
 | 
			
		||||
                            'x-component': 'ActionBar',
 | 
			
		||||
@ -1069,96 +1152,13 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
                              },
 | 
			
		||||
                              fullScreen: {
 | 
			
		||||
                                type: 'void',
 | 
			
		||||
                                'x-component': 'Action',
 | 
			
		||||
                                'x-component-props': {
 | 
			
		||||
                                  component: forwardRef(() => {
 | 
			
		||||
                                    const [isFullscreen, { toggleFullscreen }] = useFullscreen(
 | 
			
		||||
                                      document.getElementById('graph_container'),
 | 
			
		||||
                                    );
 | 
			
		||||
                                    return (
 | 
			
		||||
                                      <Tooltip title={t('Full Screen')} getPopupContainer={getPopupContainer}>
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                          onClick={() => {
 | 
			
		||||
                                            toggleFullscreen();
 | 
			
		||||
                                          }}
 | 
			
		||||
                                        >
 | 
			
		||||
                                          {isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                      </Tooltip>
 | 
			
		||||
                                    );
 | 
			
		||||
                                  }),
 | 
			
		||||
                                  useAction: () => {
 | 
			
		||||
                                    return {
 | 
			
		||||
                                      run() {},
 | 
			
		||||
                                    };
 | 
			
		||||
                                'x-component': 'FullscreenAction',
 | 
			
		||||
                              },
 | 
			
		||||
                                },
 | 
			
		||||
                              },
 | 
			
		||||
                              collectionList: {
 | 
			
		||||
                              locateCollection: {
 | 
			
		||||
                                type: 'void',
 | 
			
		||||
                                'x-component': function Com() {
 | 
			
		||||
                                  const { handleSearchCollection, collectionList } = useContext(CollapsedContext);
 | 
			
		||||
                                  const [selectedKeys, setSelectKey] = useState([]);
 | 
			
		||||
                                  const content = (
 | 
			
		||||
                                    <div>
 | 
			
		||||
                                      <Input
 | 
			
		||||
                                        style={{ margin: '4px 0' }}
 | 
			
		||||
                                        bordered={false}
 | 
			
		||||
                                        placeholder={t('Collection Search')}
 | 
			
		||||
                                        onChange={handleSearchCollection}
 | 
			
		||||
                                      />
 | 
			
		||||
                                      <Menu
 | 
			
		||||
                                        selectedKeys={selectedKeys}
 | 
			
		||||
                                        selectable={true}
 | 
			
		||||
                                        className={css`
 | 
			
		||||
                                          .ant-menu-item {
 | 
			
		||||
                                            height: 32px;
 | 
			
		||||
                                            line-height: 32px;
 | 
			
		||||
                                          }
 | 
			
		||||
                                        `}
 | 
			
		||||
                                        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
                                        items={[
 | 
			
		||||
                                          { type: 'divider' },
 | 
			
		||||
                                          ...collectionList.map((v) => {
 | 
			
		||||
                                            return {
 | 
			
		||||
                                              key: v.name,
 | 
			
		||||
                                              label: compile(v.title),
 | 
			
		||||
                                              onClick: (e: any) => {
 | 
			
		||||
                                                if (e.key !== selectedKeys[0]) {
 | 
			
		||||
                                                  setSelectKey([e.key]);
 | 
			
		||||
                                                  handleFiterCollections(e.key);
 | 
			
		||||
                                                } else {
 | 
			
		||||
                                                  targetGraph.filterConfig = null;
 | 
			
		||||
                                                  handleFiterCollections(false);
 | 
			
		||||
                                                  setSelectKey([]);
 | 
			
		||||
                                                }
 | 
			
		||||
                                              },
 | 
			
		||||
                                            };
 | 
			
		||||
                                          }),
 | 
			
		||||
                                        ]}
 | 
			
		||||
                                      />
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                  );
 | 
			
		||||
                                  return (
 | 
			
		||||
                                    <Popover
 | 
			
		||||
                                      content={content}
 | 
			
		||||
                                      autoAdjustOverflow
 | 
			
		||||
                                      placement="bottomRight"
 | 
			
		||||
                                      trigger={['click']}
 | 
			
		||||
                                      getPopupContainer={getPopupContainer}
 | 
			
		||||
                                      overlayClassName={css`
 | 
			
		||||
                                        .ant-popover-inner-content {
 | 
			
		||||
                                          padding: 0;
 | 
			
		||||
                                        }
 | 
			
		||||
                                      `}
 | 
			
		||||
                                    >
 | 
			
		||||
                                      <Button>
 | 
			
		||||
                                        <MenuOutlined />
 | 
			
		||||
                                      </Button>
 | 
			
		||||
                                    </Popover>
 | 
			
		||||
                                  );
 | 
			
		||||
                                },
 | 
			
		||||
                                'x-component': 'LocateCollectionAction',
 | 
			
		||||
                                'x-component-props': {
 | 
			
		||||
                                  handleFiterCollections,
 | 
			
		||||
                                  icon: 'MenuOutlined',
 | 
			
		||||
                                  useAction: () => {
 | 
			
		||||
                                    return {
 | 
			
		||||
@ -1193,71 +1193,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
                              },
 | 
			
		||||
                              connectionType: {
 | 
			
		||||
                                type: 'void',
 | 
			
		||||
                                'x-component': () => {
 | 
			
		||||
                                  const menuItems = [
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: ConnectionType.Both,
 | 
			
		||||
                                      label: 'All relationships',
 | 
			
		||||
                                    },
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: ConnectionType.Entity,
 | 
			
		||||
                                      label: 'Entity relationship only',
 | 
			
		||||
                                    },
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: ConnectionType.Inherit,
 | 
			
		||||
                                      label: 'Inheritance relationship only',
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ];
 | 
			
		||||
                                  const content = (
 | 
			
		||||
                                    <div>
 | 
			
		||||
                                      <Menu
 | 
			
		||||
                                        defaultSelectedKeys={[ConnectionType.Both]}
 | 
			
		||||
                                        selectable={true}
 | 
			
		||||
                                        className={css`
 | 
			
		||||
                                          .ant-menu-item {
 | 
			
		||||
                                            height: 32px;
 | 
			
		||||
                                            line-height: 32px;
 | 
			
		||||
                                          }
 | 
			
		||||
                                        `}
 | 
			
		||||
                                        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
                                        items={[
 | 
			
		||||
                                          { type: 'divider' },
 | 
			
		||||
                                          ...menuItems.map((v) => {
 | 
			
		||||
                                            return {
 | 
			
		||||
                                              key: v.key,
 | 
			
		||||
                                              label: t(v.label),
 | 
			
		||||
                                              onClick: (e: any) => {
 | 
			
		||||
                                                targetGraph.connectionType = v.key;
 | 
			
		||||
                                                const { filterConfig } = targetGraph;
 | 
			
		||||
                                                filterConfig && handleFiterCollections(filterConfig.key);
 | 
			
		||||
                                                handleSetRelationshipType(v.key);
 | 
			
		||||
                                              },
 | 
			
		||||
                                            };
 | 
			
		||||
                                          }),
 | 
			
		||||
                                        ]}
 | 
			
		||||
                                      />
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                  );
 | 
			
		||||
                                  return (
 | 
			
		||||
                                    <Popover
 | 
			
		||||
                                      content={content}
 | 
			
		||||
                                      autoAdjustOverflow
 | 
			
		||||
                                      placement="bottomRight"
 | 
			
		||||
                                      trigger={['click']}
 | 
			
		||||
                                      getPopupContainer={getPopupContainer}
 | 
			
		||||
                                      overlayClassName={css`
 | 
			
		||||
                                        .ant-popover-inner-content {
 | 
			
		||||
                                          padding: 0;
 | 
			
		||||
                                        }
 | 
			
		||||
                                      `}
 | 
			
		||||
                                    >
 | 
			
		||||
                                      <Button>
 | 
			
		||||
                                        <ShareAltOutlined />
 | 
			
		||||
                                      </Button>
 | 
			
		||||
                                    </Popover>
 | 
			
		||||
                                  );
 | 
			
		||||
                                },
 | 
			
		||||
                                'x-component': 'ConnectorAction',
 | 
			
		||||
                                'x-component-props': {
 | 
			
		||||
                                  onClick: handleSetRelationshipType,
 | 
			
		||||
                                  icon: 'MenuOutlined',
 | 
			
		||||
                                  useAction: () => {
 | 
			
		||||
                                    return {
 | 
			
		||||
@ -1268,70 +1206,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
                              },
 | 
			
		||||
                              direction: {
 | 
			
		||||
                                type: 'void',
 | 
			
		||||
                                'x-component': () => {
 | 
			
		||||
                                  const menuItems = [
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: DirectionType.Both,
 | 
			
		||||
                                      label: 'All directions',
 | 
			
		||||
                                    },
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: DirectionType.Target,
 | 
			
		||||
                                      label: 'Target index',
 | 
			
		||||
                                    },
 | 
			
		||||
                                    {
 | 
			
		||||
                                      key: DirectionType.Source,
 | 
			
		||||
                                      label: 'Source index',
 | 
			
		||||
                                    },
 | 
			
		||||
                                  ];
 | 
			
		||||
                                  const content = (
 | 
			
		||||
                                    <div>
 | 
			
		||||
                                      <Menu
 | 
			
		||||
                                        defaultSelectedKeys={[DirectionType.Target]}
 | 
			
		||||
                                        selectable={true}
 | 
			
		||||
                                        className={css`
 | 
			
		||||
                                          .ant-menu-item {
 | 
			
		||||
                                            height: 32px;
 | 
			
		||||
                                            line-height: 32px;
 | 
			
		||||
                                          }
 | 
			
		||||
                                        `}
 | 
			
		||||
                                        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
                                        items={[
 | 
			
		||||
                                          { type: 'divider' },
 | 
			
		||||
                                          ...menuItems.map((v) => {
 | 
			
		||||
                                            return {
 | 
			
		||||
                                              key: v.key,
 | 
			
		||||
                                              label: t(v.label),
 | 
			
		||||
                                              onClick: (e: any) => {
 | 
			
		||||
                                                targetGraph.direction = v.key;
 | 
			
		||||
                                                const { filterConfig } = targetGraph;
 | 
			
		||||
                                                if (filterConfig) {
 | 
			
		||||
                                                  handleFiterCollections(filterConfig.key);
 | 
			
		||||
                                                }
 | 
			
		||||
                                              },
 | 
			
		||||
                                            };
 | 
			
		||||
                                          }),
 | 
			
		||||
                                        ]}
 | 
			
		||||
                                      />
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                  );
 | 
			
		||||
                                  return (
 | 
			
		||||
                                    <Popover
 | 
			
		||||
                                      content={content}
 | 
			
		||||
                                      autoAdjustOverflow
 | 
			
		||||
                                      placement="bottomRight"
 | 
			
		||||
                                      trigger={['click']}
 | 
			
		||||
                                      getPopupContainer={getPopupContainer}
 | 
			
		||||
                                      overlayClassName={css`
 | 
			
		||||
                                        .ant-popover-inner-content {
 | 
			
		||||
                                          padding: 0;
 | 
			
		||||
                                        }
 | 
			
		||||
                                      `}
 | 
			
		||||
                                    >
 | 
			
		||||
                                      <Button>
 | 
			
		||||
                                        <LineHeightOutlined />
 | 
			
		||||
                                      </Button>
 | 
			
		||||
                                    </Popover>
 | 
			
		||||
                                  );
 | 
			
		||||
                                'x-component': 'DirectionAction',
 | 
			
		||||
                                'x-component-props': {
 | 
			
		||||
                                  onClick: handleDirectionChange,
 | 
			
		||||
                                },
 | 
			
		||||
                              },
 | 
			
		||||
                              selectMode: {
 | 
			
		||||
@ -1364,7 +1241,7 @@ export const GraphDrawPage = React.memo(() => {
 | 
			
		||||
              </div>
 | 
			
		||||
            </CollapsedContext.Provider>
 | 
			
		||||
          </CollectionManagerProvider>
 | 
			
		||||
          <div id="container" style={{ width: '100vw', height: '100vh' }}></div>
 | 
			
		||||
          <div id="container" style={{ width: '100vw' }}></div>
 | 
			
		||||
          <div
 | 
			
		||||
            id="graph-minimap"
 | 
			
		||||
            className={styles.graphMinimap}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Button } from 'antd';
 | 
			
		||||
import { BranchesOutlined } from '@ant-design/icons';
 | 
			
		||||
 | 
			
		||||
export const ConnectAssociationAction = (props) => {
 | 
			
		||||
  const { targetGraph, item } = props;
 | 
			
		||||
  return (
 | 
			
		||||
    <BranchesOutlined
 | 
			
		||||
      className="btn-assocition"
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        targetGraph.onConnectionAssociation(item);
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Tooltip } from 'antd';
 | 
			
		||||
import { FallOutlined } from '@ant-design/icons';
 | 
			
		||||
import { useCollectionManager } from '@nocobase/client';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
 | 
			
		||||
export const ConnectChildAction = (props) => {
 | 
			
		||||
  const { targetGraph, item } = props;
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
  const { getChildrenCollections } = useCollectionManager();
 | 
			
		||||
  const childs = getChildrenCollections(item.name);
 | 
			
		||||
 | 
			
		||||
  const isShowChild = childs?.some(({ name }) => {
 | 
			
		||||
    return !targetGraph.hasCell(name);
 | 
			
		||||
  });
 | 
			
		||||
  return isShowChild ? (
 | 
			
		||||
    <Tooltip title={t('Show child')} getPopupContainer={getPopupContainer}>
 | 
			
		||||
      <FallOutlined
 | 
			
		||||
        className="btn-inheriedChild"
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          targetGraph.onConnectionChilds(childs.map((v) => v.name));
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  ) : (
 | 
			
		||||
    ''
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Tooltip } from 'antd';
 | 
			
		||||
import { RiseOutlined } from '@ant-design/icons';
 | 
			
		||||
import { useCollectionManager } from '@nocobase/client';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
 | 
			
		||||
export const ConnectParentAction = (props) => {
 | 
			
		||||
  const { targetGraph, item } = props;
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
  const { getInheritCollections } = useCollectionManager();
 | 
			
		||||
  const parents = getInheritCollections(item.name);
 | 
			
		||||
  const isShowParent = parents?.some((name) => {
 | 
			
		||||
    return !targetGraph.hasCell(name);
 | 
			
		||||
  });
 | 
			
		||||
  return isShowParent ? (
 | 
			
		||||
    <Tooltip title={t('Show parent')} getPopupContainer={getPopupContainer}>
 | 
			
		||||
      <RiseOutlined
 | 
			
		||||
        className="btn-inheriedParent"
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          targetGraph.onConnectionParents(parents);
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  ) : (
 | 
			
		||||
    ''
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,71 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { ShareAltOutlined } from '@ant-design/icons';
 | 
			
		||||
import { Menu, Popover, Button } from 'antd';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
import { ConnectionType } from '../GraphDrawPage';
 | 
			
		||||
 | 
			
		||||
export const ConnectorAction = (props) => {
 | 
			
		||||
  const { onClick } = props;
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
 | 
			
		||||
  const menuItems = [
 | 
			
		||||
    {
 | 
			
		||||
      key: ConnectionType.Both,
 | 
			
		||||
      label: 'All relationships',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: ConnectionType.Entity,
 | 
			
		||||
      label: 'Entity relationship only',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: ConnectionType.Inherit,
 | 
			
		||||
      label: 'Inheritance relationship only',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  const content = (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Menu
 | 
			
		||||
        defaultSelectedKeys={[ConnectionType.Both]}
 | 
			
		||||
        selectable={true}
 | 
			
		||||
        className={css`
 | 
			
		||||
          .ant-menu-item {
 | 
			
		||||
            height: 32px;
 | 
			
		||||
            line-height: 32px;
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
        items={[
 | 
			
		||||
          { type: 'divider' },
 | 
			
		||||
          ...menuItems.map((v) => {
 | 
			
		||||
            return {
 | 
			
		||||
              key: v.key,
 | 
			
		||||
              label: t(v.label),
 | 
			
		||||
              onClick: (e: any) => {
 | 
			
		||||
                onClick?.(v.key);
 | 
			
		||||
              },
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
        ]}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover
 | 
			
		||||
      content={content}
 | 
			
		||||
      autoAdjustOverflow
 | 
			
		||||
      placement="bottomRight"
 | 
			
		||||
      trigger={['click']}
 | 
			
		||||
      getPopupContainer={getPopupContainer}
 | 
			
		||||
      overlayClassName={css`
 | 
			
		||||
        .ant-popover-inner-content {
 | 
			
		||||
          padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <Button>
 | 
			
		||||
        <ShareAltOutlined />
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Popover>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,70 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Popover, Button, Menu } from 'antd';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { LineHeightOutlined } from '@ant-design/icons';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
import { DirectionType } from '../GraphDrawPage';
 | 
			
		||||
 | 
			
		||||
export const DirectionAction = (props) => {
 | 
			
		||||
  const { onClick } = props;
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
  const menuItems = [
 | 
			
		||||
    {
 | 
			
		||||
      key: DirectionType.Both,
 | 
			
		||||
      label: 'All directions',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: DirectionType.Target,
 | 
			
		||||
      label: 'Target index',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: DirectionType.Source,
 | 
			
		||||
      label: 'Source index',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
  const content = (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Menu
 | 
			
		||||
        defaultSelectedKeys={[DirectionType.Target]}
 | 
			
		||||
        selectable={true}
 | 
			
		||||
        className={css`
 | 
			
		||||
          .ant-menu-item {
 | 
			
		||||
            height: 32px;
 | 
			
		||||
            line-height: 32px;
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
        items={[
 | 
			
		||||
          { type: 'divider' },
 | 
			
		||||
          ...menuItems.map((v) => {
 | 
			
		||||
            return {
 | 
			
		||||
              key: v.key,
 | 
			
		||||
              label: t(v.label),
 | 
			
		||||
              onClick: () => {
 | 
			
		||||
                onClick?.(v.key);
 | 
			
		||||
              },
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
        ]}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover
 | 
			
		||||
      content={content}
 | 
			
		||||
      autoAdjustOverflow
 | 
			
		||||
      placement="bottomRight"
 | 
			
		||||
      trigger={['click']}
 | 
			
		||||
      getPopupContainer={getPopupContainer}
 | 
			
		||||
      overlayClassName={css`
 | 
			
		||||
        .ant-popover-inner-content {
 | 
			
		||||
          padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <Button>
 | 
			
		||||
        <LineHeightOutlined />
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Popover>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -35,11 +35,19 @@ import { EditFieldAction } from './EditFieldAction';
 | 
			
		||||
import { FieldSummary } from './FieldSummary';
 | 
			
		||||
import { OverrideFieldAction } from './OverrideFieldAction';
 | 
			
		||||
import { ViewFieldAction } from './ViewFieldAction';
 | 
			
		||||
import { ConnectAssociationAction } from './ConnectAssociationAction';
 | 
			
		||||
import { ConnectChildAction } from './ConnectChildAction';
 | 
			
		||||
import { ConnectParentAction } from './ConnectParentAction';
 | 
			
		||||
 | 
			
		||||
const OperationButton: any = React.memo((props: any) => {
 | 
			
		||||
  const { property, loadCollections, collectionData, setTargetNode, node, handelOpenPorts, title, name } = props;
 | 
			
		||||
  const { property, loadCollections, collectionData, setTargetNode, node, handelOpenPorts, title, name, targetGraph } =
 | 
			
		||||
    props;
 | 
			
		||||
  const isInheritField = !(property.collectionName !== name);
 | 
			
		||||
  const options = useContext(SchemaOptionsContext);
 | 
			
		||||
  const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(property.type);
 | 
			
		||||
  const isShowAssocition =
 | 
			
		||||
    isAssociationField &&
 | 
			
		||||
    !(property.through ? targetGraph.hasCell(property.through) : targetGraph.hasCell(property.target));
 | 
			
		||||
  const {
 | 
			
		||||
    data: { database },
 | 
			
		||||
  } = useCurrentAppInfo();
 | 
			
		||||
@ -63,6 +71,7 @@ const OperationButton: any = React.memo((props: any) => {
 | 
			
		||||
          OverrideFieldAction,
 | 
			
		||||
          ViewFieldAction,
 | 
			
		||||
          EditFieldAction,
 | 
			
		||||
          ConnectAssociationAction,
 | 
			
		||||
          ...options.components,
 | 
			
		||||
        }}
 | 
			
		||||
        scope={{
 | 
			
		||||
@ -74,6 +83,7 @@ const OperationButton: any = React.memo((props: any) => {
 | 
			
		||||
          useValuesFromRecord,
 | 
			
		||||
          useUpdateCollectionActionAndRefreshCM,
 | 
			
		||||
          isInheritField,
 | 
			
		||||
          isShowAssocition,
 | 
			
		||||
          ...options.scope,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
@ -164,6 +174,20 @@ const OperationButton: any = React.memo((props: any) => {
 | 
			
		||||
                    },
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
                connectAssociation: {
 | 
			
		||||
                  type: 'void',
 | 
			
		||||
                  'x-action': 'view',
 | 
			
		||||
                  'x-visible': '{{isShowAssocition}}',
 | 
			
		||||
                  'x-component': 'ConnectAssociationAction',
 | 
			
		||||
                  'x-component-props': {
 | 
			
		||||
                    item: {
 | 
			
		||||
                      ...property,
 | 
			
		||||
                      title,
 | 
			
		||||
                      __parent: collectionData.current,
 | 
			
		||||
                    },
 | 
			
		||||
                    targetGraph,
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
@ -173,7 +197,7 @@ const OperationButton: any = React.memo((props: any) => {
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const PopoverContent = React.memo((props: any) => {
 | 
			
		||||
const PopoverContent = React.forwardRef((props: any, ref) => {
 | 
			
		||||
  const { property, node, ...other } = props;
 | 
			
		||||
  const {
 | 
			
		||||
    store: {
 | 
			
		||||
@ -218,20 +242,7 @@ const PopoverContent = React.memo((props: any) => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover
 | 
			
		||||
      content={CollectionConten(property)}
 | 
			
		||||
      getPopupContainer={getPopupContainer}
 | 
			
		||||
      mouseLeaveDelay={0}
 | 
			
		||||
      zIndex={100}
 | 
			
		||||
      title={
 | 
			
		||||
    <div>
 | 
			
		||||
          {compile(property.uiSchema?.title)}
 | 
			
		||||
          <span style={{ color: '#ffa940', float: 'right' }}>{compile(getInterface(property.interface)?.title)}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
      }
 | 
			
		||||
      key={property.id}
 | 
			
		||||
      placement="right"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className="body-item"
 | 
			
		||||
        key={property.id}
 | 
			
		||||
@ -244,15 +255,31 @@ const PopoverContent = React.memo((props: any) => {
 | 
			
		||||
          setIsHovered(true);
 | 
			
		||||
        }}
 | 
			
		||||
        onMouseLeave={() => setIsHovered(false)}
 | 
			
		||||
      >
 | 
			
		||||
        <Popover
 | 
			
		||||
          content={CollectionConten(property)}
 | 
			
		||||
          getPopupContainer={getPopupContainer}
 | 
			
		||||
          mouseLeaveDelay={0}
 | 
			
		||||
          title={
 | 
			
		||||
            <div>
 | 
			
		||||
              {compile(property.uiSchema?.title)}
 | 
			
		||||
              <span style={{ color: '#ffa940', float: 'right' }}>
 | 
			
		||||
                {compile(getInterface(property.interface)?.title)}
 | 
			
		||||
              </span>
 | 
			
		||||
            </div>
 | 
			
		||||
          }
 | 
			
		||||
          key={property.id}
 | 
			
		||||
          placement="right"
 | 
			
		||||
        >
 | 
			
		||||
          <div className="name">
 | 
			
		||||
            <Badge color={typeColor(property)} />
 | 
			
		||||
            {compile(property.uiSchema?.title)}
 | 
			
		||||
          </div>
 | 
			
		||||
        </Popover>
 | 
			
		||||
        <div className={`type  field_type`}>{compile(getInterface(property.interface)?.title)}</div>
 | 
			
		||||
        {isHovered && <OperationButton property={property} {...operatioBtnProps} />}
 | 
			
		||||
      </div>
 | 
			
		||||
    </Popover>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -292,6 +319,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
 | 
			
		||||
    loadCollections,
 | 
			
		||||
    handelOpenPorts,
 | 
			
		||||
    node,
 | 
			
		||||
    targetGraph,
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="body">
 | 
			
		||||
@ -339,7 +367,7 @@ const Entity: React.FC<{
 | 
			
		||||
  const { node, setTargetNode, targetGraph } = props;
 | 
			
		||||
  const {
 | 
			
		||||
    store: {
 | 
			
		||||
      data: { title, name, item, attrs, select },
 | 
			
		||||
      data: { title, name, item, attrs, select, actived },
 | 
			
		||||
    },
 | 
			
		||||
    id,
 | 
			
		||||
  } = node;
 | 
			
		||||
@ -403,15 +431,37 @@ const Entity: React.FC<{
 | 
			
		||||
                    loadCategories,
 | 
			
		||||
                    useAsyncDataSource,
 | 
			
		||||
                    enableInherits: database?.dialect === 'postgres',
 | 
			
		||||
                    actived: actived === true,
 | 
			
		||||
                  }}
 | 
			
		||||
                  components={{
 | 
			
		||||
                    EditOutlined,
 | 
			
		||||
                    EditCollectionAction,
 | 
			
		||||
                    ConnectChildAction,
 | 
			
		||||
                    ConnectParentAction,
 | 
			
		||||
                    ...options.components,
 | 
			
		||||
                  }}
 | 
			
		||||
                  schema={{
 | 
			
		||||
                    type: 'object',
 | 
			
		||||
                    name: uid(),
 | 
			
		||||
                    properties: {
 | 
			
		||||
                      connectParent: {
 | 
			
		||||
                        type: 'void',
 | 
			
		||||
                        'x-visible': '{{actived}}',
 | 
			
		||||
                        'x-component': 'ConnectParentAction',
 | 
			
		||||
                        'x-component-props': {
 | 
			
		||||
                          item: collectionData.current,
 | 
			
		||||
                          targetGraph,
 | 
			
		||||
                        },
 | 
			
		||||
                      },
 | 
			
		||||
                      connectChild: {
 | 
			
		||||
                        type: 'void',
 | 
			
		||||
                        'x-component': 'ConnectChildAction',
 | 
			
		||||
                        'x-component-props': {
 | 
			
		||||
                          item: collectionData.current,
 | 
			
		||||
                          targetGraph,
 | 
			
		||||
                        },
 | 
			
		||||
                        'x-visible': '{{actived}}',
 | 
			
		||||
                      },
 | 
			
		||||
                      update: {
 | 
			
		||||
                        type: 'void',
 | 
			
		||||
                        title: '{{ t("Edit") }}',
 | 
			
		||||
@ -430,7 +480,6 @@ const Entity: React.FC<{
 | 
			
		||||
                          component: DeleteOutlined,
 | 
			
		||||
                          icon: 'DeleteOutlined',
 | 
			
		||||
                          className: 'btn-del',
 | 
			
		||||
 | 
			
		||||
                          confirm: {
 | 
			
		||||
                            title: "{{t('Delete record')}}",
 | 
			
		||||
                            getContainer: getPopupContainer,
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
import React, { forwardRef } from 'react';
 | 
			
		||||
import { Tooltip, Button } from 'antd';
 | 
			
		||||
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
import { useFullscreen } from 'ahooks';
 | 
			
		||||
 | 
			
		||||
export const FullscreenAction = forwardRef(() => {
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
 | 
			
		||||
  const [isFullscreen, { toggleFullscreen }] = useFullscreen(document.getElementById('graph_container'));
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip title={t('Full Screen')} getPopupContainer={getPopupContainer}>
 | 
			
		||||
      <Button
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          toggleFullscreen();
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,73 @@
 | 
			
		||||
import React, { useState, useContext } from 'react';
 | 
			
		||||
import { Input, Menu, Popover, Button } from 'antd';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import { MenuOutlined } from '@ant-design/icons';
 | 
			
		||||
import { useCompile } from '@nocobase/client';
 | 
			
		||||
import { getPopupContainer, useGCMTranslation } from '../utils';
 | 
			
		||||
import { CollapsedContext } from '../GraphDrawPage';
 | 
			
		||||
 | 
			
		||||
export const LocateCollectionAction = (props) => {
 | 
			
		||||
  const { handleFiterCollections } = props;
 | 
			
		||||
  const { handleSearchCollection, collectionList } = useContext(CollapsedContext);
 | 
			
		||||
  const [selectedKeys, setSelectKey] = useState([]);
 | 
			
		||||
  const { t } = useGCMTranslation();
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
 | 
			
		||||
  const content = (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Input
 | 
			
		||||
        style={{ margin: '4px 0' }}
 | 
			
		||||
        bordered={false}
 | 
			
		||||
        placeholder={t('Collection Search')}
 | 
			
		||||
        onChange={handleSearchCollection}
 | 
			
		||||
      />
 | 
			
		||||
      <Menu
 | 
			
		||||
        selectedKeys={selectedKeys}
 | 
			
		||||
        selectable={true}
 | 
			
		||||
        className={css`
 | 
			
		||||
          .ant-menu-item {
 | 
			
		||||
            height: 32px;
 | 
			
		||||
            line-height: 32px;
 | 
			
		||||
          }
 | 
			
		||||
        `}
 | 
			
		||||
        style={{ maxHeight: '70vh', overflowY: 'auto', border: 'none' }}
 | 
			
		||||
        items={[
 | 
			
		||||
          { type: 'divider' },
 | 
			
		||||
          ...collectionList.map((v) => {
 | 
			
		||||
            return {
 | 
			
		||||
              key: v.name,
 | 
			
		||||
              label: compile(v.title),
 | 
			
		||||
              onClick: (e: any) => {
 | 
			
		||||
                if (e.key !== selectedKeys[0]) {
 | 
			
		||||
                  setSelectKey([e.key]);
 | 
			
		||||
                  handleFiterCollections(e.key);
 | 
			
		||||
                } else {
 | 
			
		||||
                  handleFiterCollections(false);
 | 
			
		||||
                  setSelectKey([]);
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            };
 | 
			
		||||
          }),
 | 
			
		||||
        ]}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <Popover
 | 
			
		||||
      content={content}
 | 
			
		||||
      autoAdjustOverflow
 | 
			
		||||
      placement="bottomRight"
 | 
			
		||||
      trigger={['click']}
 | 
			
		||||
      getPopupContainer={getPopupContainer}
 | 
			
		||||
      overlayClassName={css`
 | 
			
		||||
        .ant-popover-inner-content {
 | 
			
		||||
          padding: 0;
 | 
			
		||||
        }
 | 
			
		||||
      `}
 | 
			
		||||
    >
 | 
			
		||||
      <Button>
 | 
			
		||||
        <MenuOutlined />
 | 
			
		||||
      </Button>
 | 
			
		||||
    </Popover>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
import React, { useContext, useMemo } from 'react';
 | 
			
		||||
import { CollapsedContext } from '../GraphDrawPage';
 | 
			
		||||
import { Select, useCompile } from '@nocobase/client';
 | 
			
		||||
import { useSearchParams } from 'react-router-dom';
 | 
			
		||||
import { getPopupContainer } from '../utils';
 | 
			
		||||
 | 
			
		||||
export const SelectCollectionsAction = (props) => {
 | 
			
		||||
  const { collectionList } = useContext(CollapsedContext);
 | 
			
		||||
  const compile = useCompile();
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  const initCollections = searchParams.get('collections');
 | 
			
		||||
  const selectKeys = initCollections?.split(',');
 | 
			
		||||
  const data = selectKeys?.filter((v) => collectionList.find((k) => k.name === v));
 | 
			
		||||
  const collectionOptions = useMemo(() => {
 | 
			
		||||
    return collectionList.map((v) => {
 | 
			
		||||
      return {
 | 
			
		||||
        label: compile(v.title),
 | 
			
		||||
        value: v.name,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }, [collectionList]);
 | 
			
		||||
 | 
			
		||||
  const handleChange = (values) => {
 | 
			
		||||
    setSearchParams([['collections', values.toString()]]);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Select
 | 
			
		||||
      value={data}
 | 
			
		||||
      showSearch
 | 
			
		||||
      getPopupContainer={getPopupContainer}
 | 
			
		||||
      mode="multiple"
 | 
			
		||||
      allowClear
 | 
			
		||||
      onSearch={(value) => {
 | 
			
		||||
        console.log(value);
 | 
			
		||||
      }}
 | 
			
		||||
      options={collectionOptions}
 | 
			
		||||
      onChange={handleChange}
 | 
			
		||||
      style={{ minWidth: 200, position: 'fixed', margin: '24px', zIndex: 1000, maxWidth: '60%' }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -52,6 +52,28 @@ const useStyles = createStyles(({ token, css }) => {
 | 
			
		||||
          background: ${token.colorBgTextHover};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .btn-inheriedParent {
 | 
			
		||||
        background: ${token.colorInfoBg};
 | 
			
		||||
        border-color: transparent;
 | 
			
		||||
        color: ${token.colorInfo};
 | 
			
		||||
        width: 20px;
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        line-height: 25px;
 | 
			
		||||
        &:hover {
 | 
			
		||||
          background-color: ${token.colorInfoBgHover};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .btn-inheriedChild {
 | 
			
		||||
        background: ${token.colorInfoBg};
 | 
			
		||||
        border-color: transparent;
 | 
			
		||||
        color: ${token.colorInfo};
 | 
			
		||||
        width: 20px;
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        margin: 0px 5px 4px;
 | 
			
		||||
        &:hover {
 | 
			
		||||
          background-color: ${token.colorInfoBgHover};
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      width: 250px;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      border-radius: ${token.borderRadiusLG}px;
 | 
			
		||||
@ -121,6 +143,14 @@ const useStyles = createStyles(({ token, css }) => {
 | 
			
		||||
              .btn-view:hover {
 | 
			
		||||
                background: ${token.colorBgTextHover};
 | 
			
		||||
              }
 | 
			
		||||
              .btn-assocition {
 | 
			
		||||
                border-color: transparent;
 | 
			
		||||
                color: ${token.colorPrimary};
 | 
			
		||||
                width: 20px;
 | 
			
		||||
              }
 | 
			
		||||
              .btn-assocition:hover {
 | 
			
		||||
                background: ${token.colorBgTextHover};
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            .field_type {
 | 
			
		||||
              display: none;
 | 
			
		||||
@ -132,6 +162,7 @@ const useStyles = createStyles(({ token, css }) => {
 | 
			
		||||
            white-space: nowrap;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
            margin-left: 8px;
 | 
			
		||||
            min-width: 50%;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          .type {
 | 
			
		||||
@ -187,6 +218,7 @@ const useStyles = createStyles(({ token, css }) => {
 | 
			
		||||
    `,
 | 
			
		||||
 | 
			
		||||
    collectionListClass: css`
 | 
			
		||||
      .nb-action-bar {
 | 
			
		||||
        float: right;
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        margin-top: 24px;
 | 
			
		||||
@ -212,10 +244,14 @@ const useStyles = createStyles(({ token, css }) => {
 | 
			
		||||
        .ant-btn {
 | 
			
		||||
          border: 0;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
 | 
			
		||||
    graphCollectionContainerClass: css`
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      #container {
 | 
			
		||||
        height: 100% !important;
 | 
			
		||||
      }
 | 
			
		||||
      .x6-graph-scroller {
 | 
			
		||||
        height: calc(100vh) !important;
 | 
			
		||||
        width: calc(100vw) !important;
 | 
			
		||||
 | 
			
		||||
@ -63,14 +63,14 @@ export const formatData = (data) => {
 | 
			
		||||
          ?.fields.map((v) => {
 | 
			
		||||
            return { ...v, sourceCollectionName: item.name };
 | 
			
		||||
          });
 | 
			
		||||
        return arr.concat(parentFields);
 | 
			
		||||
        return parentFields ? arr.concat(parentFields) : arr;
 | 
			
		||||
      },
 | 
			
		||||
      [],
 | 
			
		||||
    );
 | 
			
		||||
    uniqBy(totalFields.concat(inheritedFields), 'name').forEach((field) => {
 | 
			
		||||
      field.uiSchema &&
 | 
			
		||||
        ports.push({
 | 
			
		||||
          id: field.key,
 | 
			
		||||
          id: field.name,
 | 
			
		||||
          group: 'list',
 | 
			
		||||
          ...field,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user