diff --git a/src/constants.tsx b/src/constants.tsx index 819a7383..0fbb3d0b 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -18,6 +18,7 @@ import type { Dataset, DiagramAlgorithm, DiagramDirection, + DiagramHoverFocus, DiagramLineStyle, DiagramLinks, DiagramMode, @@ -380,6 +381,13 @@ export const DESIGNER_ALGORITHMS: Selectable[] = [ { label: "Spaced", value: "spaced" }, ]; +export const DESIGNER_HOVER_FOCUS: Selectable[] = [ + { label: "Default", value: "default" }, + { label: "None", value: "none" }, + { label: "Dim", value: "dim" }, + { label: "Dim recursive", value: "recursive" }, +]; + export const SCHEMA_MODES: Selectable[] = [ { label: "Schemaless", value: "schemaless" }, { label: "Schemafull", value: "schemafull" }, diff --git a/src/screens/surrealist/views/designer/TableGraphPane/index.tsx b/src/screens/surrealist/views/designer/TableGraphPane/index.tsx index 990d08e4..449bd1b4 100644 --- a/src/screens/surrealist/views/designer/TableGraphPane/index.tsx +++ b/src/screens/surrealist/views/designer/TableGraphPane/index.tsx @@ -63,6 +63,7 @@ import { import { DESIGNER_ALGORITHMS, DESIGNER_DIRECTIONS, + DESIGNER_HOVER_FOCUS, DESIGNER_LINE_STYLES, DESIGNER_LINKS, DESIGNER_NODE_MODES, @@ -71,6 +72,7 @@ import { import type { DiagramAlgorithm, DiagramDirection, + DiagramHoverFocus, DiagramLineStyle, DiagramLinks, DiagramMode, @@ -120,6 +122,7 @@ export function TableGraphPane(props: TableGraphPaneProps) { diagramLineStyle, diagramLinkMode, diagramMode, + diagramHoverFocus, ] = useConnection((c) => [ c?.id ?? "", c?.designerTableList, @@ -128,6 +131,7 @@ export function TableGraphPane(props: TableGraphPaneProps) { c?.diagramLineStyle, c?.diagramLinkMode, c?.diagramMode, + c?.diagramHoverFocus, ]); const [isExporting, setIsExporting] = useState(false); @@ -144,17 +148,22 @@ export function TableGraphPane(props: TableGraphPaneProps) { const nodesInitialized = useNodesInitialized({ includeHiddenNodes: true }); const isLayedOut = useRef(false); + const [hoveredNode, setHoveredNode] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const [defaultAlgorithm] = useSetting("appearance", "defaultDiagramAlgorithm"); const [defaultDirection] = useSetting("appearance", "defaultDiagramDirection"); const [defaultLineStyle] = useSetting("appearance", "defaultDiagramLineStyle"); const [defaultLinkMode] = useSetting("appearance", "defaultDiagramLinkMode"); const [defaultNodeMode] = useSetting("appearance", "defaultDiagramMode"); + const [defaultHoverFocus] = useSetting("appearance", "defaultDiagramHoverFocus"); const algorithm = applyDefault(diagramAlgorithm, defaultAlgorithm); const direction = applyDefault(diagramDirection, defaultDirection); const lineStyle = applyDefault(diagramLineStyle, defaultLineStyle); const linkMode = applyDefault(diagramLinkMode, defaultLinkMode); const nodeMode = applyDefault(diagramMode, defaultNodeMode); + const hoverFocus = applyDefault(diagramHoverFocus, defaultHoverFocus); useLayoutEffect(() => { if (isLayedOut.current || !nodesInitialized) { @@ -216,6 +225,24 @@ export function TableGraphPane(props: TableGraphPaneProps) { }; }), ); + + setIsDragging(true); + }); + + const handleNodeDragStop = useStable((_: MouseEvent, node: Node) => { + setIsDragging(false); + }); + + const handleNodeMouseEnter = useStable((_: MouseEvent, node: Node) => { + if (hoverFocus === "dim" || hoverFocus === "recursive") { + setHoveredNode(node.id); + } + }); + + const handleNodeMouseLeave = useStable(() => { + if (hoverFocus === "dim" || hoverFocus === "recursive") { + setHoveredNode(null); + } }); const saveImage = useStable(async (type: "png" | "svg") => { @@ -298,6 +325,13 @@ export function TableGraphPane(props: TableGraphPaneProps) { }); }); + const setDiagramHoverFocus = useStable((mode: string) => { + updateConnection({ + id: connectionId, + diagramHoverFocus: mode as DiagramHoverFocus, + }); + }); + const handleZoomIn = useStable(() => { zoomIn({ duration: 150 }); }); @@ -339,6 +373,116 @@ export function TableGraphPane(props: TableGraphPaneProps) { }); }, [props.active]); + useEffect(() => { + const shouldApplyDimming = + (hoverFocus === "dim" || hoverFocus === "recursive") && + hoveredNode !== null && + !isDragging; + + if (!shouldApplyDimming) { + setNodes((nodes) => + nodes.map((node) => ({ + ...node, + className: node.className + ? node.className + .split(" ") + .filter((c) => c !== "dimmed") + .join(" ") + : undefined, + })), + ); + + setEdges((edges) => + edges.map((edge) => ({ + ...edge, + className: edge.className + ? edge.className + .split(" ") + .filter((c) => c !== "dimmed") + .join(" ") + : edge.type === "elk" + ? "record-link" + : undefined, + })), + ); + + return; + } + + const relatedNodes = new Set([hoveredNode]); + + if (hoverFocus === "recursive") { + const traverseGraph = (nodeId: string, visited: Set, isForward: boolean) => { + if (visited.has(nodeId)) return; + + visited.add(nodeId); + relatedNodes.add(nodeId); + + for (const edge of edges) { + if (isForward && edge.source === nodeId && !visited.has(edge.target)) { + traverseGraph(edge.target, visited, true); + } + if (!isForward && edge.target === nodeId && !visited.has(edge.source)) { + traverseGraph(edge.source, visited, false); + } + } + }; + + traverseGraph(hoveredNode, new Set(), true); + traverseGraph(hoveredNode, new Set(), false); + } else { + for (const edge of edges) { + if (edge.source === hoveredNode) { + relatedNodes.add(edge.target); + } + if (edge.target === hoveredNode) { + relatedNodes.add(edge.source); + } + } + } + + setNodes((nodes) => + nodes.map((node) => { + const isRelated = relatedNodes.has(node.id); + const classes = node.className + ? node.className.split(" ").filter((c) => c !== "dimmed") + : []; + + if (!isRelated) { + classes.push("dimmed"); + } + + return { + ...node, + className: classes.length > 0 ? classes.join(" ") : undefined, + }; + }), + ); + + setEdges((edges) => + edges.map((edge) => { + const isRelated = relatedNodes.has(edge.source) && relatedNodes.has(edge.target); + + const baseClasses = edge.className + ? edge.className.split(" ").filter((c) => c !== "dimmed") + : []; + + if (edge.type === "elk" && baseClasses.length === 0) { + baseClasses.push("record-link"); + } + + if (!isRelated) { + baseClasses.push("dimmed"); + } + + return { + ...edge, + className: baseClasses.join(" "), + }; + }), + ); + }, [hoveredNode, hoverFocus, edges, isDragging]); + useIntent("focus-table", ({ table }) => { const node = getNodes().find((node) => node.id === table); @@ -468,6 +612,13 @@ export function TableGraphPane(props: TableGraphPaneProps) { onChange={setDiagramLinkMode as any} comboboxProps={{ withinPortal: false }} /> + +