From eeb3adf928f5f10574cbeec31fd6d3c335cde44c Mon Sep 17 00:00:00 2001 From: katherinehhh Date: Wed, 6 Sep 2023 09:43:26 +0800 Subject: [PATCH] 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 --- .../loadConstrueLocale.ts | 2 +- .../Configuration/AddFieldAction.tsx | 1 - .../src/client/GraphDrawPage.tsx | 401 ++++++------------ .../components/ConnectAssociationAction.tsx | 15 + .../client/components/ConnectChildAction.tsx | 28 ++ .../client/components/ConnectParentAction.tsx | 27 ++ .../src/client/components/ConnectorAction.tsx | 71 ++++ .../src/client/components/DirectionAction.tsx | 70 +++ .../src/client/components/Entity.tsx | 95 ++++- .../client/components/FullScreenAction.tsx | 22 + .../components/LocateCollectionAction.tsx | 73 ++++ .../components/SelectCollectionsAction.tsx | 41 ++ .../src/client/style.tsx | 80 +++- .../src/client/utils.tsx | 4 +- 14 files changed, 619 insertions(+), 311 deletions(-) create mode 100644 packages/plugins/graph-collection-manager/src/client/components/ConnectAssociationAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/ConnectChildAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/ConnectParentAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/ConnectorAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/DirectionAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/FullScreenAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/LocateCollectionAction.tsx create mode 100644 packages/plugins/graph-collection-manager/src/client/components/SelectCollectionsAction.tsx diff --git a/packages/core/client/src/antd-config-provider/loadConstrueLocale.ts b/packages/core/client/src/antd-config-provider/loadConstrueLocale.ts index 933e9fb60..1080536d1 100644 --- a/packages/core/client/src/antd-config-provider/loadConstrueLocale.ts +++ b/packages/core/client/src/antd-config-provider/loadConstrueLocale.ts @@ -1,7 +1,7 @@ import cronstrue from 'cronstrue'; class CronstrueLocale { - constructor(protected data: any) { } + constructor(protected data: any) {} atX0SecondsPastTheMinuteGt20(): string | null { return this.data['atX0SecondsPastTheMinuteGt20']; } diff --git a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx index 0041b5968..0b7664bb4 100644 --- a/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx +++ b/packages/core/client/src/collection-manager/Configuration/AddFieldAction.tsx @@ -282,7 +282,6 @@ export const AddFieldAction = (props) => { items, }; }, [getInterface, items, record]); - return ( record.template !== 'view' && ( diff --git a/packages/plugins/graph-collection-manager/src/client/GraphDrawPage.tsx b/packages/plugins/graph-collection-manager/src/client/GraphDrawPage.tsx index 61da77dec..94d28899a 100644 --- a/packages/plugins/graph-collection-manager/src/client/GraphDrawPage.tsx +++ b/packages/plugins/graph-collection-manager/src/client/GraphDrawPage.tsx @@ -1,19 +1,13 @@ -import { - ApartmentOutlined, - FullscreenExitOutlined, - FullscreenOutlined, - LineHeightOutlined, - MenuOutlined, - ShareAltOutlined, -} from '@ant-design/icons'; +import { ApartmentOutlined } from '@ant-design/icons'; import { Graph } from '@antv/x6'; import { MiniMap } from '@antv/x6-plugin-minimap'; import { Scroller } from '@antv/x6-plugin-scroller'; import { Selection } from '@antv/x6-plugin-selection'; import { Snapline } from '@antv/x6-plugin-snapline'; import { register } from '@antv/x6-react-shape'; -import { css, cx } from '@emotion/css'; +import { cx } from '@emotion/css'; import { SchemaOptionsContext } from '@formily/react'; +import { useSearchParams } from 'react-router-dom'; import { APIClientProvider, CollectionCategroriesContext, @@ -21,7 +15,6 @@ import { CollectionManagerContext, CollectionManagerProvider, CurrentAppInfoContext, - Popover, SchemaComponent, SchemaComponentOptions, Select, @@ -32,10 +25,9 @@ import { useCurrentAppInfo, useGlobalTheme, } from '@nocobase/client'; -import { useFullscreen } from 'ahooks'; -import { App, Button, ConfigProvider, Input, Layout, Menu, Spin, Switch, Tooltip } from 'antd'; -import dagre from 'dagre'; import lodash from 'lodash'; +import { Button, ConfigProvider, Layout, Switch, Tooltip, App, Spin } from 'antd'; +import dagre from 'dagre'; import React, { createContext, forwardRef, useContext, useEffect, useLayoutEffect, useState } from 'react'; import { useAsyncDataSource, useCreateActionAndRefreshCM } from './action-hooks'; import { AddCollectionAction } from './components/AddCollectionAction'; @@ -51,7 +43,12 @@ import { getPopupContainer, useGCMTranslation, } from './utils'; -const { drop, groupBy, last, maxBy, minBy, take } = lodash; +import { LocateCollectionAction } from './components/LocateCollectionAction'; +import { SelectCollectionsAction } from './components/SelectCollectionsAction'; +import { DirectionAction } from './components/DirectionAction'; +import { ConnectorAction } from './components/ConnectorAction'; +import { FullscreenAction } from './components/FullScreenAction'; +const { drop, groupBy, last, maxBy, minBy, take, uniq } = lodash; const LINE_HEIGHT = 40; const NODE_WIDTH = 250; @@ -72,7 +69,7 @@ export enum ConnectionType { } const getGridData = (num, arr) => { const newArr = []; - while (arr.length > 0 && num) { + while (arr?.length > 0 && num) { newArr.push(arr.splice(0, num)); } return newArr; @@ -105,7 +102,7 @@ async function layout(createPositions) { return v.collectionName === node.store.data.name; })) || {}; - const calculatedPosition = { x: col * 325 + 50, y: row * 400 + 60 }; + const calculatedPosition = { x: col * 325 + 50, y: row * 400 + 100 }; node.position(targetPosition.x || calculatedPosition.x, targetPosition.y || calculatedPosition.y); if (positions && !positions.find((v) => v.collectionName === node.store.data.name)) { // 位置表中没有的表都自动保存 @@ -142,8 +139,8 @@ function optimizeEdge(edge) { } = edge; const source = edge.getSource(); const target = edge.getTarget(); - const sorceNodeX = targetGraph.getCellById(source.cell).position().x; - const targeNodeX = targetGraph.getCellById(target.cell).position().x; + const sorceNodeX = targetGraph.getCellById(source.cell)?.position().x; + const targeNodeX = targetGraph.getCellById(target.cell)?.position().x; const leftAnchor = connectionType ? { name: 'topLeft', @@ -220,7 +217,7 @@ function optimizeEdge(edge) { } } -const CollapsedContext = createContext({}); +export const CollapsedContext = createContext({}); const formatNodeData = () => { const layoutNodes = []; const edges = targetGraph.getEdges(); @@ -239,10 +236,10 @@ const formatNodeData = () => { return nodeGroup; }; //自动布局 -const handelResetLayout = () => { - const { linkNodes = [], rawNodes } = formatNodeData(); +const handelResetLayout = (isTemporaryLayout?) => { + const { linkNodes = [], rawNodes = [] } = formatNodeData(); const { positions } = targetGraph; - const nodes = linkNodes.concat(rawNodes); + const nodes = linkNodes.concat(rawNodes || []); const edges = targetGraph.getEdges(); const g = new dagre.graphlib.Graph(); let alternateNum; @@ -342,12 +339,16 @@ const handelResetLayout = () => { optimizeEdge(edge); }); targetGraph.positionCell(nodes[0], 'top-left', { padding: 100 }); - targetGraph.updatePositionAction(updatePositionData, true); + if (!isTemporaryLayout) { + targetGraph.updatePositionAction(updatePositionData, true); + } }; export const GraphDrawPage = React.memo(() => { const { theme } = useGlobalTheme(); const { styles } = useStyles(); + const [searchParams, setSearchParams] = useSearchParams(); + const selectedCollections = searchParams.get('collections'); const options = useContext(SchemaOptionsContext); const ctx = useContext(CollectionManagerContext); const api = useAPIClient(); @@ -364,22 +365,25 @@ export const GraphDrawPage = React.memo(() => { const categoryCtx = useContext(CollectionCategroriesContext); const scope = { ...options?.scope }; const components = { ...options?.components }; + const saveGraphPositionAction = async (data) => { await api.resource('graphPositions').create({ values: data }); await refreshPositions(); }; const updatePositionAction = async (data, isbatch = false) => { - if (isbatch) { - await api.resource('graphPositions').update({ - values: data, - }); - } else { - await api.resource('graphPositions').update({ - filter: { collectionName: data.collectionName }, - values: { ...data }, - }); + if (!selectedCollections) { + if (isbatch) { + await api.resource('graphPositions').update({ + values: data, + }); + } else { + await api.resource('graphPositions').update({ + filter: { collectionName: data.collectionName }, + values: { ...data }, + }); + } + await refreshPositions(); } - await refreshPositions(); }; const refreshPositions = async () => { const { data } = await api.resource('graphPositions').list({ paginate: false }); @@ -400,7 +404,9 @@ export const GraphDrawPage = React.memo(() => { setCollectionData(data); setCollectionList(data); if (!currentNodes.length) { - renderInitGraphCollection(data); + if (!selectedCollections) { + renderInitGraphCollection(data); + } } else { renderDiffGraphCollection(data); } @@ -428,6 +434,9 @@ export const GraphDrawPage = React.memo(() => { targetGraph.connectionType = ConnectionType.Both; targetGraph.direction = DirectionType.Target; targetGraph.cacheCollection = {}; + targetGraph.onConnectionAssociation = handleConnectionAssociation; + targetGraph.onConnectionChilds = handleConnectionChilds; + targetGraph.onConnectionParents = handleConnectionParents; Graph.registerPortLayout( 'erPortPosition', (portsPositionArgs) => { @@ -503,16 +512,19 @@ export const GraphDrawPage = React.memo(() => { }); targetGraph.use( new Scroller({ + autoResize: true, enabled: true, pannable: true, - padding: { top: 0, left: 500, right: 300, bottom: 400 }, + pageVisible: true, + pageBreak: false, + padding: { top: 10, left: 500, right: 300, bottom: 300 }, }), ); targetGraph.use( new MiniMap({ container: document.getElementById('graph-minimap'), width: 300, - height: 250, + height: 200, padding: 10, graphOptions: { async: true, @@ -546,6 +558,10 @@ export const GraphDrawPage = React.memo(() => { e.stopPropagation(); handleEdgeUnActive(targetEdge); }); + targetGraph.on('node:mouseleave', ({ e, node }) => { + e.stopPropagation(); + node.setProp({ actived: false }); + }); targetGraph.on('node:moved', ({ e, node }) => { e.stopPropagation(); const connectEdges = targetGraph.getConnectedEdges(node); @@ -567,9 +583,13 @@ export const GraphDrawPage = React.memo(() => { optimizeEdge(edge); }); }); - targetGraph.on('cell:mouseenter', ({ e, cell, edge }) => { + targetGraph.on('cell:mouseenter', ({ e, cell, edge, node }) => { e.stopPropagation(); cell.toFront(); + if (node) { + cell.setProp({ actived: true }); + } + if (edge) { handleEdgeActive(edge); } @@ -580,7 +600,7 @@ export const GraphDrawPage = React.memo(() => { } targetGraph.collapseNodes?.map((v) => { const node = targetGraph.getCellById(Object.keys(v)[0]); - Object.values(v)[0] && node.setData({ collapse: false }); + Object.values(v)[0] && node?.setData({ collapse: false }); }); targetGraph.cleanSelection(); }); @@ -591,6 +611,30 @@ export const GraphDrawPage = React.memo(() => { node.setProp({ select: false }); }); }; + const handleConnectionAssociation = ({ target, through }) => { + const data = targetGraph.selectedCollections.split(',') || []; + data.push(target); + through && data.push(through); + const queryString = uniq(data).toString(); + setSearchParams([['collections', queryString]]); + targetGraph.selectedCollections = queryString; + }; + + const handleConnectionChilds = (collections) => { + let data = targetGraph.selectedCollections.split(',') || []; + data = data.concat(collections); + const queryString = uniq(data).toString(); + setSearchParams([['collections', queryString]]); + targetGraph.selectedCollections = queryString; + }; + const handleConnectionParents = (collections) => { + console.log(collections); + let data = targetGraph.selectedCollections.split(',') || []; + data = data.concat(collections); + const queryString = uniq(data).toString(); + setSearchParams([['collections', queryString]]); + targetGraph.selectedCollections = queryString; + }; const handleEdgeUnActive = (targetEdge) => { targetGraph.activeEdge = null; @@ -680,11 +724,14 @@ export const GraphDrawPage = React.memo(() => { lightUp(targetEdge); m2mEdge && lightUp(m2mEdge); }; - // 首次渲染 + // 全量渲染 const renderInitGraphCollection = (rawData) => { + targetGraph.clearCells(); const { nodesData, edgesData, inheritEdges } = formatData(rawData); targetGraph.data = { nodes: nodesData, edges: edgesData }; - targetGraph.fromJSON({ nodes: nodesData, edges: inheritEdges.concat(edgesData) }); + targetGraph.fromJSON({ nodes: nodesData }); + targetGraph.addEdges(edgesData); + targetGraph.addEdges(inheritEdges); layout(saveGraphPositionAction); }; @@ -704,8 +751,8 @@ export const GraphDrawPage = React.memo(() => { const diffNodes = getDiffNode(nodesData, currentNodes); const diffEdges = getDiffEdge(edgesData, currentEdgesGroup.currentRelateEdges || []); const diffInheritEdge = getDiffEdge(inheritEdges, currentEdgesGroup.currentInheritEdges || []); - const maxY = maxBy(positions, 'y').y; - const minX = minBy(positions, 'x').x; + const maxY = maxBy(positions, 'y')?.y; + const minX = minBy(positions, 'x')?.x; const yNodes = positions.filter((v) => { return Math.abs(v.y - maxY) < 100; }); @@ -726,10 +773,12 @@ export const GraphDrawPage = React.memo(() => { ...node, position, }); - saveGraphPositionAction({ - collectionName: node.name, - ...position, - }); + if (!selectedCollections) { + saveGraphPositionAction({ + collectionName: node.name, + ...position, + }); + } targetGraph && targetGraph.positionCell(targetNode, 'top', { padding: 200 }); break; case 'insertPort': @@ -947,11 +996,16 @@ export const GraphDrawPage = React.memo(() => { }; }, 0); } else { + targetGraph.filterConfig = null; handleCleanHighlight(); } }; + //切换连线类型 const handleSetRelationshipType = (type) => { + targetGraph.connectionType = type; + const { filterConfig } = targetGraph; + filterConfig && handleFiterCollections(filterConfig.key); handleSetEdgeVisible(type); }; @@ -982,6 +1036,14 @@ export const GraphDrawPage = React.memo(() => { }); }; + const handleDirectionChange = (direction) => { + targetGraph.direction = direction; + const { filterConfig } = targetGraph; + if (filterConfig) { + handleFiterCollections(filterConfig.key); + } + }; + useLayoutEffect(() => { initGraphCollections(); return () => { @@ -1007,6 +1069,18 @@ export const GraphDrawPage = React.memo(() => { }); }, []); + useEffect(() => { + if (selectedCollections && collectionList.length) { + const selectKeys = selectedCollections?.split(','); + const data = collectionList.filter((v) => selectKeys.includes(v.name)); + renderInitGraphCollection(data); + handelResetLayout(true); + targetGraph.selectedCollections = selectedCollections; + } else { + !selectedCollections && renderInitGraphCollection(collections); + } + }, [searchParams, collectionList]); + const loadCollections = async () => { return targetGraph.collections?.map((collection: any) => ({ label: compile(collection.title), @@ -1026,6 +1100,11 @@ export const GraphDrawPage = React.memo(() => { - { - 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([]); - } - }, - }; - }), - ]} - /> - - ); - return ( - - - - ); - }, + 'x-component': 'LocateCollectionAction', 'x-component-props': { + handleFiterCollections, icon: 'MenuOutlined', useAction: () => { return { @@ -1193,71 +1193,9 @@ export const GraphDrawPage = React.memo(() => { }, connectionType: { type: 'void', - 'x-component': () => { - const menuItems = [ - { - key: ConnectionType.Both, - label: 'All relationships', - }, - { - key: ConnectionType.Entity, - label: 'Entity relationship only', - }, - { - key: ConnectionType.Inherit, - label: 'Inheritance relationship only', - }, - ]; - const content = ( -
- { - 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); - }, - }; - }), - ]} - /> -
- ); - return ( - - - - ); - }, + 'x-component': 'ConnectorAction', 'x-component-props': { + onClick: handleSetRelationshipType, icon: 'MenuOutlined', useAction: () => { return { @@ -1268,70 +1206,9 @@ export const GraphDrawPage = React.memo(() => { }, direction: { type: 'void', - 'x-component': () => { - const menuItems = [ - { - key: DirectionType.Both, - label: 'All directions', - }, - { - key: DirectionType.Target, - label: 'Target index', - }, - { - key: DirectionType.Source, - label: 'Source index', - }, - ]; - const content = ( -
- { - return { - key: v.key, - label: t(v.label), - onClick: (e: any) => { - targetGraph.direction = v.key; - const { filterConfig } = targetGraph; - if (filterConfig) { - handleFiterCollections(filterConfig.key); - } - }, - }; - }), - ]} - /> -
- ); - return ( - - - - ); + 'x-component': 'DirectionAction', + 'x-component-props': { + onClick: handleDirectionChange, }, }, selectMode: { @@ -1364,7 +1241,7 @@ export const GraphDrawPage = React.memo(() => { -
+
{ + const { targetGraph, item } = props; + return ( + { + targetGraph.onConnectionAssociation(item); + }} + /> + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/ConnectChildAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/ConnectChildAction.tsx new file mode 100644 index 000000000..5b79542d0 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/ConnectChildAction.tsx @@ -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 ? ( + + { + targetGraph.onConnectionChilds(childs.map((v) => v.name)); + }} + /> + + ) : ( + '' + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/ConnectParentAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/ConnectParentAction.tsx new file mode 100644 index 000000000..ffa7a0bd5 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/ConnectParentAction.tsx @@ -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 ? ( + + { + targetGraph.onConnectionParents(parents); + }} + /> + + ) : ( + '' + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/ConnectorAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/ConnectorAction.tsx new file mode 100644 index 000000000..31b9c71ca --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/ConnectorAction.tsx @@ -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 = ( +
+ { + return { + key: v.key, + label: t(v.label), + onClick: (e: any) => { + onClick?.(v.key); + }, + }; + }), + ]} + /> +
+ ); + return ( + + + + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/DirectionAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/DirectionAction.tsx new file mode 100644 index 000000000..8f500a164 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/DirectionAction.tsx @@ -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 = ( +
+ { + return { + key: v.key, + label: t(v.label), + onClick: () => { + onClick?.(v.key); + }, + }; + }), + ]} + /> +
+ ); + return ( + + + + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/Entity.tsx b/packages/plugins/graph-collection-manager/src/client/components/Entity.tsx index 07e80f830..1d77afa4e 100644 --- a/packages/plugins/graph-collection-manager/src/client/components/Entity.tsx +++ b/packages/plugins/graph-collection-manager/src/client/components/Entity.tsx @@ -35,11 +35,19 @@ import { EditFieldAction } from './EditFieldAction'; import { FieldSummary } from './FieldSummary'; import { OverrideFieldAction } from './OverrideFieldAction'; import { ViewFieldAction } from './ViewFieldAction'; +import { ConnectAssociationAction } from './ConnectAssociationAction'; +import { ConnectChildAction } from './ConnectChildAction'; +import { ConnectParentAction } from './ConnectParentAction'; const OperationButton: any = React.memo((props: any) => { - const { property, loadCollections, collectionData, setTargetNode, node, handelOpenPorts, title, name } = props; + const { property, loadCollections, collectionData, setTargetNode, node, handelOpenPorts, title, name, targetGraph } = + props; const isInheritField = !(property.collectionName !== name); const options = useContext(SchemaOptionsContext); + const isAssociationField = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(property.type); + const isShowAssocition = + isAssociationField && + !(property.through ? targetGraph.hasCell(property.through) : targetGraph.hasCell(property.target)); const { data: { database }, } = useCurrentAppInfo(); @@ -63,6 +71,7 @@ const OperationButton: any = React.memo((props: any) => { OverrideFieldAction, ViewFieldAction, EditFieldAction, + ConnectAssociationAction, ...options.components, }} scope={{ @@ -74,6 +83,7 @@ const OperationButton: any = React.memo((props: any) => { useValuesFromRecord, useUpdateCollectionActionAndRefreshCM, isInheritField, + isShowAssocition, ...options.scope, }} > @@ -164,6 +174,20 @@ const OperationButton: any = React.memo((props: any) => { }, }, }, + connectAssociation: { + type: 'void', + 'x-action': 'view', + 'x-visible': '{{isShowAssocition}}', + 'x-component': 'ConnectAssociationAction', + 'x-component-props': { + item: { + ...property, + title, + __parent: collectionData.current, + }, + targetGraph, + }, + }, }, }} /> @@ -173,7 +197,7 @@ const OperationButton: any = React.memo((props: any) => { ); }); -const PopoverContent = React.memo((props: any) => { +const PopoverContent = React.forwardRef((props: any, ref) => { const { property, node, ...other } = props; const { store: { @@ -218,20 +242,7 @@ const PopoverContent = React.memo((props: any) => { } }; return ( - - {compile(property.uiSchema?.title)} - {compile(getInterface(property.interface)?.title)} -
- } - key={property.id} - placement="right" - > +
{ }} onMouseLeave={() => setIsHovered(false)} > -
- - {compile(property.uiSchema?.title)} -
+ + {compile(property.uiSchema?.title)} + + {compile(getInterface(property.interface)?.title)} + +
+ } + key={property.id} + placement="right" + > +
+ + {compile(property.uiSchema?.title)} +
+
{compile(getInterface(property.interface)?.title)}
{isHovered && }
- + ); }); @@ -292,6 +319,7 @@ const PortsCom = React.memo(({ targetGraph, collectionData, setTargetNode, loadCollections, handelOpenPorts, node, + targetGraph, }; return (
@@ -339,7 +367,7 @@ const Entity: React.FC<{ const { node, setTargetNode, targetGraph } = props; const { store: { - data: { title, name, item, attrs, select }, + data: { title, name, item, attrs, select, actived }, }, id, } = node; @@ -403,15 +431,37 @@ const Entity: React.FC<{ loadCategories, useAsyncDataSource, enableInherits: database?.dialect === 'postgres', + actived: actived === true, }} components={{ EditOutlined, EditCollectionAction, + ConnectChildAction, + ConnectParentAction, ...options.components, }} schema={{ type: 'object', + name: uid(), properties: { + connectParent: { + type: 'void', + 'x-visible': '{{actived}}', + 'x-component': 'ConnectParentAction', + 'x-component-props': { + item: collectionData.current, + targetGraph, + }, + }, + connectChild: { + type: 'void', + 'x-component': 'ConnectChildAction', + 'x-component-props': { + item: collectionData.current, + targetGraph, + }, + 'x-visible': '{{actived}}', + }, update: { type: 'void', title: '{{ t("Edit") }}', @@ -430,7 +480,6 @@ const Entity: React.FC<{ component: DeleteOutlined, icon: 'DeleteOutlined', className: 'btn-del', - confirm: { title: "{{t('Delete record')}}", getContainer: getPopupContainer, diff --git a/packages/plugins/graph-collection-manager/src/client/components/FullScreenAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/FullScreenAction.tsx new file mode 100644 index 000000000..d56a356d8 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/FullScreenAction.tsx @@ -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 ( + + + + ); +}); diff --git a/packages/plugins/graph-collection-manager/src/client/components/LocateCollectionAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/LocateCollectionAction.tsx new file mode 100644 index 000000000..8d75e9806 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/LocateCollectionAction.tsx @@ -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 = ( +
+ + { + 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([]); + } + }, + }; + }), + ]} + /> +
+ ); + return ( + + + + ); +}; diff --git a/packages/plugins/graph-collection-manager/src/client/components/SelectCollectionsAction.tsx b/packages/plugins/graph-collection-manager/src/client/components/SelectCollectionsAction.tsx new file mode 100644 index 000000000..d0eec97c5 --- /dev/null +++ b/packages/plugins/graph-collection-manager/src/client/components/SelectCollectionsAction.tsx @@ -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 ( +