Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,25 @@ export default function EditorFileStructure() {
[getTab, removeTabAndSelectFallback],
)

const configurationsRootPath = useMemo(() => {
const paths = project?.filepaths
if (!paths?.length) return

const segments = paths.map((path) => path.replaceAll('\\', '/').split('/').slice(0, -1))

const common = segments.reduce((a, b) => {
let i = 0
while (i < a.length && i < b.length && a[i] === b[i]) i++
return a.slice(0, i)
})

return common.length > 0 ? common.join('/') : undefined
}, [project?.filepaths])

const editorContextMenu = useFileTreeContextMenu({
projectName: project?.name,
dataProvider,
configurationsRootPath,
onAfterRename,
onAfterDelete,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import type { TreeItemIndex } from 'react-complex-tree'
import { createFile, deleteFile, renameFile } from '~/services/file-service'
import { createFolderInProject } from '~/services/file-tree-service'
import { clearConfigurationFileCache } from '~/services/configuration-file-service'
import { clearConfigurationFileCache, createConfigurationFile } from '~/services/configuration-file-service'
import useTabStore from '~/stores/tab-store'
import useEditorTabStore from '~/stores/editor-tab-store'
import { showErrorToast } from '~/components/toast'
import { FILE_NAME_PATTERNS, FOLDER_OR_ADAPTER_NAME_PATTERNS } from '~/components/file-structure/name-input-dialog'
import { logApiError } from '~/utils/logger'
import { openInEditor } from '~/actions/navigationActions'

export interface ContextMenuState {
position: { x: number; y: number }
Expand Down Expand Up @@ -43,6 +45,7 @@ export interface DataProviderLike {
interface UseFileTreeContextMenuOptions {
projectName: string | undefined
dataProvider: DataProviderLike | null
configurationsRootPath?: string
onAfterRename?: (oldPath: string, newName: string) => void
onAfterDelete?: (path: string) => void
}
Expand Down Expand Up @@ -70,9 +73,11 @@ function buildNewPath(oldPath: string, newName: string): string {
export function useFileTreeContextMenu({
projectName,
dataProvider,
configurationsRootPath,
onAfterRename,
onAfterDelete,
}: UseFileTreeContextMenuOptions) {
const navigate = useNavigate()
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null)
const [nameDialog, setNameDialog] = useState<NameDialogState | null>(null)
const [deleteTarget, setDeleteTarget] = useState<DeleteTargetState | null>(null)
Expand Down Expand Up @@ -127,9 +132,20 @@ export function useFileTreeContextMenu({
return
}

const filePath = `${parentPath}/${name}`
const isXml = name.toLowerCase().endsWith('.xml')
const configsRoot = configurationsRootPath?.replaceAll('\\', '/')
const normalizedParent = parentPath.replaceAll('\\', '/')
const isInsideConfigurations = !!configsRoot && normalizedParent.startsWith(configsRoot)

try {
await createFile(projectName, `${parentPath}/${name}`)
await (isXml && isInsideConfigurations
? createConfigurationFile(projectName, filePath)
: createFile(projectName, filePath))

await dataProvider.reloadDirectory(parentItemId)

openInEditor(name, filePath, navigate)
} catch (error) {
logApiError('Failed to create file', error as Error)
}
Expand All @@ -138,7 +154,7 @@ export function useFileTreeContextMenu({
patterns: FILE_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, configurationsRootPath, navigate, closeContextMenu],
)

const handleNewFolder = useCallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useRef, useState } from 'react'
import { useNavigate } from 'react-router'
import type { TreeItemIndex } from 'react-complex-tree'
import { deleteFile, renameFile } from '~/services/file-service'
import { createFolderInProject } from '~/services/file-tree-service'
Expand All @@ -12,6 +13,8 @@ import {
FILE_NAME_PATTERNS,
FOLDER_OR_ADAPTER_NAME_PATTERNS,
} from '~/components/file-structure/name-input-dialog'
import { openInStudio } from '~/actions/navigationActions'
import { findAdaptersInXml } from '~/routes/editor/xml-utils'

export type StudioItemType = 'root' | 'folder' | 'configuration' | 'adapter' | 'file'

Expand Down Expand Up @@ -123,6 +126,7 @@ function getRenamePatterns(itemType: StudioItemType): Record<string, RegExp> {
}

export function useStudioContextMenu({ projectName, dataProvider }: UseStudioContextMenuOptions) {
const navigate = useNavigate()
const [contextMenu, setContextMenu] = useState<StudioContextMenuState | null>(null)
const [nameDialog, setNameDialog] = useState<NameDialogState | null>(null)
const [deleteTarget, setDeleteTarget] = useState<DeleteTargetState | null>(null)
Expand Down Expand Up @@ -176,14 +180,12 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
onSubmit: async (name: string) => {
const fileName = ensureXmlExtension(name)
try {
const rootPath = dataProvider.getRootPath().replace(/[/\\]$/, '')
const folderPath = menu.folderPath.replace(/[/\\]$/, '')
const relativePath =
folderPath === rootPath
? fileName
: `${folderPath.slice(rootPath.length + 1).replaceAll('\\', '/')}/${fileName}`
await createConfigurationFile(projectName, relativePath)
const absoluteFilePath = `${folderPath}/${fileName}`
await createConfigurationFile(projectName, absoluteFilePath)
await dataProvider.reloadDirectory('root')

openInStudio(navigate, { adapterName: 'SampleAdapter', filepath: absoluteFilePath, adapterPosition: 0 })
} catch (error) {
logApiError('Failed to create configuration', error as Error)
}
Expand All @@ -192,7 +194,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
patterns: CONFIGURATION_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, navigate, closeContextMenu],
)

const handleNewAdapter = useCallback(
Expand All @@ -206,8 +208,14 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
submitLabel: 'Create',
onSubmit: async (name: string) => {
try {
await createAdapter(projectName, name, menu.path)
const { xmlContent } = await createAdapter(projectName, name, menu.path)
await dataProvider.reloadDirectory('root')

const adapterIndex = findAdaptersInXml(xmlContent).findIndex((adapter) => adapter.name === name)

if (adapterIndex !== -1) {
openInStudio(navigate, { adapterName: name, filepath: menu.path, adapterPosition: adapterIndex })
}
} catch (error) {
logApiError('Failed to create adapter', error as Error)
}
Expand All @@ -216,7 +224,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
patterns: FOLDER_OR_ADAPTER_NAME_PATTERNS,
})
},
[projectName, dataProvider, closeContextMenu],
[projectName, dataProvider, navigate, closeContextMenu],
)

const handleNewFolder = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ export default function AddConfigurationModal({
configname = `${configname}.xml`
}

await createConfigurationFile(currentConfiguration.name, `${rootLocationName}/${configname}`)
const folderPath = rootLocationName.replace(/[/\\]$/, '')
const absoluteFilePath = `${folderPath}/${configname}`
await createConfigurationFile(currentConfiguration.name, absoluteFilePath)
const updatedProject = await fetchProject(currentConfiguration.name)
setProject(updatedProject)
onSuccess?.()
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/services/adapter-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ export async function createAdapter(
projectName: string,
adapterName: string,
configurationPath: string,
): Promise<void> {
await apiFetch<void>(`/projects/${encodeURIComponent(projectName)}/adapters`, {
): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`/projects/${encodeURIComponent(projectName)}/adapters`, {
method: 'POST',
body: JSON.stringify({ adapterName, configurationPath }),
})
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/services/configuration-file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export async function saveConfigurationFile(
})
}

export async function createConfigurationFile(projectName: string, filename: string): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?name=${encodeURIComponent(filename)}`, { method: 'POST' })
export async function createConfigurationFile(projectName: string, filepath: string): Promise<XmlResponse> {
return apiFetch<XmlResponse>(`${getBaseUrl(projectName)}?path=${encodeURIComponent(filepath)}`, { method: 'POST' })
}

function getBaseUrl(projectName: string): string {
Expand Down
4 changes: 2 additions & 2 deletions src/main/frontend/app/utils/path-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
export function toRelativePath(absolutePath: string, marker: string): string | null {
const normalizedPath = normalizePath(absolutePath)
const normalizedMarker = normalizePath(marker)
const idx = normalizedPath.indexOf(marker)
return idx === -1 ? null : normalizedMarker.slice(idx + marker.length)
const idx = normalizedPath.indexOf(normalizedMarker)
return idx === -1 ? null : normalizedPath.slice(idx + normalizedMarker.length)
}

export function normalizePath(path: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ public ResponseEntity<Void> updateAdapter(@RequestBody AdapterUpdateDTO dto) thr
}

@PostMapping("/{projectName}/adapters")
public ResponseEntity<Void> createAdapter(@PathVariable String projectName, @RequestBody AdapterCreateDTO dto) throws IOException {
adapterService.createAdapter(dto.configurationPath(), dto.adapterName());
return ResponseEntity.ok().build();
public ResponseEntity<ConfigurationXmlDTO> createAdapter(@PathVariable String projectName, @RequestBody AdapterCreateDTO dto) throws IOException {
String content = adapterService.createAdapter(dto.configurationPath(), dto.adapterName());
return ResponseEntity.ok(new ConfigurationXmlDTO(content));
}

@PatchMapping("/{projectName}/adapters/rename")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ public ConfigurationXmlDTO getAdapter(String projectName, String configurationPa
throws IOException, ApiException, SAXException, ParserConfigurationException, TransformerException {

ConfigurationProject configurationProject = configurationProjectService.getProject(projectName);
String normalizedConfigPath = configurationPath.replace("\\", "/");
ConfigurationFile config = configurationProject.getConfigurationFiles().stream()
.filter(configurationFile -> configurationFile.getFilepath().equals(configurationPath))
.filter(configurationFile -> configurationFile.getFilepath().replace("\\", "/").equals(normalizedConfigPath))
.findFirst()
.orElseThrow(() -> new ApiException(String.format("Configuration File with path: %s not found", configurationPath), HttpStatus.NOT_FOUND));

Expand Down Expand Up @@ -84,7 +85,7 @@ public boolean updateAdapter(Path configurationFile, String adapterName, String
}
}

public void createAdapter(String configurationPath, String adapterName) throws IOException {
public String createAdapter(String configurationPath, String adapterName) throws IOException {
if (configurationPath == null || configurationPath.isBlank()) {
throw new IllegalArgumentException("Configuration path must not be empty");
}
Expand All @@ -107,6 +108,7 @@ public void createAdapter(String configurationPath, String adapterName) throws I

String updatedXml = XmlConfigurationUtils.convertNodeToString(configDoc);
Files.writeString(absConfigFile, updatedXml, StandardCharsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING);
return updatedXml;
} catch (Exception exception) {
throw new IOException("Failed to create adapter: " + exception.getMessage(), exception);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public ResponseEntity<ConfigurationXmlDTO> updateConfiguration(
@PostMapping()
public ResponseEntity<ConfigurationXmlDTO> addConfiguration(
@PathVariable String projectName,
@RequestParam String name
@RequestParam String path
) throws ApiException, IOException, TransformerException, ParserConfigurationException, SAXException {
String content = configurationService.addConfiguration(projectName, name);
String content = configurationService.addConfiguration(projectName, path);
ConfigurationXmlDTO configurationXmlDTO = new ConfigurationXmlDTO(content);
return ResponseEntity.ok(configurationXmlDTO);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
@Service
public class ConfigurationService {

private static final String CONFIGURATIONS_DIR = "src/main/configurations";

private final FileSystemStorage fileSystemStorage;
private final ConfigurationProjectService configurationProjectService;
private final FileTreeService fileTreeService;
Expand Down Expand Up @@ -94,26 +92,28 @@ public String updateConfiguration(String projectName, String filepath, String co
}
}

public String addConfiguration(String projectName, String configurationName) throws IOException, ApiException, TransformerException, ParserConfigurationException, SAXException {
ConfigurationProject configurationProject = configurationProjectService.getProject(projectName);
Path absProjectPath = fileSystemStorage.toAbsolutePath(configurationProject.getRootPath());
Path configDir = absProjectPath.resolve(CONFIGURATIONS_DIR).normalize();

if (!Files.exists(configDir)) {
Files.createDirectories(configDir);
public String addConfiguration(String projectName, String filepath) throws IOException, ApiException, TransformerException, ParserConfigurationException, SAXException {
if (filepath == null || filepath.isBlank()) {
throw new ApiException("Configuration path must not be empty", HttpStatus.BAD_REQUEST);
}
if (filepath.contains("..")) {
throw new ApiException("Invalid configuration path: " + filepath, HttpStatus.BAD_REQUEST);
}

ConfigurationProject configurationProject = configurationProjectService.getProject(projectName);
Path projectRoot = fileSystemStorage.toAbsolutePath(configurationProject.getRootPath()).normalize();
Path absoluteFilePath = fileSystemStorage.toAbsolutePath(filepath).normalize();

Path filePath = configDir.resolve(configurationName).normalize();
if (!filePath.startsWith(configDir)) {
throw new ApiException("Invalid configuration name: " + configurationName, HttpStatus.BAD_REQUEST);
if (!absoluteFilePath.startsWith(projectRoot)) {
throw new ApiException("Invalid configuration path: " + filepath, HttpStatus.BAD_REQUEST);
}

Files.createDirectories(filePath.getParent());
Files.createDirectories(absoluteFilePath.getParent());

String defaultXml = loadDefaultConfigurationXml();
Document updatedDocument = XmlConfigurationUtils.insertFlowNamespace(defaultXml);
String updatedContent = XmlConfigurationUtils.convertNodeToString(updatedDocument);
fileSystemStorage.writeFile(filePath.toString(), updatedContent);
fileSystemStorage.writeFile(absoluteFilePath.toString(), updatedContent);
fileTreeService.invalidateTreeCache(projectName);
return updatedContent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@

private String toNodePath(Path path, Path relativizeRoot, boolean useRelativePaths) {
if (!useRelativePaths) {
return path.toAbsolutePath().toString();
return path.toAbsolutePath().toString().replace("\\", "/");
}
String relativePath = relativizeRoot.relativize(path).toString().replace("\\", "/");
return relativePath.isEmpty() ? "." : relativePath;
Expand Down Expand Up @@ -246,7 +246,7 @@
return node;
}

private ProjectDirectory resolveProjectDirectory(String projectName, String directoryPath) throws IOException {

Check warning on line 249 in src/main/java/org/frankframework/flow/file/FileTreeService.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.io.IOException', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=frankframework_flow&issues=AZ7Ld76J0Ty7K8C6Za_A&open=AZ7Ld76J0Ty7K8C6Za_A&pullRequest=550
try {
ConfigurationProject configurationProject = configurationProjectService.getProject(projectName);
Path projectPath = fileSystemStorage.toAbsolutePath(configurationProject.getRootPath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public ConfigurationProject importProjectFromFiles(String projectName, List<Mult
}

public ConfigurationProjectDTO toDto(ConfigurationProject configurationProject) {
String cleanPath = fileSystemStorage.toRelativePath(configurationProject.getRootPath());
String cleanPath = fileSystemStorage.toRelativePath(configurationProject.getRootPath()).replace("\\", "/");

// Dynamically fetch configurations from disk as the single source of truth
List<String> filepaths = getConfigurationFilesDynamically(configurationProject.getRootPath());
Expand Down Expand Up @@ -265,7 +265,7 @@ private List<String> getConfigurationFilesDynamically(String projectRoot) {
try (Stream<Path> stream = Files.walk(absolutePath)) {
return stream.filter(Files::isRegularFile)
.filter(path -> path.toString().toLowerCase().endsWith(".xml"))
.map(path -> fileSystemStorage.toRelativePath(path.toString()))
.map(path -> fileSystemStorage.toRelativePath(path.toString()).replace("\\", "/"))
.toList();
}
} catch (IOException exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ void addConfigurationReturnsDefaultContent() throws Exception {
when(settings.getFilters()).thenReturn(Map.of(FilterType.ADAPTER, true));
when(configurationProject.getConfigurationSettings()).thenReturn(settings);

when(configurationService.addConfiguration(TEST_PROJECT_NAME, "NewConfig.xml"))
String filepath = "/path/to/" + TEST_PROJECT_NAME + "/NewConfig.xml";

when(configurationService.addConfiguration(TEST_PROJECT_NAME, filepath))
.thenReturn("");
when(configurationProjectService.toDto(configurationProject))
.thenReturn(new ConfigurationProjectDTO(
Expand All @@ -148,11 +150,11 @@ void addConfigurationReturnsDefaultContent() throws Exception {
false
));

mockMvc.perform(post("/api/projects/" + TEST_PROJECT_NAME + "/configuration?name=NewConfig.xml")
mockMvc.perform(post("/api/projects/" + TEST_PROJECT_NAME + "/configuration?path=" + filepath)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.xmlContent").value(""));

verify(configurationService).addConfiguration(TEST_PROJECT_NAME, "NewConfig.xml");
verify(configurationService).addConfiguration(TEST_PROJECT_NAME, filepath);
}
}
Loading
Loading