diff --git a/app/src/pages/mapping_page/components/MainPanel/components/LabeledHandle.tsx b/app/src/pages/mapping_page/components/MainPanel/components/LabeledHandle.tsx index c2b0667..be93f00 100644 --- a/app/src/pages/mapping_page/components/MainPanel/components/LabeledHandle.tsx +++ b/app/src/pages/mapping_page/components/MainPanel/components/LabeledHandle.tsx @@ -20,13 +20,20 @@ const LabeledHandle = React.forwardRef< position: 'relative', display: 'flex', alignItems: 'center', - textAlign: 'center', + textAlign: 'left', + overflow: 'hidden', + maxWidth: '150px', }} > {bigTitle ? (

{title} @@ -37,6 +44,10 @@ const LabeledHandle = React.forwardRef< padding: '0 0.75rem', color: 'var(--foreground)', width: '100%', + textAlign: 'center', + overflowWrap: 'break-word', + + hyphens: 'auto', }} className={labelClassName} > diff --git a/app/src/pages/mapping_page/components/MainPanel/index.tsx b/app/src/pages/mapping_page/components/MainPanel/index.tsx index b29f667..9ae8742 100644 --- a/app/src/pages/mapping_page/components/MainPanel/index.tsx +++ b/app/src/pages/mapping_page/components/MainPanel/index.tsx @@ -5,10 +5,13 @@ import { Background, Connection, Controls, + EdgeChange, EdgeTypes, MarkerType, MiniMap, + NodeChange, NodeTypes, + OnConnectEnd, ReactFlow, useEdgesState, useNodesState, @@ -16,7 +19,7 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { MappingGraph } from '../../../../lib/api/mapping_service/types'; import ConnectionLineComponent from '@/pages/mapping_page/components/MainPanel/components/ConnectionLineComponent'; @@ -26,7 +29,14 @@ import { LiteralNode } from '@/pages/mapping_page/components/MainPanel/component import { URIRefNode } from '@/pages/mapping_page/components/MainPanel/components/URIRefNode'; import { useBackendMappingGraph } from '@/pages/mapping_page/hooks/useBackendMappingGraph'; -import { XYEdgeType, XYNodeTypes } from './types'; +import useMappingPage from '@/pages/mapping_page/state'; +import { + EntityNodeType, + LiteralNodeType, + URIRefNodeType, + XYEdgeType, + XYNodeTypes, +} from './types'; type MainPanelProps = { initialGraph: MappingGraph | null; @@ -54,11 +64,51 @@ const defaultEdgeOptions = { const MainPanel = ({ initialGraph }: MainPanelProps) => { const reactflow = useReactFlow(); + const setIsSaved = useMappingPage(state => state.setIsSaved); const { nodes: initialNodes, edges: initialEdges } = useBackendMappingGraph(initialGraph); + const [dropConnection, setDropConnection] = useState<{ + source: string; + sourceHandle: string; + } | null>(null); + const [newNode, setNewNode] = useState(null); + + const [nodes, setNodes, xyOnNodesChange] = useNodesState([]); + const [edges, setEdges, xyOnEdgesChange] = useEdgesState([]); + const [ignoreNodesChange, setIgnoreNodesChange] = useState(false); + + const onNodesChange = useCallback( + (nodes: NodeChange[]) => { + setIsSaved(ignoreNodesChange); + if (ignoreNodesChange) { + setIgnoreNodesChange(false); + } + xyOnNodesChange(nodes); + }, + [setIsSaved, ignoreNodesChange, xyOnNodesChange], + ); - const [nodes, setNodes, onNodesChange] = useNodesState([]); - const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const onEdgesChange = useCallback( + (edges: EdgeChange[]) => { + setIsSaved(ignoreNodesChange); + if (ignoreNodesChange) { + setIgnoreNodesChange(false); + } + xyOnEdgesChange(edges); + }, + [setIsSaved, xyOnEdgesChange, ignoreNodesChange], + ); + + const onConnect = useCallback( + (params: Connection) => { + setIsSaved(ignoreNodesChange); + if (ignoreNodesChange) { + setIgnoreNodesChange(false); + } + setEdges(edges => addEdge(params, edges)); + }, + [setEdges, setIsSaved, ignoreNodesChange], + ); const screenToFlowPosition = reactflow.screenToFlowPosition; @@ -66,14 +116,26 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { setNodes(initialNodes); setEdges(initialEdges); reactflow.fitView(); + setIgnoreNodesChange(true); }, [initialNodes, initialEdges, setNodes, setEdges, reactflow]); - const onConnect = useCallback( - (params: Connection) => { - setEdges(edges => addEdge(params, edges)); - }, - [setEdges], - ); + useEffect(() => { + if (dropConnection && newNode) { + setEdges(edges => + addEdge( + { + source: dropConnection.source, + target: newNode.id, + sourceHandle: dropConnection.sourceHandle, + targetHandle: newNode.id, + }, + edges, + ), + ); + setDropConnection(null); + setNewNode(null); + } + }, [dropConnection, newNode, setEdges]); const handleAddEntityNode = useCallback( (e: React.MouseEvent) => { @@ -81,25 +143,24 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { x: e.clientX, y: e.clientY, }); - setNodes(nodes => [ - ...nodes, - { + const newNode = { + id: `node-${nodes.length}`, + data: { id: `node-${nodes.length}`, - data: { - id: `node-${nodes.length}`, - label: 'New Entity', - rdf_type: [], - uri_pattern: '', - properties: [], - type: 'entity', - position: position, - }, - position: position, + label: 'New Entity', + rdf_type: [], + uri_pattern: '', + properties: [], type: 'entity', + position: position, }, - ]); + position: position, + type: 'entity', + } as EntityNodeType; + setNodes(nodes => [...nodes, newNode]); + setNewNode(newNode); }, - [setNodes, screenToFlowPosition], + [setNodes, screenToFlowPosition, nodes, setNewNode], ); const handleAddUriRefNode = useCallback( @@ -108,22 +169,21 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { x: e.clientX, y: e.clientY, }); - setNodes(nodes => [ - ...nodes, - { + const newNode = { + id: `node-${nodes.length}`, + data: { id: `node-${nodes.length}`, - data: { - id: `node-${nodes.length}`, - uri_pattern: 'http://example.com/', - type: 'uri_ref', - position: position, - }, - position: position, + uri_pattern: 'http://example.com/', type: 'uri_ref', + position: position, }, - ]); + position: position, + type: 'uri_ref', + } as URIRefNodeType; + setNodes(nodes => [...nodes, newNode]); + setNewNode(newNode); }, - [setNodes, screenToFlowPosition], + [setNodes, screenToFlowPosition, nodes, setNewNode], ); const handleAddLiteralNode = useCallback( @@ -132,40 +192,78 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { x: e.clientX, y: e.clientY, }); - setNodes(nodes => [ - ...nodes, - { + const newNode = { + id: `node-${nodes.length}`, + data: { id: `node-${nodes.length}`, - data: { - id: `node-${nodes.length}`, - label: 'New Literal', - value: '', - literal_type: 'string', - type: 'literal', - position: position, - }, - position: position, + label: 'New Literal', + value: '', + literal_type: 'string', type: 'literal', + position: position, }, - ]); + position: position, + type: 'literal', + } as LiteralNodeType; + setNodes(nodes => [...nodes, newNode]); + setNewNode(newNode); }, - [setNodes, screenToFlowPosition], + [setNodes, screenToFlowPosition, nodes, setNewNode], ); - const openMenu = (e: React.MouseEvent) => { - e.preventDefault(); - showContextMenu({ - content: ( - - - - - - ), - targetOffset: { left: e.clientX, top: e.clientY }, - isDarkTheme: true, - }); - }; + const openMenu = useCallback( + (targetOffset: { left: number; top: number }) => { + showContextMenu({ + content: ( + + + + + + ), + targetOffset, + isDarkTheme: true, + }); + }, + [handleAddEntityNode, handleAddLiteralNode, handleAddUriRefNode], + ); + + const onConnectEnd = useCallback( + (event, { fromNode, fromHandle }) => { + const targetIsPane = + event.target instanceof HTMLElement && + event.target.classList.contains('react-flow__pane'); + + if ( + targetIsPane && + fromNode && + fromHandle && + fromHandle.type === 'source' + ) { + if (!fromHandle.id) throw new Error('fromHandle.id is undefined'); + + setDropConnection({ + source: fromNode.id, + sourceHandle: fromHandle.id, + }); + + if (event instanceof MouseEvent) + openMenu({ + left: event.clientX, + top: event.clientY, + }); + else if (event instanceof TouchEvent) + openMenu({ + left: event.touches[0].clientX, + top: event.touches[0].clientY, + }); + } + }, + [openMenu], + ); return ( // disable default right click menu @@ -181,7 +279,14 @@ const MainPanel = ({ initialGraph }: MainPanelProps) => { onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} colorMode='dark' - onContextMenu={openMenu} + onContextMenu={e => { + e.preventDefault(); + openMenu({ + left: e.clientX, + top: e.clientY, + }); + }} + onConnectEnd={onConnectEnd} > { const navigation = useNavigate(); const saveMapping = useMappingPage(state => state.saveMapping); + const isSaved = useMappingPage(state => state.isSaved); const mapping = useMappingPage(state => state.mapping); const nodes = useNodes(); @@ -52,7 +53,11 @@ const Navbar = ({ - diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/EntityProperties.tsx similarity index 100% rename from app/src/pages/mapping_page/components/NodeProperties/components/EntityProperties.tsx rename to app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/EntityProperties.tsx diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/LiteralProperties.tsx similarity index 100% rename from app/src/pages/mapping_page/components/NodeProperties/components/LiteralProperties.tsx rename to app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/LiteralProperties.tsx diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/URIRefProperties.tsx similarity index 100% rename from app/src/pages/mapping_page/components/NodeProperties/components/URIRefProperties.tsx rename to app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/URIRefProperties.tsx diff --git a/app/src/pages/mapping_page/components/NodeProperties/components/styles.scss b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/styles.scss similarity index 100% rename from app/src/pages/mapping_page/components/NodeProperties/components/styles.scss rename to app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/components/styles.scss diff --git a/app/src/pages/mapping_page/components/NodeProperties/index.tsx b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/index.tsx similarity index 81% rename from app/src/pages/mapping_page/components/NodeProperties/index.tsx rename to app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/index.tsx index 1071225..b216655 100644 --- a/app/src/pages/mapping_page/components/NodeProperties/index.tsx +++ b/app/src/pages/mapping_page/components/SidePanel/components/NodeProperties/index.tsx @@ -1,12 +1,12 @@ -import LiteralNodeProperties from '@/pages/mapping_page/components/NodeProperties/components/LiteralProperties'; -import URIRefProperties from '@/pages/mapping_page/components/NodeProperties/components/URIRefProperties'; +import LiteralNodeProperties from '@/pages/mapping_page/components/SidePanel/components/NodeProperties/components/LiteralProperties'; +import URIRefProperties from '@/pages/mapping_page/components/SidePanel/components/NodeProperties/components/URIRefProperties'; import { NonIdealState } from '@blueprintjs/core'; import { useStore } from '@xyflow/react'; import { EntityNodeType, LiteralNodeType, URIRefNodeType, -} from '../MainPanel/types'; +} from '../../../MainPanel/types'; import EntityNodeProperties from './components/EntityProperties'; const NodeProperties = () => { diff --git a/app/src/pages/mapping_page/components/SidePanel/components/SearchPanel/index.tsx b/app/src/pages/mapping_page/components/SidePanel/components/SearchPanel/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/app/src/pages/mapping_page/components/SidePanel/index.tsx b/app/src/pages/mapping_page/components/SidePanel/index.tsx index 5e94f9a..480b7a6 100644 --- a/app/src/pages/mapping_page/components/SidePanel/index.tsx +++ b/app/src/pages/mapping_page/components/SidePanel/index.tsx @@ -1,5 +1,5 @@ import { Card } from '@blueprintjs/core'; -import NodeProperties from '../NodeProperties'; +import NodeProperties from './components/NodeProperties'; type SidePanelProps = { selectedTab: string | undefined; diff --git a/app/src/pages/mapping_page/index.tsx b/app/src/pages/mapping_page/index.tsx index 2a8bc04..1c96b9f 100644 --- a/app/src/pages/mapping_page/index.tsx +++ b/app/src/pages/mapping_page/index.tsx @@ -38,7 +38,6 @@ const MappingPage = () => { const isLoading = useMappingPage(state => state.isLoading); const error = useMappingPage(state => state.error); const loadMapping = useMappingPage(state => state.loadMapping); - const saveMapping = useMappingPage(state => state.saveMapping); useRegisterTheme('mapping-theme', mapping_theme); useRegisterLanguage('mapping_language', mapping_language, {}); diff --git a/app/src/pages/mapping_page/state.ts b/app/src/pages/mapping_page/state.ts index 46efb76..fc39789 100644 --- a/app/src/pages/mapping_page/state.ts +++ b/app/src/pages/mapping_page/state.ts @@ -21,6 +21,7 @@ interface MappingPageState { prefixes: Prefix[] | null; isLoading: string | null; error: string | null; + isSaved: boolean; } interface MappingPageStateActions { @@ -32,6 +33,7 @@ interface MappingPageStateActions { nodes: XYNodeTypes[], edges: XYEdgeType[], ) => Promise; + setIsSaved: (isSaved: boolean) => void; } const defaultState: MappingPageState = { @@ -41,6 +43,7 @@ const defaultState: MappingPageState = { prefixes: null, isLoading: null, error: null, + isSaved: true, }; const functions: ZustandActions = ( @@ -104,7 +107,7 @@ const functions: ZustandActions = ( } as MappingGraph; try { await MappingService.updateMapping(workspaceUuid, mappingUuid, mapping); - set({ error: null }); + set({ error: null, isSaved: true }); } catch (error) { if (error instanceof Error) { set({ error: error.message }); @@ -113,6 +116,9 @@ const functions: ZustandActions = ( set({ isLoading: null }); } }, + setIsSaved(isSaved: boolean) { + set({ isSaved }); + }, }); export const useMappingPage = create<