From d4e2c538657e56bbd25455cdc6b08ab6a565bdaf Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Tue, 16 Jun 2026 13:51:03 +0200 Subject: [PATCH] Enhance adapter glyph handling and improve flow tab management --- .../frontend/app/actions/navigationActions.ts | 8 ++- src/main/frontend/app/app.css | 24 +++++++ .../frontend/app/routes/editor/editor.tsx | 65 +++++++------------ .../frontend/app/routes/editor/xml-utils.ts | 20 +++++- .../app/routes/studio/canvas/flow.tsx | 40 ++++++++++-- .../app/routes/studio/xml-to-json-parser.ts | 14 ++-- src/main/frontend/app/stores/tab-store.ts | 1 + 7 files changed, 116 insertions(+), 56 deletions(-) diff --git a/src/main/frontend/app/actions/navigationActions.ts b/src/main/frontend/app/actions/navigationActions.ts index 0f4a1d29..259ab5cf 100644 --- a/src/main/frontend/app/actions/navigationActions.ts +++ b/src/main/frontend/app/actions/navigationActions.ts @@ -10,12 +10,16 @@ export function openInStudio( const tabId = `${filepath}::${adapterName}::${adapterPosition}` - if (!getTab(tabId)) { + const existing = getTab(tabId) + if (existing) { + setTabData(tabId, { ...existing, pendingRecenter: true, pendingNodeSelection: null }) + } else { setTabData(tabId, { name: adapterName, configurationPath: filepath, adapterPosition, flowJson: {}, + pendingRecenter: true, }) } @@ -78,7 +82,7 @@ export function openInStudioAtNode( const existing = getTab(tabId) if (existing) { - setTabData(tabId, { ...existing, pendingNodeSelection: { subtype, name } }) + setTabData(tabId, { ...existing, pendingNodeSelection: { subtype, name }, pendingRecenter: null }) } else { setTabData(tabId, { name: adapterName, diff --git a/src/main/frontend/app/app.css b/src/main/frontend/app/app.css index 3f141b35..b29ac09e 100644 --- a/src/main/frontend/app/app.css +++ b/src/main/frontend/app/app.css @@ -177,6 +177,30 @@ body { background-repeat: no-repeat; } +.monaco-editor .frank-adapter-glyph { + cursor: pointer !important; + display: flex !important; + align-items: center; + justify-content: center; + opacity: 0.6; + transition: opacity 0.15s; +} + +.monaco-editor .frank-adapter-glyph:hover { + opacity: 1; +} + +.monaco-editor .frank-adapter-glyph::after { + content: ''; + display: block; + width: 13px; + height: 13px; + margin-left: 5px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect x='1' y='1' width='14' height='14' rx='3' fill='%232563eb'/%3E%3Cpath d='M6 5l5 3-5 3V5z' fill='white'/%3E%3C/svg%3E"); + background-size: 13px 13px; + background-repeat: no-repeat; +} + .monaco-editor .xml-lint.xml-lint--fatal-error { border-color: #ff2424; } diff --git a/src/main/frontend/app/routes/editor/editor.tsx b/src/main/frontend/app/routes/editor/editor.tsx index 06239508..44ae59e7 100644 --- a/src/main/frontend/app/routes/editor/editor.tsx +++ b/src/main/frontend/app/routes/editor/editor.tsx @@ -1,4 +1,3 @@ -import RulerCrossPenIcon from '/icons/solar/Ruler Cross Pen.svg?react' import Editor, { type Monaco, type OnMount } from '@monaco-editor/react' type ITextModel = Monaco['editor']['ITextModel'] type FindMatch = Monaco['editor']['FindMatch'] @@ -15,7 +14,6 @@ import { openInStudio } from '~/actions/navigationActions' import EditorFileStructure from '~/components/file-structure/editor-file-structure' import DiffTabView from '~/components/git/diff-tab-view' import GitPanel from '~/components/git/git-panel' -import Button from '~/components/inputs/button' import SegmentedButton from '~/components/inputs/segmented-button' import SidebarContentClose from '~/components/sidebars-layout/sidebar-content-close' import SidebarHeader from '~/components/sidebars-layout/sidebar-header' @@ -35,13 +33,11 @@ import { useSettingsStore } from '~/stores/settings-store' import { logApiError } from '~/utils/logger' import flowXsd from '../../../src/assets/xsd/FlowConfig.xsd?raw' import { + ADAPTER_GLYPH_SUBTYPE, extractFlowElements, - findAdapterIndexAtOffset, - findAdaptersInXml, findElementRangeInXml, findFrankElementsForGlyphs, findFlowElementsStartLine, - lineToOffset, wrapFlowXml, } from './xml-utils' import { openInStudioAtNode } from '~/actions/navigationActions' @@ -323,13 +319,18 @@ export default function CodeEditor() { const elements = findFrankElementsForGlyphs(content) frankElementsRef.current = elements - const decorations = elements.map((element) => ({ - range: { startLineNumber: element.startLine, startColumn: 1, endLineNumber: element.startLine, endColumn: 1 }, - options: { - glyphMarginClassName: 'frank-node-glyph', - glyphMarginHoverMessage: { value: `Open **${element.name}** in Studio` }, - }, - })) + const decorations = elements.map((element) => { + const isAdapter = element.subtype === ADAPTER_GLYPH_SUBTYPE + return { + range: { startLineNumber: element.startLine, startColumn: 1, endLineNumber: element.startLine, endColumn: 1 }, + options: { + glyphMarginClassName: isAdapter ? 'frank-adapter-glyph' : 'frank-node-glyph', + glyphMarginHoverMessage: { + value: isAdapter ? `Open adapter **${element.name}** in Studio` : `Open **${element.name}** in Studio`, + }, + }, + } + }) if (frankGlyphsDecorationsRef.current) { frankGlyphsDecorationsRef.current.set(decorations) @@ -571,6 +572,15 @@ export default function CodeEditor() { const { adapterName, adapterPosition, subtype, name } = element + if (subtype === ADAPTER_GLYPH_SUBTYPE) { + openInStudio(navigate, { + adapterName, + adapterPosition, + filepath: editorTab.configurationPath, + }) + return + } + openInStudioAtNode(navigate, { adapterName, adapterPosition, @@ -742,27 +752,6 @@ export default function CodeEditor() { ]) }, [pendingHighlight, fileContent, isDiffTab, editorMounted]) - const handleOpenInStudio = useCallback(() => { - const editorTab = useEditorTabStore.getState().getTab(activeTabFilePath) - if (!editorTab) return - - const xml = editorReference.current?.getValue() || fileContent - if (!xml) return - - const adapters = findAdaptersInXml(xml) - if (adapters.length === 0) return - - const cursorLine = editorReference.current?.getPosition()?.lineNumber - const adapterPosition = - adapters.length === 1 || !cursorLine ? 0 : findAdapterIndexAtOffset(adapters, lineToOffset(xml, cursorLine)) - - openInStudio(navigate, { - adapterName: adapters[adapterPosition].name, - filepath: editorTab.configurationPath, - adapterPosition, - }) - }, [activeTabFilePath, fileContent, navigate]) - const isGitRepo = !!project?.isGitRepository return ( @@ -802,16 +791,8 @@ export default function CodeEditor() { ) : (
-
+
-
= 0; i--) { if (adapters[i].offset <= cursorOffset) return i @@ -243,7 +255,13 @@ export function findFrankElementsForGlyphs(xml: string): FrankElementLocation[] if (adapters.length === 0) return [] const lines = xml.split('\n') - const results: FrankElementLocation[] = [] + const results: FrankElementLocation[] = adapters.map((adapter, adapterPosition) => ({ + subtype: ADAPTER_GLYPH_SUBTYPE, + name: adapter.name, + startLine: offsetToLine(xml, adapter.offset), + adapterName: adapter.name, + adapterPosition, + })) const tagStack: string[] = [] for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 3fe669dc..fcd2c966 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -243,6 +243,8 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { const canvasRef = useRef(null) const fitAfterLayoutRef = useRef<{ id: string }[] | null>(null) const pendingInitialRelayoutRef = useRef<{ pendingSelection: { subtype: string; name: string } | null } | null>(null) + const [relayoutNonce, setRelayoutNonce] = useState(0) + const loadedTabIdRef = useRef(null) const applySelectionToNodes = useCallback((pendingSelection: { subtype: string; name: string }) => { const currentNodes = useFlowStore.getState().nodes @@ -702,6 +704,7 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { } }, [ nodesInitialized, + relayoutNonce, layoutGraph, waitForStableCanvasDimensions, computeAdapterCenteredViewport, @@ -1451,10 +1454,26 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { flowStore.resetStore() } - function loadFromCache(tab: TabData, pendingSelection: { subtype: string; name: string } | null) { + function loadFromCache( + tab: TabData, + pendingSelection: { subtype: string; name: string } | null, + recenter: boolean, + ) { const hasPendingSelection = !!pendingSelection - restoreFlowFromTab(tab, { skipViewport: hasPendingSelection, forceRemeasure: hasPendingSelection }) - if (pendingSelection) applySelectionToNodes(pendingSelection) + restoreFlowFromTab(tab, { + skipViewport: hasPendingSelection || recenter, + forceRemeasure: hasPendingSelection, + }) + if (pendingSelection) { + applySelectionToNodes(pendingSelection) + } else if (recenter) { + const cachedNodes = useFlowStore.getState().nodes + waitForStableCanvasDimensions((canvasWidth, canvasHeight) => { + const viewport = computeAdapterCenteredViewport(cachedNodes, canvasWidth, canvasHeight) + useFlowStore.getState().setViewport(viewport) + reactFlowRef.current?.setViewport(viewport) + }) + } } async function loadFromApi(tab: TabData, pendingSelection: { subtype: string; name: string } | null) { @@ -1476,21 +1495,28 @@ function FlowCanvas({ onOpenInEditor }: { onOpenInEditor: () => void }) { flowStore.setHistory([]) flowStore.setFuture([]) pendingInitialRelayoutRef.current = { pendingSelection } + setRelayoutNonce((nonce) => nonce + 1) } async function loadFlowFromTab(tab: TabData) { + const tabId = useTabStore.getState().activeTab + const pendingSelection = tab.pendingNodeSelection ?? null + const recenter = tab.pendingRecenter ?? false + + if (tabId === loadedTabIdRef.current && !pendingSelection && !recenter) return + loadedTabIdRef.current = tabId + isLoadingTabRef.current = true setLoading(true) - const pendingSelection = tab.pendingNodeSelection ?? null - if (pendingSelection) { - useTabStore.getState().setTabData(useTabStore.getState().activeTab, { ...tab, pendingNodeSelection: null }) + if (pendingSelection || recenter) { + useTabStore.getState().setTabData(tabId, { ...tab, pendingNodeSelection: null, pendingRecenter: null }) } try { const hasCachedFlow = tab.flowJson && Object.keys(tab.flowJson).length > 0 if (hasCachedFlow) { - loadFromCache(tab, pendingSelection) + loadFromCache(tab, pendingSelection, recenter) } else if (tab.configurationPath && tab.name) { await loadFromApi(tab, pendingSelection) } diff --git a/src/main/frontend/app/routes/studio/xml-to-json-parser.ts b/src/main/frontend/app/routes/studio/xml-to-json-parser.ts index d1393d60..2c12594c 100644 --- a/src/main/frontend/app/routes/studio/xml-to-json-parser.ts +++ b/src/main/frontend/app/routes/studio/xml-to-json-parser.ts @@ -10,6 +10,14 @@ import type { GroupNode } from './canvas/nodetypes/group-node' import { isFrankNode } from '~/stores/flow-store' import type { StickyNote } from './canvas/nodetypes/sticky-note' +function parseConfigurationXml(xmlString: string): Document { + const withFlowNamespace = /\bxmlns:flow\s*=/.test(xmlString) + ? xmlString + : xmlString.replace(/<([A-Za-z_][\w.-]*)/, (_match, tagName) => `<${tagName} xmlns:flow="urn:frank-flow"`) + + return new DOMParser().parseFromString(withFlowNamespace, 'text/xml') +} + interface IdCounter { current: number } @@ -26,8 +34,7 @@ export interface AdapterInfo { export async function getAdaptersFromConfiguration(projectName: string, filepath: string): Promise { const xmlString = await fetchConfigurationFileCached(projectName, filepath) - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(xmlString, 'text/xml') + const xmlDoc = parseConfigurationXml(xmlString) const adapters: AdapterInfo[] = [] const adapterElements = xmlDoc.querySelectorAll('Adapter, adapter') @@ -63,8 +70,7 @@ export async function getAdapterFromConfiguration( adapterPosition?: number, ): Promise { const xmlString = await fetchConfigurationFileCached(projectname, filename) - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(xmlString, 'text/xml') + const xmlDoc = parseConfigurationXml(xmlString) const adapterList = [...xmlDoc.querySelectorAll('Adapter, adapter')] diff --git a/src/main/frontend/app/stores/tab-store.ts b/src/main/frontend/app/stores/tab-store.ts index 0430822d..fa34e69b 100644 --- a/src/main/frontend/app/stores/tab-store.ts +++ b/src/main/frontend/app/stores/tab-store.ts @@ -11,6 +11,7 @@ export interface TabData { history?: FlowSnapshot[] future?: FlowSnapshot[] pendingNodeSelection?: { subtype: string; name: string } | null + pendingRecenter?: boolean | null } interface TabStoreState {