Skip to content
Merged
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 @@ -37,10 +37,8 @@ export default class EditorFilesDataProvider extends BaseFilesDataProvider<FileN
}
}

public override async reloadDirectory(_itemId: TreeItemIndex): Promise<void> {
this.data = {}
this.loadedDirectories.clear()
await this.fetchAndBuildTree()
public override async reloadDirectory(itemId: TreeItemIndex): Promise<void> {
await super.reloadDirectory(itemId)
await this.preloadExpandedItems()
this.notifyListeners(Object.keys(this.data))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ContextMenu from './context-menu'
import NameInputDialog, { FILE_NAME_PATTERNS, FOLDER_OR_ADAPTER_NAME_PATTERNS } from './name-input-dialog'
import NameInputDialog from './name-input-dialog'
import ConfirmDeleteDialog from './confirm-delete-dialog'
import type { ContextMenuState, NameDialogState, DeleteTargetState } from './use-file-tree-context-menu'

Expand Down Expand Up @@ -51,9 +51,7 @@ export default function FileTreeDialogs({
initialValue={nameDialog.initialValue}
onSubmit={nameDialog.onSubmit}
onCancel={onCloseNameDialog}
patterns={
nameDialog.title.toLowerCase().includes('folder') ? FOLDER_OR_ADAPTER_NAME_PATTERNS : FILE_NAME_PATTERNS
}
patterns={nameDialog.patterns}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default function StudioContextMenu({

const showNewConfigurationAndNewFolder = itemType === 'root' || itemType === 'folder'
const showNewAdapter = itemType === 'configuration'
const showRenameAndDelete = itemType === 'configuration' || itemType === 'adapter' || itemType === 'folder'
const showRenameAndDelete =
itemType === 'configuration' || itemType === 'adapter' || itemType === 'folder' || itemType === 'file'

return createPortal(
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import FilesDataProvider, {
} from '~/components/file-structure/studio-files-data-provider'
import { useProjectStore } from '~/stores/project-store'
import { useTreeStore } from '~/stores/tree-store'
import useFlowStore from '~/stores/flow-store'
import { useStudioContextMenu, detectItemType, getItemName, resolveItemPaths } from './use-studio-context-menu'
import StudioFileTreeDialogs from './studio-file-tree-dialogs'

Expand Down Expand Up @@ -80,7 +79,7 @@ export default function StudioFileStructure() {
const item = await dataProvider.getTreeItem(itemId)
if (!item) return null

const itemType = detectItemType(item.data)
const itemType = detectItemType(item.data, item.isFolder)
const name = getItemName(item.data)
const { path, folderPath } = resolveItemPaths(item.data, itemType, dataProvider)

Expand All @@ -91,9 +90,6 @@ export default function StudioFileStructure() {

const triggerExplorerAction = useCallback(
(action: (menuState: StudioContextMenuState) => void, requireSelection: boolean) => {
const { nodes, edges } = useFlowStore.getState()
if (nodes.some((n) => n.selected) || edges.some((e) => e.selected)) return

const itemId = selectedItemId ?? (requireSelection ? null : 'root')
if (!itemId || (itemId === 'root' && requireSelection)) return

Expand All @@ -110,7 +106,6 @@ export default function StudioFileStructure() {
'studio-explorer.new-folder': () => triggerExplorerAction(studioContextMenu.handleNewFolder, false),
'studio-explorer.rename': () => triggerExplorerAction(studioContextMenu.handleRename, true),
'studio-explorer.delete': () => triggerExplorerAction(studioContextMenu.handleDelete, true),
'studio-explorer.delete-mac': () => triggerExplorerAction(studioContextMenu.handleDelete, true),
})

useEffect(() => {
Expand Down Expand Up @@ -352,7 +347,7 @@ export default function StudioFileStructure() {

return (
<div
className="flex min-w-0 cursor-pointer items-center"
className="flex w-full min-w-0 cursor-pointer items-center"
onContextMenu={(e) => studioContextMenu.openContextMenu(e, item.index)}
>
<Icon className="fill-foreground w-4 flex-shrink-0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ export function useFileTreeContextMenu({
(menuState?: ContextMenuState) => {
const menu = resolveMenu(menuState)
if (!menu || !projectName || !dataProvider) return
const parentPath = menu.path
const parentItemId = menu.itemId
const parentPath = menu.isFolder ? menu.path : buildNewPath(menu.path, '').slice(0, -1)
const parentItemId = menu.isFolder ? menu.itemId : getParentItemId(menu.itemId)
closeContextMenu()

setNameDialog({
Expand Down Expand Up @@ -142,8 +142,8 @@ export function useFileTreeContextMenu({
(menuState?: ContextMenuState) => {
const menu = resolveMenu(menuState)
if (!menu || !projectName || !dataProvider) return
const parentPath = menu.path
const parentItemId = menu.itemId
const parentPath = menu.isFolder ? menu.path : buildNewPath(menu.path, '').slice(0, -1)
const parentItemId = menu.isFolder ? menu.itemId : getParentItemId(menu.itemId)
closeContextMenu()

setNameDialog({
Expand Down Expand Up @@ -179,13 +179,13 @@ export function useFileTreeContextMenu({
if (newName === oldName) {
setNameDialog(null)
return
} else if (!ensureHasCorrectExtension(newName)) {
} else if (!menu.isFolder && !ensureHasCorrectExtension(newName)) {
showErrorToast(`Filename must have one of the following extensions: ${ALLOWED_EXTENSIONS.join(', ')}`)
return
}

try {
await renameFile(projectName, `${oldPath}`, `${oldPath}`.replace(oldName, newName))
await renameFile(projectName, oldPath, buildNewPath(oldPath, newName))
clearConfigurationCache(projectName, oldPath)
const newPath = buildNewPath(oldPath, newName)
useTabStore.getState().renameTabsForConfig(oldPath, newPath)
Expand Down Expand Up @@ -227,10 +227,11 @@ export function useFileTreeContextMenu({
useTabStore.getState().removeTabsForConfig(deleteTarget.path)
useEditorTabStore.getState().refreshAllTabs()
onAfterDelete?.(deleteTarget.path)
await dataProvider.reloadDirectory(deleteTarget.parentItemId)
} catch (error) {
showErrorToastFrom('Failed to delete', error)
}

await dataProvider.reloadDirectory(deleteTarget.parentItemId)
setDeleteTarget(null)
}, [deleteTarget, projectName, dataProvider, onAfterDelete])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { showErrorToastFrom } from '~/components/toast'
import type { StudioItemData, StudioFolderData, StudioAdapterData } from './studio-files-data-provider'
import {
CONFIGURATION_NAME_PATTERNS,
FILE_NAME_PATTERNS,
FOLDER_OR_ADAPTER_NAME_PATTERNS,
} from '~/components/file-structure/name-input-dialog'

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

export interface StudioContextMenuState {
position: { x: number; y: number }
Expand Down Expand Up @@ -42,10 +43,20 @@ export interface StudioDataProviderLike {
getRootPath(): string
}

export function detectItemType(data: StudioItemData): StudioItemType {
export function detectItemType(data: StudioItemData, isFolder?: boolean): StudioItemType {
if (typeof data === 'string') return 'root'

if ('adapterName' in data) return 'adapter'
if ('path' in data && (data as StudioFolderData).path.endsWith('.xml')) return 'configuration'

if (!('path' in data)) return 'folder'

const path = (data as StudioFolderData).path
if (path.endsWith('.xml')) return 'configuration'

if (isFolder) return 'folder'

const lastSegment = path.split(/[/\\]/).at(-1) ?? path
if (lastSegment.includes('.')) return 'file'
return 'folder'
}

Expand All @@ -62,7 +73,7 @@ function getParentDir(filePath: string): string {
}

function ensureXmlExtension(name: string): string {
if (name.includes('.')) return name
if (name.endsWith('.xml')) return name
return `${name}.xml`
}

Expand All @@ -82,7 +93,7 @@ export function resolveItemPaths(
}

const folderData = data as StudioFolderData
if (itemType === 'configuration') {
if (itemType === 'configuration' || itemType === 'file') {
return { path: folderData.path, folderPath: getParentDir(folderData.path) }
}

Expand All @@ -104,6 +115,12 @@ interface UseStudioContextMenuOptions {
dataProvider: StudioDataProviderLike | null
}

function getRenamePatterns(itemType: StudioItemType): Record<string, RegExp> {
if (itemType === 'folder' || itemType === 'adapter') return FOLDER_OR_ADAPTER_NAME_PATTERNS
if (itemType === 'file') return FILE_NAME_PATTERNS
return CONFIGURATION_NAME_PATTERNS
}

export function useStudioContextMenu({ projectName, dataProvider }: UseStudioContextMenuOptions) {
const [contextMenu, setContextMenu] = useState<StudioContextMenuState | null>(null)
const [nameDialog, setNameDialog] = useState<NameDialogState | null>(null)
Expand All @@ -119,7 +136,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
const item = await dataProvider.getTreeItem(itemId)
if (!item) return

const itemType = detectItemType(item.data)
const itemType = detectItemType(item.data, item.isFolder)
const name = getItemName(item.data)
const { path, folderPath } = resolveItemPaths(item.data, itemType, dataProvider)

Expand Down Expand Up @@ -157,7 +174,13 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
onSubmit: async (name: string) => {
const fileName = ensureXmlExtension(name)
try {
await createConfiguration(projectName, `${menu.folderPath}/${fileName}`)
const rootPath = dataProvider.getRootPath().replace(/[/\\]$/, '')
const folderPath = menu.folderPath.replace(/[/\\]$/, '')
const relativePath =
folderPath === rootPath
? fileName
: `${folderPath.slice(rootPath.length + 1).replaceAll('\\', '/')}/${fileName}`
await createConfiguration(projectName, relativePath)
await dataProvider.reloadDirectory('root')
} catch (error) {
showErrorToastFrom('Failed to create configuration', error)
Expand Down Expand Up @@ -234,23 +257,22 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
try {
if (menu.itemType === 'adapter') {
await renameAdapter(projectName, oldName, newName, menu.path)
} else {
} else if (menu.itemType === 'configuration' || menu.itemType === 'file') {
const finalName = menu.itemType === 'configuration' ? ensureXmlExtension(newName) : newName
await renameFile(projectName, `${menu.path}/${oldName}`, `${menu.path}/${newName}`)
const newPath = `${menu.folderPath}/${finalName}`
await renameFile(projectName, menu.path, newPath)
clearConfigurationCache(projectName, menu.path)
const newPath = `${getParentDir(menu.path)}/${finalName}`
useTabStore.getState().renameTabsForConfig(menu.path, newPath)
} else {
await renameFile(projectName, menu.path, `${getParentDir(menu.path)}/${newName}`)
}
await dataProvider.reloadDirectory('root')
} catch (error) {
showErrorToastFrom('Failed to rename', error)
}
setNameDialog(null)
},
patterns:
menu.itemType === 'folder' || menu.itemType === 'adapter'
? FOLDER_OR_ADAPTER_NAME_PATTERNS
: CONFIGURATION_NAME_PATTERNS,
patterns: getRenamePatterns(menu.itemType),
})
},
[projectName, dataProvider, closeContextMenu],
Expand Down Expand Up @@ -282,10 +304,10 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
clearConfigurationCache(projectName, deleteTarget.path)
useTabStore.getState().removeTabsForConfig(deleteTarget.path)
}
await dataProvider.reloadDirectory('root')
} catch (error) {
showErrorToastFrom('Failed to delete', error)
}
await dataProvider.reloadDirectory('root')
setDeleteTarget(null)
}, [deleteTarget, projectName, dataProvider])

Expand Down
8 changes: 5 additions & 3 deletions src/main/frontend/app/hooks/use-shortcut-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ export function useShortcutListener() {
continue
}

event.preventDefault()
shortcut.handler()
return
const result = shortcut.handler()
if (result !== false) {
event.preventDefault()
return
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/app/hooks/use-shortcut.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'
import { useShortcutStore } from '~/stores/shortcut-store'

type ShortcutHandlers = Record<string, () => void>
type ShortcutHandlers = Record<string, () => boolean | void>

export function useShortcut(handlers: ShortcutHandlers) {
const handlersRef = useRef(handlers)
Expand Down
11 changes: 6 additions & 5 deletions src/main/frontend/app/routes/studio/canvas/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import { useShallow } from 'zustand/react/shallow'
import { FlowConfig } from '~/routes/studio/canvas/flow.config'
import { getElementTypeFromName } from '~/routes/studio/node-translator-module'
import { useCallback, useEffect, useRef, useState } from 'react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { NodeContextMenuContext, useNodeContextMenu } from './node-context-menu-context'
import StickyNoteComponent, { type StickyNote } from '~/routes/studio/canvas/nodetypes/sticky-note'
import useTabStore, { type TabData } from '~/stores/tab-store'
Expand Down Expand Up @@ -184,7 +184,7 @@
showErrorToast(`Failed to save XML: ${error instanceof Error ? error.message : error}`)
setSaveStatus('idle')
}
}, [project])

Check warning on line 187 in src/main/frontend/app/routes/studio/canvas/flow.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

React Hook useCallback has an unnecessary dependency: 'project'. Either exclude it or remove the dependency array

const autosaveEnabled = useSettingsStore((s) => s.general.autoSave.enabled)
const autosaveDelay = useSettingsStore((s) => s.general.autoSave.delayMs)
Expand Down Expand Up @@ -520,14 +520,15 @@
setChildParentId(null)
}

const deleteSelection = useCallback(() => {
if (isEditing) return
const deleteSelection = useCallback((): boolean => {
if (isEditing) return false
const { nodes, edges, setNodes, setEdges } = useFlowStore.getState()
const selectedNodeIds = new Set(nodes.filter((n) => n.selected).map((n) => n.id))
const hasSelection = selectedNodeIds.size > 0 || edges.some((e) => e.selected)
if (!hasSelection) return
if (!hasSelection) return false
setNodes(nodes.filter((n) => !n.selected))
setEdges(edges.filter((e) => !e.selected && !selectedNodeIds.has(e.source) && !selectedNodeIds.has(e.target)))
return true
}, [isEditing])

useShortcut({
Expand All @@ -539,7 +540,7 @@
'studio.redo-alt': () => useFlowStore.getState().redo(),
'studio.group': () => handleGrouping(),
'studio.ungroup': () => handleUngroup(),
'studio.save': () => saveFlow(),
'studio.save': () => void saveFlow(),
'studio.close-context': () => closeEditNodeContextOnEscape(),
'studio.delete': () => deleteSelection(),
})
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/app/stores/shortcut-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ShortcutDefinition {
modifiers?: PlatformValue<KeyModifiers>
allowInInput?: boolean
displayOnly?: boolean
handler?: () => void
handler?: () => boolean | void
}

function capitalize(s: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import org.frankframework.flow.exception.ApiException;
import org.frankframework.flow.file.FileTreeService;
import org.frankframework.flow.filesystem.FileSystemStorage;
import org.frankframework.flow.project.Project;
import org.frankframework.flow.project.ProjectService;
Expand All @@ -24,10 +25,12 @@ public class ConfigurationService {

private final FileSystemStorage fileSystemStorage;
private final ProjectService projectService;
private final FileTreeService fileTreeService;

public ConfigurationService(FileSystemStorage fileSystemStorage, ProjectService projectService) {
public ConfigurationService(FileSystemStorage fileSystemStorage, ProjectService projectService, FileTreeService fileTreeService) {
this.fileSystemStorage = fileSystemStorage;
this.projectService = projectService;
this.fileTreeService = fileTreeService;
}

public ConfigurationDTO getConfigurationContent(String projectName, String filepath) throws IOException, ApiException {
Expand Down Expand Up @@ -74,11 +77,13 @@ public String addConfiguration(String projectName, String configurationName) thr
throw new ApiException("Invalid configuration name: " + configurationName, HttpStatus.BAD_REQUEST);
}

Files.createDirectories(filePath.getParent());

String defaultXml = loadDefaultConfigurationXml();
Document updatedDocument = XmlConfigurationUtils.insertFlowNamespace(defaultXml);
String updatedContent = XmlConfigurationUtils.convertNodeToString(updatedDocument);
fileSystemStorage.writeFile(filePath.toString(), updatedContent);

fileTreeService.invalidateTreeCache(projectName);
return updatedContent;
}

Expand Down
Loading
Loading