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({