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 @@ -9,13 +9,15 @@ interface SidebarLayoutProperties {
name: string
defaultVisible?: VisibilityState
windowResizeOnChange?: boolean
hideLeft?: boolean
}

export default function SidebarLayout({
children,
name,
defaultVisible,
windowResizeOnChange,
hideLeft,
}: Readonly<SidebarLayoutProperties>) {
const initializeInstance = useSidebarStore((state) => state.initializeInstance)
const setSizes = useSidebarStore((state) => state.setSizes)
Expand All @@ -39,8 +41,7 @@ export default function SidebarLayout({
if (!allotmentReady || !allotmentRef.current) return
if (sizes.length === 0) return

const target = sizes.map((size, i) => (visible[i] ? size : 0))

const target = sizes.map((size, index) => (visible[index] ? size : 0))
allotmentRef.current.resize(target)
}, [sizes, visible, allotmentReady])

Expand All @@ -50,7 +51,7 @@ export default function SidebarLayout({

const saveSizes = (newSizes: number[]) => {
const previous = useSidebarStore.getState().getSizes(name) ?? []
const merged = newSizes.map((size, i) => (size === 0 ? (previous[i] ?? 0) : size))
const merged = newSizes.map((size, index) => (size === 0 ? (previous[index] ?? 0) : size))
setSizes(name, merged)
if (windowResizeOnChange) {
globalThis.dispatchEvent(new Event('resize'))
Expand All @@ -76,7 +77,7 @@ export default function SidebarLayout({
minSize={200}
maxSize={500}
preferredSize={300}
visible={visible[SidebarSide.LEFT]}
visible={!hideLeft && visible[SidebarSide.LEFT]}
className="bg-background flex h-full flex-col"
>
{childrenArray[SidebarSide.LEFT]}
Expand Down
102 changes: 102 additions & 0 deletions src/main/frontend/app/routes/configurations/adapter-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react'
import Button from '~/components/inputs/button'
import Input from '~/components/inputs/input'
import { deleteAdapter, renameAdapter } from '~/services/adapter-service'

export interface AdapterEditorState {
configPath: string
adapterName: string
adapterPosition: number
}

interface AdapterContextProperties {
projectName: string
editor: AdapterEditorState
onSaved: () => void
onDeleted: () => void
onNameChange?: (name: string) => void
}

export default function AdapterContext({
projectName,
editor,
onSaved,
onDeleted,
onNameChange,
}: Readonly<AdapterContextProperties>) {
const [name, setName] = useState(editor.adapterName)
const [isSaving, setIsSaving] = useState(false)
const [errorMessage, setErrorMessage] = useState('')

useEffect(() => {
onNameChange?.(name)
}, [name, onNameChange])

const trimmedName = name.trim()
const canSave = trimmedName !== '' && trimmedName !== editor.adapterName && !isSaving

const handleSave = async () => {
if (!canSave) return
setIsSaving(true)
setErrorMessage('')
try {
await renameAdapter(projectName, editor.adapterName, trimmedName, editor.configPath)
onSaved()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : `Failed to rename ${editor.adapterName}`)
setIsSaving(false)
}
}

const handleDelete = async () => {
setIsSaving(true)
setErrorMessage('')
try {
await deleteAdapter(projectName, editor.adapterName, editor.configPath)
onDeleted()
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : `Failed to delete ${editor.adapterName}`)
setIsSaving(false)
}
}

return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex-1 overflow-y-auto px-4">
<div className="text-foreground-muted mt-2 text-xs font-semibold tracking-wider uppercase">{name}</div>

<div className="bg-background w-full space-y-4 rounded-md p-6">
<div className="space-y-1">
<label htmlFor="adapter-name" className="text-foreground text-sm">
name
</label>
<Input
id="adapter-name"
value={name}
disabled={isSaving}
onChange={(event) => setName(event.target.value)}
/>
</div>
</div>
</div>

<div className="border-t-border bg-background border-t p-4">
<div className="flex w-full items-center justify-between">
<Button
onClick={handleSave}
disabled={!canSave}
className="disabled:text-foreground-muted w-auto disabled:cursor-not-allowed disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save & Close'}
</Button>

<Button className="w-auto" onClick={handleDelete} disabled={isSaving}>
Delete
</Button>
</div>

{errorMessage && <p className="text-error mt-2 text-sm">{errorMessage}</p>}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { useMemo, useState, type ChangeEvent } from 'react'
import { createPortal } from 'react-dom'
import { useFFDoc } from '@frankframework/doc-library-react'
import Button from '~/components/inputs/button'
import Search from '~/components/search/search'
import { useFrankConfigXsd } from '~/providers/frankconfig-xsd-provider'
import { getAddableNonCanvasComponentNames } from '~/services/non-canvas-component-service'

interface AddNonCanvasComponentMenuProperties {
isOpen: boolean
onClose: () => void
onSelect: (tagName: string) => void
}

export default function AddNonCanvasComponentMenu({
isOpen,
onClose,
onSelect,
}: Readonly<AddNonCanvasComponentMenuProperties>) {
const { elements } = useFFDoc()
const { xsdContent } = useFrankConfigXsd()
const [search, setSearch] = useState('')
const [selected, setSelected] = useState<string | null>(null)

const addableNames = useMemo(() => getAddableNonCanvasComponentNames(xsdContent, elements), [xsdContent, elements])

const filteredNames = useMemo(
() => addableNames.filter((name) => name.toLowerCase().includes(search.toLowerCase())),
[addableNames, search],
)

if (!isOpen) return null

const clearAndClose = () => {
setSearch('')
setSelected(null)
onClose()
}

const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) clearAndClose()
}

const handleSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
setSearch(value)
const filtered = addableNames.find((name) => name.toLowerCase().includes(value.toLowerCase()))
setSelected(filtered ?? null)
}

const handleConfirm = (name?: string) => {
const tagName = name ?? selected
if (!tagName) return
onSelect(tagName)
clearAndClose()
}

return createPortal(
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/40" onClick={handleBackdropClick}>
<div className="bg-background border-border relative flex h-1/2 w-1/3 min-w-[400px] flex-col rounded-lg border p-6 shadow-lg">
<Button
onClick={clearAndClose}
className="text-foreground-muted hover:text-foreground absolute top-3 right-3 cursor-pointer text-lg leading-none"
>
&times;
</Button>

<h2 className="mb-4 text-lg font-bold">Add non-canvas component</h2>

<Search placeholder="Search components..." value={search} onChange={handleSearchChange} />
<div className="border-border bg-background my-3 w-full flex-1 overflow-hidden rounded border">
<ul className="h-full overflow-y-auto">
{filteredNames.length > 0 ? (
filteredNames.map((name) => {
const isSelected = selected === name
return (
<li
key={name}
onClick={() => setSelected(name)}
onDoubleClick={() => handleConfirm(name)}
className={`cursor-pointer px-3 py-2 ${
isSelected ? 'bg-foreground-active text-background' : 'hover:bg-hover'
}`}
>
{name}
</li>
)
})
) : (
<li className="text-foreground-muted px-3 py-2">
{addableNames.length === 0 ? 'No addable components found.' : 'No results found.'}
</li>
)}
</ul>
</div>

<Button onClick={() => handleConfirm()} disabled={!selected}>
Add component
</Button>
</div>
</div>,
document.body,
)
}
Loading
Loading