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
36 changes: 15 additions & 21 deletions src/main/frontend/app/routes/editor/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import RulerCrossPenIcon from '/icons/solar/Ruler Cross Pen.svg?react'
import Editor, { type Monaco, type OnMount } from '@monaco-editor/react'
import prettier from 'prettier/standalone'
import prettierPluginXml from '@prettier/plugin-xml'
type ITextModel = Monaco['editor']['ITextModel']
type FindMatch = Monaco['editor']['FindMatch']
type IModelDeltaDecoration = Monaco['editor']['IModelDeltaDecoration']
Expand Down Expand Up @@ -164,14 +162,6 @@
return fileExtension === 'xml'
}

function prettierFormat(xml: string): Promise<string> {
return prettier.format(xml, {
parser: 'xml',
plugins: [prettierPluginXml],
tabWidth: 2,
})
}

async function validateFlow(content: string, model: ITextModel): Promise<ValidationError[]> {
const flowFragment = extractFlowElements(content)
if (!flowFragment) return []
Expand Down Expand Up @@ -238,7 +228,7 @@
]
}

export default function CodeEditor() {

Check warning on line 231 in src/main/frontend/app/routes/editor/editor.tsx

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
const theme = useTheme()
const project = useProjectStore.getState().project
const [activeTabFilePath, setActiveTabFilePath] = useState<string>(useEditorTabStore.getState().activeTabFilePath)
Expand Down Expand Up @@ -393,27 +383,31 @@
)
}, [])

const runPrettierReformat = async () => {
const runReformat = useCallback(async () => {
const editor = editorReference.current
if (!editor) return
const model = editor.getModel()
if (!model) return
if (!editor || !project || !activeTabFilePath) return

const activeTab = useEditorTabStore.getState().getTab(activeTabFilePath)
const configPath = activeTab?.configurationPath
if (!configPath) return

try {
const formattedValue = await prettierFormat(model.getValue())
if (formattedValue === model.getValue()) return
const current = editor.getValue()
const { xmlContent } = await saveConfiguration(project.name, configPath, current, true)
contentCacheRef.current.set(activeTabFilePath, { type: 'xml', content: xmlContent })

const selection = editor.getSelection()
editor.pushUndoStop()
editor.executeEdits(
'prettier-reformat',
[{ range: model.getFullModelRange(), text: formattedValue, forceMoveMarkers: true }],
'reformat',
[{ range: editor.getModel()!.getFullModelRange(), text: xmlContent, forceMoveMarkers: true }],
selection ? [selection] : undefined,
)
editor.pushUndoStop()
} catch (error) {
console.error('Failed to reformat XML:', error)
}
}
}, [project, activeTabFilePath])

const runSchemaValidation = useCallback(
async (content: string) => {
Expand Down Expand Up @@ -513,7 +507,7 @@
})

editor.addAction({
id: 'reformat-xml-prettier',
id: 'reformat-xml',
label: 'Reformat',
contextMenuGroupId: 'navigation',
contextMenuOrder: 3,
Expand All @@ -522,7 +516,7 @@
monacoReference.current.KeyMod.Shift |
monacoReference.current.KeyCode.KeyF,
],
run: runPrettierReformat,
run: runReformat,
})
}

Expand Down
16 changes: 5 additions & 11 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note'
import useTabStore, { type TabData } from '~/stores/tab-store'
import { convertAdapterXmlToJson, getAdapterFromConfiguration } from '~/routes/studio/xml-to-json-parser'
import { exportFlowToXml } from '~/routes/studio/flow-to-xml-parser'
import { exportFlowToXml, replaceAdapterInXml } from '~/routes/studio/flow-to-xml-parser'
import useNodeContextStore from '~/stores/node-context-store'
import CreateNodeModal from '~/components/flow/create-node-modal'
import { useFFDoc } from '@frankframework/doc-library-react'
Expand Down Expand Up @@ -160,18 +160,12 @@
existingAdapterXml,
)

const newAdapterDoc = new DOMParser().parseFromString(
`<root xmlns:flow="urn:frank-flow">${newAdapterXml}</root>`,
'text/xml',
)
const newAdapterEl = newAdapterDoc.querySelector('Adapter, adapter')
if (!newAdapterEl) throw new Error('Failed to parse generated adapter XML')

existingAdapter.parentNode!.replaceChild(configDoc.importNode(newAdapterEl, true), existingAdapter)
const adapterIndex = allAdapters.indexOf(existingAdapter)
if (adapterIndex === -1) showErrorToast('Could not determine adapter position for replacement')

const updatedConfigXml = new XMLSerializer().serializeToString(configDoc).replace(/^<\?xml[^?]*\?>\s*/, '')
const updatedConfigXml = replaceAdapterInXml(fullConfigXml, adapterIndex, newAdapterXml.trim())

await saveConfiguration(currentProject.name, configurationPath, updatedConfigXml)
await saveConfiguration(currentProject.name, configurationPath, updatedConfigXml, true)
clearConfigurationCache(currentProject.name, configurationPath)
useEditorTabStore.getState().refreshAllTabs()
if (currentProject.isGitRepository) await refreshOpenDiffs(currentProject.name)
Expand All @@ -184,7 +178,7 @@
showErrorToast(`Failed to save XML: ${error instanceof Error ? error.message : error}`)
setSaveStatus('idle')
}
}, [project])

Check warning on line 181 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 an unnecessary dependency: 'project'. Either exclude it or remove the dependency array

const autosaveEnabled = useSettingsStore((s) => s.general.autoSave.enabled)
const autosaveDelay = useSettingsStore((s) => s.general.autoSave.delayMs)
Expand Down
74 changes: 51 additions & 23 deletions src/main/frontend/app/routes/studio/flow-to-xml-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ ${pipelineParts.join('\n')}
</Adapter>`
}

export function replaceAdapterInXml(configXml: string, adapterIndex: number, newAdapterXml: string): string {
const matches = [...configXml.matchAll(/<(Adapter|adapter)\b/g)]

if (adapterIndex >= matches.length) return configXml

const match = matches[adapterIndex]
const start = match.index
const closingTag = `</${match[1]}>`
const closeIndex = configXml.indexOf(closingTag, start)
if (closeIndex === -1) return configXml

return configXml.slice(0, start) + newAdapterXml + configXml.slice(closeIndex + closingTag.length)
}

function buildEdgeMaps(edges: Edge[]) {
const outgoing: Record<string, string[]> = {}
const incoming: Record<string, string[]> = {}
Expand Down Expand Up @@ -163,15 +177,21 @@ function generateXmlElement(
width = node.measured.width
}

const height: number | undefined = node.height ?? undefined
const height: number | null = node.height ?? null
const attributes = (node.data as NodeData).attributes || {}
const children = (node.data as NodeData).children || []

const attributeString = ` name="${escapeXml(name)}"${Object.entries(attributes)
.map(([key, value]) => ` ${key}="${escapeXml(value)}"`)
.join('')}`

const flowNamespaceString = `flow:y="${roundedY}" flow:x="${roundedX}" flow:width="${width}"${height === undefined ? '' : ` flow:height="${height}"`}`
const allAttrs: Record<string, string> = {
...attributes,
name,
'flow:x': String(roundedX),
'flow:y': String(roundedY),
'flow:width': String(width),
...(height === null ? {} : { 'flow:height': String(height) }),
}
const attrStr = Object.entries(allAttrs)
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
.join(' ')

const childXml = children.map((child: ChildNode) => generateChildXml(child, 4)).join('\n')

Expand All @@ -195,30 +215,31 @@ function generateXmlElement(
.join('\n')

const content = [childXml, forwards].filter(Boolean).join('\n')
return content
? ` <${subtype}${attributeString} ${flowNamespaceString} >\n${content}\n </${subtype}>`
: ` <${subtype}${attributeString} ${flowNamespaceString} />`
return content ? ` <${subtype} ${attrStr} >\n${content}\n </${subtype}>` : ` <${subtype} ${attrStr} />`
}

function generateChildXml(child: ChildNode, indent: number): string {
const spaces = ' '.repeat(indent)

const attributes =
(child.name ? ` name="${escapeXml(child.name)}"` : '') +
Object.entries(child.attributes || {})
.map(([key, value]) => ` ${key}="${escapeXml(value)}"`)
.join('')
const childAttrs: Record<string, string> = {
...(child.name ? { name: child.name } : {}),
...child.attributes,
}

const attrStr = Object.entries(childAttrs)
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
.join(' ')

const attrs = attrStr ? ` ${attrStr}` : ''
const hasChildren = child.children && child.children.length > 0

if (!hasChildren) {
return `${spaces}<${child.subtype}${attributes}/>`
return `${spaces}<${child.subtype}${attrs}/>`
}

// Recursive case
const childXmlStrings = child.children!.map((nested) => generateChildXml(nested, indent + 2))

return `${spaces}<${child.subtype}${attributes}>
return `${spaces}<${child.subtype}${attrs}>
${childXmlStrings.join('\n')}
${spaces}</${child.subtype}>`
}
Expand All @@ -237,13 +258,20 @@ function generateExitsXml(exitNodes: FlowNode[]): string {
width = node.measured.width
height = node.measured.height
}
const attributes = data.attributes || {}
const flowNamespaceString = `flow:y="${roundedY}" flow:x="${roundedX}" flow:width="${width}" flow:height="${height}"`
const attributeString = ` name="${escapeXml(name)}"${Object.entries(attributes)
.map(([key, value]) => ` ${key}="${escapeXml(value)}"`)
.join('')}`

return ` <Exit ${attributeString} ${flowNamespaceString} />`
const allAttrs: Record<string, string> = {
...data.attributes,
name,
'flow:x': String(roundedX),
'flow:y': String(roundedY),
'flow:width': String(width),
'flow:height': String(height),
}
const attrStr = Object.entries(allAttrs)
.map(([k, v]) => `${k}="${escapeXml(v)}"`)
.join(' ')

return ` <Exit ${attrStr} />`
})
.join('\n')
}
Expand Down
10 changes: 8 additions & 2 deletions src/main/frontend/app/services/configuration-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,14 @@ export async function fetchConfiguration(projectName: string, filepath: string,
return content
}

export async function saveConfiguration(projectName: string, filepath: string, content: string): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?path=${encodeURIComponent(filepath)}`, {
export async function saveConfiguration(
projectName: string,
filepath: string,
content: string,
format = false,
): Promise<XmlResponse> {
const formatParam = format ? '&format=true' : ''
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?path=${encodeURIComponent(filepath)}${formatParam}`, {
method: 'PUT',
body: content,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ public ResponseEntity<ConfigurationDTO> getConfigurationByPath(
public ResponseEntity<XmlDTO> updateConfiguration(
@PathVariable String projectName,
@RequestParam String path,
@RequestParam(defaultValue = "false") boolean format,
@RequestBody String content
) throws ApiException, IOException, ParserConfigurationException, SAXException, TransformerException {
String updatedContent = configurationService.updateConfiguration(projectName, path, content);
) throws ApiException {
String updatedContent = configurationService.updateConfiguration(projectName, path, content, format);
XmlDTO xmlDTO = new XmlDTO(updatedContent);
return ResponseEntity.ok(xmlDTO);
}
Expand Down
Loading
Loading