Skip to content

Commit 883bc23

Browse files
DylanMerigaudclaude
andcommitted
#7 open the pipeline tab when the URL carries a run; #2b node edit panel as a page portal
#7: a ?run=<id> URL is a shared/refreshed run, which lives on the "Run it on invoices" tab. Read it for the initial tab so the run shows straight away instead of landing on "Build the workflow" and needing a manual switch. (force-dynamic page, so useSearchParams needs no Suspense boundary.) #2b: the node edit panel was anchored inside the graph box (absolute right-0 w-320), so it was clipped and cramped. Render it as a full-page portal drawer instead: a dimmed backdrop over the page + a right-side sheet that owns its scroll, so a tall condition editor is never cut off. Escape and a backdrop click close it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c70b833 commit 883bc23

2 files changed

Lines changed: 51 additions & 19 deletions

File tree

components/app-view.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useSearchParams } from "next/navigation";
34
import { useState } from "react";
45

56
import { Dashboard } from "@/components/dashboard";
@@ -32,7 +33,14 @@ import type { ApprovalWorkflow } from "@/lib/approval-workflow";
3233
type View = "onboarding" | "pipeline";
3334

3435
export const AppView = ({ queue }: { queue: QueueItem[] }) => {
35-
const [view, setView] = useState<View>("onboarding");
36+
// A `?run=<id>` in the URL is a shared/refreshed RUN, which lives on the pipeline tab.
37+
// Open straight there so the run shows without a manual tab switch (the Dashboard's
38+
// own URL effect then replays it). Read once for the initial tab; the user can still
39+
// switch tabs afterwards.
40+
const searchParams = useSearchParams();
41+
const [view, setView] = useState<View>(
42+
searchParams.get("run") ? "pipeline" : "onboarding",
43+
);
3644
// The single approval workflow shared across both tabs. null until the user
3745
// runs discovery; the pipeline falls back to its default DAG meanwhile.
3846
const [workflow, setWorkflow] = useState<ApprovalWorkflow | null>(null);

components/node-edit-panel.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useEffect, useState } from "react";
4+
import { createPortal } from "react-dom";
45

56
import { ConditionEditor } from "@/components/condition-editor";
67
import { Button } from "@/components/ui/button";
@@ -272,25 +273,48 @@ const PanelShell = ({
272273
title: string;
273274
onClose: () => void;
274275
children: React.ReactNode;
275-
}) => (
276-
// Overlay on the right edge of the graph; the canvas stays full width behind it.
277-
// On a narrow screen it spans the bottom instead of a thin right column.
278-
<div className="absolute inset-x-0 bottom-0 z-20 max-h-[70%] overflow-y-auto rounded-t-xl bg-surface p-4 shadow-lift ring-1 ring-inset ring-line sm:inset-y-0 sm:left-auto sm:right-0 sm:max-h-none sm:w-[320px] sm:rounded-l-xl sm:rounded-tr-none">
279-
<div className="mb-3 flex items-center justify-between gap-2">
280-
<span className="truncate text-[13px] font-semibold text-ink">
281-
{title}
282-
</span>
276+
}) => {
277+
// A full-page portal drawer (not anchored inside the graph box, where it was clipped
278+
// and cramped). A dimmed backdrop covers the page; the panel is a right-side sheet
279+
// that owns its own scroll, so a tall condition editor is never cut off. Escape and a
280+
// backdrop click close it. Portalled to <body> after mount (SSR-safe).
281+
const [mounted, setMounted] = useState(false);
282+
useEffect(() => {
283+
setMounted(true);
284+
const onKey = (e: KeyboardEvent) => {
285+
if (e.key === "Escape") onClose();
286+
};
287+
window.addEventListener("keydown", onKey);
288+
return () => window.removeEventListener("keydown", onKey);
289+
}, [onClose]);
290+
if (!mounted) return null;
291+
return createPortal(
292+
<div className="fixed inset-0 z-50 flex justify-end">
283293
<button
294+
type="button"
295+
aria-label="Close panel"
284296
onClick={onClose}
285-
aria-label="Close"
286-
className="grid size-6 shrink-0 place-items-center rounded-md text-faint ring-1 ring-inset ring-line-strong transition-colors hover:text-ink"
287-
>
288-
×
289-
</button>
290-
</div>
291-
<div className="space-y-3">{children}</div>
292-
</div>
293-
);
297+
className="absolute inset-0 bg-ink/20 backdrop-blur-[1px]"
298+
/>
299+
<div className="relative flex h-full w-full max-w-[380px] flex-col overflow-y-auto bg-surface p-5 shadow-lift ring-1 ring-inset ring-line">
300+
<div className="mb-3 flex items-center justify-between gap-2">
301+
<span className="truncate text-[14px] font-semibold text-ink">
302+
{title}
303+
</span>
304+
<button
305+
onClick={onClose}
306+
aria-label="Close"
307+
className="grid size-6 shrink-0 place-items-center rounded-md text-faint ring-1 ring-inset ring-line-strong transition-colors hover:text-ink"
308+
>
309+
×
310+
</button>
311+
</div>
312+
<div className="space-y-3">{children}</div>
313+
</div>
314+
</div>,
315+
document.body,
316+
);
317+
};
294318

295319
const Field = ({
296320
label,

0 commit comments

Comments
 (0)