Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type FrankEdgeProperties = {
selected?: boolean
sourceHandleId?: string | null
targetHandleId?: string | null
data?: { label?: string; faded?: boolean }
type: string
}

Expand All @@ -28,8 +29,10 @@ export default function FrankEdge({
targetPosition,
selected,
sourceHandleId,
data,
}: Readonly<FrankEdgeProperties>) {
const deleteEdge = useFlowStore((state) => state.deleteEdge)
const faded = data?.faded ?? false

const sourceHandleType = useFlowStore((state) => {
const node = state.nodes.find((n) => n.id === source)
Expand Down Expand Up @@ -61,13 +64,20 @@ export default function FrankEdge({

return (
<>
<BaseEdge id={id} path={edgePath} style={{ strokeWidth: 3 }} />
<BaseEdge
id={id}
path={edgePath}
style={{ strokeWidth: 3, opacity: faded ? 0 : 1, transition: 'opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)' }}
interactionWidth={faded ? 0 : undefined}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does null also work instead of undefined?

/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',
pointerEvents: faded ? 'none' : 'all',
opacity: faded ? 0 : 1,
transition: 'opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)',
zIndex: 20,
}}
className="nodrag flex flex-col items-center"
Expand Down
97 changes: 94 additions & 3 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import { useShallow } from 'zustand/react/shallow'
import { FlowConfig } from '~/routes/studio/canvas/flow.config'
import { getElementTypeFromName } from '~/routes/studio/node-translator-module'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { logApiError } from '~/utils/logger'
import { NodeContextMenuContext, useNodeContextMenu } from './node-context-menu-context'
import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note'
Expand All @@ -53,6 +53,7 @@
import { showErrorToast } from '~/components/toast'
import { useSettingsStore } from '~/stores/settings-store'
import { useShortcut } from '~/hooks/use-shortcut'
import LightbulbIcon from '/icons/solar/Lightbulb.svg?react'
import CanvasContextMenu from '~/components/flow/canvas-context-menu'
import { useSidebarStore, SidebarSide } from '~/stores/sidebar-layout-store'
import { openInEditorAtElement } from '~/actions/navigationActions'
Expand Down Expand Up @@ -159,6 +160,10 @@
function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) {
const showNodeContextMenu = useNodeContextMenu()
const [loading, setLoading] = useState(false)
const hoveredNodeId = useNodeContextStore((state) => state.hoveredNodeId)
const showAllForwards = useNodeContextStore((state) => state.showAllForwards)
const setHoveredNodeId = useNodeContextStore((state) => state.setHoveredNodeId)
const toggleShowAllForwards = useNodeContextStore((state) => state.toggleShowAllForwards)
const {
isEditing,
isDirty,
Expand Down Expand Up @@ -276,6 +281,54 @@

const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onReconnect } = useFlowStore(useShallow(selector))

const hiddenForwardNodeIds = useMemo(() => {
const ids = new Set<string>()
for (const node of nodes) {
if (isFrankNode(node) && node.data.hiddenForwards) ids.add(node.id)
}

return ids
}, [nodes])

const revealedHiddenIds = useMemo(() => {
const revealed = new Set<string>()
if (hiddenForwardNodeIds.size === 0) return revealed
if (showAllForwards) {
for (const id of hiddenForwardNodeIds) revealed.add(id)
return revealed
}

if (hoveredNodeId !== null) {
if (hiddenForwardNodeIds.has(hoveredNodeId)) revealed.add(hoveredNodeId)
for (const edge of edges) {
if (edge.source === hoveredNodeId && hiddenForwardNodeIds.has(edge.target)) revealed.add(edge.target)
}
}

return revealed
}, [edges, hiddenForwardNodeIds, hoveredNodeId, showAllForwards])

const displayEdges = useMemo(() => {
if (hiddenForwardNodeIds.size === 0) return edges
const isHiddenAndNotRevealed = (nodeId: string) =>
hiddenForwardNodeIds.has(nodeId) && !revealedHiddenIds.has(nodeId)

return edges.map((edge) => {
const faded = isHiddenAndNotRevealed(edge.source) || isHiddenAndNotRevealed(edge.target)
return faded ? { ...edge, data: { ...edge.data, faded: true } } : edge
})
}, [edges, hiddenForwardNodeIds, revealedHiddenIds])

const displayNodes = useMemo(() => {
if (hiddenForwardNodeIds.size === 0) return nodes

return nodes.map((node) => {
if (!hiddenForwardNodeIds.has(node.id)) return node
const opacity = revealedHiddenIds.has(node.id) ? 1 : 0.5
return { ...node, style: { ...node.style, opacity, transition: 'opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)' } }
})
}, [nodes, hiddenForwardNodeIds, revealedHiddenIds])

const saveFlow = useCallback(async () => {
const { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } = useFlowStore.getState()
const flowData = { nodes: flowNodes, edges: flowEdges, viewport: flowViewport }
Expand Down Expand Up @@ -338,7 +391,7 @@
logApiError('Failed to save XML', error as Error)
setIdle()
}
}, [])

Check warning on line 394 in src/main/frontend/app/routes/studio/canvas/flow.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has missing dependencies: 'setIdle', 'setSaved', and 'setSaving'. Either include them or remove the dependency array

const autosaveEnabled = useSettingsStore((s) => s.general.autoSave.enabled)
const autosaveDelay = useSettingsStore((s) => s.general.autoSave.delayMs)
Expand Down Expand Up @@ -956,6 +1009,19 @@
return true
}, [isEditing, showNodeContextMenu])

const toggleSelectedHidden = useCallback(() => {
const selected = useFlowStore
.getState()
.nodes.filter((node): node is FrankNodeType => Boolean(node.selected) && isFrankNode(node))
if (selected.length === 0) return false

const shouldHide = selected.some((node) => !node.data.hiddenForwards)
useFlowStore.getState().setNodesHiddenForwards(
selected.map((node) => node.id),
shouldHide,
)
}, [])

useShortcut({
'studio.copy': () => copySelection(),
'studio.paste': () => pasteSelection(),
Expand All @@ -965,6 +1031,7 @@
'studio.redo-alt': () => useFlowStore.getState().redo(),
'studio.group': () => handleGrouping(),
'studio.ungroup': () => handleUngroup(),
'studio.hide': () => toggleSelectedHidden(),
'studio.save': () => void saveFlow(),
'studio.close-context': () => closeEditNodeContextOnEscape(),
'studio.delete': () => deleteSelection(),
Expand Down Expand Up @@ -1089,6 +1156,17 @@
],
)

const handleNodeMouseEnter = useCallback(
(_event: React.MouseEvent, node: FlowNode) => {
setHoveredNodeId(node.id)
},
[setHoveredNodeId],
)

const handleNodeMouseLeave = useCallback(() => {
setHoveredNodeId(null)
}, [setHoveredNodeId])

const handleEdgeClick = useCallback(() => {
setIsMultiSelect(false)
setSelectedStickyId(null)
Expand Down Expand Up @@ -1220,7 +1298,7 @@
setParentId(null)
}

function addNodeAtPosition(

Check warning on line 1301 in src/main/frontend/app/routes/studio/canvas/flow.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed
position: { x: number; y: number },
elementName: string,
sourceInfo?: { nodeId: string | null; handleId: string | null; handleType: 'source' | 'target' | null },
Expand Down Expand Up @@ -1664,8 +1742,8 @@
)}

<ReactFlow
nodes={nodes}
edges={edges}
nodes={displayNodes}
edges={displayEdges}
onViewportChange={(viewPort) => {
useFlowStore.getState().setViewport(viewPort)
}}
Expand All @@ -1675,6 +1753,8 @@
onReconnect={onReconnect}
onNodeClick={handleNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
onNodeMouseEnter={handleNodeMouseEnter}
onNodeMouseLeave={handleNodeMouseLeave}
onNodeDragStop={handleNodeDragStop}
onEdgeClick={handleEdgeClick}
onSelectionChange={handleSelectionChange}
Expand All @@ -1697,6 +1777,17 @@
minZoom={0.2}
>
<Controls position="top-left">
{hiddenForwardNodeIds.size > 0 && (
<ControlButton
onClick={toggleShowAllForwards}
title={showAllForwards ? 'Hide forwards again' : 'Temporarily show all forwards'}
>
<LightbulbIcon
className={showAllForwards ? 'fill-brand' : 'fill-foreground-muted'}
style={{ width: '100%', height: '100%' }}
/>
</ControlButton>
)}
<ControlButton onClick={handleAutoLayout} title="Auto layout">
<svg
viewBox="0 0 24 24"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type HandleProperties = {
onChangeType: (newType: string) => void
absolutePosition: { x: number; y: number }
typesAllowed?: Record<string, ElementProperty>
dimmed?: boolean
}

const HANDLE_TYPE_COLOURS: Record<string, string> = {
Expand Down Expand Up @@ -75,6 +76,8 @@ export function CustomHandle(properties: Readonly<HandleProperties>) {
backgroundColor: translateHandleTypeToColour(type),
border: '1px solid rgba(107, 114, 128, 0.5)',
pointerEvents: 'all',
opacity: properties.dimmed ? 0.5 : 1,
transition: 'opacity 150ms cubic-bezier(0.4, 0, 0.2, 1)',
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
} from '@xyflow/react'
import DangerIcon from '../../../../../icons/solar/Danger Triangle.svg?react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import useFlowStore from '~/stores/flow-store'
import { useShallow } from 'zustand/react/shallow'
import useFlowStore, { isFrankNode } from '~/stores/flow-store'
import { CustomHandle } from '~/routes/studio/canvas/nodetypes/components/handle'
import { FlowConfig } from '~/routes/studio/canvas/flow.config'
import { useNodeContextMenu } from '~/routes/studio/canvas/node-context-menu-context'
Expand Down Expand Up @@ -47,6 +48,7 @@
attributes?: Record<string, string>
children: ChildNode[]
manuallyResized?: boolean
hiddenForwards?: boolean
}> & {
width?: number
height?: number
Expand Down Expand Up @@ -95,6 +97,34 @@
const dangerTriangleReference = useRef<HTMLDivElement>(null)
const availableHandleTypes = useHandleTypes(frankElement?.forwards)

const hoveredNodeId = useNodeContextStore((state) => state.hoveredNodeId)
const showAllForwards = useNodeContextStore((state) => state.showAllForwards)
const edges = useFlowStore((state) => state.edges)
const hiddenForwardNodeIds = useFlowStore(
useShallow((state) =>
state.nodes.filter((node) => isFrankNode(node) && node.data.hiddenForwards).map((node) => node.id),
),
)

const dimmedHandleIndices = useMemo(() => {
const dimmed = new Set<number>()
if (hiddenForwardNodeIds.length === 0) return dimmed
const hiddenSet = new Set(hiddenForwardNodeIds)
const isRevealed = (targetId: string) =>
showAllForwards ||
hoveredNodeId === targetId ||
(hoveredNodeId !== null && edges.some((edge) => edge.source === hoveredNodeId && edge.target === targetId))
Comment on lines +113 to +116

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should be its own function


for (const edge of edges) {
if (edge.source !== properties.id || !hiddenSet.has(edge.target) || isRevealed(edge.target)) continue

const handleIndex = Number(edge.sourceHandle)
if (!Number.isNaN(handleIndex)) dimmed.add(handleIndex)
}

return dimmed
}, [edges, hiddenForwardNodeIds, hoveredNodeId, showAllForwards, properties.id])

const allowedChildNames = useMemo(
() => (xsdDoc ? new Set(getAllowedChildElementsForElement(xsdDoc, properties.data.subtype)) : null),
[xsdDoc, properties.data.subtype],
Expand Down Expand Up @@ -346,7 +376,7 @@

addChild(properties.id, child)
},
[

Check warning on line 379 in src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has missing dependencies: 'setAttributes' and 'setNodeId'. Either include them or remove the dependency array
properties.id,
addChild,
setIsNewNode,
Expand Down Expand Up @@ -573,6 +603,7 @@
onChangeType={(newType) => changeHandleType(handle.index, newType)}
absolutePosition={{ x: properties.positionAbsoluteX, y: properties.positionAbsoluteY }}
typesAllowed={frankElement?.forwards}
dimmed={dimmedHandleIndices.has(handle.index)}
/>
))}
{/* Only show the add handle button if there are available handle types that are not yet used on this node */}
Expand Down
19 changes: 19 additions & 0 deletions src/main/frontend/app/routes/studio/context/node-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useShortcut } from '~/hooks/use-shortcut'
import useFlowStore, { isFrankNode } from '~/stores/flow-store'
import Button from '~/components/inputs/button'
import Toggle from '~/components/inputs/toggle'
import { useShallow } from 'zustand/react/shallow'
import ContextInput from './context-input'
import { findChildRecursive } from '~/stores/child-utilities'
Expand Down Expand Up @@ -312,6 +313,15 @@ export default function NodeContext({

const currentName = inputValues['name'] ?? ''

const editedNode = !parentId && !childParentId ? nodes.find((node) => node.id === nodeId.toString()) : undefined
const canHideForwards = !!editedNode && isFrankNode(editedNode) && !isNewNode
const hiddenForwards = editedNode && isFrankNode(editedNode) ? Boolean(editedNode.data.hiddenForwards) : false

const handleToggleHiddenForwards = (value: boolean) => {
useFlowStore.getState().setNodesHiddenForwards([nodeId.toString()], value)
void useNodeContextStore.getState().saveFlow?.()
}

const categorizedAttributes = (() => {
if (!attributes) return []

Expand Down Expand Up @@ -347,6 +357,15 @@ export default function NodeContext({
<>
<div className="flex-1 overflow-y-auto px-4">
{currentName && <h2 className="mb-2 font-semibold">{currentName}</h2>}
{canHideForwards && (
<div className="bg-background mb-4 flex items-center justify-between gap-4 rounded-md p-4">
<div>
<p className="text-sm font-medium">Hide forwards</p>
<p className="text-foreground-muted text-xs">Hide this node&apos;s forwards to declutter the canvas</p>
</div>
<Toggle checked={hiddenForwards} onChange={handleToggleHiddenForwards} />
</div>
)}
<div className="bg-background w-full space-y-4 rounded-md p-6">
{categorizedAttributes.map(([key, attribute, originalIndex]) => (
<div key={originalIndex}>
Expand Down
2 changes: 2 additions & 0 deletions src/main/frontend/app/routes/studio/flow-to-xml-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type NodeData = {
attributes?: Record<string, string>
sourceHandles?: []
children?: ChildNode[]
hiddenForwards?: boolean
}

function hasDataProperty(node: FlowNode): node is Extract<FlowNode, { data: NodeData }> {
Expand Down Expand Up @@ -183,6 +184,7 @@ function generateXmlElement(
'flow:x': String(roundedX),
'flow:y': String(roundedY),
...(height === null ? {} : { 'flow:width': String(width), 'flow:height': String(height) }),
...((node.data as NodeData).hiddenForwards ? { 'flow:hiddenForwards': 'true' } : {}),
}
const attrStr = Object.entries(allAttrs)
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
Expand Down
13 changes: 11 additions & 2 deletions src/main/frontend/app/routes/studio/xml-to-json-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
/**
* Handles creating edges from a set of <Forward> elements
*/
function addForwardEdges(

Check warning on line 253 in src/main/frontend/app/routes/studio/xml-to-json-parser.ts

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Refactor this function to reduce its Cognitive Complexity from 21 to the 15 allowed
forwards: Element[],
sourceId: string,
nameToId: Map<string, string>,
Expand Down Expand Up @@ -559,7 +559,7 @@
const thisId = (idCounter.current++).toString()
const { subtype, usedClassName } = translateElementFromOldToNewFormat(element)

const { attributes, name, x, y, width, height } = parseElementAttributes(
const { attributes, name, x, y, width, height, hiddenForwards } = parseElementAttributes(
element.attributes,
FlowConfig.NODE_DEFAULT_WIDTH,
FlowConfig.NODE_MIN_HEIGHT,
Expand All @@ -582,6 +582,7 @@
sourceHandles,
attributes: Object.keys(attributes).length > 0 ? attributes : undefined,
manuallyResized,
hiddenForwards: hiddenForwards || undefined,
},
}
}
Expand Down Expand Up @@ -631,8 +632,8 @@

const x = parseNumericAttribute(note.getAttribute('flow:x'), 0)
const y = parseNumericAttribute(note.getAttribute('flow:y'), 0)
const width = parseNumericAttribute(note.getAttribute('flow:width'), FlowConfig.STICKY_NOTE_DEFAULT_WIDTH)

Check warning on line 635 in src/main/frontend/app/routes/studio/xml-to-json-parser.ts

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Define a constant instead of duplicating this literal 3 times
const height = parseNumericAttribute(note.getAttribute('flow:height'), FlowConfig.STICKY_NOTE_DEFAULT_HEIGHT)

Check warning on line 636 in src/main/frontend/app/routes/studio/xml-to-json-parser.ts

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Define a constant instead of duplicating this literal 3 times

const collapsed = note.getAttribute('flow:collapsed') === 'true'
const attachedToName = note.getAttribute('flow:attachedTo') || null
Expand Down Expand Up @@ -764,7 +765,7 @@
return edges.some((edge) => edge.target === nodeId)
}

function parseElementAttributes(

Check warning on line 768 in src/main/frontend/app/routes/studio/xml-to-json-parser.ts

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed
attrs: NamedNodeMap,
defaultWidth: number,
defaultHeight: number,
Expand All @@ -777,6 +778,7 @@
let y = 0
let width = defaultWidth
let height: number | undefined = undefined
let hiddenForwards = false

for (const attr of attrs) {
const attrName = attr.name
Expand Down Expand Up @@ -806,16 +808,22 @@
width = parseNumericAttribute(value, defaultWidth)
continue
}

if (attrName === 'flow:height') {
height = parseNumericAttribute(value, defaultHeight)
continue
}

if (attrName === 'flow:hiddenForwards') {
hiddenForwards = value === 'true'
continue
}

// Store all other attributes
attributes[attrName] = value
}

return { attributes, name, x, y, width, height }
return { attributes, name, x, y, width, height, hiddenForwards }
}

type FrankEdge = {
Expand All @@ -834,4 +842,5 @@
y: number
width: number
height: number | undefined
hiddenForwards: boolean
}
Loading
Loading