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
8 changes: 6 additions & 2 deletions src/main/frontend/app/actions/navigationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/main/frontend/app/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
65 changes: 23 additions & 42 deletions src/main/frontend/app/routes/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -15,7 +14,6 @@
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'
Expand All @@ -35,13 +33,11 @@
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'
Expand Down Expand Up @@ -323,13 +319,18 @@
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)
Expand Down Expand Up @@ -377,7 +378,7 @@
})
}
},
[project, activeTabFilePath, isDiffTab],

Check warning on line 381 in src/main/frontend/app/routes/editor/editor.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 flushPendingSave = useCallback(() => {
Expand Down Expand Up @@ -571,6 +572,15 @@

const { adapterName, adapterPosition, subtype, name } = element

if (subtype === ADAPTER_GLYPH_SUBTYPE) {
openInStudio(navigate, {
adapterName,
adapterPosition,
filepath: editorTab.configurationPath,
})
return
}

openInStudioAtNode(navigate, {
adapterName,
adapterPosition,
Expand Down Expand Up @@ -742,27 +752,6 @@
])
}, [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 (
Expand Down Expand Up @@ -802,16 +791,8 @@
<DiffTabView diffData={activeTab.diffData} />
) : (
<div className="flex h-full flex-col">
<div className="border-b-border bg-background flex h-10 shrink-0 items-center justify-between border-b px-3">
<div className="border-b-border bg-background flex h-10 shrink-0 items-center border-b px-3">
<SaveStatusIndicator />
<Button
onClick={handleOpenInStudio}
className="flex items-center gap-1.5 text-xs"
title="Open in Studio"
>
<RulerCrossPenIcon className="h-3.5 w-3.5 fill-current" />
Open in Studio
</Button>
</div>
<div className="relative min-h-0 flex-1">
<Editor
Expand Down
20 changes: 19 additions & 1 deletion src/main/frontend/app/routes/editor/xml-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
adapterPosition: number
}

export const ADAPTER_GLYPH_SUBTYPE = 'Adapter'

const STRUCTURAL_TAGS = new Set([
'Adapter',
'Configuration',
Expand Down Expand Up @@ -44,7 +46,7 @@
return tag.charAt(0).toUpperCase() + tag.slice(1)
}

function analyzeTagStructure(lines: string[], startLine: number): { isSelfClosing: boolean; endLine: number } {

Check warning on line 49 in src/main/frontend/app/routes/editor/xml-utils.ts

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Refactor this function to reduce its Cognitive Complexity from 26 to the 15 allowed
let isInsideString = false
let stringDelimiter = ''
let isInsideTag = false
Expand Down Expand Up @@ -142,6 +144,16 @@
return offset
}

export function offsetToLine(xml: string, offset: number): number {
let line = 1

for (let i = 0; i < offset && i < xml.length; i++) {
if (xml[i] === '\n') line++
}

return line
}

export function findAdapterIndexAtOffset(adapters: AdapterLocation[], cursorOffset: number): number {
for (let i = adapters.length - 1; i >= 0; i--) {
if (adapters[i].offset <= cursorOffset) return i
Expand Down Expand Up @@ -243,7 +255,13 @@
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++) {
Expand Down
40 changes: 33 additions & 7 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@
const canvasRef = useRef<HTMLDivElement>(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<string | null>(null)

const applySelectionToNodes = useCallback((pendingSelection: { subtype: string; name: string }) => {
const currentNodes = useFlowStore.getState().nodes
Expand Down Expand Up @@ -336,7 +338,7 @@
logApiError('Failed to save XML', error as Error)
setIdle()
}
}, [])

Check warning on line 341 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 @@ -702,6 +704,7 @@
}
}, [
nodesInitialized,
relayoutNonce,
layoutGraph,
waitForStableCanvasDimensions,
computeAdapterCenteredViewport,
Expand Down Expand Up @@ -1217,7 +1220,7 @@
setParentId(null)
}

function addNodeAtPosition(

Check warning on line 1223 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 @@ -1451,10 +1454,26 @@
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) {
Expand All @@ -1476,21 +1495,28 @@
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)
}
Expand Down
14 changes: 10 additions & 4 deletions src/main/frontend/app/routes/studio/xml-to-json-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
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
}
Expand All @@ -26,8 +34,7 @@

export async function getAdaptersFromConfiguration(projectName: string, filepath: string): Promise<AdapterInfo[]> {
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')
Expand Down Expand Up @@ -63,8 +70,7 @@
adapterPosition?: number,
): Promise<Element | null> {
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')]

Expand Down Expand Up @@ -244,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 @@ -622,8 +628,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 631 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 632 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
1 change: 1 addition & 0 deletions src/main/frontend/app/stores/tab-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface TabData {
history?: FlowSnapshot[]
future?: FlowSnapshot[]
pendingNodeSelection?: { subtype: string; name: string } | null
pendingRecenter?: boolean | null
}

interface TabStoreState {
Expand Down
Loading