From a7b5aa1ddf32ea101f0d445bbbabf6452cf5c36b Mon Sep 17 00:00:00 2001 From: Vivy <4380412+Matthbo@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:22:56 +0200 Subject: [PATCH 1/5] Improve git service & helper logic --- .../clone-configuration-modal.tsx | 23 ++++---- .../frontend/app/services/project-service.ts | 2 +- .../flow/git/GitCredentialHelper.java | 50 ++++++++--------- .../ConfigurationProjectController.java | 5 +- .../project/ConfigurationProjectService.java | 53 +++++++++---------- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx index 15480621..0ee2ee28 100644 --- a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx +++ b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx @@ -26,23 +26,20 @@ export default function CloneConfigurationModal({ const [showPicker, setShowPicker] = useState(false) useEffect(() => { - if (!isOpen || !isLocal) { - if (isOpen) setLocation(initialPath ?? '') + if (!isOpen) return + if (isLocal) { + filesystemService + .resolveNearestAccessiblePath(initialPath ?? '') + .then(setLocation) + .catch(() => setLocation('')) return } - - filesystemService - .resolveNearestAccessiblePath(initialPath ?? '') - .then(setLocation) - .catch(() => setLocation('')) + setLocation(initialPath ?? '') }, [isOpen, isLocal, initialPath]) if (!isOpen) return null - const repoName = repoUrl - .split('/') - .pop() - ?.replace(/\.git$/, '') + const repoName = repoUrl.split('/').pop()?.replace('.git', '') const handleClone = () => { if (!repoUrl.trim()) return @@ -58,7 +55,7 @@ export default function CloneConfigurationModal({ finalPath = location ? `${location}/${name}` : name } - onClone(repoUrl.trim(), finalPath, token || undefined) + onClone(repoUrl.trim(), finalPath, token) handleClose() } @@ -73,7 +70,7 @@ export default function CloneConfigurationModal({ return ( <>
-
+

Clone Repository

{isLocal ? 'Clone a Git repository to a local folder' : 'Clone a Git repository into the workspace'} diff --git a/src/main/frontend/app/services/project-service.ts b/src/main/frontend/app/services/project-service.ts index c40edfb7..3e95773b 100644 --- a/src/main/frontend/app/services/project-service.ts +++ b/src/main/frontend/app/services/project-service.ts @@ -15,7 +15,7 @@ export async function openProject(rootPath: string): Promise { return apiFetch('/projects/clone', { method: 'POST', - body: JSON.stringify({ repoUrl, localPath, token: token || null }), + body: JSON.stringify({ repoUrl, localPath, token: token ?? null }), }) } diff --git a/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java b/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java index 5e38ea13..b39dd694 100644 --- a/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java +++ b/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java @@ -7,7 +7,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.TimeUnit; + import lombok.extern.log4j.Log4j2; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.CredentialsProvider; @@ -45,7 +48,10 @@ public static CredentialsProvider resolve(Repository repo, String explicitToken, * Resolves credentials for a URL directly (used for clone where no repo exists yet). */ public static CredentialsProvider resolveForUrl( - String remoteUrl, String explicitToken, boolean isLocalEnvironment) { + String remoteUrl, + String explicitToken, + boolean isLocalEnvironment + ) { if (explicitToken != null && !explicitToken.isBlank()) { return new UsernamePasswordCredentialsProvider("token", explicitToken); } @@ -73,8 +79,8 @@ private static CredentialsProvider fromRemoteUrl(Repository repo) { } return queryGitCredential(uri); - } catch (Exception e) { - log.debug("Could not resolve credentials from system helper: {}", e.getMessage()); + } catch (Exception exception) { + log.debug("Could not resolve credentials from system helper: {}", exception.getMessage()); return null; } } @@ -132,8 +138,7 @@ private static CredentialsProvider queryGitCredential(URI uri) { String username = null; String password = null; - try (BufferedReader reader = - new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { if (line.startsWith("username=")) { @@ -150,12 +155,12 @@ private static CredentialsProvider queryGitCredential(URI uri) { } return null; - } catch (InterruptedException e) { + } catch (InterruptedException exception) { log.debug("Git credential helper interrupted"); Thread.currentThread().interrupt(); return null; - } catch (Exception e) { - log.debug("Failed to query system git credential helper: {}", e.getMessage()); + } catch (Exception exception) { + log.debug("Failed to query system git credential helper: {}", exception.getMessage()); return null; } } @@ -182,26 +187,23 @@ private static String getGitExecutable() { * Uses absolute paths in ProcessBuilder to satisfy SonarQube S4036. */ private static String findGitOnPath() { - boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win"); - String[] candidates = isWindows ? new String[] {"git.exe", "git.cmd"} : new String[] {"git"}; - - String pathEnv = System.getenv("PATH"); - if (pathEnv == null) { - log.debug("PATH environment variable not set; git credential helper unavailable"); + String environmentalPaths = System.getenv("PATH"); + if (environmentalPaths == null) { + log.debug("PATH environment variable not set, git credential helper unavailable"); return null; } + boolean isWindows = System.getProperty("os.name", "").toLowerCase().contains("win"); String separator = isWindows ? ";" : ":"; - for (String dir : pathEnv.split(separator)) { - Path dirPath = Path.of(dir); - if (!Files.isDirectory(dirPath)) continue; - for (String candidate : candidates) { - Path exe = dirPath.resolve(candidate); - if (Files.isRegularFile(exe) && Files.isExecutable(exe)) { - String absolutePath = exe.toAbsolutePath().toString(); - log.debug("Resolved git executable: {}", absolutePath); - return absolutePath; - } + + List environmentDirectories = Arrays.stream(environmentalPaths.split(separator)).map(Path::of).filter(Files::isDirectory).toList(); + for (Path directory : environmentDirectories) { + String executableName = isWindows ? "git.exe" : "git"; + Path executable = directory.resolve(executableName); + if (Files.isRegularFile(executable) && Files.isExecutable(executable)) { + String absolutePath = executable.toAbsolutePath().toString(); + log.debug("Resolved git executable: {}", absolutePath); + return absolutePath; } } diff --git a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java index 2f1d6d32..7a64bbea 100644 --- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java +++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java @@ -74,7 +74,10 @@ public ResponseEntity createProject(@RequestBody Config @PostMapping("/clone") public ResponseEntity cloneProject(@RequestBody ConfigurationProjectCloneDTO configurationProjectCloneDTO) throws IOException { ConfigurationProject configurationProject = configurationProjectService.cloneAndOpenProject( - configurationProjectCloneDTO.repoUrl(), configurationProjectCloneDTO.localPath(), configurationProjectCloneDTO.token()); + configurationProjectCloneDTO.repoUrl(), + configurationProjectCloneDTO.localPath(), + configurationProjectCloneDTO.token() + ); recentProjectsService.addRecentProject(configurationProject.getName(), configurationProject.getRootPath()); return ResponseEntity.ok(configurationProjectService.toDto(configurationProject)); } diff --git a/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java b/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java index 6369cda3..25847b1c 100644 --- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java +++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java @@ -137,27 +137,25 @@ public ConfigurationProject cloneAndOpenProject(String repoUrl, String localPath Path targetDir = fileSystemStorage.toAbsolutePath(localPath); if (Files.exists(targetDir)) { - throw new IllegalArgumentException("Project already exists at \"" + localPath + "\""); + throw new ApiException("Project already exists at \"" + localPath + "\""); } - try { - CloneCommand cloneCommand = Git.cloneRepository().setURI(repoUrl).setDirectory(targetDir.toFile()); + CloneCommand cloneCommand = Git.cloneRepository().setURI(repoUrl).setDirectory(targetDir.toFile()); - CredentialsProvider credentials = GitCredentialHelper.resolveForUrl(repoUrl, token, fileSystemStorage.isLocalEnvironment()); - if (credentials != null) { - cloneCommand.setCredentialsProvider(credentials); - } + CredentialsProvider credentials = GitCredentialHelper.resolveForUrl(repoUrl, token, fileSystemStorage.isLocalEnvironment()); + if (credentials != null) { + cloneCommand.setCredentialsProvider(credentials); + } - try (Git git = cloneCommand.call()) { - log.info("Cloned repository {} to {}", repoUrl, targetDir); - GitService.hardenRepository(git.getRepository()); - } + try (Git git = cloneCommand.call()) { + log.info("Cloned repository {} to {}", repoUrl, targetDir); + GitService.hardenRepository(git.getRepository()); } catch (GitAPIException exception) { String msg = exception.getMessage() != null ? exception.getMessage().toLowerCase() : ""; if (msg.contains("auth") || msg.contains("not permitted") || msg.contains("403") || msg.contains("401")) { throw new IllegalArgumentException("Cloning authentication error. Please provide a valid Personal Access Token (PAT)", exception); } - throw new IllegalArgumentException("Cloning failed: " + exception.getMessage(), exception); + throw new ApiException("Cloning failed", exception); } ConfigurationProject configurationProject = loadProjectAndCache(targetDir.toString()); @@ -175,8 +173,7 @@ public void invalidateProject(String projectName) { projectCache.entrySet().removeIf(entry -> entry.getValue().getName().equals(projectName)); } - public ConfigurationProject enableFilter(String projectName, String type) - throws ApiException { + public ConfigurationProject enableFilter(String projectName, String type) throws ApiException { ConfigurationProject configurationProject = getProject(projectName); configurationProject.enableFilter(parseFilterType(type)); return configurationProject; @@ -196,14 +193,14 @@ public void exportProjectAsZip(String projectName, OutputStream outputStream) th throw new ApiException("Project directory not found: " + projectName, HttpStatus.NOT_FOUND); } - try (ZipOutputStream zos = new ZipOutputStream(outputStream); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); Stream paths = Files.walk(projectPath)) { paths.filter(Files::isRegularFile).forEach(filePath -> { try { String entryName = projectPath.relativize(filePath).toString().replace("\\", "/"); - zos.putNextEntry(new ZipEntry(entryName)); - Files.copy(filePath, zos); - zos.closeEntry(); + zipOutputStream.putNextEntry(new ZipEntry(entryName)); + Files.copy(filePath, zipOutputStream); + zipOutputStream.closeEntry(); } catch (IOException exception) { throw new RuntimeException("Error zipping file: " + filePath, exception); } @@ -255,19 +252,17 @@ public ConfigurationProjectDTO toDto(ConfigurationProject configurationProject) } private List getConfigurationFilesDynamically(String projectRoot) { - try { - Path absolutePath = fileSystemStorage.toAbsolutePath(projectRoot); + Path absolutePath = fileSystemStorage.toAbsolutePath(projectRoot); - if (!Files.exists(absolutePath) || !Files.isDirectory(absolutePath)) { - return List.of(); - } + if (!Files.exists(absolutePath) || !Files.isDirectory(absolutePath)) { + return List.of(); + } - try (Stream stream = Files.walk(absolutePath)) { - return stream.filter(Files::isRegularFile) - .filter(path -> path.toString().toLowerCase().endsWith(".xml")) - .map(path -> fileSystemStorage.toRelativePath(path.toString())) - .toList(); - } + try (Stream stream = Files.walk(absolutePath)) { + return stream.filter(Files::isRegularFile) + .filter(path -> path.toString().toLowerCase().endsWith(".xml")) + .map(path -> fileSystemStorage.toRelativePath(path.toString())) + .toList(); } catch (IOException exception) { log.error("Failed to read configurations from disk for project {}", projectRoot, exception); return List.of(); From 04b6e5efa3eef42f9ed171eb9037c68f96934df5 Mon Sep 17 00:00:00 2001 From: Vivy <4380412+Matthbo@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:24:36 +0200 Subject: [PATCH 2/5] Separate sub components of project-landing --- .../directory-picker/directory-picker.tsx | 20 +-- .../add-configuration-modal.tsx | 15 +- .../clone-configuration-modal.tsx | 26 ++- .../new-configuration-modal.tsx | 29 ++-- .../routes/projectlanding/project-landing.tsx | 160 ++++-------------- .../routes/projectlanding/project-list.tsx | 65 +++++++ .../app/routes/projectlanding/sidebar.tsx | 29 ++++ .../app/routes/projectlanding/toolbar.tsx | 21 +++ 8 files changed, 184 insertions(+), 181 deletions(-) create mode 100644 src/main/frontend/app/routes/projectlanding/project-list.tsx create mode 100644 src/main/frontend/app/routes/projectlanding/sidebar.tsx create mode 100644 src/main/frontend/app/routes/projectlanding/toolbar.tsx diff --git a/src/main/frontend/app/components/directory-picker/directory-picker.tsx b/src/main/frontend/app/components/directory-picker/directory-picker.tsx index 9f0fea3b..dcc05bc8 100644 --- a/src/main/frontend/app/components/directory-picker/directory-picker.tsx +++ b/src/main/frontend/app/components/directory-picker/directory-picker.tsx @@ -9,7 +9,6 @@ import Button from '../inputs/button' import CloseButton from '../inputs/close-button' interface DirectoryPickerProperties { - isOpen: boolean onSelect: (absolutePath: string) => void onCancel: () => void rootLabel?: string @@ -17,7 +16,6 @@ interface DirectoryPickerProperties { } export default function DirectoryPicker({ - isOpen, onSelect, onCancel, rootLabel = 'Computer', @@ -52,16 +50,12 @@ export default function DirectoryPicker({ } }, []) - useDirectoryWatcher(isOpen ? currentPath : null, () => void loadEntries(currentPath)) + useDirectoryWatcher(currentPath, () => void loadEntries(currentPath)) useEffect(() => { - if (isOpen) { - setSelectedEntry(null) - loadEntries(initialPath ?? '') - } - }, [isOpen, loadEntries, initialPath]) - - if (!isOpen) return null + setSelectedEntry(null) + loadEntries(initialPath ?? '') + }, [loadEntries, initialPath]) const handleClick = (entry: FilesystemEntry) => { setSelectedEntry(entry.path) @@ -89,8 +83,8 @@ export default function DirectoryPicker({ const activePath = selectedEntry ?? currentPath return ( -

-
+
+

Select Directory

@@ -135,7 +129,7 @@ export default function DirectoryPicker({ selectedEntry === entry.path ? 'bg-backdrop font-medium' : 'hover:bg-backdrop/50' }`} > - + {entry.projectRoot && ( diff --git a/src/main/frontend/app/routes/configurations/add-configuration-modal.tsx b/src/main/frontend/app/routes/configurations/add-configuration-modal.tsx index 630bcc2f..f8716715 100644 --- a/src/main/frontend/app/routes/configurations/add-configuration-modal.tsx +++ b/src/main/frontend/app/routes/configurations/add-configuration-modal.tsx @@ -141,13 +141,14 @@ export default function AddConfigurationModal({ {error &&

{error}

}
- setIsOpenPickerOpen(false)} - rootLabel={currentConfiguration.rootPath} - initialPath={rootLocationName === '' ? configurationsDirPath : rootLocationName} - /> + {isOpenPickerOpen && ( + setIsOpenPickerOpen(false)} + rootLabel={currentConfiguration.rootPath} + initialPath={rootLocationName === '' ? configurationsDirPath : rootLocationName} + /> + )}
) } diff --git a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx index 0ee2ee28..4972cd0a 100644 --- a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx +++ b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx @@ -6,7 +6,6 @@ import Input from '~/components/inputs/input' import { filesystemService } from '~/services/filesystem-service' interface CloneProjectModalProperties { - isOpen: boolean isLocal: boolean onClose: () => void onClone: (repoUrl: string, localPath: string, token?: string) => void @@ -14,7 +13,6 @@ interface CloneProjectModalProperties { } export default function CloneConfigurationModal({ - isOpen, isLocal, onClose, onClone, @@ -26,7 +24,6 @@ export default function CloneConfigurationModal({ const [showPicker, setShowPicker] = useState(false) useEffect(() => { - if (!isOpen) return if (isLocal) { filesystemService .resolveNearestAccessiblePath(initialPath ?? '') @@ -35,9 +32,7 @@ export default function CloneConfigurationModal({ return } setLocation(initialPath ?? '') - }, [isOpen, isLocal, initialPath]) - - if (!isOpen) return null + }, [isLocal, initialPath]) const repoName = repoUrl.split('/').pop()?.replace('.git', '') @@ -138,15 +133,16 @@ export default function CloneConfigurationModal({
- { - setLocation(path) - setShowPicker(false) - }} - onCancel={() => setShowPicker(false)} - initialPath={location} - /> + {showPicker && ( + { + setLocation(path) + setShowPicker(false) + }} + onCancel={() => setShowPicker(false)} + initialPath={location} + /> + )} ) } diff --git a/src/main/frontend/app/routes/projectlanding/new-configuration-modal.tsx b/src/main/frontend/app/routes/projectlanding/new-configuration-modal.tsx index 5ebf17f5..cfaf6225 100644 --- a/src/main/frontend/app/routes/projectlanding/new-configuration-modal.tsx +++ b/src/main/frontend/app/routes/projectlanding/new-configuration-modal.tsx @@ -6,7 +6,6 @@ import Input from '~/components/inputs/input' import { filesystemService } from '~/services/filesystem-service' interface NewProjectModalProperties { - isOpen: boolean isLocal: boolean onClose: () => void onCreate: (name: string, rootPath: string) => void @@ -16,7 +15,6 @@ interface NewProjectModalProperties { const CONFIG_DIR = 'src/main/configurations' export default function NewConfigurationModal({ - isOpen, isLocal, onClose, onCreate, @@ -27,8 +25,8 @@ export default function NewConfigurationModal({ const [showPicker, setShowPicker] = useState(false) useEffect(() => { - if (!isOpen || !isLocal) { - if (isOpen) setLocation(initialPath) + if (!isLocal) { + setLocation(initialPath) return } @@ -36,9 +34,7 @@ export default function NewConfigurationModal({ .resolveNearestAccessiblePath(initialPath) .then(setLocation) .catch(() => setLocation('')) - }, [isOpen, isLocal, initialPath]) - - if (!isOpen) return null + }, [isLocal, initialPath]) const handleCreate = () => { if (!name.trim() || (isLocal && !location)) return @@ -109,15 +105,16 @@ export default function NewConfigurationModal({
- { - setLocation(path) - setShowPicker(false) - }} - onCancel={() => setShowPicker(false)} - initialPath={location} - /> + {showPicker && ( + { + setLocation(path) + setShowPicker(false) + }} + onCancel={() => setShowPicker(false)} + initialPath={location} + /> + )} ) } diff --git a/src/main/frontend/app/routes/projectlanding/project-landing.tsx b/src/main/frontend/app/routes/projectlanding/project-landing.tsx index 48606613..6ea38b97 100644 --- a/src/main/frontend/app/routes/projectlanding/project-landing.tsx +++ b/src/main/frontend/app/routes/projectlanding/project-landing.tsx @@ -1,20 +1,18 @@ import React, { useEffect, useState, useCallback, useRef } from 'react' import { useNavigate } from 'react-router' import FfIcon from '/icons/custom/ff!-icon.svg?react' -import LibraryIcon from '/icons/solar/Library.svg?react' +import ProjectList from '~/routes/projectlanding/project-list' +import Sidebar from '~/routes/projectlanding/sidebar' +import Toolbar from '~/routes/projectlanding/toolbar' import { fetchInstanceConfigurations, type FFConfiguration } from '~/services/frank-framework-service' import { useProjectStore } from '~/stores/project-store' import { ApiError } from '~/utils/api' import { logApiError } from '~/utils/logger' import { getParentPath, normalizePath } from '~/utils/path-utils' -import ConfigurationRow from './configuration-row' -import Search from '~/components/search/search' -import ActionButton from './action-button' import NewConfigurationModal from './new-configuration-modal' import CloneConfigurationModal from './clone-configuration-modal' import DirectoryPicker from '~/components/directory-picker/directory-picker' -import type { RecentConfigurationProject } from '~/types/project.types' import { fetchAppInfo } from '~/services/app-info-service' import { removeRecentProject } from '~/services/recent-project-service' import useTabStore from '~/stores/tab-store' @@ -40,7 +38,7 @@ export default function ProjectLanding() { const [searchTerm, setSearchTerm] = useState('') const [isModalOpen, setIsModalOpen] = useState(false) const [isCloneModalOpen, setIsCloneModalOpen] = useState(false) - const [isOpenPickerOpen, setIsOpenPickerOpen] = useState(false) + const [isDirectoryPickerOpen, setIsDirectoryPickerOpen] = useState(false) const [isLocalEnvironment, setIsLocalEnvironment] = useState(true) const [rootLocationName, setRootLocationName] = useState('Computer') const [isOpeningProject, setIsOpeningProject] = useState(false) @@ -114,7 +112,7 @@ export default function ProjectLanding() { ) const onOpenFolder = async (selectedPath: string) => { - setIsOpenPickerOpen(false) + setIsDirectoryPickerOpen(false) await handleOpenProject(selectedPath) } @@ -203,7 +201,7 @@ export default function ProjectLanding() { setIsModalOpen(true)} - onOpenClick={() => setIsOpenPickerOpen(true)} + onOpenClick={() => setIsDirectoryPickerOpen(true)} onCloneClick={() => setIsCloneModalOpen(true)} onImportClick={() => importInputRef.current?.click()} /> @@ -227,20 +225,22 @@ export default function ProjectLanding() {
)} - setIsModalOpen(false)} - onCreate={onCreateProject} - isLocal={isLocalEnvironment} - initialPath={getParentPath(lastRecentRootPath)} - /> - setIsCloneModalOpen(false)} - onClone={onCloneProject} - initialPath={getParentPath(lastRecentRootPath)} - /> + {isModalOpen && ( + setIsModalOpen(false)} + onCreate={onCreateProject} + isLocal={isLocalEnvironment} + initialPath={getParentPath(lastRecentRootPath)} + /> + )} + {isCloneModalOpen && ( + setIsCloneModalOpen(false)} + onClone={onCloneProject} + initialPath={getParentPath(lastRecentRootPath)} + /> + )} {!isLocalEnvironment && ( )} - setIsOpenPickerOpen(false)} - rootLabel={rootLocationName} - initialPath={getParentPath(lastRecentRootPath)} - /> + {isDirectoryPickerOpen && ( + setIsDirectoryPickerOpen(false)} + rootLabel={rootLocationName} + initialPath={getParentPath(lastRecentRootPath)} + /> + )}
) } @@ -272,107 +273,6 @@ const Header = () => ( ) -const Sidebar = ({ - isLocal, - onNewClick, - onOpenClick, - onCloneClick, - onImportClick, -}: { - isLocal?: boolean - onNewClick: () => void - onOpenClick: () => void - onCloneClick: () => void - onImportClick: () => void -}) => ( - -) - -const ProjectList = ({ - projects, - isLocal, - onProjectClick, - onRemoveProject, - onExportProject, - frameworkInstanceName, - frameworkConfigurations, - isDiscovering, -}: { - projects: RecentConfigurationProject[] - isLocal: boolean - onProjectClick: (rootPath: string) => void - onRemoveProject: (rootPath: string) => void - onExportProject: (projectName: string) => void - frameworkInstanceName: string - frameworkConfigurations: FFConfiguration[] - isDiscovering: boolean -}) => ( -
- {frameworkConfigurations.length > 0 && ( -
-

Remote

- {frameworkConfigurations.map((configuration) => ( -
-
-
{configuration.name}
- {configuration.filename &&

{configuration.filename}

} -
- {frameworkInstanceName} -
- ))} -
- )} - {isDiscovering && frameworkConfigurations.length === 0 && ( -

Scanning for remote instances...

- )} - {projects.length === 0 && frameworkConfigurations.length === 0 && !isDiscovering && ( -

No configurations found

- )} - {projects.length > 0 && ( - <> - {projects.map((project) => ( - onProjectClick(project.rootPath)} - onRemove={() => onRemoveProject(project.rootPath)} - onExport={() => onExportProject(project.name)} - /> - ))} - - )} -
-) - -const Toolbar = ({ onSearchChange }: { onSearchChange: (value: string) => void }) => ( -
-
- -

Configurations

-
-
- onSearchChange(changeEvent.target.value)} - /> -
-
-) - const LoadingState = () => (
Initializing workspace... diff --git a/src/main/frontend/app/routes/projectlanding/project-list.tsx b/src/main/frontend/app/routes/projectlanding/project-list.tsx new file mode 100644 index 00000000..12b557d1 --- /dev/null +++ b/src/main/frontend/app/routes/projectlanding/project-list.tsx @@ -0,0 +1,65 @@ +import ConfigurationRow from '~/routes/projectlanding/configuration-row' +import type { FFConfiguration } from '~/services/frank-framework-service' +import type { RecentConfigurationProject } from '~/types/project.types' + +export default function ProjectList({ + projects, + isLocal, + onProjectClick, + onRemoveProject, + onExportProject, + frameworkInstanceName, + frameworkConfigurations, + isDiscovering, +}: { + projects: RecentConfigurationProject[] + isLocal: boolean + onProjectClick: (rootPath: string) => void + onRemoveProject: (rootPath: string) => void + onExportProject: (projectName: string) => void + frameworkInstanceName: string + frameworkConfigurations: FFConfiguration[] + isDiscovering: boolean +}) { + return ( +
+ {frameworkConfigurations.length > 0 && ( +
+

Remote

+ {frameworkConfigurations.map((configuration) => ( +
+
+
{configuration.name}
+ {configuration.filename &&

{configuration.filename}

} +
+ {frameworkInstanceName} +
+ ))} +
+ )} + {isDiscovering && frameworkConfigurations.length === 0 && ( +

Scanning for remote instances...

+ )} + {projects.length === 0 && frameworkConfigurations.length === 0 && !isDiscovering && ( +

No configurations found

+ )} + {projects.length > 0 && ( + <> + {projects.map((project) => ( + onProjectClick(project.rootPath)} + onRemove={() => onRemoveProject(project.rootPath)} + onExport={() => onExportProject(project.name)} + /> + ))} + + )} +
+ ) +} diff --git a/src/main/frontend/app/routes/projectlanding/sidebar.tsx b/src/main/frontend/app/routes/projectlanding/sidebar.tsx new file mode 100644 index 00000000..d6314f03 --- /dev/null +++ b/src/main/frontend/app/routes/projectlanding/sidebar.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import ActionButton from '~/routes/projectlanding/action-button' + +export default function Sidebar({ + isLocal, + onNewClick, + onOpenClick, + onCloneClick, + onImportClick, +}: { + isLocal?: boolean + onNewClick: () => void + onOpenClick: () => void + onCloneClick: () => void + onImportClick: () => void +}) { + return ( + + ) +} diff --git a/src/main/frontend/app/routes/projectlanding/toolbar.tsx b/src/main/frontend/app/routes/projectlanding/toolbar.tsx new file mode 100644 index 00000000..29d052f4 --- /dev/null +++ b/src/main/frontend/app/routes/projectlanding/toolbar.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import Search from '~/components/search/search' +import LibraryIcon from '/icons/solar/Library.svg?react' + +export default function Toolbar({ onSearchChange }: { onSearchChange: (value: string) => void }) { + return ( +
+
+ +

Configurations

+
+
+ onSearchChange(changeEvent.target.value)} + /> +
+
+ ) +} From 7a6df2d18143a164d404b6e9b9140addf918b020 Mon Sep 17 00:00:00 2001 From: Vivy <4380412+Matthbo@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:27:55 +0200 Subject: [PATCH 3/5] Linting --- src/main/frontend/app/routes/studio/studio.tsx | 2 +- .../java/org/frankframework/flow/git/GitCredentialHelper.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/frontend/app/routes/studio/studio.tsx b/src/main/frontend/app/routes/studio/studio.tsx index 8c0046a0..9c346a5d 100644 --- a/src/main/frontend/app/routes/studio/studio.tsx +++ b/src/main/frontend/app/routes/studio/studio.tsx @@ -205,7 +205,7 @@ export default function Studio() { if (!fileName) return openInEditor(fileName, activeTabPath, navigate) - }, [activeTabPath]) + }, [activeTabPath, navigate]) const rightPanelTitle = getRightPanelTitle( isMultiSelect, diff --git a/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java b/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java index b39dd694..7b0e30fd 100644 --- a/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java +++ b/src/main/java/org/frankframework/flow/git/GitCredentialHelper.java @@ -10,7 +10,6 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; - import lombok.extern.log4j.Log4j2; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.CredentialsProvider; From 07cbfa9a1dc5672ec1618fc22cfc840ecb1c7f6c Mon Sep 17 00:00:00 2001 From: Vivy <4380412+Matthbo@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:35:53 +0200 Subject: [PATCH 4/5] Fix test --- .../flow/project/ConfigurationProjectServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java index 094f9dcb..e5272487 100644 --- a/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java +++ b/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java @@ -677,11 +677,11 @@ void testCloneAndOpenProjectThrowsWhenTargetDirectoryAlreadyExists() throws Exce when(fileSystemStorage.toAbsolutePath("already_exists")).thenReturn(existing); - IllegalArgumentException ex = assertThrows( - IllegalArgumentException.class, + ApiException exception = assertThrows( + ApiException.class, () -> configurationProjectService.cloneAndOpenProject("https://example.com/repo.git", "already_exists", null) ); - assertTrue(ex.getMessage().contains("already_exists")); + assertTrue(exception.getMessage().contains("already_exists")); } } From 044ebee2f6bcf28108fbcc330c50de8265ab9d44 Mon Sep 17 00:00:00 2001 From: Vivy <4380412+Matthbo@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:33:31 +0200 Subject: [PATCH 5/5] Copilot feedback --- .../app/routes/projectlanding/clone-configuration-modal.tsx | 2 +- .../flow/project/ConfigurationProjectService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx index 4972cd0a..67ce0095 100644 --- a/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx +++ b/src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx @@ -34,7 +34,7 @@ export default function CloneConfigurationModal({ setLocation(initialPath ?? '') }, [isLocal, initialPath]) - const repoName = repoUrl.split('/').pop()?.replace('.git', '') + const repoName = repoUrl.split('/').pop()?.replace(/\.git$/i, '') const handleClone = () => { if (!repoUrl.trim()) return diff --git a/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java b/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java index 25847b1c..d36fe198 100644 --- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java +++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java @@ -137,7 +137,7 @@ public ConfigurationProject cloneAndOpenProject(String repoUrl, String localPath Path targetDir = fileSystemStorage.toAbsolutePath(localPath); if (Files.exists(targetDir)) { - throw new ApiException("Project already exists at \"" + localPath + "\""); + throw new ApiException("Project already exists at \"" + localPath + "\"", HttpStatus.CONFLICT); } CloneCommand cloneCommand = Git.cloneRepository().setURI(repoUrl).setDirectory(targetDir.toFile());