From 365ae6694527fb673220b4abfeb6162fefe93c41 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 11:49:05 +0100 Subject: [PATCH 1/8] fix: solved regression bugs --- .../file-structure/editor-data-provider.ts | 2 +- .../file-structure/editor-file-structure.tsx | 8 ++++- .../file-structure/studio-file-structure.tsx | 6 +++- .../studio-files-data-provider.ts | 4 +-- .../use-file-tree-context-menu.ts | 9 +++++ src/main/frontend/app/routes/app-layout.tsx | 9 +++-- .../app/routes/studio/canvas/flow.tsx | 14 ++++++++ .../routes/studio/context/node-context.tsx | 6 ++++ .../app/services/file-tree-service.ts | 6 ++++ .../frontend/app/stores/node-context-store.ts | 4 +++ src/main/frontend/app/stores/project-store.ts | 20 ++++++++--- src/main/frontend/app/stores/tab-store.ts | 14 ++++++++ .../configuration/ConfigurationService.java | 25 ++++++++----- .../CloudFileSystemStorageService.java | 13 +++---- .../flow/filetree/FileTreeService.java | 36 +++++++++++++++++++ .../flow/project/ProjectService.java | 3 +- 16 files changed, 150 insertions(+), 29 deletions(-) diff --git a/src/main/frontend/app/components/file-structure/editor-data-provider.ts b/src/main/frontend/app/components/file-structure/editor-data-provider.ts index 33d4a087..b2cc06f5 100644 --- a/src/main/frontend/app/components/file-structure/editor-data-provider.ts +++ b/src/main/frontend/app/components/file-structure/editor-data-provider.ts @@ -86,7 +86,7 @@ export default class EditorFilesDataProvider extends BaseFilesDataProvider + return ( + ctxMenu.openContextMenu(e, item.index)} + className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" + /> + ) } const renderItemTitle = ({ diff --git a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx index 7cf540e8..324fce29 100644 --- a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx @@ -228,7 +228,11 @@ export default function StudioFileStructure() { } return ( - + e.stopPropagation()} + className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" + /> ) } diff --git a/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts b/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts index 2d13a370..8cd9a8b6 100644 --- a/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts +++ b/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts @@ -1,6 +1,6 @@ import type { TreeItemIndex } from 'react-complex-tree' import { sortChildren } from './tree-utilities' -import { fetchProjectTree, fetchDirectoryByPath } from '~/services/file-tree-service' +import { fetchShallowConfigurationsTree, fetchDirectoryByPath } from '~/services/file-tree-service' import type { FileTreeNode } from '~/types/filesystem.types' import { BaseFilesDataProvider } from './base-files-data-provider' @@ -47,7 +47,7 @@ export default class FilesDataProvider extends BaseFilesDataProvider (rootPath ? openProject(rootPath) : Promise.reject(new Error('No root path stored')))) .then((fetched: Project) => { useProjectStore.getState().setProject(fetched) }) .catch(() => { - sessionStorage.removeItem('active-project-name') + useProjectStore.getState().clearProject() }) .finally(() => { setRestoring(false) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 5b1893ad..3bc8da9a 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -32,6 +32,7 @@ import useNodeContextStore from '~/stores/node-context-store' import CreateNodeModal from '~/components/flow/create-node-modal' import { useProjectStore } from '~/stores/project-store' import { clearConfigurationCache, fetchConfigurationCached, saveConfiguration } from '~/services/configuration-service' +import useEditorTabStore from '~/stores/editor-tab-store' import { cloneWithRemappedIds } from '~/utils/flow-utils' import { showErrorToast } from '~/components/toast' import clsx from 'clsx' @@ -58,6 +59,7 @@ function FlowCanvas() { const { isEditing, isDirty, + pendingImmediateSave, setIsEditing, setIsNewNode, setParentId, @@ -68,6 +70,7 @@ function FlowCanvas() { useShallow((s) => ({ isEditing: s.isEditing, isDirty: s.isDirty, + pendingImmediateSave: s.pendingImmediateSave, setIsEditing: s.setIsEditing, setIsNewNode: s.setIsNewNode, setParentId: s.setParentId, @@ -150,6 +153,7 @@ function FlowCanvas() { await saveConfiguration(project.name, configurationPath, updatedConfigXml) clearConfigurationCache(project.name, configurationPath) + useEditorTabStore.getState().refreshAllTabs() setSaveStatus('saved') if (savedTimerRef.current) clearTimeout(savedTimerRef.current) @@ -186,6 +190,16 @@ function FlowCanvas() { } }, [nodes, edges, scheduleAutoSave]) + useEffect(() => { + if (!pendingImmediateSave) return + useNodeContextStore.getState().setPendingImmediateSave(false) + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current) + autoSaveTimerRef.current = null + } + void saveFlow() + }, [pendingImmediateSave, saveFlow]) + const sourceInfoReference = useRef<{ nodeId: string | null handleId: string | null diff --git a/src/main/frontend/app/routes/studio/context/node-context.tsx b/src/main/frontend/app/routes/studio/context/node-context.tsx index 7737dbe6..eb7b0ddb 100644 --- a/src/main/frontend/app/routes/studio/context/node-context.tsx +++ b/src/main/frontend/app/routes/studio/context/node-context.tsx @@ -35,6 +35,7 @@ export default function NodeContext({ setChildParentId, childParentId, setIsDirty, + setPendingImmediateSave, } = useNodeContextStore( useShallow((s) => ({ attributes: s.attributes, @@ -46,6 +47,7 @@ export default function NodeContext({ setChildParentId: s.setChildParentId, childParentId: s.childParentId, setIsDirty: s.setIsDirty, + setPendingImmediateSave: s.setPendingImmediateSave, })), ) @@ -232,6 +234,7 @@ export default function NodeContext({ setShowNodeContext(false) setParentId(null) setChildParentId(null) + setPendingImmediateSave(true) return } updateChild(parentNode.id, updatedChild) @@ -239,6 +242,7 @@ export default function NodeContext({ setShowNodeContext(false) setParentId(null) setChildParentId(null) + setPendingImmediateSave(true) return } @@ -250,6 +254,7 @@ export default function NodeContext({ setIsNewNode(false) setIsEditing(false) setShowNodeContext(false) + setPendingImmediateSave(true) return } setAttributes(nodeId.toString(), newAttributesObject) @@ -258,6 +263,7 @@ export default function NodeContext({ } setIsEditing(false) setShowNodeContext(false) + setPendingImmediateSave(true) } const canSaveRef = useRef(canSave) diff --git a/src/main/frontend/app/services/file-tree-service.ts b/src/main/frontend/app/services/file-tree-service.ts index c1d3e55e..c80dbea1 100644 --- a/src/main/frontend/app/services/file-tree-service.ts +++ b/src/main/frontend/app/services/file-tree-service.ts @@ -5,6 +5,12 @@ export async function fetchProjectTree(projectName: string, signal?: AbortSignal return apiFetch(`/projects/${encodeURIComponent(projectName)}/tree/configurations`, { signal }) } +export async function fetchShallowConfigurationsTree(projectName: string, signal?: AbortSignal): Promise { + return apiFetch(`/projects/${encodeURIComponent(projectName)}/tree/configurations?shallow=true`, { + signal, + }) +} + export async function fetchProjectRootTree(projectName: string, signal?: AbortSignal): Promise { return apiFetch(`/projects/${encodeURIComponent(projectName)}/tree`, { signal }) } diff --git a/src/main/frontend/app/stores/node-context-store.ts b/src/main/frontend/app/stores/node-context-store.ts index 6206fc83..9f861fa5 100644 --- a/src/main/frontend/app/stores/node-context-store.ts +++ b/src/main/frontend/app/stores/node-context-store.ts @@ -7,6 +7,7 @@ interface NodeContextStore { isEditing: boolean isNewNode: boolean isDirty: boolean + pendingImmediateSave: boolean parentId: string | null childParentId: string | null draggedName: string | null @@ -16,6 +17,7 @@ interface NodeContextStore { setIsEditing: (value: boolean) => void setIsNewNode: (value: boolean) => void setIsDirty: (v: boolean) => void + setPendingImmediateSave: (v: boolean) => void resetAttributes: () => void setParentId: (id: string | null) => void setChildParentId: (id: string | null) => void @@ -29,6 +31,7 @@ const useNodeContextStore = create((set) => ({ isEditing: false, isNewNode: false, isDirty: false, + pendingImmediateSave: false, parentId: null, childParentId: null, draggedName: null, @@ -38,6 +41,7 @@ const useNodeContextStore = create((set) => ({ setIsEditing: (value) => set({ isEditing: value }), setIsNewNode: (value) => set({ isNewNode: value }), setIsDirty: (isDirty) => set({ isDirty }), + setPendingImmediateSave: (pendingImmediateSave) => set({ pendingImmediateSave }), resetAttributes: () => set({ attributes: undefined }), setParentId: (parentId: string | null) => set({ parentId }), setChildParentId: (childParentId: string | null) => set({ childParentId }), diff --git a/src/main/frontend/app/stores/project-store.ts b/src/main/frontend/app/stores/project-store.ts index 1e5c4a75..57692298 100644 --- a/src/main/frontend/app/stores/project-store.ts +++ b/src/main/frontend/app/stores/project-store.ts @@ -2,8 +2,10 @@ import { create } from 'zustand' import type { Project } from '~/types/project.types' import { useTreeStore } from '~/stores/tree-store' import useTabStore from '~/stores/tab-store' +import useEditorTabStore from '~/stores/editor-tab-store' -const SESSION_KEY = 'active-project-name' +const STORAGE_KEY = 'active-project-name' +const STORAGE_ROOT_PATH_KEY = 'active-project-root-path' interface ProjectStoreState { project?: Project @@ -15,22 +17,30 @@ export const useProjectStore = create((set) => ({ project: undefined, setProject: (project: Project) => { set((state) => { - if (state.project && state.project.name !== project.name) { + if (state.project?.name !== project.name) { useTreeStore.getState().clearExpandedItems() useTabStore.getState().clearTabs() + useEditorTabStore.getState().clearTabs() } return { project } }) - sessionStorage.setItem(SESSION_KEY, project.name) + localStorage.setItem(STORAGE_KEY, project.name) + localStorage.setItem(STORAGE_ROOT_PATH_KEY, project.rootPath) }, clearProject: () => { - sessionStorage.removeItem(SESSION_KEY) + localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem(STORAGE_ROOT_PATH_KEY) useTreeStore.getState().clearExpandedItems() useTabStore.getState().clearTabs() + useEditorTabStore.getState().clearTabs() set({ project: undefined }) }, })) export function getStoredProjectName(): string | null { - return sessionStorage.getItem(SESSION_KEY) + return localStorage.getItem(STORAGE_KEY) +} + +export function getStoredProjectRootPath(): string | null { + return localStorage.getItem(STORAGE_ROOT_PATH_KEY) } diff --git a/src/main/frontend/app/stores/tab-store.ts b/src/main/frontend/app/stores/tab-store.ts index aa676097..3ebd2543 100644 --- a/src/main/frontend/app/stores/tab-store.ts +++ b/src/main/frontend/app/stores/tab-store.ts @@ -20,6 +20,7 @@ interface TabStoreState { setActiveTab: (tabId: string | undefined) => void removeTab: (tabId: string) => void removeTabAndSelectFallback: (tabId: string) => void + removeTabsForConfig: (configPath: string) => void clearTabs: () => void } @@ -49,6 +50,19 @@ const useTabStore = create()( set((state) => { const newTabs = { ...state.tabs } delete newTabs[tabId] + const remainingKeys = Object.keys(newTabs) + return { + tabs: newTabs, + activeTab: remainingKeys.includes(state.activeTab) ? state.activeTab : (remainingKeys.at(-1) ?? ''), + } + }), + removeTabsForConfig: (configPath) => + set((state) => { + const newTabs = { ...state.tabs } + for (const [tabId, tab] of Object.entries(newTabs)) { + if (tab.configurationPath === configPath) delete newTabs[tabId] + } + const remainingKeys = Object.keys(newTabs) return { tabs: newTabs, diff --git a/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java b/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java index ae3d5607..440fd11c 100644 --- a/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java +++ b/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java @@ -36,22 +36,22 @@ public String getConfigurationContent(String filepath) throws IOException, Confi throw new ConfigurationNotFoundException("Invalid configuration path: " + filepath); } - return fileSystemStorage.readFile(filepath); + return fileSystemStorage.readFile(filePath.toString()); } public void updateConfiguration(String projectName, String filepath, String content) throws IOException, ConfigurationNotFoundException, ProjectNotFoundException { - Path filePath = fileSystemStorage.toAbsolutePath(filepath); + Path absolutePath = fileSystemStorage.toAbsolutePath(filepath); - if (!Files.exists(filePath)) { + if (!Files.exists(absolutePath)) { throw new ConfigurationNotFoundException("Invalid file path: " + filepath); } - if (Files.isDirectory(filePath)) { + if (Files.isDirectory(absolutePath)) { throw new ConfigurationNotFoundException("Invalid file path: " + filepath); } - fileSystemStorage.writeFile(filepath, content); + fileSystemStorage.writeFile(absolutePath.toString(), content); projectService.updateConfigurationXml(projectName, filepath, content); } @@ -61,7 +61,10 @@ public Project addConfiguration(String projectName, String configurationName) Path absProjectPath = fileSystemStorage.toAbsolutePath(project.getRootPath()); Path configDir = absProjectPath.resolve(CONFIGURATIONS_DIR).normalize(); - Files.createDirectories(configDir); + + if (!Files.exists(configDir)) { + Files.createDirectories(configDir); + } Path filePath = configDir.resolve(configurationName).normalize(); if (!filePath.startsWith(configDir)) { @@ -71,7 +74,8 @@ public Project addConfiguration(String projectName, String configurationName) String defaultXml = loadDefaultConfigurationXml(); fileSystemStorage.writeFile(filePath.toString(), defaultXml); - Configuration configuration = new Configuration(filePath.toString()); + String relativePath = fileSystemStorage.toRelativePath(filePath.toString()); + Configuration configuration = new Configuration(relativePath); configuration.setXmlContent(defaultXml); project.addConfiguration(configuration); return project; @@ -88,7 +92,9 @@ public Project addConfigurationToFolder(String projectName, String configuration throw new SecurityException("Configuration location must be within the project directory"); } - Files.createDirectories(targetDir); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } Path filePath = targetDir.resolve(configurationName).normalize(); if (!filePath.startsWith(targetDir)) { @@ -102,7 +108,8 @@ public Project addConfigurationToFolder(String projectName, String configuration String defaultXml = loadDefaultConfigurationXml(); fileSystemStorage.writeFile(filePath.toString(), defaultXml); - Configuration configuration = new Configuration(filePath.toString()); + String relativePath = fileSystemStorage.toRelativePath(filePath.toString()); + Configuration configuration = new Configuration(relativePath); configuration.setXmlContent(defaultXml); project.addConfiguration(configuration); return project; diff --git a/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java b/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java index 4f661f94..fa43d193 100644 --- a/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java +++ b/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java @@ -70,15 +70,13 @@ public List listRoots() { @Override public List listDirectory(String path) throws IOException { - Path userRoot = getOrCreateUserRoot(); Path dir = resolveSecurely(path); List entries = new ArrayList<>(); try (Stream stream = Files.list(dir)) { stream.filter(Files::isDirectory).sorted().forEach(p -> { - String relativePath = - userRoot.relativize(p.toAbsolutePath()).toString().replace("\\", "/"); + String relativePath = toRelativePath(p.toAbsolutePath().toString()); boolean isProjectRoot = Files.isDirectory(p.resolve("src/main/configurations")); entries.add(new FilesystemEntry(p.getFileName().toString(), relativePath, "DIRECTORY", isProjectRoot)); @@ -113,11 +111,14 @@ public Path toAbsolutePath(String path) throws IOException { public String toRelativePath(String absolutePath) { String normalized = absolutePath.replace("\\", "/"); String userRoot = getUserRootPath().toString().replace("\\", "/"); + if (normalized.startsWith(userRoot)) { String relative = normalized.substring(userRoot.length()); - if (relative.isEmpty()) return "/"; - if (!relative.startsWith("/")) relative = "/" + relative; - return relative; + + while (relative.startsWith("/")) { + relative = relative.substring(1); + } + return relative.isEmpty() ? "" : relative; } return normalized; } diff --git a/src/main/java/org/frankframework/flow/filetree/FileTreeService.java b/src/main/java/org/frankframework/flow/filetree/FileTreeService.java index f88df62d..efafd727 100644 --- a/src/main/java/org/frankframework/flow/filetree/FileTreeService.java +++ b/src/main/java/org/frankframework/flow/filetree/FileTreeService.java @@ -5,18 +5,25 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; import org.frankframework.flow.configuration.ConfigurationService; import org.frankframework.flow.exception.ApiException; import org.frankframework.flow.filesystem.FileSystemStorage; import org.frankframework.flow.project.Project; import org.frankframework.flow.project.ProjectNotFoundException; import org.frankframework.flow.project.ProjectService; +import org.frankframework.flow.utility.XmlSecurityUtils; import org.springframework.stereotype.Service; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.helpers.DefaultHandler; @Service public class FileTreeService { @@ -245,11 +252,36 @@ private FileTreeNode buildTree(Path path, Path relativizeRoot, boolean useRelati } else { node.setType(NodeType.FILE); node.setChildren(null); + if (path.getFileName().toString().toLowerCase().endsWith(".xml")) { + node.setAdapterNames(extractAdapterNames(path)); + } } return node; } + private List extractAdapterNames(Path xmlFile) { + try { + DocumentBuilder builder = XmlSecurityUtils.createSecureDocumentBuilder(); + builder.setErrorHandler(new DefaultHandler()); + Document doc = builder.parse(Files.newInputStream(xmlFile)); + NodeList adapters = doc.getElementsByTagName("Adapter"); + if (adapters.getLength() == 0) { + adapters = doc.getElementsByTagName("adapter"); + } + List names = new ArrayList<>(); + for (int i = 0; i < adapters.getLength(); i++) { + String name = ((Element) adapters.item(i)).getAttribute("name"); + if (!name.isBlank()) { + names.add(name); + } + } + return names; + } catch (Exception e) { + return List.of(); + } + } + private String toNodePath(Path path, Path relativizeRoot, boolean useRelativePaths) { if (!useRelativePaths) { return path.toAbsolutePath().toString(); @@ -275,6 +307,10 @@ private FileTreeNode buildShallowTree(Path path, Path relativizeRoot, boolean us child.setName(p.getFileName().toString()); child.setPath(toNodePath(p, relativizeRoot, useRelativePaths)); child.setType(Files.isDirectory(p) ? NodeType.DIRECTORY : NodeType.FILE); + if (!Files.isDirectory(p) + && p.getFileName().toString().toLowerCase().endsWith(".xml")) { + child.setAdapterNames(extractAdapterNames(p)); + } return child; }) .toList(); diff --git a/src/main/java/org/frankframework/flow/project/ProjectService.java b/src/main/java/org/frankframework/flow/project/ProjectService.java index d836f157..30d28328 100644 --- a/src/main/java/org/frankframework/flow/project/ProjectService.java +++ b/src/main/java/org/frankframework/flow/project/ProjectService.java @@ -316,7 +316,8 @@ private Project loadProjectFromStorage(String path) throws IOException { s.filter(p -> p.toString().endsWith(".xml")).forEach(p -> { try { String content = fileSystemStorage.readFile(p.toString()); - Configuration c = new Configuration(p.toString()); + String relativePath = fileSystemStorage.toRelativePath(p.toString()); + Configuration c = new Configuration(relativePath); c.setXmlContent(content); project.addConfiguration(c); } catch (IOException e) { From e52c13bea3226b696fc369251970c6d516529398 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 12:05:50 +0100 Subject: [PATCH 2/8] fix: solved tests according to new logic --- .../flow/configuration/ConfigurationServiceTest.java | 2 ++ .../flow/filesystem/CloudFileSystemStorageServiceTest.java | 4 ++-- .../org/frankframework/flow/project/ProjectServiceTest.java | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java index 18a3bf07..53544039 100644 --- a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java +++ b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java @@ -131,6 +131,7 @@ void updateConfiguration_FileNotFound_ThrowsConfigurationNotFoundException() thr void addConfiguration_Success() throws Exception { stubToAbsolutePath(); stubWriteFile(); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); Path projectDir = tempDir.resolve("myproject"); Files.createDirectories(projectDir); @@ -174,6 +175,7 @@ void addConfiguration_PathTraversal_ThrowsSecurityException() throws Exception { void addConfigurationToFolder_Success() throws Exception { stubToAbsolutePath(); stubWriteFile(); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); Path projectDir = tempDir.resolve("myproject"); Files.createDirectories(projectDir); diff --git a/src/test/java/org/frankframework/flow/filesystem/CloudFileSystemStorageServiceTest.java b/src/test/java/org/frankframework/flow/filesystem/CloudFileSystemStorageServiceTest.java index eaef89b8..f8796011 100644 --- a/src/test/java/org/frankframework/flow/filesystem/CloudFileSystemStorageServiceTest.java +++ b/src/test/java/org/frankframework/flow/filesystem/CloudFileSystemStorageServiceTest.java @@ -145,7 +145,7 @@ void toRelativePathStripsUserRoot() throws IOException { String relative = service.toRelativePath(absolutePath); - assertTrue(relative.startsWith("/")); + assertTrue(relative.startsWith("")); assertTrue(relative.contains("project")); assertTrue(relative.contains("file.xml")); } @@ -156,7 +156,7 @@ void toRelativePathReturnsSlashForUserRoot() throws IOException { String relative = service.toRelativePath(userRoot.toString()); - assertEquals("/", relative); + assertEquals("", relative); } @Test diff --git a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java index bd158187..ad69ad16 100644 --- a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java +++ b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java @@ -184,6 +184,7 @@ public void testGetProjectsFromRecentList() throws IOException { @Test public void testUpdateConfigurationXmlSuccess() throws Exception { stubFileSystemForProjectCreation(); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); projectService.createProjectOnDisk("proj"); Project project = projectService.getProject("proj"); @@ -211,6 +212,7 @@ public void testUpdateConfigurationXmlThrowsProjectNotFound() { @Test public void testUpdateConfigurationXmlConfigNotFound() throws Exception { stubFileSystemForProjectCreation(); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); projectService.createProjectOnDisk("proj"); From d1dc581d4f7d5ad3fa5404856da8462b3ea98a42 Mon Sep 17 00:00:00 2001 From: Stijn Potters Date: Thu, 12 Mar 2026 12:57:16 +0100 Subject: [PATCH 3/8] Update src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java Co-authored-by: Vivy <4380412+Matthbo@users.noreply.github.com> Signed-off-by: Stijn Potters --- .../flow/filesystem/CloudFileSystemStorageService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java b/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java index fa43d193..f0491e9b 100644 --- a/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java +++ b/src/main/java/org/frankframework/flow/filesystem/CloudFileSystemStorageService.java @@ -118,7 +118,7 @@ public String toRelativePath(String absolutePath) { while (relative.startsWith("/")) { relative = relative.substring(1); } - return relative.isEmpty() ? "" : relative; + return relative; } return normalized; } From 9042aa01a0cd73712eaf410186baadf941d83621 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 13:10:10 +0100 Subject: [PATCH 4/8] fix: improved code according to feedback --- .../app/components/file-structure/editor-file-structure.tsx | 2 +- .../app/components/file-structure/studio-file-structure.tsx | 2 +- src/main/frontend/app/routes/studio/canvas/flow.tsx | 5 +++-- .../app/routes/studio/context/element-hover-card.tsx | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx index 0bf65733..eb46fdb8 100644 --- a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx @@ -223,7 +223,7 @@ export default function EditorFileStructure() { return ( ctxMenu.openContextMenu(e, item.index)} + onContextMenu={(mouseEvent) => ctxMenu.openContextMenu(mouseEvent, item.index)} className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" /> ) diff --git a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx index 324fce29..7335fc5c 100644 --- a/src/main/frontend/app/components/file-structure/studio-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/studio-file-structure.tsx @@ -230,7 +230,7 @@ export default function StudioFileStructure() { return ( e.stopPropagation()} + onContextMenu={(mouseEvent: React.MouseEvent) => mouseEvent.stopPropagation()} className="rct-tree-item-arrow-isFolder rct-tree-item-arrow fill-foreground" /> ) diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 3bc8da9a..28a7ed50 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -192,12 +192,13 @@ function FlowCanvas() { useEffect(() => { if (!pendingImmediateSave) return - useNodeContextStore.getState().setPendingImmediateSave(false) if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) autoSaveTimerRef.current = null } - void saveFlow() + void saveFlow().finally(() => { + useNodeContextStore.getState().setPendingImmediateSave(false) + }) }, [pendingImmediateSave, saveFlow]) const sourceInfoReference = useRef<{ diff --git a/src/main/frontend/app/routes/studio/context/element-hover-card.tsx b/src/main/frontend/app/routes/studio/context/element-hover-card.tsx index ae29c696..0ec4095d 100644 --- a/src/main/frontend/app/routes/studio/context/element-hover-card.tsx +++ b/src/main/frontend/app/routes/studio/context/element-hover-card.tsx @@ -1,4 +1,4 @@ -import {useFFDoc, useJavadocTransform} from "@frankframework/doc-library-react"; +import { useFFDoc, useJavadocTransform } from '@frankframework/doc-library-react' import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' import type { ElementDetails } from '~/types/ff-doc.types' From a1ada6a77898e518d4149fcaaec3e65c1f4138bd Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 17:24:40 +0100 Subject: [PATCH 5/8] fix: improved code and removed project caching in backend memory --- .../file-structure/editor-data-provider.ts | 24 ++++- .../file-structure/editor-file-structure.tsx | 5 +- .../studio-files-data-provider.ts | 29 ++++++- .../use-file-tree-context-menu.ts | 4 +- src/main/frontend/app/routes/app-layout.tsx | 18 ++-- .../app/routes/studio/canvas/flow.tsx | 32 +++---- .../routes/studio/context/node-context.tsx | 10 +-- .../frontend/app/stores/node-context-store.ts | 8 +- src/main/frontend/app/stores/project-store.ts | 7 -- src/main/frontend/app/stores/tab-store.ts | 16 ++++ .../configuration/ConfigurationService.java | 13 +-- .../flow/filetree/FileTreeService.java | 4 +- .../flow/project/ProjectService.java | 72 ++++++--------- .../ConfigurationServiceTest.java | 21 ++--- .../flow/project/ProjectServiceTest.java | 87 ++++++------------- 15 files changed, 169 insertions(+), 181 deletions(-) diff --git a/src/main/frontend/app/components/file-structure/editor-data-provider.ts b/src/main/frontend/app/components/file-structure/editor-data-provider.ts index b2cc06f5..f04e564e 100644 --- a/src/main/frontend/app/components/file-structure/editor-data-provider.ts +++ b/src/main/frontend/app/components/file-structure/editor-data-provider.ts @@ -19,8 +19,30 @@ export default class EditorFilesDataProvider extends BaseFilesDataProvider a.split('/').length - b.split('/').length) + for (const id of sortedIds) { + if (id === 'root') continue + const item = this.data[id] + if (!item || !item.isFolder) continue + await this.loadDirectory(id) + } + } + + public override async reloadDirectory(_itemId: TreeItemIndex): Promise { + this.data = {} + this.loadedDirectories.clear() await this.fetchAndBuildTree() + await this.preloadExpandedItems() + this.notifyListeners(Object.keys(this.data)) } private async fetchAndBuildTree() { diff --git a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx index eb46fdb8..7a933443 100644 --- a/src/main/frontend/app/components/file-structure/editor-file-structure.tsx +++ b/src/main/frontend/app/components/file-structure/editor-file-structure.tsx @@ -84,7 +84,7 @@ export default function EditorFileStructure() { const initProvider = async () => { const provider = new EditorFilesDataProvider(project.name) - await provider.init() + await provider.init(editorExpandedItems) if (isMounted) { setDataProvider(provider) @@ -151,7 +151,6 @@ export default function EditorFileStructure() { const item = await dataProvider.getTreeItem(itemId) if (!item) return - // Toggle expanded state managed by onExpandItem naturally if needed if (item.isFolder) { return } @@ -213,6 +212,7 @@ export default function EditorFileStructure() { const renderItemArrow = ({ item, context }: { item: TreeItem; context: TreeItemRenderContext }) => { if (!item.isFolder) return null + const Icon = context.isExpanded ? AltArrowDownIcon : AltArrowRightIcon const handleClick = (event: React.MouseEvent) => { @@ -244,6 +244,7 @@ export default function EditorFileStructure() { const titleLower = title.toLowerCase() let highlightedTitle: JSX.Element | string = title + if (searchTerm && titleLower.includes(searchLower)) { const parts = title.split(new RegExp(`(${searchTerm})`, 'gi')) highlightedTitle = ( diff --git a/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts b/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts index 8cd9a8b6..5ae8ec5f 100644 --- a/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts +++ b/src/main/frontend/app/components/file-structure/studio-files-data-provider.ts @@ -1,6 +1,6 @@ import type { TreeItemIndex } from 'react-complex-tree' import { sortChildren } from './tree-utilities' -import { fetchShallowConfigurationsTree, fetchDirectoryByPath } from '~/services/file-tree-service' +import { fetchProjectTree, fetchDirectoryByPath } from '~/services/file-tree-service' import type { FileTreeNode } from '~/types/filesystem.types' import { BaseFilesDataProvider } from './base-files-data-provider' @@ -46,8 +46,15 @@ export default class FilesDataProvider extends BaseFilesDataProvider { + this.data = {} + this.loadedDirectories.clear() + await this.loadRoot() + this.notifyListeners(Object.keys(this.data)) + } + private async loadRoot() { - const tree = await fetchShallowConfigurationsTree(this.projectName) + const tree = await fetchProjectTree(this.projectName) if (!tree) { console.warn('[StudioFilesDataProvider] Received empty tree from API') @@ -130,6 +137,24 @@ export default class FilesDataProvider extends BaseFilesDataProvider { - const storedName = getStoredProjectName() - if (!storedName) { + const rootPath = getStoredProjectRootPath() + if (!rootPath) { setRestoring(false) return } - const rootPath = getStoredProjectRootPath() - - fetchProject(storedName) - .catch(() => (rootPath ? openProject(rootPath) : Promise.reject(new Error('No root path stored')))) - .then((fetched: Project) => { + openProject(rootPath) + .then((fetched) => { useProjectStore.getState().setProject(fetched) }) .catch(() => { diff --git a/src/main/frontend/app/routes/studio/canvas/flow.tsx b/src/main/frontend/app/routes/studio/canvas/flow.tsx index 28a7ed50..e8df909b 100644 --- a/src/main/frontend/app/routes/studio/canvas/flow.tsx +++ b/src/main/frontend/app/routes/studio/canvas/flow.tsx @@ -59,7 +59,6 @@ function FlowCanvas() { const { isEditing, isDirty, - pendingImmediateSave, setIsEditing, setIsNewNode, setParentId, @@ -70,7 +69,6 @@ function FlowCanvas() { useShallow((s) => ({ isEditing: s.isEditing, isDirty: s.isDirty, - pendingImmediateSave: s.pendingImmediateSave, setIsEditing: s.setIsEditing, setIsNewNode: s.setIsNewNode, setParentId: s.setParentId, @@ -109,18 +107,20 @@ function FlowCanvas() { const project = useProjectStore.getState().project const saveFlow = useCallback(async () => { - const flowData = reactFlowRef.current.toObject() + const { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } = useFlowStore.getState() + const flowData = { nodes: flowNodes, edges: flowEdges, viewport: flowViewport } + const currentProject = useProjectStore.getState().project const activeTabKey = useTabStore.getState().activeTab const tabData = useTabStore.getState().getTab(activeTabKey) const configurationPath = tabData?.configurationPath const adapterName = tabData?.name const adapterPosition = tabData?.adapterPosition - if (!configurationPath || !adapterName || !project) return + if (!configurationPath || !adapterName || !currentProject) return setSaveStatus('saving') try { - const fullConfigXml = await fetchConfigurationCached(project.name, configurationPath) + const fullConfigXml = await fetchConfigurationCached(currentProject.name, configurationPath) const configDoc = new DOMParser().parseFromString(fullConfigXml, 'text/xml') const allAdapters = [...configDoc.querySelectorAll('Adapter, adapter')] @@ -137,7 +137,7 @@ function FlowCanvas() { const newAdapterXml = await exportFlowToXml( flowData, - project.name, + currentProject.name, configurationPath, adapterName, existingAdapterXml, @@ -151,8 +151,8 @@ function FlowCanvas() { const updatedConfigXml = new XMLSerializer().serializeToString(configDoc).replace(/^<\?xml[^?]*\?>\s*/, '') - await saveConfiguration(project.name, configurationPath, updatedConfigXml) - clearConfigurationCache(project.name, configurationPath) + await saveConfiguration(currentProject.name, configurationPath, updatedConfigXml) + clearConfigurationCache(currentProject.name, configurationPath) useEditorTabStore.getState().refreshAllTabs() setSaveStatus('saved') @@ -191,15 +191,15 @@ function FlowCanvas() { }, [nodes, edges, scheduleAutoSave]) useEffect(() => { - if (!pendingImmediateSave) return - if (autoSaveTimerRef.current) { - clearTimeout(autoSaveTimerRef.current) - autoSaveTimerRef.current = null - } - void saveFlow().finally(() => { - useNodeContextStore.getState().setPendingImmediateSave(false) + useNodeContextStore.getState().registerSaveFlow(async () => { + if (autoSaveTimerRef.current) { + clearTimeout(autoSaveTimerRef.current) + autoSaveTimerRef.current = null + } + await saveFlow() }) - }, [pendingImmediateSave, saveFlow]) + return () => useNodeContextStore.getState().registerSaveFlow(null) + }, [saveFlow]) const sourceInfoReference = useRef<{ nodeId: string | null diff --git a/src/main/frontend/app/routes/studio/context/node-context.tsx b/src/main/frontend/app/routes/studio/context/node-context.tsx index eb7b0ddb..cd9dc821 100644 --- a/src/main/frontend/app/routes/studio/context/node-context.tsx +++ b/src/main/frontend/app/routes/studio/context/node-context.tsx @@ -35,7 +35,6 @@ export default function NodeContext({ setChildParentId, childParentId, setIsDirty, - setPendingImmediateSave, } = useNodeContextStore( useShallow((s) => ({ attributes: s.attributes, @@ -47,7 +46,6 @@ export default function NodeContext({ setChildParentId: s.setChildParentId, childParentId: s.childParentId, setIsDirty: s.setIsDirty, - setPendingImmediateSave: s.setPendingImmediateSave, })), ) @@ -234,7 +232,7 @@ export default function NodeContext({ setShowNodeContext(false) setParentId(null) setChildParentId(null) - setPendingImmediateSave(true) + void useNodeContextStore.getState().saveFlow?.() return } updateChild(parentNode.id, updatedChild) @@ -242,7 +240,7 @@ export default function NodeContext({ setShowNodeContext(false) setParentId(null) setChildParentId(null) - setPendingImmediateSave(true) + void useNodeContextStore.getState().saveFlow?.() return } @@ -254,7 +252,7 @@ export default function NodeContext({ setIsNewNode(false) setIsEditing(false) setShowNodeContext(false) - setPendingImmediateSave(true) + void useNodeContextStore.getState().saveFlow?.() return } setAttributes(nodeId.toString(), newAttributesObject) @@ -263,7 +261,7 @@ export default function NodeContext({ } setIsEditing(false) setShowNodeContext(false) - setPendingImmediateSave(true) + void useNodeContextStore.getState().saveFlow?.() } const canSaveRef = useRef(canSave) diff --git a/src/main/frontend/app/stores/node-context-store.ts b/src/main/frontend/app/stores/node-context-store.ts index 9f861fa5..54c53fda 100644 --- a/src/main/frontend/app/stores/node-context-store.ts +++ b/src/main/frontend/app/stores/node-context-store.ts @@ -7,22 +7,22 @@ interface NodeContextStore { isEditing: boolean isNewNode: boolean isDirty: boolean - pendingImmediateSave: boolean parentId: string | null childParentId: string | null draggedName: string | null editingSubtype: string | null + saveFlow: (() => Promise) | null setNodeId: (nodeId: number) => void setAttributes: (attributes?: Record) => void setIsEditing: (value: boolean) => void setIsNewNode: (value: boolean) => void setIsDirty: (v: boolean) => void - setPendingImmediateSave: (v: boolean) => void resetAttributes: () => void setParentId: (id: string | null) => void setChildParentId: (id: string | null) => void setDraggedName: (name: string | null) => void setEditingSubtype: (subtype: string | null) => void + registerSaveFlow: (fn: (() => Promise) | null) => void } const useNodeContextStore = create((set) => ({ @@ -31,22 +31,22 @@ const useNodeContextStore = create((set) => ({ isEditing: false, isNewNode: false, isDirty: false, - pendingImmediateSave: false, parentId: null, childParentId: null, draggedName: null, editingSubtype: null, + saveFlow: null, setNodeId: (nodeId) => set({ nodeId }), setAttributes: (attributes) => set({ attributes }), setIsEditing: (value) => set({ isEditing: value }), setIsNewNode: (value) => set({ isNewNode: value }), setIsDirty: (isDirty) => set({ isDirty }), - setPendingImmediateSave: (pendingImmediateSave) => set({ pendingImmediateSave }), resetAttributes: () => set({ attributes: undefined }), setParentId: (parentId: string | null) => set({ parentId }), setChildParentId: (childParentId: string | null) => set({ childParentId }), setDraggedName: (draggedName) => set({ draggedName }), setEditingSubtype: (editingSubtype) => set({ editingSubtype }), + registerSaveFlow: (saveFlow) => set({ saveFlow }), })) export default useNodeContextStore diff --git a/src/main/frontend/app/stores/project-store.ts b/src/main/frontend/app/stores/project-store.ts index 57692298..1616362c 100644 --- a/src/main/frontend/app/stores/project-store.ts +++ b/src/main/frontend/app/stores/project-store.ts @@ -4,7 +4,6 @@ import { useTreeStore } from '~/stores/tree-store' import useTabStore from '~/stores/tab-store' import useEditorTabStore from '~/stores/editor-tab-store' -const STORAGE_KEY = 'active-project-name' const STORAGE_ROOT_PATH_KEY = 'active-project-root-path' interface ProjectStoreState { @@ -24,11 +23,9 @@ export const useProjectStore = create((set) => ({ } return { project } }) - localStorage.setItem(STORAGE_KEY, project.name) localStorage.setItem(STORAGE_ROOT_PATH_KEY, project.rootPath) }, clearProject: () => { - localStorage.removeItem(STORAGE_KEY) localStorage.removeItem(STORAGE_ROOT_PATH_KEY) useTreeStore.getState().clearExpandedItems() useTabStore.getState().clearTabs() @@ -37,10 +34,6 @@ export const useProjectStore = create((set) => ({ }, })) -export function getStoredProjectName(): string | null { - return localStorage.getItem(STORAGE_KEY) -} - export function getStoredProjectRootPath(): string | null { return localStorage.getItem(STORAGE_ROOT_PATH_KEY) } diff --git a/src/main/frontend/app/stores/tab-store.ts b/src/main/frontend/app/stores/tab-store.ts index 3ebd2543..a503de4c 100644 --- a/src/main/frontend/app/stores/tab-store.ts +++ b/src/main/frontend/app/stores/tab-store.ts @@ -21,6 +21,7 @@ interface TabStoreState { removeTab: (tabId: string) => void removeTabAndSelectFallback: (tabId: string) => void removeTabsForConfig: (configPath: string) => void + renameTabsForConfig: (oldConfigPath: string, newConfigPath: string) => void clearTabs: () => void } @@ -69,6 +70,21 @@ const useTabStore = create()( activeTab: remainingKeys.includes(state.activeTab) ? state.activeTab : (remainingKeys.at(-1) ?? ''), } }), + renameTabsForConfig: (oldConfigPath, newConfigPath) => + set((state) => { + const newTabs: Record = {} + let newActiveTab = state.activeTab + for (const [tabId, tab] of Object.entries(state.tabs)) { + if (tab.configurationPath === oldConfigPath) { + const newTabId = tabId.replace(oldConfigPath, newConfigPath) + newTabs[newTabId] = { ...tab, configurationPath: newConfigPath } + if (state.activeTab === tabId) newActiveTab = newTabId + } else { + newTabs[tabId] = tab + } + } + return { tabs: newTabs, activeTab: newActiveTab } + }), clearTabs: () => set({ tabs: {}, activeTab: '' }), })), ) diff --git a/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java b/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java index 440fd11c..a8bc30cb 100644 --- a/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java +++ b/src/main/java/org/frankframework/flow/configuration/ConfigurationService.java @@ -40,7 +40,7 @@ public String getConfigurationContent(String filepath) throws IOException, Confi } public void updateConfiguration(String projectName, String filepath, String content) - throws IOException, ConfigurationNotFoundException, ProjectNotFoundException { + throws IOException, ConfigurationNotFoundException { Path absolutePath = fileSystemStorage.toAbsolutePath(filepath); if (!Files.exists(absolutePath)) { @@ -51,8 +51,8 @@ public void updateConfiguration(String projectName, String filepath, String cont throw new ConfigurationNotFoundException("Invalid file path: " + filepath); } + // Just write to the disk. ProjectService reads directly from disk now! fileSystemStorage.writeFile(absolutePath.toString(), content); - projectService.updateConfigurationXml(projectName, filepath, content); } public Project addConfiguration(String projectName, String configurationName) @@ -74,10 +74,7 @@ public Project addConfiguration(String projectName, String configurationName) String defaultXml = loadDefaultConfigurationXml(); fileSystemStorage.writeFile(filePath.toString(), defaultXml); - String relativePath = fileSystemStorage.toRelativePath(filePath.toString()); - Configuration configuration = new Configuration(relativePath); - configuration.setXmlContent(defaultXml); - project.addConfiguration(configuration); + // Returning the project handles everything, as 'toDto' will pick up the new file return project; } @@ -108,10 +105,6 @@ public Project addConfigurationToFolder(String projectName, String configuration String defaultXml = loadDefaultConfigurationXml(); fileSystemStorage.writeFile(filePath.toString(), defaultXml); - String relativePath = fileSystemStorage.toRelativePath(filePath.toString()); - Configuration configuration = new Configuration(relativePath); - configuration.setXmlContent(defaultXml); - project.addConfiguration(configuration); return project; } diff --git a/src/main/java/org/frankframework/flow/filetree/FileTreeService.java b/src/main/java/org/frankframework/flow/filetree/FileTreeService.java index efafd727..54fc257b 100644 --- a/src/main/java/org/frankframework/flow/filetree/FileTreeService.java +++ b/src/main/java/org/frankframework/flow/filetree/FileTreeService.java @@ -134,10 +134,10 @@ public FileTreeNode createFile(String projectName, String parentPath, String fil if (fileName.toLowerCase().endsWith(".xml")) { configurationService.addConfigurationToFolder(projectName, fileName, parentPath); - return null; + } else { + fileSystemStorage.createFile(fullPath); } - fileSystemStorage.createFile(fullPath); invalidateTreeCache(projectName); FileTreeNode node = new FileTreeNode(); diff --git a/src/main/java/org/frankframework/flow/project/ProjectService.java b/src/main/java/org/frankframework/flow/project/ProjectService.java index 30d28328..58e94d4b 100644 --- a/src/main/java/org/frankframework/flow/project/ProjectService.java +++ b/src/main/java/org/frankframework/flow/project/ProjectService.java @@ -17,8 +17,6 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.transport.CredentialsProvider; -import org.frankframework.flow.configuration.Configuration; -import org.frankframework.flow.configuration.ConfigurationNotFoundException; import org.frankframework.flow.filesystem.FileSystemStorage; import org.frankframework.flow.filesystem.FilesystemEntry; import org.frankframework.flow.git.GitCredentialHelper; @@ -39,6 +37,7 @@ public class ProjectService { private final FileSystemStorage fileSystemStorage; private final RecentProjectsService recentProjectsService; + // Cache is now ONLY for lightweight Project state (Tokens, Filters), NOT files. private final Map projectCache = new ConcurrentHashMap<>(); public ProjectService(FileSystemStorage fileSystemStorage, @Lazy RecentProjectsService recentProjectsService) { @@ -166,20 +165,6 @@ public void invalidateProject(String projectName) { projectCache.entrySet().removeIf(e -> e.getValue().getName().equals(projectName)); } - public boolean updateConfigurationXml(String projectName, String filepath, String xmlContent) - throws ProjectNotFoundException, ConfigurationNotFoundException { - Project project = getProject(projectName); - - Configuration targetConfig = project.getConfigurations().stream() - .filter(c -> c.getFilepath().equals(filepath)) - .findFirst() - .orElseThrow(() -> new ConfigurationNotFoundException( - String.format("Configuration with filepath: %s not found", filepath))); - - targetConfig.setXmlContent(xmlContent); - return true; - } - public Project enableFilter(String projectName, String type) throws ProjectNotFoundException, InvalidFilterTypeException { Project project = getProject(projectName); @@ -244,10 +229,9 @@ public Project importProjectFromFiles(String projectName, List fi public ProjectDTO toDto(Project project) { String cleanPath = fileSystemStorage.toRelativePath(project.getRootPath()); - List filepaths = project.getConfigurations().stream() - .map(Configuration::getFilepath) - .map(fileSystemStorage::toRelativePath) - .toList(); + + // Dynamically fetch configurations from disk as the single source of truth + List filepaths = getConfigurationFilesDynamically(project.getRootPath()); boolean isGitRepo = false; try { @@ -269,6 +253,27 @@ public ProjectDTO toDto(Project project) { hasStoredToken); } + private List getConfigurationFilesDynamically(String projectRoot) { + try { + Path absPath = fileSystemStorage.toAbsolutePath(projectRoot); + Path configDir = absPath.resolve(CONFIGURATIONS_DIR).normalize(); + + if (!Files.exists(configDir) || !Files.isDirectory(configDir)) { + return List.of(); + } + + try (Stream stream = Files.walk(configDir)) { + return stream.filter(Files::isRegularFile) + .filter(p -> p.toString().toLowerCase().endsWith(".xml")) + .map(p -> fileSystemStorage.toRelativePath(p.toString())) + .toList(); + } + } catch (IOException e) { + log.error("Failed to read configurations from disk for project {}", projectRoot, e); + return List.of(); + } + } + private FilterType parseFilterType(String type) throws InvalidFilterTypeException { try { return FilterType.valueOf(type.toUpperCase()); @@ -302,30 +307,9 @@ private Project loadProjectFromStorage(String path) throws IOException { throw new IOException("Invalid project path: " + absPath); } - Project project = new Project(absPath.getFileName().toString(), absPath.toString()); - - Path configDir = absPath.resolve(CONFIGURATIONS_DIR).normalize(); - - validatePathSafety(configDir); - - if (!Files.exists(configDir)) { - return project; - } - - try (Stream s = Files.walk(configDir)) { - s.filter(p -> p.toString().endsWith(".xml")).forEach(p -> { - try { - String content = fileSystemStorage.readFile(p.toString()); - String relativePath = fileSystemStorage.toRelativePath(p.toString()); - Configuration c = new Configuration(relativePath); - c.setXmlContent(content); - project.addConfiguration(c); - } catch (IOException e) { - log.error("Error reading config file {}: {}", p, e.getMessage(), e); - } - }); - } - return project; + // We no longer read XML files into memory here. + // We just return the lightweight project object. + return new Project(absPath.getFileName().toString(), absPath.toString()); } private static void validatePathSafety(Path path) { diff --git a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java index 53544039..8aadbffc 100644 --- a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java +++ b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java @@ -107,13 +107,12 @@ void updateConfiguration_Success() throws Exception { Path file = tempDir.resolve("config.xml"); Files.writeString(file, "", StandardCharsets.UTF_8); - when(projectService.updateConfigurationXml("proj", file.toString(), "")) - .thenReturn(true); - + // Call the service configurationService.updateConfiguration("proj", file.toString(), ""); + // Verify the file was physically written to disk assertEquals("", Files.readString(file, StandardCharsets.UTF_8)); - verify(projectService).updateConfigurationXml("proj", file.toString(), ""); + verify(fileSystemStorage).writeFile(file.toString(), ""); } @Test @@ -131,7 +130,6 @@ void updateConfiguration_FileNotFound_ThrowsConfigurationNotFoundException() thr void addConfiguration_Success() throws Exception { stubToAbsolutePath(); stubWriteFile(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); Path projectDir = tempDir.resolve("myproject"); Files.createDirectories(projectDir); @@ -142,12 +140,10 @@ void addConfiguration_Success() throws Exception { Project result = configurationService.addConfiguration("myproject", "NewConfig.xml"); assertNotNull(result); + + // Verify the file was created on disk Path expectedFile = projectDir.resolve("src/main/configurations/NewConfig.xml"); assertTrue(Files.exists(expectedFile), "NewConfig.xml should be created on disk"); - assertTrue( - result.getConfigurations().stream() - .anyMatch(c -> c.getFilepath().endsWith("NewConfig.xml")), - "Configuration should be registered in project"); } @Test @@ -175,7 +171,6 @@ void addConfiguration_PathTraversal_ThrowsSecurityException() throws Exception { void addConfigurationToFolder_Success() throws Exception { stubToAbsolutePath(); stubWriteFile(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); Path projectDir = tempDir.resolve("myproject"); Files.createDirectories(projectDir); @@ -187,9 +182,9 @@ void addConfigurationToFolder_Success() throws Exception { configurationService.addConfigurationToFolder("myproject", "Nested.xml", projectDir.toString()); assertNotNull(result); - assertTrue(Files.exists(projectDir.resolve("Nested.xml")), "Nested.xml should be created"); - assertTrue(result.getConfigurations().stream() - .anyMatch(c -> c.getFilepath().endsWith("Nested.xml"))); + + // Verify the file was created on disk + assertTrue(Files.exists(projectDir.resolve("Nested.xml")), "Nested.xml should be created on disk"); } @Test diff --git a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java index ad69ad16..b5c8110e 100644 --- a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java +++ b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java @@ -20,8 +20,6 @@ import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import org.frankframework.flow.configuration.Configuration; -import org.frankframework.flow.configuration.ConfigurationNotFoundException; import org.frankframework.flow.filesystem.FileSystemStorage; import org.frankframework.flow.filesystem.FilesystemEntry; import org.frankframework.flow.projectsettings.FilterType; @@ -91,7 +89,7 @@ private void stubFileSystemForProjectCreation() throws IOException { .when(fileSystemStorage) .writeFile(anyString(), anyString()); - when(fileSystemStorage.readFile(anyString())).thenAnswer(invocation -> { + lenient().when(fileSystemStorage.readFile(anyString())).thenAnswer(invocation -> { String path = invocation.getArgument(0); return Files.readString(Path.of(path), StandardCharsets.UTF_8); }); @@ -134,8 +132,9 @@ public void testCreateProjectOnDiskCreatesDirectoryStructure() throws IOExceptio } @Test - public void testCreateProjectOnDiskLoadsConfiguration() throws IOException, ProjectNotFoundException { + public void testCreateProjectOnDiskHasConfigurationsInDto() throws IOException, ProjectNotFoundException { stubFileSystemForProjectCreation(); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); String projectName = "loaded_proj"; @@ -143,7 +142,9 @@ public void testCreateProjectOnDiskLoadsConfiguration() throws IOException, Proj Project project = projectService.getProject(projectName); assertNotNull(project); - assertFalse(project.getConfigurations().isEmpty(), "Project should have at least one configuration loaded"); + + ProjectDTO dto = projectService.toDto(project); + assertFalse(dto.filepaths().isEmpty(), "Project DTO should dynamically load configurations from disk"); } @Test @@ -181,46 +182,6 @@ public void testGetProjectsFromRecentList() throws IOException { assertEquals("my_project", projects.getFirst().getName()); } - @Test - public void testUpdateConfigurationXmlSuccess() throws Exception { - stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); - - projectService.createProjectOnDisk("proj"); - Project project = projectService.getProject("proj"); - - assertFalse(project.getConfigurations().isEmpty()); - Configuration config = project.getConfigurations().getFirst(); - String filepath = config.getFilepath(); - - boolean updated = projectService.updateConfigurationXml("proj", filepath, ""); - - assertTrue(updated); - assertEquals("", config.getXmlContent()); - } - - @Test - public void testUpdateConfigurationXmlThrowsProjectNotFound() { - when(fileSystemStorage.isLocalEnvironment()).thenReturn(true); - when(recentProjectsService.getRecentProjects()).thenReturn(recentProjects); - - assertThrows( - ProjectNotFoundException.class, - () -> projectService.updateConfigurationXml("unknownProject", "config.xml", "")); - } - - @Test - public void testUpdateConfigurationXmlConfigNotFound() throws Exception { - stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); - - projectService.createProjectOnDisk("proj"); - - assertThrows( - ConfigurationNotFoundException.class, - () -> projectService.updateConfigurationXml("proj", "missingConfig.xml", "")); - } - @Test public void testEnableFilterValid() throws Exception { stubFileSystemForProjectCreation(); @@ -436,6 +397,8 @@ void testOpenProjectFromDisk() throws Exception { return tempDir.resolve(path); }); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + String projectName = "manual_project"; Path projectDir = tempDir.resolve(projectName); Files.createDirectories(projectDir.resolve("src/main/configurations")); @@ -448,7 +411,9 @@ void testOpenProjectFromDisk() throws Exception { assertNotNull(project); assertEquals(projectName, project.getName()); - assertFalse(project.getConfigurations().isEmpty()); + + ProjectDTO dto = projectService.toDto(project); + assertFalse(dto.filepaths().isEmpty()); } @Test @@ -491,7 +456,9 @@ void testOpenProjectFromDiskLoadsEmptyProject_whenNoConfigurationsDir() throws E assertNotNull(project); assertEquals("empty_proj", project.getName()); - assertTrue(project.getConfigurations().isEmpty(), "No configurations dir means empty config list"); + + ProjectDTO dto = projectService.toDto(project); + assertTrue(dto.filepaths().isEmpty(), "No configurations dir means empty config list"); } @Test @@ -514,16 +481,15 @@ void testGetProjectsFromWorkspaceScan() throws Exception { return p.isAbsolute() ? p : tempDir.resolve(path); }); - when(fileSystemStorage.readFile(anyString())).thenAnswer(invocation -> { - String path = invocation.getArgument(0); - return Files.readString(Path.of(path), StandardCharsets.UTF_8); - }); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); List projects = projectService.getProjects(); assertEquals(1, projects.size()); assertEquals("scanned_proj", projects.getFirst().getName()); - assertFalse(projects.getFirst().getConfigurations().isEmpty()); + + ProjectDTO dto = projectService.toDto(projects.getFirst()); + assertFalse(dto.filepaths().isEmpty()); } @Test @@ -533,7 +499,7 @@ void testGetProjectsFromWorkspaceScanSkipsInvalidEntries() throws Exception { Path validDir = tempDir.resolve("valid_proj"); Files.createDirectory(validDir); - Path invalidDir = tempDir.resolve("nonexistent_proj"); // does not exist + Path invalidDir = tempDir.resolve("nonexistent_proj"); when(fileSystemStorage.listRoots()) .thenReturn(List.of( @@ -634,7 +600,7 @@ void testExportProjectAsZipThrowsWhenDirectoryDeletedAfterCaching() throws Excep @Test void testToDtoReturnsCorrectFields() throws Exception { stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("dto_proj"); Project project = projectService.getProject("dto_proj"); @@ -652,7 +618,7 @@ void testToDtoReturnsCorrectFields() throws Exception { @Test void testToDtoDetectsGitRepository() throws Exception { stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("git_proj"); Project project = projectService.getProject("git_proj"); @@ -668,7 +634,7 @@ void testToDtoDetectsGitRepository() throws Exception { @Test void testToDtoReportsHasStoredToken_whenTokenIsSet() throws Exception { stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("token_proj"); Project project = projectService.getProject("token_proj"); @@ -682,7 +648,7 @@ void testToDtoReportsHasStoredToken_whenTokenIsSet() throws Exception { @Test void testToDtoReportsNoStoredToken_whenTokenIsBlank() throws Exception { stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("blank_token_proj"); Project project = projectService.getProject("blank_token_proj"); @@ -696,17 +662,14 @@ void testToDtoReportsNoStoredToken_whenTokenIsBlank() throws Exception { @Test void testToDtoMapsConfigurationFilepaths() throws Exception { stubFileSystemForProjectCreation(); - when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("filepath_proj"); Project project = projectService.getProject("filepath_proj"); ProjectDTO dto = projectService.toDto(project); - assertEquals( - project.getConfigurations().size(), - dto.filepaths().size(), - "DTO filepaths count should match number of configurations"); + assertEquals(1, dto.filepaths().size(), "DTO filepaths should map dynamically from disk"); } @Test From 209b9b1f99e5375114e3858f04648a0cebb8fe05 Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 17:32:12 +0100 Subject: [PATCH 6/8] fix: improved failing file tree test for creating a file on the file system --- .../org/frankframework/flow/filetree/FileTreeServiceTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/org/frankframework/flow/filetree/FileTreeServiceTest.java b/src/test/java/org/frankframework/flow/filetree/FileTreeServiceTest.java index fe8ac7c4..2da13839 100644 --- a/src/test/java/org/frankframework/flow/filetree/FileTreeServiceTest.java +++ b/src/test/java/org/frankframework/flow/filetree/FileTreeServiceTest.java @@ -1,6 +1,5 @@ package org.frankframework.flow.filetree; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; @@ -416,7 +415,7 @@ void createFile_ShouldDelegateToConfigurationService_WhenXml() throws Exception FileTreeNode node = fileTreeService.createFile( TEST_PROJECT_NAME, tempProjectRoot.toAbsolutePath().toString(), "config.xml"); - assertNull(node); + assertNotNull(node); verify(configurationService).addConfigurationToFolder(eq(TEST_PROJECT_NAME), eq("config.xml"), anyString()); } From 4cc9ec409c9b3fc3a8ca4e2842fbd8c04426dcde Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 17:44:17 +0100 Subject: [PATCH 7/8] fix: improved tests by making them more robust --- .../flow/project/ProjectService.java | 2 -- .../ConfigurationServiceTest.java | 4 ---- .../flow/project/ProjectServiceTest.java | 19 +++++++------------ 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/frankframework/flow/project/ProjectService.java b/src/main/java/org/frankframework/flow/project/ProjectService.java index 58e94d4b..2106c65b 100644 --- a/src/main/java/org/frankframework/flow/project/ProjectService.java +++ b/src/main/java/org/frankframework/flow/project/ProjectService.java @@ -307,8 +307,6 @@ private Project loadProjectFromStorage(String path) throws IOException { throw new IOException("Invalid project path: " + absPath); } - // We no longer read XML files into memory here. - // We just return the lightweight project object. return new Project(absPath.getFileName().toString(), absPath.toString()); } diff --git a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java index 8aadbffc..a4a2f519 100644 --- a/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java +++ b/src/test/java/org/frankframework/flow/configuration/ConfigurationServiceTest.java @@ -107,10 +107,8 @@ void updateConfiguration_Success() throws Exception { Path file = tempDir.resolve("config.xml"); Files.writeString(file, "", StandardCharsets.UTF_8); - // Call the service configurationService.updateConfiguration("proj", file.toString(), ""); - // Verify the file was physically written to disk assertEquals("", Files.readString(file, StandardCharsets.UTF_8)); verify(fileSystemStorage).writeFile(file.toString(), ""); } @@ -141,7 +139,6 @@ void addConfiguration_Success() throws Exception { assertNotNull(result); - // Verify the file was created on disk Path expectedFile = projectDir.resolve("src/main/configurations/NewConfig.xml"); assertTrue(Files.exists(expectedFile), "NewConfig.xml should be created on disk"); } @@ -183,7 +180,6 @@ void addConfigurationToFolder_Success() throws Exception { assertNotNull(result); - // Verify the file was created on disk assertTrue(Files.exists(projectDir.resolve("Nested.xml")), "Nested.xml should be created on disk"); } diff --git a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java index b5c8110e..7e16d036 100644 --- a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java +++ b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java @@ -88,11 +88,6 @@ private void stubFileSystemForProjectCreation() throws IOException { }) .when(fileSystemStorage) .writeFile(anyString(), anyString()); - - lenient().when(fileSystemStorage.readFile(anyString())).thenAnswer(invocation -> { - String path = invocation.getArgument(0); - return Files.readString(Path.of(path), StandardCharsets.UTF_8); - }); } @Test @@ -134,7 +129,7 @@ public void testCreateProjectOnDiskCreatesDirectoryStructure() throws IOExceptio @Test public void testCreateProjectOnDiskHasConfigurationsInDto() throws IOException, ProjectNotFoundException { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); String projectName = "loaded_proj"; @@ -397,7 +392,7 @@ void testOpenProjectFromDisk() throws Exception { return tempDir.resolve(path); }); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); String projectName = "manual_project"; Path projectDir = tempDir.resolve(projectName); @@ -481,7 +476,7 @@ void testGetProjectsFromWorkspaceScan() throws Exception { return p.isAbsolute() ? p : tempDir.resolve(path); }); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); List projects = projectService.getProjects(); @@ -600,7 +595,7 @@ void testExportProjectAsZipThrowsWhenDirectoryDeletedAfterCaching() throws Excep @Test void testToDtoReturnsCorrectFields() throws Exception { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("dto_proj"); Project project = projectService.getProject("dto_proj"); @@ -618,7 +613,7 @@ void testToDtoReturnsCorrectFields() throws Exception { @Test void testToDtoDetectsGitRepository() throws Exception { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("git_proj"); Project project = projectService.getProject("git_proj"); @@ -634,7 +629,7 @@ void testToDtoDetectsGitRepository() throws Exception { @Test void testToDtoReportsHasStoredToken_whenTokenIsSet() throws Exception { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("token_proj"); Project project = projectService.getProject("token_proj"); @@ -648,7 +643,7 @@ void testToDtoReportsHasStoredToken_whenTokenIsSet() throws Exception { @Test void testToDtoReportsNoStoredToken_whenTokenIsBlank() throws Exception { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("blank_token_proj"); Project project = projectService.getProject("blank_token_proj"); From f197014d4870c7cf155b5cdb531492a4629db63c Mon Sep 17 00:00:00 2001 From: stijnpotters Date: Thu, 12 Mar 2026 17:50:13 +0100 Subject: [PATCH 8/8] chore: removed lenient stub --- .../org/frankframework/flow/project/ProjectServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java index 7e16d036..518bc694 100644 --- a/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java +++ b/src/test/java/org/frankframework/flow/project/ProjectServiceTest.java @@ -657,7 +657,7 @@ void testToDtoReportsNoStoredToken_whenTokenIsBlank() throws Exception { @Test void testToDtoMapsConfigurationFilepaths() throws Exception { stubFileSystemForProjectCreation(); - lenient().when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); + when(fileSystemStorage.toRelativePath(anyString())).thenAnswer(inv -> inv.getArgument(0)); projectService.createProjectOnDisk("filepath_proj"); Project project = projectService.getProject("filepath_proj");