From 5ab4b9a9e346e82f85a471a3411ab23b7d707290 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Wed, 13 May 2026 11:12:14 +0200 Subject: [PATCH 1/3] Add group node color and description support; enhance multi-selection context handling --- src/main/frontend/app/app.css | 14 ++ .../app/routes/studio/canvas/flow.tsx | 50 ++++++- .../studio/canvas/nodetypes/frank-node.tsx | 4 +- .../studio/canvas/nodetypes/group-node.tsx | 44 ++++-- .../routes/studio/context/group-context.tsx | 84 +++++++++++ .../app/routes/studio/flow-to-xml-parser.ts | 14 +- .../frontend/app/routes/studio/studio.tsx | 139 ++++++++++-------- .../app/routes/studio/xml-to-json-parser.ts | 6 +- src/main/frontend/app/stores/flow-store.ts | 32 +++- .../frontend/app/stores/node-context-store.ts | 4 + .../frontend/src/assets/xsd/FlowConfig.xsd | 8 +- 11 files changed, 308 insertions(+), 91 deletions(-) create mode 100644 src/main/frontend/app/routes/studio/context/group-context.tsx diff --git a/src/main/frontend/app/app.css b/src/main/frontend/app/app.css index fc5078fe..ed3e8003 100644 --- a/src/main/frontend/app/app.css +++ b/src/main/frontend/app/app.css @@ -60,6 +60,13 @@ --sticky-color-purple: #e9d5ff; --sticky-color-orange: #fed7aa; + --group-color-blue: #93c5fd; + --group-color-violet: #c4b5fd; + --group-color-rose: #fda4af; + --group-color-green: #86efac; + --group-color-amber: #fcd34d; + --group-color-cyan: #67e8f9; + /* Palette Styling */ --palette-pipes: #68d250; --palette-listeners: #d250bf; @@ -110,6 +117,13 @@ --sticky-color-purple: #581c87; --sticky-color-orange: #9a3412; + --group-color-blue: #1d4ed8; + --group-color-violet: #6d28d9; + --group-color-rose: #be123c; + --group-color-green: #15803d; + --group-color-amber: #b45309; + --group-color-cyan: #0e7490; + --palette-pipes: #136502; --palette-listeners: #853279; --palette-senders: #1c7c6a; diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index f9438836..1fafa01a 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -122,6 +122,7 @@ function FlowCanvas() { setDropSuccessful, setIsMultiSelect, setSelectedStickyId, + setSelectedGroupId, } = useNodeContextStore( useShallow((s) => ({ isEditing: s.isEditing, @@ -139,6 +140,7 @@ function FlowCanvas() { setIsMultiSelect: s.setIsMultiSelect, setSelectedStickyId: s.setSelectedStickyId, selectedStickyId: s.selectedStickyId, + setSelectedGroupId: s.setSelectedGroupId, })), ) const { elements } = useFFDoc() @@ -461,6 +463,7 @@ function FlowCanvas() { if (fullySelectedGroupIds.length > 1) { handleMultiGroupMerge(fullySelectedGroupIds, selectedNodes) + showNodeContextMenu(true) return } @@ -468,10 +471,12 @@ function FlowCanvas() { if (shouldMergeUngroupedIntoGroup(selectedNodes)) { handleMergeUngroupedIntoGroup(selectedNodes) + showNodeContextMenu(true) return } groupNodes(selectedNodes, nodes) + showNodeContextMenu(true) }, [ nodes, allSelectedInSameGroup, @@ -479,6 +484,7 @@ function FlowCanvas() { handleMergeUngroupedIntoGroup, handleMultiGroupMerge, shouldMergeUngroupedIntoGroup, + showNodeContextMenu, ]) const copySelection = useCallback(() => { @@ -652,6 +658,14 @@ function FlowCanvas() { if (node.type === 'stickyNote') { setSelectedStickyId(node.id) + setSelectedGroupId(null) + showNodeContextMenu(true) + return + } + + if (node.type === 'groupNode') { + setSelectedGroupId(node.id) + setSelectedStickyId(null) showNodeContextMenu(true) return } @@ -661,12 +675,21 @@ function FlowCanvas() { if (frankElement) { deselectOtherNodes(node.id) setSelectedStickyId(null) + setSelectedGroupId(null) applyNodeContext(node, frankElement) showNodeContextMenu(true) } } }, - [isDirty, lookupFrankElement, deselectOtherNodes, applyNodeContext, showNodeContextMenu, setSelectedStickyId], + [ + isDirty, + lookupFrankElement, + deselectOtherNodes, + applyNodeContext, + showNodeContextMenu, + setSelectedStickyId, + setSelectedGroupId, + ], ) const handleNodeDoubleClick = useCallback( @@ -703,21 +726,31 @@ function FlowCanvas() { const handleEdgeClick = useCallback(() => { setIsMultiSelect(false) setSelectedStickyId(null) + setSelectedGroupId(null) showNodeContextMenu(false) setIsEditing(false) - }, [setIsMultiSelect, setSelectedStickyId, showNodeContextMenu, setIsEditing]) + }, [setIsMultiSelect, setSelectedStickyId, setSelectedGroupId, showNodeContextMenu, setIsEditing]) const handleSelectionChange = useCallback( ({ nodes: selectedNodes }: { nodes: FlowNode[] }) => { const frankNodes = selectedNodes.filter((n) => isFrankNode(n)) if (frankNodes.length > 1) { + const firstParent = frankNodes[0]?.parentId + const allInSameGroup = Boolean(firstParent) && frankNodes.every((n) => n.parentId === firstParent) + setIsMultiSelect(true) setSelectedStickyId(null) - showNodeContextMenu(false) + setSelectedGroupId(null) setIsEditing(false) setParentId(null) setChildParentId(null) + + if (allInSameGroup) { + showNodeContextMenu(true) + } else { + showNodeContextMenu(false) + } return } @@ -727,6 +760,7 @@ function FlowCanvas() { const frankElement = lookupFrankElement((frankNodes[0] as FrankNodeType).data.subtype) if (!frankElement) return setSelectedStickyId(null) + setSelectedGroupId(null) applyNodeContext(frankNodes[0] as FrankNodeType, frankElement) showContextIfSidebarOpen() } @@ -736,6 +770,7 @@ function FlowCanvas() { setIsEditing, setIsMultiSelect, setSelectedStickyId, + setSelectedGroupId, setParentId, setChildParentId, lookupFrankElement, @@ -1066,13 +1101,19 @@ function FlowCanvas() { const unsub = useFlowStore.subscribe( (state) => state.nodes, (nodes) => { - const { selectedStickyId, setSelectedStickyId, setIsEditing } = useNodeContextStore.getState() + const { selectedStickyId, setSelectedStickyId, selectedGroupId, setSelectedGroupId, setIsEditing } = + useNodeContextStore.getState() if (selectedStickyId && !nodes.some((node) => node.id === selectedStickyId)) { setSelectedStickyId(null) showNodeContextMenu(false) setIsEditing(false) } + + if (selectedGroupId && !nodes.some((node) => node.id === selectedGroupId)) { + setSelectedGroupId(null) + showNodeContextMenu(false) + } }, ) return () => unsub() @@ -1158,6 +1199,7 @@ function FlowCanvas() { onPaneClick={() => { setContextMenu(null) setSelectedStickyId(null) + setSelectedGroupId(null) if (!isDirty) { showNodeContextMenu(false) setIsEditing(false) diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx index 1a994b1d..0eb1731f 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/frank-node.tsx @@ -492,9 +492,7 @@ export default function FrankNode(properties: NodeProps) { {properties.data.attributes && Object.entries(properties.data.attributes).map(([key, value]) => (
-

- {key} -

+

{key}

{value}

))} diff --git a/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx b/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx index 1671b2a8..edce617a 100644 --- a/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx +++ b/src/main/frontend/app/routes/studio/canvas/nodetypes/group-node.tsx @@ -3,8 +3,21 @@ import { useState, type ChangeEvent } from 'react' import { ResizeIcon } from '~/routes/studio/canvas/nodetypes/frank-node' import useFlowStore from '~/stores/flow-store' +export const GROUP_COLORS = [ + { label: 'Blue', value: 'var(--group-color-blue)' }, + { label: 'Violet', value: 'var(--group-color-violet)' }, + { label: 'Rose', value: 'var(--group-color-rose)' }, + { label: 'Green', value: 'var(--group-color-green)' }, + { label: 'Amber', value: 'var(--group-color-amber)' }, + { label: 'Cyan', value: 'var(--group-color-cyan)' }, +] + +export const GROUP_DEFAULT_COLOR = 'var(--group-color-blue)' + export type GroupNode = Node<{ label: string + description?: string + color?: string width: number height: number childrenNames?: string[] @@ -16,15 +29,20 @@ export default function GroupNodeComponent({ id, data, selected }: NodeProps { + setEditValue(data.label) + setIsEditing(true) + } const handleBlur = () => setIsEditing(false) - const handleClick = () => setIsEditing(true) const handleSave = (event: ChangeEvent) => { - setGroupnodeLabel(id, event.target.value) - setLabel(event.target.value) + setEditValue(event.target.value) + useFlowStore.getState().setGroupnodeLabel(id, event.target.value) } const handleKeyDown = (event: React.KeyboardEvent) => { @@ -36,7 +54,7 @@ export default function GroupNodeComponent({ id, data, selected }: NodeProps { + onResize={(_event, resizeData) => { setDimensions({ width: resizeData.width, height: resizeData.height, @@ -47,16 +65,18 @@ export default function GroupNodeComponent({ id, data, selected }: NodeProps
@@ -67,7 +87,7 @@ export default function GroupNodeComponent({ id, data, selected }: NodeProps ) : ( <> -
- {label} +
+ {data.label}
-
+
diff --git a/src/main/frontend/app/routes/studio/context/group-context.tsx b/src/main/frontend/app/routes/studio/context/group-context.tsx new file mode 100644 index 00000000..df80766e --- /dev/null +++ b/src/main/frontend/app/routes/studio/context/group-context.tsx @@ -0,0 +1,84 @@ +import useFlowStore, { isGroupNode } from '~/stores/flow-store' +import { GROUP_COLORS, GROUP_DEFAULT_COLOR } from '~/routes/studio/canvas/nodetypes/group-node' +import { ALL_SHORTCUTS, formatShortcutParts, useShortcutStore } from '~/stores/shortcut-store' +import Button from '~/components/inputs/button' + +export default function GroupContext({ nodeId }: Readonly<{ nodeId: string }>) { + const node = useFlowStore((state) => state.nodes.find((node) => node.id === nodeId)) + const platform = useShortcutStore((state) => state.platform) + + if (!node || !isGroupNode(node)) return null + + const { label, description = '', color = GROUP_DEFAULT_COLOR } = node.data + + const ungroupDef = ALL_SHORTCUTS.find((state) => state.id === 'studio.ungroup')! + const ungroupParts = formatShortcutParts(ungroupDef, platform) + const triggerUngroup = () => useShortcutStore.getState().shortcuts.get('studio.ungroup')?.handler?.() + + return ( +
+
+ +
+ +
+ +
+
+ + useFlowStore.getState().setGroupnodeLabel(nodeId, changeEvent.target.value)} + className="border-border bg-background text-foreground focus:ring-ring w-full rounded border px-3 py-2 text-sm focus:ring-1 focus:outline-none" + /> +
+ +
+ +