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
3 changes: 2 additions & 1 deletion src/main/frontend/app/actions/navigationActions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type NavigateFunction } from 'react-router'
import useTabStore from '~/stores/tab-store'
import useEditorTabStore from '~/stores/editor-tab-store'
import { getBaseName } from '~/utils/path-utils'

export function openInStudio(
navigate: NavigateFunction,
Expand Down Expand Up @@ -42,7 +43,7 @@ export function openInEditorAtElement(
{ subtype, filepath, name }: { subtype: string; filepath: string; name?: string },
) {
const editorStore = useEditorTabStore.getState()
const fileName = filepath.split(/[/\\]/).pop() ?? filepath
const fileName = getBaseName(filepath)

if (!editorStore.getTab(filepath)) {
editorStore.setTabData(filepath, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { filesystemService } from '~/services/filesystem-service'
import type { FilesystemEntry } from '~/types/filesystem.types'
import { ApiError } from '~/utils/api'
import { useDirectoryWatcher } from '~/hooks/use-file-watcher'
import { normalizePath } from '~/utils/path-utils'
import Button from '../inputs/button'
import CloseButton from '../inputs/close-button'

Expand Down Expand Up @@ -36,9 +37,9 @@ export default function DirectoryPicker({
setIsCreatingFolder(false)
try {
const result = await filesystemService.browse(path)
setEntries(result.entries)
setCurrentPath(result.resolvedPath)
setParentPath(result.parentPath)
setEntries(result.entries.map((entry) => ({ ...entry, path: normalizePath(entry.path) })))
setCurrentPath(normalizePath(result.resolvedPath))
setParentPath(normalizePath(result.parentPath))
} catch (error) {
if (error instanceof ApiError && error.httpCode === 403) {
setError('Access denied')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export const CONFIGURATION_NAME_PATTERNS: Record<string, RegExp> = {

export const FOLDER_OR_ADAPTER_NAME_PATTERNS: Record<string, RegExp> = BASE_NAME_PATTERNS

export const PROJECT_NAME_PATTERNS: Record<string, RegExp> = {
...BASE_NAME_PATTERNS,
'Cannot have a file extension': /^(?!.*\.[^.\\/]+$).*$/,
}

interface NameInputDialogProps {
title: string
initialValue?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ function studioTabItemId(activeTab: string, dataProvider: StudioFilesDataProvide
const lastSep = rest.lastIndexOf('::')
const adapterName = lastSep === -1 ? rest : rest.slice(0, lastSep)
const position = lastSep === -1 ? '0' : rest.slice(lastSep + 2)
const rootPath = dataProvider.getRootPath().replace(/[/\\]$/, '')
return `${toTreeItemId(configPath, rootPath)}/${adapterName}::${position}`
return `${toTreeItemId(configPath, dataProvider.getRootPath())}/${adapterName}::${position}`
}

function getItemTitle(item: TreeItem<StudioItemData>): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import MailIcon from '../../../icons/solar/Mailbox.svg?react'
import FolderIcon from '../../../icons/solar/Folder.svg?react'
import type { FileTreeNode } from '~/types/filesystem.types'
import type { TreeRef } from 'react-complex-tree'
import { relativeTo } from '~/utils/path-utils'

export function getListenerIcon(listenerType: string | null) {
if (!listenerType) return CodeIcon
Expand All @@ -31,8 +32,8 @@ export function getAncestorIds(itemId: string): string[] {
}

export function toTreeItemId(absolutePath: string, rootPath: string): string {
const relativePath = absolutePath.slice(rootPath.length).replace(/^[/\\]/, '')
return `root/${relativePath.split(/[/\\]/).join('/')}`
const relativePath = relativeTo(rootPath, absolutePath)
return relativePath ? `root/${relativePath}` : 'root'
}

export function isVisibleInTree(itemId: string | null, expandedItems: string[]): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
FILE_NAME_PATTERNS,
FOLDER_OR_ADAPTER_NAME_PATTERNS,
} from '~/components/file-structure/name-input-dialog'
import { getBaseName, getParentPath, joinPath, relativeTo } from '~/utils/path-utils'

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

Expand Down Expand Up @@ -56,7 +57,7 @@ export function detectItemType(data: StudioItemData, isFolder?: boolean): Studio

if (isFolder) return 'folder'

const lastSegment = path.split(/[/\\]/).at(-1) ?? path
const lastSegment = getBaseName(path)
if (lastSegment.includes('.')) return 'file'
return 'folder'
}
Expand All @@ -68,11 +69,6 @@ export function getItemName(data: StudioItemData): string {
return 'Unnamed'
}

function getParentDir(filePath: string): string {
const lastSep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
return lastSep > 0 ? filePath.slice(0, lastSep) : filePath
}

function ensureXmlExtension(name: string): string {
if (name.endsWith('.xml')) return name
return `${name}.xml`
Expand All @@ -90,12 +86,12 @@ export function resolveItemPaths(

if (itemType === 'adapter') {
const configPath = (data as StudioAdapterData).configPath
return { path: configPath, folderPath: getParentDir(configPath) }
return { path: configPath, folderPath: getParentPath(configPath) }
}

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

return { path: folderData.path, folderPath: folderData.path }
Expand Down Expand Up @@ -176,12 +172,8 @@ 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}`
const relativeFolder = relativeTo(dataProvider.getRootPath(), menu.folderPath)
const relativePath = relativeFolder ? joinPath(relativeFolder, fileName) : fileName
await createConfigurationFile(projectName, relativePath)
await dataProvider.reloadDirectory('root')
} catch (error) {
Expand Down Expand Up @@ -268,7 +260,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
clearConfigurationFileCache(projectName, menu.path)
useTabStore.getState().renameTabsForConfig(menu.path, newPath)
} else {
await renameFile(projectName, menu.path, `${getParentDir(menu.path)}/${newName}`)
await renameFile(projectName, menu.path, `${getParentPath(menu.path)}/${newName}`)
}
await dataProvider.reloadDirectory('root')
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import CloseButton from '~/components/inputs/close-button'
import Input from '~/components/inputs/input'
import DirectoryPicker from '~/components/directory-picker/directory-picker'
import { fetchProject } from '~/services/project-service'
import { containsPathSeparator, joinPath, relativeTo } from '~/utils/path-utils'

interface AddConfigurationModalProperties {
isOpen: boolean
Expand Down Expand Up @@ -47,12 +48,21 @@ export default function AddConfigurationModal({
setLoading(false)
return
}

if (containsPathSeparator(configname) || configname.includes('..')) {
setError(String.raw`Filename cannot contain "/", "\" or ".."`)
setLoading(false)
return
}
// Ensure .xml suffix
if (!configname.toLowerCase().endsWith('.xml')) {
configname = `${configname}.xml`
}

await createConfigurationFile(currentConfiguration.name, `${rootLocationName}/${configname}`)
const relativeFolder = relativeTo(currentConfiguration.rootPath, rootLocationName)
const relativePath = relativeFolder ? joinPath(relativeFolder, configname) : configname

await createConfigurationFile(currentConfiguration.name, relativePath)
const updatedProject = await fetchProject(currentConfiguration.name)
setProject(updatedProject)
onSuccess?.()
Expand Down Expand Up @@ -83,10 +93,13 @@ export default function AddConfigurationModal({
setIsOpenPickerOpen(false)
}

const trimmedFilename = filename.trim()
const filenameHasInvalidChars = containsPathSeparator(trimmedFilename) || trimmedFilename.includes('..')
const isFilenameValid = trimmedFilename.length > 0 && !filenameHasInvalidChars

const displayFilename = (() => {
const trimmed = filename.trim()
if (!trimmed) return ''
return trimmed.toLowerCase().endsWith('.xml') ? trimmed : `${trimmed}.xml`
if (!trimmedFilename) return ''
return trimmedFilename.toLowerCase().endsWith('.xml') ? trimmedFilename : `${trimmedFilename}.xml`
})()

return (
Expand Down Expand Up @@ -120,19 +133,25 @@ export default function AddConfigurationModal({
<label className="text-sm font-medium" htmlFor="configuration-filename-input">
Filename
</label>
<div className="ml-2 flex w-full items-center">
<Input
id="configuration-filename-input"
value={filename}
onChange={(event) => setFilename(event.target.value)}
placeholder="Choose a filename"
aria-label="configuration filename"
/>
<div className="ml-2 flex w-full flex-col">
<div className="flex w-full items-center gap-1">
<Input
id="configuration-filename-input"
value={filename}
onChange={(event) => setFilename(event.target.value)}
placeholder="Choose a filename"
aria-label="configuration filename"
/>
<span className="text-foreground-muted text-sm">.xml</span>
</div>
{filenameHasInvalidChars && (
<p className="mt-1 text-xs text-red-500">{String.raw`Filename cannot contain "/", "\" or ".."`}</p>
)}
</div>
</div>

<div className="flex gap-2">
<Button onClick={handleAdd} disabled={loading} className="disabled:opacity-50">
<Button onClick={handleAdd} disabled={loading || !isFilenameValid} className="disabled:opacity-50">
{loading ? 'Adding...' : `Add ${displayFilename || 'configuration file'} to ${currentConfiguration.name}`}
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import IconButton from '~/components/inputs/icon-button'
import IconLabelButton from '~/components/inputs/icon-label-button'
import ConfirmDeleteDialog from '~/components/file-structure/confirm-delete-dialog'
import { useState } from 'react'
import { getBaseName } from '~/utils/path-utils'

interface ConfigurationFileTileProperties {
filepath: string
Expand All @@ -29,7 +30,7 @@ export default function ConfigurationFileTile({
}

const handleOpenInEditor = () => {
const fileName = relativePath.split(/[/\\]/).pop()
const fileName = getBaseName(relativePath)
if (!fileName) return
openInEditor(fileName, filepath, navigate)
}
Expand Down Expand Up @@ -82,7 +83,7 @@ export default function ConfigurationFileTile({

{showDeleteDialog && (
<ConfirmDeleteDialog
name={relativePath.split(/[/\\]/).pop() ?? relativePath}
name={getBaseName(relativePath)}
isFolder={false}
onCancel={() => setShowDeleteDialog(false)}
onConfirm={handleConfirmDelete}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,14 @@ import type { FileTreeNode } from '~/types/filesystem.types'
import { fetchProjectTree } from '~/services/file-tree-service'
import Button from '~/components/inputs/button'
import Search from '~/components/search/search'
import { normalizePath, toRelativePath } from '~/utils/path-utils'
import { relativeTo } from '~/utils/path-utils'

interface ConfigurationFile {
path: string
relativePath: string
adapterNames: string[]
}

function findConfigurationsDir(node: FileTreeNode | undefined | null): FileTreeNode | null {
if (!node || !node.path) return null

const normalizedPath = normalizePath(node.path)

if (node.type === 'DIRECTORY' && normalizedPath.endsWith(`/src/main/configurations/${node.name}`)) {
return node
}

if (!node.children) return null

for (const child of node.children) {
const found = findConfigurationsDir(child)
if (found) return found
}

return null
}

function collectXmlFiles(node: FileTreeNode | undefined | null): FileTreeNode[] {
let result: FileTreeNode[] = []
if (!node) return result
Expand All @@ -61,7 +42,6 @@ export default function ConfigurationOverview() {
const [showModal, setShowModal] = useState(false)
const [tree, setTree] = useState<FileTreeNode | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [configurationsDir, setConfigurationsDir] = useState<FileTreeNode | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [debouncedQuery, setDebouncedQuery] = useState(searchQuery)

Expand Down Expand Up @@ -91,13 +71,6 @@ export default function ConfigurationOverview() {
return () => controller.abort()
}, [loadTree])

useEffect(() => {
if (tree) {
const configDir = findConfigurationsDir(tree)
setConfigurationsDir(configDir)
}
}, [tree])

const handleConfigAdded = useCallback(() => {
setShowModal(false)
loadTree()
Expand All @@ -112,12 +85,9 @@ export default function ConfigurationOverview() {
const configFiles = useMemo(() => {
if (!tree || !currentConfigurationProject) return []

const configurationDirectory = findConfigurationsDir(tree)
if (!configurationDirectory) return []

const xmlFiles = collectXmlFiles(configurationDirectory)
const xmlFiles = collectXmlFiles(tree)
return xmlFiles.map((file) => {
const relativePath = toRelativePath(file.path, `${configurationDirectory.path}/`) ?? file.name
const relativePath = relativeTo(tree.path, file.path) || file.name
return { ...file, relativePath, path: file.path }
})
}, [tree, currentConfigurationProject])
Expand Down Expand Up @@ -208,7 +178,7 @@ export default function ConfigurationOverview() {
onClose={() => setShowModal(false)}
onSuccess={handleConfigAdded}
currentConfiguration={currentConfigurationProject}
configurationsDirPath={configurationsDir?.path ?? ''}
configurationsDirPath={tree?.path ?? ''}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Button from '~/components/inputs/button'
import CloseButton from '~/components/inputs/close-button'
import Input from '~/components/inputs/input'
import { filesystemService } from '~/services/filesystem-service'
import { joinPath } from '~/utils/path-utils'

interface CloneProjectModalProperties {
isLocal: boolean
Expand Down Expand Up @@ -34,7 +35,13 @@ export default function CloneConfigurationModal({
setLocation(initialPath ?? '')
}, [isLocal, initialPath])

const repoName = repoUrl.split('/').pop()?.replace(/\.git$/i, '')
const repoName = repoUrl
.split('/')
.pop()
?.replace(/\.git$/i, '')

const targetName = repoName || 'cloned-project'
const targetPath = location ? joinPath(location, targetName) : targetName

const handleClone = () => {
if (!repoUrl.trim()) return
Expand Down Expand Up @@ -110,14 +117,7 @@ export default function CloneConfigurationModal({
</div>
)}

{repoName && (
<p className="text-foreground-muted mb-4 text-xs">
Will clone to:{' '}
{isLocal
? `${location}${location.includes('/') ? '/' : '\\'}${repoName}`
: `${location ? `${location}/` : ''}${repoName}`}
</p>
)}
{repoName && <p className="text-foreground-muted mb-4 text-xs">Will clone to: {targetPath}</p>}

<div className="flex gap-2">
<Button
Expand Down
Loading
Loading