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: 3 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ packages:
- src/main/frontend
- src/main/frontend/cypress

allowBuilds:
cypress: true

overrides:
'@babel/core': '^7.29.6'
brace-expansion@>=5.0.0 <5.0.6: '>=5.0.6'
Expand Down
47 changes: 47 additions & 0 deletions src/main/frontend/app/components/context-editor-footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type ReactNode } from 'react'
import Button from '~/components/inputs/button'

type ContextEditorFooterProperties = {
onSave: () => void
onDelete: () => void
saveDisabled?: boolean
deleteDisabled?: boolean
saveLabel?: string
deleteLabel?: string
errorMessage?: string
leadingActions?: ReactNode
}

export default function ContextEditorFooter({
onSave,
onDelete,
saveDisabled = false,
deleteDisabled = false,
saveLabel = 'Save & Close',
deleteLabel = 'Delete',
errorMessage,
leadingActions,
}: Readonly<ContextEditorFooterProperties>) {
return (
<div className="border-t-border bg-background border-t p-4">
<div className="flex w-full items-center justify-between">
<Button
onClick={onSave}
disabled={saveDisabled}
className="disabled:text-foreground-muted w-auto disabled:cursor-not-allowed disabled:opacity-50"
>
{saveLabel}
</Button>

<div className="flex items-center gap-2">
{leadingActions}
<Button className="w-auto" onClick={onDelete} disabled={deleteDisabled}>
{deleteLabel}
</Button>
</div>
</div>

{errorMessage && <p className="text-error mt-2 text-sm">{errorMessage}</p>}
</div>
)
}
31 changes: 31 additions & 0 deletions src/main/frontend/app/components/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type MouseEvent, type ReactNode } from 'react'
import { createPortal } from 'react-dom'
import CloseButton from '~/components/inputs/close-button'

type DialogProperties = {
isOpen: boolean
onClose: () => void
title: string
children: ReactNode
className?: string
overlay?: ReactNode
}
export default function Dialog({ isOpen, onClose, title, children, className, overlay }: Readonly<DialogProperties>) {
if (!isOpen) return null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of isOpen property I rather see this component wrapped in an if statement


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

return createPortal(
<div className="bg-background/50 fixed inset-0 z-50 flex items-center justify-center" onClick={handleBackdropClick}>
<div className={`bg-background border-border relative rounded-lg border p-6 shadow-lg ${className ?? ''}`}>
<h2 className="mb-4 text-lg font-semibold">{title}</h2>
{children}
<CloseButton onClick={onClose} className="absolute top-3 right-3" />
</div>
{overlay}
</div>,
document.body,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function CreateNodeModal({ onClose, addNodeAtPosition, positions, sourceInfo }:
</div>

<CloseButton onClick={onClose} className="absolute top-3 right-3" />
<Button onClick={handleCreateNode} className="mt-4">
<Button disabled={!selectedElement} onClick={handleCreateNode} className="mt-4 w-full">
Create Node
</Button>
</div>
Expand Down
38 changes: 38 additions & 0 deletions src/main/frontend/app/components/inputs/active-icon-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react'
import clsx from 'clsx'

type IconComponent = React.FC<React.SVGProps<SVGSVGElement>>

type TileViewButtonProps = {
isActive: boolean
label: string
Icon: IconComponent
onClick: () => void
}

export function ActiveIconButton({ isActive, label, Icon, onClick }: Readonly<TileViewButtonProps>) {
return (
<li className="m-0 list-none p-0">
<button
type="button"
onClick={onClick}
className="group relative flex w-full cursor-pointer flex-col items-center py-1"
>
<div
className={clsx('absolute bottom-1 left-1/2 h-0.5 w-10/12 -translate-x-1/2 rounded', isActive && 'bg-brand')}
/>
<div className="hover:bg-hover rounded p-2">
<Icon
className={clsx(
'h-8 w-auto',
isActive ? 'fill-brand' : 'fill-foreground-muted group-hover:fill-foreground',
)}
/>
</div>
<span className="bg-backdrop text-foreground border-border absolute top-full left-1/2 z-10 mt-2 hidden -translate-x-1/2 rounded border px-2 py-1 text-sm whitespace-nowrap shadow-md group-hover:block">
{label}
</span>
</button>
</li>
)
}
4 changes: 2 additions & 2 deletions src/main/frontend/app/components/inputs/button.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react'
import clsx from 'clsx'

type ButtonVariant = 'default' | 'ghost' | 'primary' | 'destructive'
type ButtonVariant = 'default' | 'ghost' | 'primary' | 'destructive' | 'unstyled'

export function buttonClasses(variant: ButtonVariant = 'default', disabled?: boolean, className?: string) {
return clsx(
'rounded-md px-4 py-2',
variant !== 'unstyled' && 'rounded-md px-4 py-2',
variant === 'default' && 'text-foreground border-border bg-backdrop border',
variant === 'ghost' && 'text-foreground border border-transparent bg-transparent',
variant === 'primary' && 'bg-brand font-medium text-white',
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/app/components/inputs/icon-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function IconButton({
role="button"
tabIndex={disabled ? -1 : 0}
className={clsx(
'icon-button group flex-shrink-0 cursor-pointer rounded p-0.5',
'icon-button group shrink-0 cursor-pointer rounded p-0.5',
disabled ? 'cursor-not-allowed opacity-40' : 'hover:bg-hover',
className,
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SidebarIcon from '/icons/solar/Sidebar Minimalistic.svg?react'
import { SidebarSide, useSidebarStore } from '~/stores/sidebar-layout-store'
import { SidebarSide, useSidebarStore } from '~/components/sidebars-layout/sidebar-layout-store'
import clsx from 'clsx'
import { useContext } from 'react'
import { SidebarContext } from '~/components/sidebars-layout/sidebar-layout'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SidebarClose, { type SidebarsCloseProperties } from '~/components/sidebars-layout/sidebar-close'
import { useSidebarStore } from '~/stores/sidebar-layout-store'
import { useSidebarStore } from '~/components/sidebars-layout/sidebar-layout-store'
import { useContext } from 'react'
import { SidebarContext } from '~/components/sidebars-layout/sidebar-layout'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SidebarClose, { type SidebarsCloseProperties } from '~/components/sidebars-layout/sidebar-close'
import { SidebarSide } from '~/stores/sidebar-layout-store'
import { SidebarSide } from '~/components/sidebars-layout/sidebar-layout-store'
import clsx from 'clsx'

type SidebarsHeaderProperties = {
Expand All @@ -12,7 +12,7 @@
return (
<div className={clsx('flex h-12 items-center px-4', isLeft ? 'gap-1' : 'justify-between')}>
{side === SidebarSide.LEFT && <SidebarClose side={side} />}
{title && <div className="text-xl ms-4">{title}</div>}

Check warning on line 15 in src/main/frontend/app/components/sidebars-layout/sidebar-header.tsx

View workflow job for this annotation

GitHub Actions / Build & Run All Tests

Replace `text-xl·ms-4` with `ms-4·text-xl`
{side === SidebarSide.RIGHT && <SidebarClose side={side} />}
</div>
)
Expand Down
35 changes: 20 additions & 15 deletions src/main/frontend/app/components/sidebars-layout/sidebar-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { createContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { Allotment, type AllotmentHandle } from 'allotment'
import { SidebarSide, useSidebarStore, type VisibilityState } from '~/stores/sidebar-layout-store'
import { SidebarSide, useSidebarStore, type VisibilityState } from '~/components/sidebars-layout/sidebar-layout-store'

export const SidebarContext = createContext<string | undefined>(undefined)

Expand All @@ -9,13 +9,15 @@ type 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,18 +41,18 @@ 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))
if (hideLeft) target.shift()
allotmentRef.current.resize(target)
}, [sizes, visible, allotmentReady])
}, [sizes, visible, allotmentReady, hideLeft])

const handleVisibilityChange = (index: SidebarSide, value: boolean) => {
setVisible(name, index, value)
}

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 @@ -70,17 +72,20 @@ export default function SidebarLayout({
onChange={onChangeHandler}
onDragEnd={saveSizes}
onVisibleChange={handleVisibilityChange}
proportionalLayout={false}
>
<Allotment.Pane
snap
minSize={200}
maxSize={500}
preferredSize={300}
visible={visible[SidebarSide.LEFT]}
className="bg-background flex h-full flex-col"
>
{childrenArray[SidebarSide.LEFT]}
</Allotment.Pane>
{hideLeft || (
<Allotment.Pane
snap
minSize={200}
maxSize={500}
preferredSize={300}
visible={visible[SidebarSide.LEFT]}
className="bg-background flex h-full flex-col"
>
{childrenArray[SidebarSide.LEFT]}
</Allotment.Pane>
)}
<Allotment.Pane className="bg-backdrop flex h-full flex-col">
{childrenArray[SidebarSide.MIDDLE]}
</Allotment.Pane>
Expand Down
2 changes: 1 addition & 1 deletion src/main/frontend/app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Toast } from '~/components/toast'
import { useTheme } from '~/hooks/use-theme'
import { useProjectStore } from '~/stores/project-store'
import { router } from './router'
import './app.css'
import 'allotment/dist/style.css'
import './app.css' // Always last for overwriting variables

function TitleSync() {
const project = useProjectStore((state) => state.project)
Expand Down
93 changes: 93 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,93 @@
import { useEffect, useState } from 'react'
import Input from '~/components/inputs/input'
import ContextEditorFooter from '~/components/context-editor-footer'
import { deleteAdapter, renameAdapter } from '~/services/adapter-service'

export type AdapterEditorState = {
configPath: string
adapterName: string
adapterPosition: number
}

type 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>

<ContextEditorFooter
onSave={handleSave}
saveDisabled={!canSave}
saveLabel={isSaving ? 'Saving...' : 'Save & Close'}
onDelete={handleDelete}
deleteDisabled={isSaving}
errorMessage={errorMessage}
/>
</div>
)
}
32 changes: 32 additions & 0 deletions src/main/frontend/app/routes/configurations/adapter-list-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import RulerCrossPenIcon from '/icons/solar/Ruler Cross Pen.svg?react'
import IconLabelButton from '~/components/inputs/icon-label-button'
import ComponentRow from './component-row'

type AdapterListItemProperties = {
adapterName: string
adapterPosition: number
onConfigure: () => void
onOpenInStudio: (adapterName: string, adapterPosition: number) => void
}

export default function AdapterListItem({
adapterName,
adapterPosition,
onConfigure,
onOpenInStudio,
}: Readonly<AdapterListItemProperties>) {
return (
<ComponentRow
typeLabel="Adapter"
primaryLabel={adapterName}
onConfigure={onConfigure}
action={
<IconLabelButton
icon={<RulerCrossPenIcon className="h-4 w-4 fill-current" />}
label="Open in Studio"
onClick={() => onOpenInStudio(adapterName, adapterPosition)}
/>
}
/>
)
}
Loading
Loading