Skip to content

Commit cb436e6

Browse files
stijnpotters1CopilotDaan0709
authored
Code formatting while saving (#382)
* fix: code formatting not every save, but on add * fix: applied linting * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Stijn Potters <stijn.potters1@gmail.com> * fix: exception error in controller * Changed formatting to 3 spaces to be in line with backend formatting. Also formats configurations when a flow gets saved * Moved function to outer scope * Updated test * fix: enhance XML configuration handling and formatting * fix: update editor options for tab size and indentation settings --------- Signed-off-by: Stijn Potters <stijn.potters1@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Daan0709 <daanvmaldegem@live.nl>
1 parent 999b701 commit cb436e6

File tree

9 files changed

+107
-15
lines changed

9 files changed

+107
-15
lines changed

pnpm-lock.yaml

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/frontend/app/routes/editor/editor.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import RulerCrossPenIcon from '/icons/solar/Ruler Cross Pen.svg?react'
22
import Editor, { type Monaco, type OnMount } from '@monaco-editor/react'
3+
import prettier from 'prettier/standalone'
4+
import prettierPluginXml from '@prettier/plugin-xml'
35
type ITextModel = Monaco['editor']['ITextModel']
46
type FindMatch = Monaco['editor']['FindMatch']
57
type IModelDeltaDecoration = Monaco['editor']['IModelDeltaDecoration']
@@ -162,6 +164,14 @@ function isConfigurationFile(fileExtension: string) {
162164
return fileExtension === 'xml'
163165
}
164166

167+
function prettierFormat(xml: string): Promise<string> {
168+
return prettier.format(xml, {
169+
parser: 'xml',
170+
plugins: [prettierPluginXml],
171+
tabWidth: 2,
172+
})
173+
}
174+
165175
async function validateFlow(content: string, model: ITextModel): Promise<ValidationError[]> {
166176
const flowFragment = extractFlowElements(content)
167177
if (!flowFragment) return []
@@ -311,7 +321,6 @@ export default function CodeEditor() {
311321
if (isConfigurationFile(fileExtension ?? '')) {
312322
saveConfiguration(project.name, configPath, updatedContent)
313323
.then(({ xmlContent }) => {
314-
setFileContent(xmlContent)
315324
contentCacheRef.current.set(activeTabFilePath, { type: 'xml', content: xmlContent })
316325
finishSaving()
317326
if (project.isGitRepository) refreshOpenDiffs(project.name)
@@ -384,6 +393,28 @@ export default function CodeEditor() {
384393
)
385394
}, [])
386395

396+
const runPrettierReformat = async () => {
397+
const editor = editorReference.current
398+
if (!editor) return
399+
const model = editor.getModel()
400+
if (!model) return
401+
try {
402+
const formattedValue = await prettierFormat(model.getValue())
403+
if (formattedValue === model.getValue()) return
404+
405+
const selection = editor.getSelection()
406+
editor.pushUndoStop()
407+
editor.executeEdits(
408+
'prettier-reformat',
409+
[{ range: model.getFullModelRange(), text: formattedValue, forceMoveMarkers: true }],
410+
selection ? [selection] : undefined,
411+
)
412+
editor.pushUndoStop()
413+
} catch (error) {
414+
console.error('Failed to reformat XML:', error)
415+
}
416+
}
417+
387418
const runSchemaValidation = useCallback(
388419
async (content: string) => {
389420
const editor = editorReference.current
@@ -431,7 +462,6 @@ export default function CodeEditor() {
431462

432463
xsdFeatures.addCompletion()
433464
xsdFeatures.addGenerateAction()
434-
xsdFeatures.addReformatAction()
435465

436466
fetchFrankConfigXsd()
437467
.then((xsdContent) => {
@@ -481,6 +511,19 @@ export default function CodeEditor() {
481511
}
482512
},
483513
})
514+
515+
editor.addAction({
516+
id: 'reformat-xml-prettier',
517+
label: 'Reformat',
518+
contextMenuGroupId: 'navigation',
519+
contextMenuOrder: 3,
520+
keybindings: [
521+
monacoReference.current.KeyMod.Alt |
522+
monacoReference.current.KeyMod.Shift |
523+
monacoReference.current.KeyCode.KeyF,
524+
],
525+
run: runPrettierReformat,
526+
})
484527
}
485528

486529
useEffect(() => {
@@ -708,7 +751,7 @@ export default function CodeEditor() {
708751
applyFlowHighlighter() // Real-time highlight updates
709752
}
710753
}}
711-
options={{ automaticLayout: true, quickSuggestions: false }}
754+
options={{ automaticLayout: true, quickSuggestions: false, tabSize: 2, insertSpaces: true, detectIndentation: false }}
712755
/>
713756
</div>
714757
</>

src/main/frontend/app/routes/studio/canvas/flow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ function FlowCanvas() {
173173
await saveConfiguration(currentProject.name, configurationPath, updatedConfigXml)
174174
clearConfigurationCache(currentProject.name, configurationPath)
175175
useEditorTabStore.getState().refreshAllTabs()
176-
if (currentProject.isGitRepository) refreshOpenDiffs(currentProject.name)
176+
if (currentProject.isGitRepository) await refreshOpenDiffs(currentProject.name)
177177

178178
setSaveStatus('saved')
179179
if (savedTimerRef.current) clearTimeout(savedTimerRef.current)

src/main/frontend/app/stores/shortcut-store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ export const ALL_SHORTCUTS: Omit<ShortcutDefinition, 'handler'>[] = [
111111
modifiers: { cmdOrCtrl: true, shift: true },
112112
displayOnly: true,
113113
},
114+
{
115+
id: 'editor.reformat',
116+
label: 'Reformat XML',
117+
scope: 'editor',
118+
key: 'f',
119+
modifiers: { alt: true, shift: true },
120+
displayOnly: true,
121+
},
114122
{ id: 'editor.search', label: 'Find', scope: 'editor', key: 'f', modifiers: { cmdOrCtrl: true }, displayOnly: true },
115123
{
116124
id: 'editor.replace',

src/main/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"eslint-plugin-sonarjs": "^3.0.7",
6262
"eslint-plugin-unicorn": "^62.0.0",
6363
"jsdom": "^27.4.0",
64+
"@prettier/plugin-xml": "^3.4.1",
6465
"prettier": "^3.8.1",
6566
"prettier-plugin-tailwindcss": "^0.6.14",
6667
"react-router-devtools": "^1.1.10",

src/main/java/org/frankframework/flow/adapter/AdapterService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ public boolean updateAdapter(Path configurationFile, String adapterName, String
8484
String updatedXml = XmlConfigurationUtils.convertNodeToString(configDoc);
8585
Files.writeString(absConfigFile, updatedXml, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
8686
return true;
87-
8887
} catch (AdapterNotFoundException e) {
8988
throw e;
9089
} catch (Exception e) {

src/main/java/org/frankframework/flow/configuration/ConfigurationController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public ResponseEntity<XmlDTO> updateConfiguration(
5252
public ResponseEntity<XmlDTO> addConfiguration(
5353
@PathVariable String projectName,
5454
@RequestParam String name
55-
) throws ApiException, IOException {
55+
) throws ApiException, IOException, TransformerException, ParserConfigurationException, SAXException {
5656
String content = configurationService.addConfiguration(projectName, name);
5757
XmlDTO xmlDTO = new XmlDTO(content);
5858
return ResponseEntity.ok(xmlDTO);

src/main/java/org/frankframework/flow/configuration/ConfigurationService.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,18 @@ public String updateConfiguration(String projectName, String filepath, String co
4949
throw new ApiException("Invalid file path: " + filepath, HttpStatus.NOT_FOUND);
5050
}
5151

52-
Document updatedDocument = XmlConfigurationUtils.insertFlowNamespace(content);
53-
String updatedContent = XmlConfigurationUtils.convertNodeToString(updatedDocument);
5452

55-
fileSystemStorage.writeFile(absolutePath.toString(), updatedContent);
56-
return updatedContent;
53+
Document document = XmlConfigurationUtils.insertFlowNamespace(content);
54+
if (document == null) {
55+
throw new ApiException("Configuration content must not be blank", HttpStatus.BAD_REQUEST);
56+
}
57+
58+
String formatted = XmlConfigurationUtils.convertNodeToString(document);
59+
fileSystemStorage.writeFile(absolutePath.toString(), formatted);
60+
return formatted;
5761
}
5862

59-
public String addConfiguration(String projectName, String configurationName) throws IOException, ApiException {
63+
public String addConfiguration(String projectName, String configurationName) throws IOException, ApiException, TransformerException, ParserConfigurationException, SAXException {
6064
Project project = projectService.getProject(projectName);
6165
Path absProjectPath = fileSystemStorage.toAbsolutePath(project.getRootPath());
6266
Path configDir = absProjectPath.resolve(CONFIGURATIONS_DIR).normalize();
@@ -71,8 +75,11 @@ public String addConfiguration(String projectName, String configurationName) thr
7175
}
7276

7377
String defaultXml = loadDefaultConfigurationXml();
74-
fileSystemStorage.writeFile(filePath.toString(), defaultXml);
75-
return defaultXml;
78+
Document updatedDocument = XmlConfigurationUtils.insertFlowNamespace(defaultXml);
79+
String updatedContent = XmlConfigurationUtils.convertNodeToString(updatedDocument);
80+
fileSystemStorage.writeFile(filePath.toString(), updatedContent);
81+
82+
return updatedContent;
7683
}
7784

7885
private String loadDefaultConfigurationXml() throws IOException {

src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.junit.jupiter.api.Assertions.*;
44
import static org.mockito.ArgumentMatchers.anyString;
5+
import static org.mockito.ArgumentMatchers.eq;
56
import static org.mockito.Mockito.*;
67

78
import java.io.IOException;
@@ -111,8 +112,9 @@ void updateConfiguration_Success() throws Exception {
111112

112113
configurationService.updateConfiguration("test", file.toString(), "<new/>");
113114

114-
assertEquals("<new/>\n", Files.readString(file, StandardCharsets.UTF_8));
115-
verify(fileSystemStorage).writeFile(file.toString(), "<new/>\n");
115+
String result = Files.readString(file, StandardCharsets.UTF_8).trim();
116+
assertEquals("<new/>", result);
117+
verify(fileSystemStorage).writeFile(eq(file.toString()), anyString());
116118
}
117119

118120
@Test

0 commit comments

Comments
 (0)