Skip to content

Commit 8553b64

Browse files
committed
add organization and network nodes
1 parent 14daf65 commit 8553b64

13 files changed

Lines changed: 294 additions & 71 deletions

File tree

web/locales/de-DE.arb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,10 @@
2525
"cancel": "Abbrechen",
2626
"add": "Hinzufügen",
2727
"eachNodeOneParent": "Jeder Knoten kann nur einen übergeordneten Knoten haben. Der Graph muss ein Baum bleiben.",
28-
"connectionWouldCreateCycle": "Diese Verbindung würde einen Zyklus erzeugen. Der Graph muss ein Baum bleiben."
28+
"connectionWouldCreateCycle": "Diese Verbindung würde einen Zyklus erzeugen. Der Graph muss ein Baum bleiben.",
29+
"nodeSettingsTitle": "Knoten-Einstellungen",
30+
"organizationIds": "Organisations-IDs",
31+
"addOrganizationIdPlaceholder": "ID hinzufügen",
32+
"remove": "Entfernen",
33+
"save": "Speichern"
2934
}

web/locales/en-US.arb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,10 @@
2525
"cancel": "Cancel",
2626
"add": "Add",
2727
"eachNodeOneParent": "Each node can have only one parent. The graph must stay a tree.",
28-
"connectionWouldCreateCycle": "This connection would create a cycle. The graph must stay a tree."
28+
"connectionWouldCreateCycle": "This connection would create a cycle. The graph must stay a tree.",
29+
"nodeSettingsTitle": "Node settings",
30+
"organizationIds": "Organization IDs",
31+
"addOrganizationIdPlaceholder": "Add ID",
32+
"remove": "Remove",
33+
"save": "Save"
2934
}

web/src/App.tsx

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react'
22
import { useNodesState, useEdgesState } from '@xyflow/react'
33
import type { Node } from '@xyflow/react'
44
import type { Edge } from '@xyflow/react'
5-
import { HelpwaveLogo } from '@helpwave/hightide'
5+
import { DialogRoot, HelpwaveLogo, LanguageDialog, ThemeDialog } from '@helpwave/hightide'
66
import { useScaffoldTranslation } from './i18n/ScaffoldTranslationContext'
77
import { GraphEditor } from './components/GraphEditor'
88
import { Sidebar } from './components/Sidebar'
@@ -20,6 +20,8 @@ function App() {
2020
const [edges, setEdges] = useEdgesState<Edge>([])
2121
const [treeError, setTreeError] = useState<string | null>(null)
2222
const [importError, setImportError] = useState<string | null>(null)
23+
const [themeDialogOpen, setThemeDialogOpen] = useState(false)
24+
const [localeDialogOpen, setLocaleDialogOpen] = useState(false)
2325
const initDone = useRef(false)
2426

2527
useEffect(() => {
@@ -76,31 +78,49 @@ function App() {
7678
</p>
7779
</div>
7880
<div className="hidden md:flex flex-col h-screen w-screen overflow-hidden bg-gray-100 dark:bg-gray-900">
79-
<div className="fixed top-4 right-4 flex gap-2 z-[1000]">
80-
<ThemeSwitcher />
81-
<LanguageSwitcher />
82-
</div>
83-
<div className="relative flex-1 min-h-0 min-w-0">
84-
<main className="absolute inset-0">
85-
<GraphEditor
86-
nodes={nodes}
87-
edges={edges}
88-
setNodes={setNodes}
89-
setEdges={setEdges}
90-
treeError={treeError}
91-
setTreeError={setTreeError}
92-
/>
93-
</main>
94-
<div className="absolute left-0 top-0 bottom-0 z-10 flex items-stretch">
95-
<Sidebar
96-
onExport={handleExport}
97-
onImport={handleImport}
98-
onClear={handleClear}
99-
importError={importError}
100-
setImportError={setImportError}
101-
/>
81+
<DialogRoot
82+
isOpen={themeDialogOpen || localeDialogOpen}
83+
onIsOpenChange={(open) => {
84+
if (!open) {
85+
setThemeDialogOpen(false)
86+
setLocaleDialogOpen(false)
87+
}
88+
}}
89+
>
90+
<div className="fixed top-4 right-4 flex gap-2 z-[1000]">
91+
<ThemeSwitcher onOpen={() => setThemeDialogOpen(true)} />
92+
<LanguageSwitcher onOpen={() => setLocaleDialogOpen(true)} />
10293
</div>
103-
</div>
94+
<ThemeDialog
95+
isOpen={themeDialogOpen}
96+
onClose={() => setThemeDialogOpen(false)}
97+
/>
98+
<LanguageDialog
99+
isOpen={localeDialogOpen}
100+
onClose={() => setLocaleDialogOpen(false)}
101+
/>
102+
<div className="relative flex-1 min-h-0 min-w-0">
103+
<main className="absolute inset-0">
104+
<GraphEditor
105+
nodes={nodes}
106+
edges={edges}
107+
setNodes={setNodes}
108+
setEdges={setEdges}
109+
treeError={treeError}
110+
setTreeError={setTreeError}
111+
/>
112+
</main>
113+
<div className="absolute left-0 top-0 bottom-0 z-10 flex items-stretch">
114+
<Sidebar
115+
onExport={handleExport}
116+
onImport={handleImport}
117+
onClear={handleClear}
118+
importError={importError}
119+
setImportError={setImportError}
120+
/>
121+
</div>
122+
</div>
123+
</DialogRoot>
104124
</div>
105125
</>
106126
)

web/src/components/GraphEditor.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { isScaffoldNodeType, SCAFFOLD_DRAG_TYPE } from '../types/scaffold'
2121
import type { ScaffoldNodeData } from '../lib/scaffoldGraph'
2222
import { ScaffoldNode } from './ScaffoldNode'
2323
import { NamePopUp } from './NamePopUp'
24+
import { NodeSettingsDialog } from './NodeSettingsDialog'
2425
import type { ScaffoldNodeType } from '../types/scaffold'
2526

2627
const nodeTypes = { scaffold: ScaffoldNode }
@@ -53,8 +54,22 @@ function GraphEditorInner({
5354
const { resolvedTheme } = useTheme()
5455
const [namePopUpOpen, setNamePopUpOpen] = useState(false)
5556
const [pendingDrop, setPendingDrop] = useState<PendingDrop | null>(null)
57+
const [settingsNodeId, setSettingsNodeId] = useState<string | null>(null)
5658
const isDark = resolvedTheme === 'dark'
5759

60+
const settingsNode = settingsNodeId ? nodes.find((n) => n.id === settingsNodeId) : null
61+
62+
const handleNodeSettingsSave = useCallback(
63+
(nodeId: string, data: Partial<ScaffoldNodeData>) => {
64+
setNodes((nds) =>
65+
nds.map((n) =>
66+
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
67+
)
68+
)
69+
},
70+
[setNodes]
71+
)
72+
5873
const onDrop = useCallback(
5974
(event: React.DragEvent) => {
6075
event.preventDefault()
@@ -143,6 +158,7 @@ function GraphEditorInner({
143158
onNodesChange={(changes) => setNodes((nds) => applyNodeChanges<Node<ScaffoldNodeData>>(changes, nds))}
144159
onEdgesChange={(changes) => setEdges((eds) => applyEdgeChanges(changes, eds))}
145160
onConnect={onConnect}
161+
onNodeDoubleClick={(_event, node) => setSettingsNodeId(node.id)}
146162
isValidConnection={isValidConnection}
147163
nodeTypes={nodeTypes as NodeTypes}
148164
defaultEdgeOptions={{ style: { strokeWidth: 2.5 } }}
@@ -166,6 +182,13 @@ function GraphEditorInner({
166182
onSubmit={handleNameSubmit}
167183
nodeType={pendingDrop?.type ?? 'HOSPITAL'}
168184
/>
185+
<NodeSettingsDialog
186+
isOpen={settingsNodeId !== null}
187+
nodeId={settingsNodeId}
188+
nodeData={settingsNode?.data ?? null}
189+
onClose={() => setSettingsNodeId(null)}
190+
onSave={handleNodeSettingsSave}
191+
/>
169192
</div>
170193
)
171194
}
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
import { useState } from 'react'
2-
import { DialogRoot, IconButton, LanguageDialog } from '@helpwave/hightide'
1+
import { IconButton } from '@helpwave/hightide'
32
import { Languages } from 'lucide-react'
43

5-
export function LanguageSwitcher() {
6-
const [isOpen, setIsOpen] = useState(false)
4+
type LanguageSwitcherProps = {
5+
onOpen: () => void
6+
}
77

8+
export function LanguageSwitcher({ onOpen }: LanguageSwitcherProps) {
89
return (
9-
<DialogRoot isOpen={isOpen} onIsOpenChange={setIsOpen}>
10-
<IconButton
11-
onClick={() => setIsOpen(true)}
12-
aria-label="Change language"
13-
>
14-
<Languages />
15-
</IconButton>
16-
<LanguageDialog isOpen={isOpen} />
17-
</DialogRoot>
10+
<IconButton onClick={onOpen} aria-label="Change language">
11+
<Languages />
12+
</IconButton>
1813
)
1914
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useEffect, useState } from 'react'
2+
import { Button, Dialog, DialogRoot, Input } from '@helpwave/hightide'
3+
import { useScaffoldTranslation } from '../i18n/ScaffoldTranslationContext'
4+
import type { ScaffoldNodeData } from '../lib/scaffoldGraph'
5+
6+
interface NodeSettingsDialogProps {
7+
isOpen: boolean,
8+
nodeId: string | null,
9+
nodeData: ScaffoldNodeData | null,
10+
onClose: () => void,
11+
onSave: (nodeId: string, data: Partial<ScaffoldNodeData>) => void,
12+
}
13+
14+
export function NodeSettingsDialog({
15+
isOpen,
16+
nodeId,
17+
nodeData,
18+
onClose,
19+
onSave,
20+
}: NodeSettingsDialogProps) {
21+
const t = useScaffoldTranslation()
22+
const [organizationIds, setOrganizationIds] = useState<string[]>([])
23+
const [newIdInput, setNewIdInput] = useState('')
24+
25+
useEffect(() => {
26+
if (isOpen && nodeData) {
27+
setOrganizationIds(nodeData.organization_ids ?? [])
28+
setNewIdInput('')
29+
}
30+
}, [isOpen, nodeData])
31+
32+
const handleAdd = () => {
33+
const trimmed = newIdInput.trim()
34+
if (trimmed && !organizationIds.includes(trimmed)) {
35+
setOrganizationIds((prev) => [...prev, trimmed])
36+
setNewIdInput('')
37+
}
38+
}
39+
40+
const handleRemove = (index: number) => {
41+
setOrganizationIds((prev) => prev.filter((_, i) => i !== index))
42+
}
43+
44+
const handleSave = () => {
45+
if (nodeId && nodeData) {
46+
onSave(nodeId, {
47+
...nodeData,
48+
organization_ids: organizationIds.length > 0 ? organizationIds : undefined,
49+
})
50+
onClose()
51+
}
52+
}
53+
54+
const handleClose = () => {
55+
setNewIdInput('')
56+
onClose()
57+
}
58+
59+
if (!nodeData || !nodeId) return null
60+
61+
return (
62+
<DialogRoot
63+
isOpen={isOpen}
64+
onIsOpenChange={(open) => {
65+
if (!open) handleClose()
66+
}}
67+
isModal
68+
>
69+
<Dialog
70+
titleElement={t('nodeSettingsTitle')}
71+
description={nodeData.name}
72+
onClose={handleClose}
73+
>
74+
<div className="flex flex-col gap-4">
75+
<div className="flex flex-col gap-2">
76+
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
77+
{t('organizationIds')}
78+
</span>
79+
<div className="flex gap-2">
80+
<Input
81+
aria-label={t('addOrganizationIdPlaceholder')}
82+
value={newIdInput}
83+
onValueChange={setNewIdInput}
84+
onEditComplete={(v) => {
85+
if (v?.trim()) handleAdd()
86+
}}
87+
placeholder={t('addOrganizationIdPlaceholder')}
88+
/>
89+
<Button size="sm" onClick={handleAdd} disabled={!newIdInput.trim()}>
90+
{t('add')}
91+
</Button>
92+
</div>
93+
{organizationIds.length > 0 && (
94+
<ul className="flex flex-col gap-1.5 max-h-[200px] overflow-auto rounded border border-gray-200 dark:border-gray-600 p-2">
95+
{organizationIds.map((id, index) => (
96+
<li
97+
key={`${id}-${index}`}
98+
className="flex items-center justify-between gap-2 rounded bg-gray-50 dark:bg-gray-800 px-2 py-1.5"
99+
>
100+
<span className="truncate text-sm font-mono">{id}</span>
101+
<Button
102+
size="sm"
103+
color="negative"
104+
coloringStyle="text"
105+
onClick={() => handleRemove(index)}
106+
>
107+
{t('remove')}
108+
</Button>
109+
</li>
110+
))}
111+
</ul>
112+
)}
113+
</div>
114+
<div className="flex gap-2 justify-end">
115+
<Button coloringStyle="text" onClick={handleClose}>
116+
{t('cancel')}
117+
</Button>
118+
<Button onClick={handleSave}>
119+
{t('save')}
120+
</Button>
121+
</div>
122+
</div>
123+
</Dialog>
124+
</DialogRoot>
125+
)
126+
}

web/src/components/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function Sidebar({ onExport, onImport, onClear, importError, setImportErr
7474
<aside className="min-w-[200px] shrink-0 flex flex-col h-[calc(100%-2rem)] my-4 ml-4 mr-2 bg-white dark:bg-[#0a0a0a] dark:border-gray-700 rounded-xl border border-gray-200 dark:border-gray-700 shadow-md p-5">
7575
<div className="pb-6 flex items-center gap-3">
7676
<ScaffoldLogo />
77-
<span className="typography-headline-lg truncate" style={{ color: '#7b4cd9' }}>
77+
<span className="typography-headline-md truncate" style={{ color: '#7b4cd9' }}>
7878
{t('helpwaveScaffold')}
7979
</span>
8080
</div>
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
import { useState } from 'react'
2-
import { DialogRoot, IconButton, ThemeDialog } from '@helpwave/hightide'
1+
import { IconButton } from '@helpwave/hightide'
32
import { Palette } from 'lucide-react'
43

5-
export function ThemeSwitcher() {
6-
const [isOpen, setIsOpen] = useState(false)
4+
type ThemeSwitcherProps = {
5+
onOpen: () => void
6+
}
77

8+
export function ThemeSwitcher({ onOpen }: ThemeSwitcherProps) {
89
return (
9-
<DialogRoot isOpen={isOpen} onIsOpenChange={setIsOpen}>
10-
<IconButton
11-
onClick={() => setIsOpen(true)}
12-
aria-label="Change theme"
13-
>
14-
<Palette />
15-
</IconButton>
16-
<ThemeDialog isOpen={isOpen} />
17-
</DialogRoot>
10+
<IconButton onClick={onOpen} aria-label="Change theme">
11+
<Palette />
12+
</IconButton>
1813
)
1914
}

0 commit comments

Comments
 (0)