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
					
				@ -1,7 +1,7 @@
 | 
				
			|||||||
import cronstrue from 'cronstrue';
 | 
					import cronstrue from 'cronstrue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CronstrueLocale {
 | 
					class CronstrueLocale {
 | 
				
			||||||
  constructor(protected data: any) { }
 | 
					  constructor(protected data: any) {}
 | 
				
			||||||
  atX0SecondsPastTheMinuteGt20(): string | null {
 | 
					  atX0SecondsPastTheMinuteGt20(): string | null {
 | 
				
			||||||
    return this.data['atX0SecondsPastTheMinuteGt20'];
 | 
					    return this.data['atX0SecondsPastTheMinuteGt20'];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -282,7 +282,6 @@ export const AddFieldAction = (props) => {
 | 
				
			|||||||
      items,
 | 
					      items,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }, [getInterface, items, record]);
 | 
					  }, [getInterface, items, record]);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    record.template !== 'view' && (
 | 
					    record.template !== 'view' && (
 | 
				
			||||||
      <RecordProvider record={record}>
 | 
					      <RecordProvider record={record}>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,13 @@
 | 
				
			|||||||
import {
 | 
					import { ApartmentOutlined } from '@ant-design/icons';
 | 
				
			||||||
  ApartmentOutlined,
 | 
					 | 
				
			||||||
  FullscreenExitOutlined,
 | 
					 | 
				
			||||||
  FullscreenOutlined,
 | 
					 | 
				
			||||||
  LineHeightOutlined,
 | 
					 | 
				
			||||||
  MenuOutlined,
 | 
					 | 
				
			||||||
  ShareAltOutlined,
 | 
					 | 
				
			||||||
} from '@ant-design/icons';
 | 
					 | 
				
			||||||
import { Graph } from '@antv/x6';
 | 
					import { Graph } from '@antv/x6';
 | 
				
			||||||
import { MiniMap } from '@antv/x6-plugin-minimap';
 | 
					import { MiniMap } from '@antv/x6-plugin-minimap';
 | 
				
			||||||
import { Scroller } from '@antv/x6-plugin-scroller';
 | 
					import { Scroller } from '@antv/x6-plugin-scroller';
 | 
				
			||||||
import { Selection } from '@antv/x6-plugin-selection';
 | 
					import { Selection } from '@antv/x6-plugin-selection';
 | 
				
			||||||
import { Snapline } from '@antv/x6-plugin-snapline';
 | 
					import { Snapline } from '@antv/x6-plugin-snapline';
 | 
				
			||||||
import { register } from '@antv/x6-react-shape';
 | 
					import { register } from '@antv/x6-react-shape';
 | 
				
			||||||
import { css, cx } from '@emotion/css';
 | 
					import { cx } from '@emotion/css';
 | 
				
			||||||
import { SchemaOptionsContext } from '@formily/react';
 | 
					import { SchemaOptionsContext } from '@formily/react';
 | 
				
			||||||
 | 
					import { useSearchParams } from 'react-router-dom';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  APIClientProvider,
 | 
					  APIClientProvider,
 | 
				
			||||||
  CollectionCategroriesContext,
 | 
					  CollectionCategroriesContext,
 | 
				
			||||||
@ -21,7 +15,6 @@ import {
 | 
				
			|||||||
  CollectionManagerContext,
 | 
					  CollectionManagerContext,
 | 
				
			||||||
  CollectionManagerProvider,
 | 
					  CollectionManagerProvider,
 | 
				
			||||||
  CurrentAppInfoContext,
 | 
					  CurrentAppInfoContext,
 | 
				
			||||||
  Popover,
 | 
					 | 
				
			||||||
  SchemaComponent,
 | 
					  SchemaComponent,
 | 
				
			||||||
  SchemaComponentOptions,
 | 
					  SchemaComponentOptions,
 | 
				
			||||||
  Select,
 | 
					  Select,
 | 
				
			||||||
@ -32,10 +25,9 @@ import {
 | 
				
			|||||||
  useCurrentAppInfo,
 | 
					  useCurrentAppInfo,
 | 
				
			||||||
  useGlobalTheme,
 | 
					  useGlobalTheme,
 | 
				
			||||||
} from '@nocobase/client';
 | 
					} 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 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 React, { createContext, forwardRef, useContext, useEffect, useLayoutEffect, useState } from 'react';
 | 
				
			||||||
import { useAsyncDataSource, useCreateActionAndRefreshCM } from './action-hooks';
 | 
					import { useAsyncDataSource, useCreateActionAndRefreshCM } from './action-hooks';
 | 
				
			||||||
import { AddCollectionAction } from './components/AddCollectionAction';
 | 
					import { AddCollectionAction } from './components/AddCollectionAction';
 | 
				
			||||||
@ -51,7 +43,12 @@ import {
 | 
				
			|||||||
  getPopupContainer,
 | 
					  getPopupContainer,
 | 
				
			||||||
  useGCMTranslation,
 | 
					  useGCMTranslation,
 | 
				
			||||||
} from './utils';
 | 
					} 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 LINE_HEIGHT = 40;
 | 
				
			||||||
const NODE_WIDTH = 250;
 | 
					const NODE_WIDTH = 250;
 | 
				
			||||||
@ -72,7 +69,7 @@ export enum ConnectionType {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
const getGridData = (num, arr) => {
 | 
					const getGridData = (num, arr) => {
 | 
				
			||||||
  const newArr = [];
 | 
					  const newArr = [];
 | 
				
			||||||
  while (arr.length > 0 && num) {
 | 
					  while (arr?.length > 0 && num) {
 | 
				
			||||||
    newArr.push(arr.splice(0, num));
 | 
					    newArr.push(arr.splice(0, num));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return newArr;
 | 
					  return newArr;
 | 
				
			||||||
@ -105,7 +102,7 @@ async function layout(createPositions) {
 | 
				
			|||||||
              return v.collectionName === node.store.data.name;
 | 
					              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);
 | 
					        node.position(targetPosition.x || calculatedPosition.x, targetPosition.y || calculatedPosition.y);
 | 
				
			||||||
        if (positions && !positions.find((v) => v.collectionName === node.store.data.name)) {
 | 
					        if (positions && !positions.find((v) => v.collectionName === node.store.data.name)) {
 | 
				
			||||||
          // 位置表中没有的表都自动保存
 | 
					          // 位置表中没有的表都自动保存
 | 
				
			||||||
@ -142,8 +139,8 @@ function optimizeEdge(edge) {
 | 
				
			|||||||
  } = edge;
 | 
					  } = edge;
 | 
				
			||||||
  const source = edge.getSource();
 | 
					  const source = edge.getSource();
 | 
				
			||||||
  const target = edge.getTarget();
 | 
					  const target = edge.getTarget();
 | 
				
			||||||
  const sorceNodeX = targetGraph.getCellById(source.cell).position().x;
 | 
					  const sorceNodeX = targetGraph.getCellById(source.cell)?.position().x;
 | 
				
			||||||
  const targeNodeX = targetGraph.getCellById(target.cell).position().x;
 | 
					  const targeNodeX = targetGraph.getCellById(target.cell)?.position().x;
 | 
				
			||||||
  const leftAnchor = connectionType
 | 
					  const leftAnchor = connectionType
 | 
				
			||||||
    ? {
 | 
					    ? {
 | 
				
			||||||
        name: 'topLeft',
 | 
					        name: 'topLeft',
 | 
				
			||||||
@ -220,7 +217,7 @@ function optimizeEdge(edge) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CollapsedContext = createContext<any>({});
 | 
					export const CollapsedContext = createContext<any>({});
 | 
				
			||||||
const formatNodeData = () => {
 | 
					const formatNodeData = () => {
 | 
				
			||||||
  const layoutNodes = [];
 | 
					  const layoutNodes = [];
 | 
				
			||||||
  const edges = targetGraph.getEdges();
 | 
					  const edges = targetGraph.getEdges();
 | 
				
			||||||
@ -239,10 +236,10 @@ const formatNodeData = () => {
 | 
				
			|||||||
  return nodeGroup;
 | 
					  return nodeGroup;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
//自动布局
 | 
					//自动布局
 | 
				
			||||||
const handelResetLayout = () => {
 | 
					const handelResetLayout = (isTemporaryLayout?) => {
 | 
				
			||||||
  const { linkNodes = [], rawNodes } = formatNodeData();
 | 
					  const { linkNodes = [], rawNodes = [] } = formatNodeData();
 | 
				
			||||||
  const { positions } = targetGraph;
 | 
					  const { positions } = targetGraph;
 | 
				
			||||||
  const nodes = linkNodes.concat(rawNodes);
 | 
					  const nodes = linkNodes.concat(rawNodes || []);
 | 
				
			||||||
  const edges = targetGraph.getEdges();
 | 
					  const edges = targetGraph.getEdges();
 | 
				
			||||||
  const g = new dagre.graphlib.Graph();
 | 
					  const g = new dagre.graphlib.Graph();
 | 
				
			||||||
  let alternateNum;
 | 
					  let alternateNum;
 | 
				
			||||||
@ -342,12 +339,16 @@ const handelResetLayout = () => {
 | 
				
			|||||||
    optimizeEdge(edge);
 | 
					    optimizeEdge(edge);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  targetGraph.positionCell(nodes[0], 'top-left', { padding: 100 });
 | 
					  targetGraph.positionCell(nodes[0], 'top-left', { padding: 100 });
 | 
				
			||||||
  targetGraph.updatePositionAction(updatePositionData, true);
 | 
					  if (!isTemporaryLayout) {
 | 
				
			||||||
 | 
					    targetGraph.updatePositionAction(updatePositionData, true);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const GraphDrawPage = React.memo(() => {
 | 
					export const GraphDrawPage = React.memo(() => {
 | 
				
			||||||
  const { theme } = useGlobalTheme();
 | 
					  const { theme } = useGlobalTheme();
 | 
				
			||||||
  const { styles } = useStyles();
 | 
					  const { styles } = useStyles();
 | 
				
			||||||
 | 
					  const [searchParams, setSearchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const selectedCollections = searchParams.get('collections');
 | 
				
			||||||
  const options = useContext(SchemaOptionsContext);
 | 
					  const options = useContext(SchemaOptionsContext);
 | 
				
			||||||
  const ctx = useContext(CollectionManagerContext);
 | 
					  const ctx = useContext(CollectionManagerContext);
 | 
				
			||||||
  const api = useAPIClient();
 | 
					  const api = useAPIClient();
 | 
				
			||||||
@ -364,22 +365,25 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
  const categoryCtx = useContext(CollectionCategroriesContext);
 | 
					  const categoryCtx = useContext(CollectionCategroriesContext);
 | 
				
			||||||
  const scope = { ...options?.scope };
 | 
					  const scope = { ...options?.scope };
 | 
				
			||||||
  const components = { ...options?.components };
 | 
					  const components = { ...options?.components };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const saveGraphPositionAction = async (data) => {
 | 
					  const saveGraphPositionAction = async (data) => {
 | 
				
			||||||
    await api.resource('graphPositions').create({ values: data });
 | 
					    await api.resource('graphPositions').create({ values: data });
 | 
				
			||||||
    await refreshPositions();
 | 
					    await refreshPositions();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  const updatePositionAction = async (data, isbatch = false) => {
 | 
					  const updatePositionAction = async (data, isbatch = false) => {
 | 
				
			||||||
    if (isbatch) {
 | 
					    if (!selectedCollections) {
 | 
				
			||||||
      await api.resource('graphPositions').update({
 | 
					      if (isbatch) {
 | 
				
			||||||
        values: data,
 | 
					        await api.resource('graphPositions').update({
 | 
				
			||||||
      });
 | 
					          values: data,
 | 
				
			||||||
    } else {
 | 
					        });
 | 
				
			||||||
      await api.resource('graphPositions').update({
 | 
					      } else {
 | 
				
			||||||
        filter: { collectionName: data.collectionName },
 | 
					        await api.resource('graphPositions').update({
 | 
				
			||||||
        values: { ...data },
 | 
					          filter: { collectionName: data.collectionName },
 | 
				
			||||||
      });
 | 
					          values: { ...data },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await refreshPositions();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    await refreshPositions();
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  const refreshPositions = async () => {
 | 
					  const refreshPositions = async () => {
 | 
				
			||||||
    const { data } = await api.resource('graphPositions').list({ paginate: false });
 | 
					    const { data } = await api.resource('graphPositions').list({ paginate: false });
 | 
				
			||||||
@ -400,7 +404,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
    setCollectionData(data);
 | 
					    setCollectionData(data);
 | 
				
			||||||
    setCollectionList(data);
 | 
					    setCollectionList(data);
 | 
				
			||||||
    if (!currentNodes.length) {
 | 
					    if (!currentNodes.length) {
 | 
				
			||||||
      renderInitGraphCollection(data);
 | 
					      if (!selectedCollections) {
 | 
				
			||||||
 | 
					        renderInitGraphCollection(data);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      renderDiffGraphCollection(data);
 | 
					      renderDiffGraphCollection(data);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -428,6 +434,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
    targetGraph.connectionType = ConnectionType.Both;
 | 
					    targetGraph.connectionType = ConnectionType.Both;
 | 
				
			||||||
    targetGraph.direction = DirectionType.Target;
 | 
					    targetGraph.direction = DirectionType.Target;
 | 
				
			||||||
    targetGraph.cacheCollection = {};
 | 
					    targetGraph.cacheCollection = {};
 | 
				
			||||||
 | 
					    targetGraph.onConnectionAssociation = handleConnectionAssociation;
 | 
				
			||||||
 | 
					    targetGraph.onConnectionChilds = handleConnectionChilds;
 | 
				
			||||||
 | 
					    targetGraph.onConnectionParents = handleConnectionParents;
 | 
				
			||||||
    Graph.registerPortLayout(
 | 
					    Graph.registerPortLayout(
 | 
				
			||||||
      'erPortPosition',
 | 
					      'erPortPosition',
 | 
				
			||||||
      (portsPositionArgs) => {
 | 
					      (portsPositionArgs) => {
 | 
				
			||||||
@ -503,16 +512,19 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
    targetGraph.use(
 | 
					    targetGraph.use(
 | 
				
			||||||
      new Scroller({
 | 
					      new Scroller({
 | 
				
			||||||
 | 
					        autoResize: true,
 | 
				
			||||||
        enabled: true,
 | 
					        enabled: true,
 | 
				
			||||||
        pannable: 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(
 | 
					    targetGraph.use(
 | 
				
			||||||
      new MiniMap({
 | 
					      new MiniMap({
 | 
				
			||||||
        container: document.getElementById('graph-minimap'),
 | 
					        container: document.getElementById('graph-minimap'),
 | 
				
			||||||
        width: 300,
 | 
					        width: 300,
 | 
				
			||||||
        height: 250,
 | 
					        height: 200,
 | 
				
			||||||
        padding: 10,
 | 
					        padding: 10,
 | 
				
			||||||
        graphOptions: {
 | 
					        graphOptions: {
 | 
				
			||||||
          async: true,
 | 
					          async: true,
 | 
				
			||||||
@ -546,6 +558,10 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
      e.stopPropagation();
 | 
					      e.stopPropagation();
 | 
				
			||||||
      handleEdgeUnActive(targetEdge);
 | 
					      handleEdgeUnActive(targetEdge);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    targetGraph.on('node:mouseleave', ({ e, node }) => {
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					      node.setProp({ actived: false });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    targetGraph.on('node:moved', ({ e, node }) => {
 | 
					    targetGraph.on('node:moved', ({ e, node }) => {
 | 
				
			||||||
      e.stopPropagation();
 | 
					      e.stopPropagation();
 | 
				
			||||||
      const connectEdges = targetGraph.getConnectedEdges(node);
 | 
					      const connectEdges = targetGraph.getConnectedEdges(node);
 | 
				
			||||||
@ -567,9 +583,13 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
        optimizeEdge(edge);
 | 
					        optimizeEdge(edge);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    targetGraph.on('cell:mouseenter', ({ e, cell, edge }) => {
 | 
					    targetGraph.on('cell:mouseenter', ({ e, cell, edge, node }) => {
 | 
				
			||||||
      e.stopPropagation();
 | 
					      e.stopPropagation();
 | 
				
			||||||
      cell.toFront();
 | 
					      cell.toFront();
 | 
				
			||||||
 | 
					      if (node) {
 | 
				
			||||||
 | 
					        cell.setProp({ actived: true });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (edge) {
 | 
					      if (edge) {
 | 
				
			||||||
        handleEdgeActive(edge);
 | 
					        handleEdgeActive(edge);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -580,7 +600,7 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      targetGraph.collapseNodes?.map((v) => {
 | 
					      targetGraph.collapseNodes?.map((v) => {
 | 
				
			||||||
        const node = targetGraph.getCellById(Object.keys(v)[0]);
 | 
					        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();
 | 
					      targetGraph.cleanSelection();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -591,6 +611,30 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
      node.setProp({ select: false });
 | 
					      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) => {
 | 
					  const handleEdgeUnActive = (targetEdge) => {
 | 
				
			||||||
    targetGraph.activeEdge = null;
 | 
					    targetGraph.activeEdge = null;
 | 
				
			||||||
@ -680,11 +724,14 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
    lightUp(targetEdge);
 | 
					    lightUp(targetEdge);
 | 
				
			||||||
    m2mEdge && lightUp(m2mEdge);
 | 
					    m2mEdge && lightUp(m2mEdge);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  // 首次渲染
 | 
					  // 全量渲染
 | 
				
			||||||
  const renderInitGraphCollection = (rawData) => {
 | 
					  const renderInitGraphCollection = (rawData) => {
 | 
				
			||||||
 | 
					    targetGraph.clearCells();
 | 
				
			||||||
    const { nodesData, edgesData, inheritEdges } = formatData(rawData);
 | 
					    const { nodesData, edgesData, inheritEdges } = formatData(rawData);
 | 
				
			||||||
    targetGraph.data = { nodes: nodesData, edges: edgesData };
 | 
					    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);
 | 
					    layout(saveGraphPositionAction);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -704,8 +751,8 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
    const diffNodes = getDiffNode(nodesData, currentNodes);
 | 
					    const diffNodes = getDiffNode(nodesData, currentNodes);
 | 
				
			||||||
    const diffEdges = getDiffEdge(edgesData, currentEdgesGroup.currentRelateEdges || []);
 | 
					    const diffEdges = getDiffEdge(edgesData, currentEdgesGroup.currentRelateEdges || []);
 | 
				
			||||||
    const diffInheritEdge = getDiffEdge(inheritEdges, currentEdgesGroup.currentInheritEdges || []);
 | 
					    const diffInheritEdge = getDiffEdge(inheritEdges, currentEdgesGroup.currentInheritEdges || []);
 | 
				
			||||||
    const maxY = maxBy(positions, 'y').y;
 | 
					    const maxY = maxBy(positions, 'y')?.y;
 | 
				
			||||||
    const minX = minBy(positions, 'x').x;
 | 
					    const minX = minBy(positions, 'x')?.x;
 | 
				
			||||||
    const yNodes = positions.filter((v) => {
 | 
					    const yNodes = positions.filter((v) => {
 | 
				
			||||||
      return Math.abs(v.y - maxY) < 100;
 | 
					      return Math.abs(v.y - maxY) < 100;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -726,10 +773,12 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
            ...node,
 | 
					            ...node,
 | 
				
			||||||
            position,
 | 
					            position,
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
          saveGraphPositionAction({
 | 
					          if (!selectedCollections) {
 | 
				
			||||||
            collectionName: node.name,
 | 
					            saveGraphPositionAction({
 | 
				
			||||||
            ...position,
 | 
					              collectionName: node.name,
 | 
				
			||||||
          });
 | 
					              ...position,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
          targetGraph && targetGraph.positionCell(targetNode, 'top', { padding: 200 });
 | 
					          targetGraph && targetGraph.positionCell(targetNode, 'top', { padding: 200 });
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        case 'insertPort':
 | 
					        case 'insertPort':
 | 
				
			||||||
@ -947,11 +996,16 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
      }, 0);
 | 
					      }, 0);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					      targetGraph.filterConfig = null;
 | 
				
			||||||
      handleCleanHighlight();
 | 
					      handleCleanHighlight();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //切换连线类型
 | 
				
			||||||
  const handleSetRelationshipType = (type) => {
 | 
					  const handleSetRelationshipType = (type) => {
 | 
				
			||||||
 | 
					    targetGraph.connectionType = type;
 | 
				
			||||||
 | 
					    const { filterConfig } = targetGraph;
 | 
				
			||||||
 | 
					    filterConfig && handleFiterCollections(filterConfig.key);
 | 
				
			||||||
    handleSetEdgeVisible(type);
 | 
					    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(() => {
 | 
					  useLayoutEffect(() => {
 | 
				
			||||||
    initGraphCollections();
 | 
					    initGraphCollections();
 | 
				
			||||||
    return () => {
 | 
					    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 () => {
 | 
					  const loadCollections = async () => {
 | 
				
			||||||
    return targetGraph.collections?.map((collection: any) => ({
 | 
					    return targetGraph.collections?.map((collection: any) => ({
 | 
				
			||||||
      label: compile(collection.title),
 | 
					      label: compile(collection.title),
 | 
				
			||||||
@ -1026,6 +1100,11 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
                      <Select popupMatchSelectWidth={false} {...props} getPopupContainer={getPopupContainer} />
 | 
					                      <Select popupMatchSelectWidth={false} {...props} getPopupContainer={getPopupContainer} />
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    AddCollectionAction,
 | 
					                    AddCollectionAction,
 | 
				
			||||||
 | 
					                    LocateCollectionAction,
 | 
				
			||||||
 | 
					                    SelectCollectionsAction,
 | 
				
			||||||
 | 
					                    DirectionAction,
 | 
				
			||||||
 | 
					                    ConnectorAction,
 | 
				
			||||||
 | 
					                    FullscreenAction,
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                  schema={{
 | 
					                  schema={{
 | 
				
			||||||
                    type: 'void',
 | 
					                    type: 'void',
 | 
				
			||||||
@ -1050,6 +1129,10 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
                          },
 | 
					                          },
 | 
				
			||||||
                        },
 | 
					                        },
 | 
				
			||||||
                        properties: {
 | 
					                        properties: {
 | 
				
			||||||
 | 
					                          selectCollections: {
 | 
				
			||||||
 | 
					                            type: 'array',
 | 
				
			||||||
 | 
					                            'x-component': 'SelectCollectionsAction',
 | 
				
			||||||
 | 
					                          },
 | 
				
			||||||
                          actions: {
 | 
					                          actions: {
 | 
				
			||||||
                            type: 'void',
 | 
					                            type: 'void',
 | 
				
			||||||
                            'x-component': 'ActionBar',
 | 
					                            'x-component': 'ActionBar',
 | 
				
			||||||
@ -1069,96 +1152,13 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
                              },
 | 
					                              },
 | 
				
			||||||
                              fullScreen: {
 | 
					                              fullScreen: {
 | 
				
			||||||
                                type: 'void',
 | 
					                                type: 'void',
 | 
				
			||||||
                                'x-component': 'Action',
 | 
					                                'x-component': 'FullscreenAction',
 | 
				
			||||||
                                '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() {},
 | 
					 | 
				
			||||||
                                    };
 | 
					 | 
				
			||||||
                                  },
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
                              collectionList: {
 | 
					                              locateCollection: {
 | 
				
			||||||
                                type: 'void',
 | 
					                                type: 'void',
 | 
				
			||||||
                                'x-component': function Com() {
 | 
					                                'x-component': 'LocateCollectionAction',
 | 
				
			||||||
                                  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-props': {
 | 
					                                'x-component-props': {
 | 
				
			||||||
 | 
					                                  handleFiterCollections,
 | 
				
			||||||
                                  icon: 'MenuOutlined',
 | 
					                                  icon: 'MenuOutlined',
 | 
				
			||||||
                                  useAction: () => {
 | 
					                                  useAction: () => {
 | 
				
			||||||
                                    return {
 | 
					                                    return {
 | 
				
			||||||
@ -1193,71 +1193,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
                              },
 | 
					                              },
 | 
				
			||||||
                              connectionType: {
 | 
					                              connectionType: {
 | 
				
			||||||
                                type: 'void',
 | 
					                                type: 'void',
 | 
				
			||||||
                                'x-component': () => {
 | 
					                                'x-component': 'ConnectorAction',
 | 
				
			||||||
                                  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-props': {
 | 
					                                'x-component-props': {
 | 
				
			||||||
 | 
					                                  onClick: handleSetRelationshipType,
 | 
				
			||||||
                                  icon: 'MenuOutlined',
 | 
					                                  icon: 'MenuOutlined',
 | 
				
			||||||
                                  useAction: () => {
 | 
					                                  useAction: () => {
 | 
				
			||||||
                                    return {
 | 
					                                    return {
 | 
				
			||||||
@ -1268,70 +1206,9 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
                              },
 | 
					                              },
 | 
				
			||||||
                              direction: {
 | 
					                              direction: {
 | 
				
			||||||
                                type: 'void',
 | 
					                                type: 'void',
 | 
				
			||||||
                                'x-component': () => {
 | 
					                                'x-component': 'DirectionAction',
 | 
				
			||||||
                                  const menuItems = [
 | 
					                                'x-component-props': {
 | 
				
			||||||
                                    {
 | 
					                                  onClick: handleDirectionChange,
 | 
				
			||||||
                                      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>
 | 
					 | 
				
			||||||
                                  );
 | 
					 | 
				
			||||||
                                },
 | 
					                                },
 | 
				
			||||||
                              },
 | 
					                              },
 | 
				
			||||||
                              selectMode: {
 | 
					                              selectMode: {
 | 
				
			||||||
@ -1364,7 +1241,7 @@ export const GraphDrawPage = React.memo(() => {
 | 
				
			|||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </CollapsedContext.Provider>
 | 
					            </CollapsedContext.Provider>
 | 
				
			||||||
          </CollectionManagerProvider>
 | 
					          </CollectionManagerProvider>
 | 
				
			||||||
          <div id="container" style={{ width: '100vw', height: '100vh' }}></div>
 | 
					          <div id="container" style={{ width: '100vw' }}></div>
 | 
				
			||||||
          <div
 | 
					          <div
 | 
				
			||||||
            id="graph-minimap"
 | 
					            id="graph-minimap"
 | 
				
			||||||
            className={styles.graphMinimap}
 | 
					            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 { FieldSummary } from './FieldSummary';
 | 
				
			||||||
import { OverrideFieldAction } from './OverrideFieldAction';
 | 
					import { OverrideFieldAction } from './OverrideFieldAction';
 | 
				
			||||||
import { ViewFieldAction } from './ViewFieldAction';
 | 
					import { ViewFieldAction } from './ViewFieldAction';
 | 
				
			||||||
 | 
					import { ConnectAssociationAction } from './ConnectAssociationAction';
 | 
				
			||||||
 | 
					import { ConnectChildAction } from './ConnectChildAction';
 | 
				
			||||||
 | 
					import { ConnectParentAction } from './ConnectParentAction';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const OperationButton: any = React.memo((props: any) => {
 | 
					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 isInheritField = !(property.collectionName !== name);
 | 
				
			||||||
  const options = useContext(SchemaOptionsContext);
 | 
					  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 {
 | 
					  const {
 | 
				
			||||||
    data: { database },
 | 
					    data: { database },
 | 
				
			||||||
  } = useCurrentAppInfo();
 | 
					  } = useCurrentAppInfo();
 | 
				
			||||||
@ -63,6 +71,7 @@ const OperationButton: any = React.memo((props: any) => {
 | 
				
			|||||||
          OverrideFieldAction,
 | 
					          OverrideFieldAction,
 | 
				
			||||||
          ViewFieldAction,
 | 
					          ViewFieldAction,
 | 
				
			||||||
          EditFieldAction,
 | 
					          EditFieldAction,
 | 
				
			||||||
 | 
					          ConnectAssociationAction,
 | 
				
			||||||
          ...options.components,
 | 
					          ...options.components,
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
        scope={{
 | 
					        scope={{
 | 
				
			||||||
@ -74,6 +83,7 @@ const OperationButton: any = React.memo((props: any) => {
 | 
				
			|||||||
          useValuesFromRecord,
 | 
					          useValuesFromRecord,
 | 
				
			||||||
          useUpdateCollectionActionAndRefreshCM,
 | 
					          useUpdateCollectionActionAndRefreshCM,
 | 
				
			||||||
          isInheritField,
 | 
					          isInheritField,
 | 
				
			||||||
 | 
					          isShowAssocition,
 | 
				
			||||||
          ...options.scope,
 | 
					          ...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 { property, node, ...other } = props;
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    store: {
 | 
					    store: {
 | 
				
			||||||
@ -218,20 +242,7 @@ const PopoverContent = React.memo((props: any) => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Popover
 | 
					    <div>
 | 
				
			||||||
      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
 | 
					      <div
 | 
				
			||||||
        className="body-item"
 | 
					        className="body-item"
 | 
				
			||||||
        key={property.id}
 | 
					        key={property.id}
 | 
				
			||||||
@ -245,14 +256,30 @@ const PopoverContent = React.memo((props: any) => {
 | 
				
			|||||||
        }}
 | 
					        }}
 | 
				
			||||||
        onMouseLeave={() => setIsHovered(false)}
 | 
					        onMouseLeave={() => setIsHovered(false)}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div className="name">
 | 
					        <Popover
 | 
				
			||||||
          <Badge color={typeColor(property)} />
 | 
					          content={CollectionConten(property)}
 | 
				
			||||||
          {compile(property.uiSchema?.title)}
 | 
					          getPopupContainer={getPopupContainer}
 | 
				
			||||||
        </div>
 | 
					          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>
 | 
					        <div className={`type  field_type`}>{compile(getInterface(property.interface)?.title)}</div>
 | 
				
			||||||
        {isHovered && <OperationButton property={property} {...operatioBtnProps} />}
 | 
					        {isHovered && <OperationButton property={property} {...operatioBtnProps} />}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </Popover>
 | 
					    </div>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -292,6 +319,7 @@ const PortsCom = React.memo<any>(({ targetGraph, collectionData, setTargetNode,
 | 
				
			|||||||
    loadCollections,
 | 
					    loadCollections,
 | 
				
			||||||
    handelOpenPorts,
 | 
					    handelOpenPorts,
 | 
				
			||||||
    node,
 | 
					    node,
 | 
				
			||||||
 | 
					    targetGraph,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="body">
 | 
					    <div className="body">
 | 
				
			||||||
@ -339,7 +367,7 @@ const Entity: React.FC<{
 | 
				
			|||||||
  const { node, setTargetNode, targetGraph } = props;
 | 
					  const { node, setTargetNode, targetGraph } = props;
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    store: {
 | 
					    store: {
 | 
				
			||||||
      data: { title, name, item, attrs, select },
 | 
					      data: { title, name, item, attrs, select, actived },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    id,
 | 
					    id,
 | 
				
			||||||
  } = node;
 | 
					  } = node;
 | 
				
			||||||
@ -403,15 +431,37 @@ const Entity: React.FC<{
 | 
				
			|||||||
                    loadCategories,
 | 
					                    loadCategories,
 | 
				
			||||||
                    useAsyncDataSource,
 | 
					                    useAsyncDataSource,
 | 
				
			||||||
                    enableInherits: database?.dialect === 'postgres',
 | 
					                    enableInherits: database?.dialect === 'postgres',
 | 
				
			||||||
 | 
					                    actived: actived === true,
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                  components={{
 | 
					                  components={{
 | 
				
			||||||
                    EditOutlined,
 | 
					                    EditOutlined,
 | 
				
			||||||
                    EditCollectionAction,
 | 
					                    EditCollectionAction,
 | 
				
			||||||
 | 
					                    ConnectChildAction,
 | 
				
			||||||
 | 
					                    ConnectParentAction,
 | 
				
			||||||
                    ...options.components,
 | 
					                    ...options.components,
 | 
				
			||||||
                  }}
 | 
					                  }}
 | 
				
			||||||
                  schema={{
 | 
					                  schema={{
 | 
				
			||||||
                    type: 'object',
 | 
					                    type: 'object',
 | 
				
			||||||
 | 
					                    name: uid(),
 | 
				
			||||||
                    properties: {
 | 
					                    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: {
 | 
					                      update: {
 | 
				
			||||||
                        type: 'void',
 | 
					                        type: 'void',
 | 
				
			||||||
                        title: '{{ t("Edit") }}',
 | 
					                        title: '{{ t("Edit") }}',
 | 
				
			||||||
@ -430,7 +480,6 @@ const Entity: React.FC<{
 | 
				
			|||||||
                          component: DeleteOutlined,
 | 
					                          component: DeleteOutlined,
 | 
				
			||||||
                          icon: 'DeleteOutlined',
 | 
					                          icon: 'DeleteOutlined',
 | 
				
			||||||
                          className: 'btn-del',
 | 
					                          className: 'btn-del',
 | 
				
			||||||
 | 
					 | 
				
			||||||
                          confirm: {
 | 
					                          confirm: {
 | 
				
			||||||
                            title: "{{t('Delete record')}}",
 | 
					                            title: "{{t('Delete record')}}",
 | 
				
			||||||
                            getContainer: getPopupContainer,
 | 
					                            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};
 | 
					          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;
 | 
					      width: 250px;
 | 
				
			||||||
      height: 100%;
 | 
					      height: 100%;
 | 
				
			||||||
      border-radius: ${token.borderRadiusLG}px;
 | 
					      border-radius: ${token.borderRadiusLG}px;
 | 
				
			||||||
@ -121,6 +143,14 @@ const useStyles = createStyles(({ token, css }) => {
 | 
				
			|||||||
              .btn-view:hover {
 | 
					              .btn-view:hover {
 | 
				
			||||||
                background: ${token.colorBgTextHover};
 | 
					                background: ${token.colorBgTextHover};
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					              .btn-assocition {
 | 
				
			||||||
 | 
					                border-color: transparent;
 | 
				
			||||||
 | 
					                color: ${token.colorPrimary};
 | 
				
			||||||
 | 
					                width: 20px;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              .btn-assocition:hover {
 | 
				
			||||||
 | 
					                background: ${token.colorBgTextHover};
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            .field_type {
 | 
					            .field_type {
 | 
				
			||||||
              display: none;
 | 
					              display: none;
 | 
				
			||||||
@ -132,6 +162,7 @@ const useStyles = createStyles(({ token, css }) => {
 | 
				
			|||||||
            white-space: nowrap;
 | 
					            white-space: nowrap;
 | 
				
			||||||
            overflow: hidden;
 | 
					            overflow: hidden;
 | 
				
			||||||
            margin-left: 8px;
 | 
					            margin-left: 8px;
 | 
				
			||||||
 | 
					            min-width: 50%;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          .type {
 | 
					          .type {
 | 
				
			||||||
@ -187,35 +218,40 @@ const useStyles = createStyles(({ token, css }) => {
 | 
				
			|||||||
    `,
 | 
					    `,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    collectionListClass: css`
 | 
					    collectionListClass: css`
 | 
				
			||||||
      float: right;
 | 
					      .nb-action-bar {
 | 
				
			||||||
      position: fixed;
 | 
					 | 
				
			||||||
      margin-top: 24px;
 | 
					 | 
				
			||||||
      right: 24px;
 | 
					 | 
				
			||||||
      z-index: 1000;
 | 
					 | 
				
			||||||
      .trigger {
 | 
					 | 
				
			||||||
        float: right;
 | 
					        float: right;
 | 
				
			||||||
        margin: 2px 4px;
 | 
					        position: fixed;
 | 
				
			||||||
        font-size: 16px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .ant-input {
 | 
					 | 
				
			||||||
        margin: 4px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .ant-menu-inline {
 | 
					 | 
				
			||||||
        border-top: 1px solid ${token.colorBorderSecondary};
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .ant-layout-sider {
 | 
					 | 
				
			||||||
        margin-top: 24px;
 | 
					        margin-top: 24px;
 | 
				
			||||||
      }
 | 
					        right: 24px;
 | 
				
			||||||
      .ant-menu-item {
 | 
					        z-index: 1000;
 | 
				
			||||||
        height: 32px;
 | 
					        .trigger {
 | 
				
			||||||
      }
 | 
					          float: right;
 | 
				
			||||||
      .ant-btn {
 | 
					          margin: 2px 4px;
 | 
				
			||||||
        border: 0;
 | 
					          font-size: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .ant-input {
 | 
				
			||||||
 | 
					          margin: 4px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .ant-menu-inline {
 | 
				
			||||||
 | 
					          border-top: 1px solid ${token.colorBorderSecondary};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .ant-layout-sider {
 | 
				
			||||||
 | 
					          margin-top: 24px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .ant-menu-item {
 | 
				
			||||||
 | 
					          height: 32px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .ant-btn {
 | 
				
			||||||
 | 
					          border: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    graphCollectionContainerClass: css`
 | 
					    graphCollectionContainerClass: css`
 | 
				
			||||||
      overflow: hidden;
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					      #container {
 | 
				
			||||||
 | 
					        height: 100% !important;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      .x6-graph-scroller {
 | 
					      .x6-graph-scroller {
 | 
				
			||||||
        height: calc(100vh) !important;
 | 
					        height: calc(100vh) !important;
 | 
				
			||||||
        width: calc(100vw) !important;
 | 
					        width: calc(100vw) !important;
 | 
				
			||||||
 | 
				
			|||||||
@ -63,14 +63,14 @@ export const formatData = (data) => {
 | 
				
			|||||||
          ?.fields.map((v) => {
 | 
					          ?.fields.map((v) => {
 | 
				
			||||||
            return { ...v, sourceCollectionName: item.name };
 | 
					            return { ...v, sourceCollectionName: item.name };
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        return arr.concat(parentFields);
 | 
					        return parentFields ? arr.concat(parentFields) : arr;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      [],
 | 
					      [],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    uniqBy(totalFields.concat(inheritedFields), 'name').forEach((field) => {
 | 
					    uniqBy(totalFields.concat(inheritedFields), 'name').forEach((field) => {
 | 
				
			||||||
      field.uiSchema &&
 | 
					      field.uiSchema &&
 | 
				
			||||||
        ports.push({
 | 
					        ports.push({
 | 
				
			||||||
          id: field.key,
 | 
					          id: field.name,
 | 
				
			||||||
          group: 'list',
 | 
					          group: 'list',
 | 
				
			||||||
          ...field,
 | 
					          ...field,
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user