+
{filteredConfigurationFiles.map((file) => (
void
onCreate: (name: string, rootPath: string) => void
- initialPath?: string
+ initialPath: string
}
const CONFIG_DIR = 'src/main/configurations'
@@ -26,12 +26,12 @@ export default function NewConfigurationModal({
useEffect(() => {
if (!isOpen || !isLocal) {
- if (isOpen) setLocation(initialPath ?? '')
+ if (isOpen) setLocation(initialPath)
return
}
filesystemService
- .resolveNearestAccessiblePath(initialPath ?? '')
+ .resolveNearestAccessiblePath(initialPath)
.then(setLocation)
.catch(() => setLocation(''))
}, [isOpen, isLocal, initialPath])
diff --git a/src/main/frontend/app/routes/projectlanding/project-landing.tsx b/src/main/frontend/app/routes/projectlanding/project-landing.tsx
index ca3bfb16..70ca29a1 100644
--- a/src/main/frontend/app/routes/projectlanding/project-landing.tsx
+++ b/src/main/frontend/app/routes/projectlanding/project-landing.tsx
@@ -6,6 +6,7 @@ import { fetchInstanceConfigurations, type FFConfiguration } from '~/services/fr
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'
@@ -187,7 +188,7 @@ export default function ProjectLanding() {
const projects = recentProjects ?? []
const filteredProjects = projects.filter((project) => project.name.toLowerCase().includes(searchTerm.toLowerCase()))
- const lastRecentRootPath = projects[0]?.rootPath
+ const lastRecentRootPath = normalizePath(projects[0]?.rootPath ?? '')
if (isLoading || isOpeningProject) return
@@ -231,14 +232,14 @@ export default function ProjectLanding() {
onClose={() => setIsModalOpen(false)}
onCreate={onCreateProject}
isLocal={isLocalEnvironment}
- initialPath={lastRecentRootPath}
+ initialPath={getParentPath(lastRecentRootPath)}
/>
setIsCloneModalOpen(false)}
onClone={onCloneProject}
- initialPath={lastRecentRootPath}
+ initialPath={getParentPath(lastRecentRootPath)}
/>
{!isLocalEnvironment && (
setIsOpenPickerOpen(false)}
rootLabel={rootLocationName}
- initialPath={lastRecentRootPath}
+ initialPath={getParentPath(lastRecentRootPath)}
/>
)
@@ -351,13 +352,13 @@ const ProjectList = ({
)
-const Toolbar = ({ onSearchChange }: { onSearchChange: (val: string) => void }) => (
+const Toolbar = ({ onSearchChange }: { onSearchChange: (value: string) => void }) => (
- onSearchChange(e.target.value)} />
+ onSearchChange(event.target.value)} />
)
diff --git a/src/main/frontend/app/utils/path-utils.ts b/src/main/frontend/app/utils/path-utils.ts
index d9bc5aca..af12ef87 100644
--- a/src/main/frontend/app/utils/path-utils.ts
+++ b/src/main/frontend/app/utils/path-utils.ts
@@ -1,32 +1,19 @@
-import type { ConfigurationProject } from '~/types/project.types'
-
/**
* Extracts the portion of a path after the first occurrence of a marker segment.
* Returns null if the marker is not found.
*/
export function toRelativePath(absolutePath: string, marker: string): string | null {
- const normalized = absolutePath.replaceAll('\\', '/')
- const idx = normalized.indexOf(marker)
- return idx === -1 ? null : normalized.slice(idx + marker.length)
+ const normalizedPath = normalizePath(absolutePath)
+ const normalizedMarker = normalizePath(marker)
+ const idx = normalizedPath.indexOf(marker)
+ return idx === -1 ? null : normalizedMarker.slice(idx + marker.length)
}
-/**
- * Converts a file path to a project-relative path for display.
- * Handles both local and cloud environments
- */
-export function toProjectRelativePath(absolutePath: string, project: ConfigurationProject): string {
- const path = absolutePath.replaceAll('\\', '/')
- const root = project.rootPath.replaceAll('\\', '/').replace(/^\/+/, '').replace(/\/+$/, '')
- const normalizedPath = path.replace(/^\/+/, '')
-
- if (normalizedPath === root) {
- return `${project.name}/`
- }
-
- const relative = toRelativePath(normalizedPath, `${root}/`)
- if (relative !== null) {
- return `${project.name}/${relative}`
- }
+export function normalizePath(path: string) {
+ return path.replaceAll('\\', '/')
+}
- return path
+export function getParentPath(path: string): string {
+ if (!path) return path // Return empty string if path is empty, small optimization to avoid regex processing
+ return path.replace(/\/?[^/]*$/, '')
}
diff --git a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java
index 4255b25e..2f1d6d32 100644
--- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java
+++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectController.java
@@ -80,7 +80,8 @@ public ResponseEntity
cloneProject(@RequestBody Configu
}
@PostMapping("/open")
- public ResponseEntity openProject(@RequestBody ConfigurationProjectCreateDTO configurationProjectCreateDTO) throws IOException, ApiException {
+ public ResponseEntity openProject(@RequestBody ConfigurationProjectCreateDTO configurationProjectCreateDTO)
+ throws IOException, ApiException {
ConfigurationProject configurationProject = configurationProjectService.openProjectFromDisk(configurationProjectCreateDTO.rootPath());
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 d6a20de6..6369cda3 100644
--- a/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java
+++ b/src/main/java/org/frankframework/flow/project/ConfigurationProjectService.java
@@ -34,8 +34,6 @@
@Log4j2
@Service
public class ConfigurationProjectService {
- private static final String CONFIGURATIONS_DIR = "src/main/configurations";
-
private final FileSystemStorage fileSystemStorage;
private final RecentProjectsService recentProjectsService;
@@ -70,7 +68,7 @@ private List getProjectsFromRecentList() {
ConfigurationProject configurationProject = loadProjectCached(recent.rootPath());
foundProjects.add(configurationProject);
} catch (Exception _) {
- log.debug("Recent project no longer valid: {}", recent.rootPath());
+ log.debug("Recent project is no longer valid: {}", recent.rootPath());
}
}
return foundProjects;
@@ -101,13 +99,16 @@ public ConfigurationProject getProject(String name) throws ApiException {
return getProjects().stream()
.filter(project -> project.getName().equals(name))
.findFirst()
- .orElseThrow(() -> new ApiException("Project not found: " + name, HttpStatus.NOT_FOUND));
+ .orElseThrow(() -> new ApiException("Project \"" + name +"\" not found", HttpStatus.NOT_FOUND));
}
public ConfigurationProject createProjectOnDisk(ConfigurationProjectCreateDTO projectCreate) throws IOException {
Path rootPath = Path.of(projectCreate.rootPath());
- String resolvedRootPath = rootPath.endsWith(CONFIGURATIONS_DIR) ? projectCreate.name() : CONFIGURATIONS_DIR + "/" + projectCreate.name();
- Path projectCreationPath = rootPath.resolve(resolvedRootPath);
+ Path projectCreationPath = rootPath.resolve(projectCreate.name());
+
+ if (Files.exists(projectCreationPath)) {
+ throw new ApiException("Project already exists at " + projectCreationPath, HttpStatus.NOT_FOUND);
+ }
Path projectPath = fileSystemStorage.createProjectDirectory(projectCreationPath.toString());
ClassPathResource resource = new ClassPathResource("templates/default-configuration.xml");
@@ -125,7 +126,9 @@ public ConfigurationProject createProjectOnDisk(ConfigurationProjectCreateDTO pr
public ConfigurationProject openProjectFromDisk(String path) throws IOException, ApiException {
Path absolutePath = fileSystemStorage.toAbsolutePath(path);
if (!Files.exists(absolutePath) || !Files.isDirectory(absolutePath)) {
- throw new ApiException("Project not found at: " + path, HttpStatus.NOT_FOUND);
+ throw new ApiException("Project not found at \"" + path + "\"", HttpStatus.NOT_FOUND);
+ } else if (!absolutePath.resolve("Configuration.xml").toFile().exists()) {
+ throw new ApiException("Project doesn't seem to be a valid configuration or Configuration.xml might be missing", HttpStatus.BAD_REQUEST);
}
return loadProjectAndCache(path);
}
@@ -134,7 +137,7 @@ 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 IllegalArgumentException("Project already exists at \"" + localPath + "\"");
}
try {
@@ -152,9 +155,9 @@ public ConfigurationProject cloneAndOpenProject(String repoUrl, String localPath
} 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("Clone failed — authentication error. Please provide a valid Personal Access Token (PAT)", exception);
+ throw new IllegalArgumentException("Cloning authentication error. Please provide a valid Personal Access Token (PAT)", exception);
}
- throw new IllegalArgumentException("Clone failed: " + exception.getMessage(), exception);
+ throw new IllegalArgumentException("Cloning failed: " + exception.getMessage(), exception);
}
ConfigurationProject configurationProject = loadProjectAndCache(targetDir.toString());
@@ -239,8 +242,7 @@ public ConfigurationProjectDTO toDto(ConfigurationProject configurationProject)
Path absolutePath = fileSystemStorage.toAbsolutePath(configurationProject.getRootPath());
boolean isGitRepo = Files.isDirectory(absolutePath.resolve(".git"));
- boolean hasStoredToken =
- configurationProject.getGitToken() != null && !configurationProject.getGitToken().isBlank();
+ boolean hasStoredToken = configurationProject.getGitToken() != null && !configurationProject.getGitToken().isBlank();
return new ConfigurationProjectDTO(
configurationProject.getName(),
diff --git a/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java b/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java
index ccda6197..094f9dcb 100644
--- a/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java
+++ b/src/test/java/org/frankframework/flow/project/ConfigurationProjectServiceTest.java
@@ -408,9 +408,9 @@ void testOpenProjectFromDisk() throws Exception {
String projectName = "manual_project";
Path projectDir = tempDir.resolve(projectName);
- Files.createDirectories(projectDir.resolve("src/main/configurations"));
+ Files.createDirectories(projectDir);
Files.writeString(
- projectDir.resolve("src/main/configurations/TestConfig.xml"),
+ projectDir.resolve("Configuration.xml"),
"",
StandardCharsets.UTF_8
);
@@ -449,26 +449,6 @@ void testOpenProjectFromDiskThrowsWhenPathIsAFile() throws Exception {
assertThrows(ApiException.class, () -> configurationProjectService.openProjectFromDisk(file.toString()));
}
- @Test
- void testOpenProjectFromDiskLoadsEmptyProject_whenNoConfigurationsDir() throws Exception {
- when(fileSystemStorage.toAbsolutePath(anyString())).thenAnswer(invocation -> {
- String pathStr = invocation.getArgument(0);
- Path path = Path.of(pathStr);
- return path.isAbsolute() ? path : tempDir.resolve(pathStr);
- });
-
- Path projDir = tempDir.resolve("empty_proj");
- Files.createDirectory(projDir);
-
- ConfigurationProject configurationProject = configurationProjectService.openProjectFromDisk(projDir.toString());
-
- assertNotNull(configurationProject);
- assertEquals("empty_proj", configurationProject.getName());
-
- ConfigurationProjectDTO dto = configurationProjectService.toDto(configurationProject);
- assertTrue(dto.filepaths().isEmpty(), "No configurations dir means empty config list");
- }
-
@Test
void testGetProjectsFromWorkspaceScan() throws Exception {
when(fileSystemStorage.isLocalEnvironment()).thenReturn(false);