Skip to content

Commit add169b

Browse files
committed
Merge branch 'ui/node-sidebar'
2 parents d131913 + 1e18422 commit add169b

3 files changed

Lines changed: 140 additions & 66 deletions

File tree

apps/web/src/components/workflow/use-workflow-state.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -300,20 +300,23 @@ export function useWorkflowState({
300300

301301
// Determine if an update to the nodes state is actually needed.
302302
// We check for two conditions:
303-
// 1. Structural difference: The data part of the nodes (excluding the createObjectUrl function itself for comparison)
303+
// 1. Structural difference: The data part of the nodes (excluding UI-only properties and functions)
304304
// has changed between the new processed initial nodes and the current nodes in state.
305305
// 2. Missing function: Any current node in state is missing the createObjectUrl function
306306
// when the initialNodes (and thus newNodesWithCreateObjectUrl) expect it to be there.
307307

308+
// Strip UI-only properties for comparison (selected, dragging, etc.)
308309
const newNodesStrippedForCompare = newNodesWithCreateObjectUrl.map((n) => ({
309-
...n,
310+
id: n.id,
311+
type: n.type,
310312
position: n.position,
311-
data: { ...n.data, createObjectUrl: undefined },
313+
data: { ...n.data, createObjectUrl: undefined, nodeTemplates: undefined },
312314
}));
313315
const currentNodesStrippedForCompare = nodesRef.current.map((n) => ({
314-
...n,
316+
id: n.id,
317+
type: n.type,
315318
position: n.position,
316-
data: { ...n.data, createObjectUrl: undefined },
319+
data: { ...n.data, createObjectUrl: undefined, nodeTemplates: undefined },
317320
}));
318321

319322
const newNodesStructurallyDifferent =
@@ -329,7 +332,22 @@ export function useWorkflowState({
329332
);
330333

331334
if (newNodesStructurallyDifferent || anyCurrentNodeMissingFunction) {
332-
setNodes(newNodesWithCreateObjectUrl);
335+
// Preserve UI-only properties like selected, dragging, etc.
336+
const currentNodesById = new Map(nodesRef.current.map((n) => [n.id, n]));
337+
const updatedNodes = newNodesWithCreateObjectUrl.map((newNode) => {
338+
const currentNode = currentNodesById.get(newNode.id);
339+
if (currentNode) {
340+
// Preserve UI-only properties from the current node
341+
return {
342+
...newNode,
343+
selected: currentNode.selected,
344+
dragging: currentNode.dragging,
345+
};
346+
}
347+
return newNode;
348+
});
349+
350+
setNodes(updatedNodes);
333351
}
334352
}, [initialNodes, readonly, setNodes, createObjectUrl]);
335353

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Edge as ReactFlowEdge } from "@xyflow/react";
22

3-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
43
import { Label } from "@/components/ui/label";
54

65
import type { WorkflowEdgeType } from "./workflow-types";
@@ -18,27 +17,38 @@ export function WorkflowEdgeInspector({
1817
if (!edge) return null;
1918

2019
return (
21-
<Card className="border-none shadow-none">
22-
<CardHeader className="pb-2">
23-
<CardTitle className="text-lg">
20+
<div className="flex flex-col h-full bg-card">
21+
{/* Header */}
22+
<div className="px-4 py-3 border-b border-border">
23+
<h1 className="text-sm font-semibold text-foreground">
2424
{readonly
2525
? "Connection Properties (Read-only)"
2626
: "Connection Properties"}
27-
</CardTitle>
28-
</CardHeader>
29-
<CardContent>
30-
<div className="space-y-4">
31-
<div className="space-y-2">
32-
<Label>Source</Label>
33-
<div className="text-sm">{edge.source}</div>
27+
</h1>
28+
</div>
29+
30+
{/* Content */}
31+
<div className="flex-1 overflow-y-auto">
32+
{/* Source Section */}
33+
<div className="px-4 py-3 border-b border-border">
34+
<Label className="text-xs font-medium text-muted-foreground">
35+
Source
36+
</Label>
37+
<div className="text-sm text-foreground mt-1 font-mono">
38+
{edge.source}
3439
</div>
40+
</div>
3541

36-
<div className="space-y-2">
37-
<Label>Target</Label>
38-
<div className="text-sm">{edge.target}</div>
42+
{/* Target Section */}
43+
<div className="px-4 py-3 border-b border-border">
44+
<Label className="text-xs font-medium text-muted-foreground">
45+
Target
46+
</Label>
47+
<div className="text-sm text-foreground mt-1 font-mono">
48+
{edge.target}
3949
</div>
4050
</div>
41-
</CardContent>
42-
</Card>
51+
</div>
52+
</div>
4353
);
4454
}

apps/web/src/components/workflow/workflow-node-inspector.tsx

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { ObjectReference } from "@dafthunk/types";
22
import type { Node as ReactFlowNode } from "@xyflow/react";
3+
import ChevronDownIcon from "lucide-react/icons/chevron-down";
34
import EyeIcon from "lucide-react/icons/eye";
45
import EyeOffIcon from "lucide-react/icons/eye-off";
56
import XCircleIcon from "lucide-react/icons/x-circle";
67
import { useEffect, useState } from "react";
78

8-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
99
import { Input } from "@/components/ui/input";
1010
import { Label } from "@/components/ui/label";
1111
import { Textarea } from "@/components/ui/textarea";
@@ -49,6 +49,10 @@ export function WorkflowNodeInspector({
4949
readonly WorkflowParameter[]
5050
>(node?.data.outputs || []);
5151

52+
// Collapsible section state
53+
const [inputsExpanded, setInputsExpanded] = useState(true);
54+
const [outputsExpanded, setOutputsExpanded] = useState(true);
55+
5256
// Update local state when node changes
5357
useEffect(() => {
5458
if (!node) return;
@@ -131,48 +135,75 @@ export function WorkflowNodeInspector({
131135
};
132136

133137
return (
134-
<Card className="border-none shadow-none rounded-none h-full">
135-
<CardHeader className="pb-2">
136-
<CardTitle className="text-lg">
138+
<div className="flex flex-col h-full bg-card">
139+
{/* Header */}
140+
<div className="px-4 py-3 border-b border-border">
141+
<h1 className="text-sm font-semibold text-foreground">
137142
{readonly ? "Node Properties (Read-only)" : "Node Properties"}
138-
</CardTitle>
139-
</CardHeader>
140-
<CardContent>
141-
<div className="space-y-4">
142-
<div className="space-y-2">
143-
<Label>Type</Label>
144-
<div className="text-sm">{node.data.nodeType || node.type}</div>
143+
</h1>
144+
</div>
145+
146+
{/* Content */}
147+
<div className="flex-1 overflow-y-auto">
148+
{/* Type Section */}
149+
<div className="px-4 py-3 border-b border-border">
150+
<Label className="text-xs font-medium text-muted-foreground">
151+
Type
152+
</Label>
153+
<div className="text-sm text-foreground mt-1">
154+
{node.data.nodeType || node.type}
145155
</div>
156+
</div>
146157

147-
<div className="space-y-2">
148-
<Label htmlFor="node-name">Name</Label>
149-
<Input
150-
id="node-name"
151-
value={localName}
152-
onChange={handleNameChange}
153-
disabled={readonly}
154-
className={readonly ? "opacity-70 cursor-not-allowed" : ""}
155-
/>
156-
</div>
158+
{/* Name Section */}
159+
<div className="px-4 py-3 border-b border-border">
160+
<Label
161+
htmlFor="node-name"
162+
className="text-xs font-medium text-muted-foreground"
163+
>
164+
Name
165+
</Label>
166+
<Input
167+
id="node-name"
168+
value={localName}
169+
onChange={handleNameChange}
170+
disabled={readonly}
171+
className={`mt-2 text-sm h-8 ${readonly ? "opacity-70 cursor-not-allowed" : ""}`}
172+
/>
173+
</div>
157174

158-
<div className="space-y-2">
159-
<h2 className="font-medium border-b border-border pb-2">Inputs</h2>
160-
<div className="space-y-2">
175+
{/* Inputs Section */}
176+
<div className="border-b border-border">
177+
<button
178+
onClick={() => setInputsExpanded(!inputsExpanded)}
179+
className="w-full px-4 py-3 bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors flex items-center justify-between"
180+
>
181+
<h2 className="text-xs font-semibold text-foreground">Inputs</h2>
182+
<ChevronDownIcon
183+
className={`h-4 w-4 text-muted-foreground transition-transform ${
184+
inputsExpanded ? "rotate-0" : "-rotate-90"
185+
}`}
186+
/>
187+
</button>
188+
{inputsExpanded && (
189+
<div className="px-4 py-3 space-y-3">
161190
{localInputs.length > 0 ? (
162191
localInputs.map((input) => (
163192
<div key={input.id} className="text-sm space-y-1">
164193
<div className="flex items-center justify-between">
165-
<div className="flex items-center gap-2">
166-
<span>{input.name}</span>
167-
<span className="text-xs text-neutral-500">
194+
<div className="flex items-center gap-2 min-w-0">
195+
<span className="text-foreground font-medium truncate">
196+
{input.name}
197+
</span>
198+
<span className="text-xs text-muted-foreground shrink-0">
168199
{input.type}
169200
</span>
170201
</div>
171-
<div className="flex items-center gap-2">
202+
<div className="flex items-center gap-1 shrink-0">
172203
{input.value !== undefined && !readonly && (
173204
<button
174205
onClick={() => handleClearValue(input.id)}
175-
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors"
206+
className="p-1 text-muted-foreground hover:text-foreground transition-colors"
176207
aria-label={`Clear ${input.name} value`}
177208
>
178209
<XCircleIcon className="h-4 w-4" />
@@ -185,7 +216,7 @@ export function WorkflowNodeInspector({
185216
handleToggleVisibility(input.id)
186217
}
187218
aria-label={`Toggle visibility for ${input.name}`}
188-
className={`bg-transparent data-[state=on]:bg-transparent hover:bg-transparent data-[state=on]:text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors ${
219+
className={`px-1 h-8 w-8 bg-transparent data-[state=on]:bg-transparent hover:bg-muted data-[state=on]:text-muted-foreground hover:text-foreground transition-colors ${
189220
readonly ? "opacity-70 cursor-not-allowed" : ""
190221
}`}
191222
disabled={readonly}
@@ -233,33 +264,48 @@ export function WorkflowNodeInspector({
233264
</div>
234265
))
235266
) : (
236-
<div className="text-sm text-neutral-500">No inputs</div>
267+
<div className="text-sm text-muted-foreground">No inputs</div>
237268
)}
238269
</div>
239-
</div>
270+
)}
271+
</div>
240272

241-
<div className="space-y-2">
242-
<h2 className="font-medium border-b border-border pb-2">Outputs</h2>
243-
<div className="space-y-2">
273+
{/* Outputs Section */}
274+
<div className="border-b border-border">
275+
<button
276+
onClick={() => setOutputsExpanded(!outputsExpanded)}
277+
className="w-full px-4 py-3 bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-200 dark:hover:bg-neutral-800 transition-colors flex items-center justify-between"
278+
>
279+
<h2 className="text-xs font-semibold text-foreground">Outputs</h2>
280+
<ChevronDownIcon
281+
className={`h-4 w-4 text-muted-foreground transition-transform ${
282+
outputsExpanded ? "rotate-0" : "-rotate-90"
283+
}`}
284+
/>
285+
</button>
286+
{outputsExpanded && (
287+
<div className="px-4 py-3 space-y-3">
244288
{localOutputs.length > 0 ? (
245289
localOutputs.map((output) => (
246290
<div key={output.id} className="text-sm space-y-1">
247291
<div className="flex items-center justify-between">
248-
<div className="flex items-center gap-2">
249-
<span>{output.name}</span>
250-
<span className="text-xs text-neutral-500">
292+
<div className="flex items-center gap-2 min-w-0">
293+
<span className="text-foreground font-medium truncate">
294+
{output.name}
295+
</span>
296+
<span className="text-xs text-muted-foreground shrink-0">
251297
{output.type}
252298
</span>
253299
</div>
254-
<div className="flex items-center gap-2">
300+
<div className="flex items-center gap-1 shrink-0">
255301
<Toggle
256302
size="sm"
257303
pressed={output.hidden}
258304
onPressedChange={() =>
259305
handleToggleOutputVisibility(output.id)
260306
}
261307
aria-label={`Toggle visibility for ${output.name}`}
262-
className={`bg-transparent data-[state=on]:bg-transparent hover:bg-transparent data-[state=on]:text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors ${
308+
className={`px-1 h-8 w-8 bg-transparent data-[state=on]:bg-transparent hover:bg-muted data-[state=on]:text-muted-foreground hover:text-foreground transition-colors ${
263309
readonly ? "opacity-70 cursor-not-allowed" : ""
264310
}`}
265311
disabled={readonly}
@@ -279,12 +325,12 @@ export function WorkflowNodeInspector({
279325
</div>
280326
))
281327
) : (
282-
<div className="text-sm text-neutral-500">No outputs</div>
328+
<div className="text-sm text-muted-foreground">No outputs</div>
283329
)}
284330
</div>
285-
</div>
331+
)}
286332
</div>
287-
</CardContent>
288-
</Card>
333+
</div>
334+
</div>
289335
);
290336
}

0 commit comments

Comments
 (0)