Ask for the name in a dialog instead of a field that is always there

Create in both overviews now opens a dialog, so the toolbar is a search box
and one button. The specs and the capture script open it first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
This commit is contained in:
2026-08-16 19:08:22 +02:00
co-authored by Claude Fable 5
parent c2321fe942
commit 949fc0bf7e
5 changed files with 110 additions and 50 deletions
+2
View File
@@ -86,6 +86,7 @@ async function captureDashboards(page, dir) {
await page.goto(`${APP_URL}/dashboards`, { waitUntil: "networkidle" }) await page.goto(`${APP_URL}/dashboards`, { waitUntil: "networkidle" })
if (!(await page.getByTestId("dashboard-card").count())) { if (!(await page.getByTestId("dashboard-card").count())) {
await page.getByTestId("new-dashboard").click()
await page.getByTestId("new-dashboard-name").fill("panel") await page.getByTestId("new-dashboard-name").fill("panel")
await page.getByTestId("create-dashboard").click() await page.getByTestId("create-dashboard").click()
await page.waitForURL(/\/dashboards\/.+/, { timeout: 15000 }) await page.waitForURL(/\/dashboards\/.+/, { timeout: 15000 })
@@ -113,6 +114,7 @@ async function captureFlows(page, dir) {
if (await page.getByTestId("flow-card").count()) { if (await page.getByTestId("flow-card").count()) {
await page.getByTestId("flow-card").first().click() await page.getByTestId("flow-card").first().click()
} else { } else {
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill("first_flow") await page.getByTestId("new-flow-name").fill("first_flow")
await page.getByTestId("create-flow").click() await page.getByTestId("create-flow").click()
} }
@@ -9,6 +9,15 @@ import {
dashboardsQueryOptions, dashboardsQueryOptions,
} from "@/components/Dashboard/queries" } from "@/components/Dashboard/queries"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import useCustomToast from "@/hooks/useCustomToast" import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils" import { handleError } from "@/utils"
@@ -24,12 +33,14 @@ function Dashboards() {
const { showErrorToast } = useCustomToast() const { showErrorToast } = useCustomToast()
const [name, setName] = useState("") const [name, setName] = useState("")
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [dialogOpen, setDialogOpen] = useState(false)
const create = useMutation({ const create = useMutation({
mutationFn: (dashboard: string) => mutationFn: (dashboard: string) =>
DashboardsService.createDashboard({ name: dashboard }), DashboardsService.createDashboard({ name: dashboard }),
onSuccess: (created) => { onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.all }) queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
setDialogOpen(false)
navigate({ navigate({
to: "/dashboards/$name", to: "/dashboards/$name",
params: { name: created.name }, params: { name: created.name },
@@ -67,37 +78,54 @@ function Dashboards() {
data-testid="search-dashboards" data-testid="search-dashboards"
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
/> />
<form <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
className="flex items-center gap-2" <DialogTrigger asChild>
onSubmit={(event) => { <Button variant="secondary" data-testid="new-dashboard">
event.preventDefault() <Plus />
if (slug) create.mutate(slug) Create
}} </Button>
> </DialogTrigger>
<Input <DialogContent>
value={name} <form
placeholder="New dashboard" className="grid gap-4"
aria-label="New dashboard name" onSubmit={(event) => {
data-testid="new-dashboard-name" event.preventDefault()
onChange={(event) => setName(event.target.value)} if (slug) create.mutate(slug)
/> }}
<Button >
type="submit" <DialogHeader>
variant="secondary" <DialogTitle>New dashboard</DialogTitle>
disabled={!slug || create.isPending} <DialogDescription>
data-testid="create-dashboard" Name it after the panel it will hang on, or what it shows.
> </DialogDescription>
<Plus /> </DialogHeader>
Create <Input
</Button> value={name}
</form> placeholder="kitchen"
aria-label="New dashboard name"
autoComplete="off"
data-testid="new-dashboard-name"
onChange={(event) => setName(event.target.value)}
/>
<DialogFooter>
<Button
type="submit"
disabled={!slug || create.isPending}
data-testid="create-dashboard"
>
Create dashboard
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div> </div>
{dashboards.length === 0 ? ( {dashboards.length === 0 ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{needle {needle
? "No dashboard matches that." ? "No dashboard matches that."
: "No dashboards yet. Name one above to start."} : "No dashboards yet. Create one to start."}
</p> </p>
) : ( ) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
+53 -25
View File
@@ -6,6 +6,15 @@ import { useState } from "react"
import { FlowsService } from "@/client" import { FlowsService } from "@/client"
import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries" import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import useCustomToast from "@/hooks/useCustomToast" import useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils" import { handleError } from "@/utils"
@@ -25,6 +34,7 @@ function Flows() {
const { showErrorToast } = useCustomToast() const { showErrorToast } = useCustomToast()
const [name, setName] = useState("") const [name, setName] = useState("")
const [search, setSearch] = useState("") const [search, setSearch] = useState("")
const [dialogOpen, setDialogOpen] = useState(false)
const create = useMutation({ const create = useMutation({
mutationFn: (flow: string) => mutationFn: (flow: string) =>
@@ -34,6 +44,7 @@ function Flows() {
}), }),
onSuccess: (_saved, flow) => { onSuccess: (_saved, flow) => {
queryClient.invalidateQueries({ queryKey: flowKeys.all }) queryClient.invalidateQueries({ queryKey: flowKeys.all })
setDialogOpen(false)
navigate({ to: "/flows/$flowName", params: { flowName: flow } }) navigate({ to: "/flows/$flowName", params: { flowName: flow } })
}, },
onError: handleError.bind(showErrorToast), onError: handleError.bind(showErrorToast),
@@ -67,37 +78,54 @@ function Flows() {
data-testid="search-flows" data-testid="search-flows"
onChange={(event) => setSearch(event.target.value)} onChange={(event) => setSearch(event.target.value)}
/> />
<form <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
className="flex items-center gap-2" <DialogTrigger asChild>
onSubmit={(event) => { <Button variant="secondary" data-testid="new-flow">
event.preventDefault() <Plus />
if (NAME.test(slug)) create.mutate(slug) Create
}} </Button>
> </DialogTrigger>
<Input <DialogContent>
value={name} <form
placeholder="New flow" className="grid gap-4"
aria-label="New flow name" onSubmit={(event) => {
data-testid="new-flow-name" event.preventDefault()
onChange={(event) => setName(event.target.value)} if (NAME.test(slug)) create.mutate(slug)
/> }}
<Button >
type="submit" <DialogHeader>
variant="secondary" <DialogTitle>New flow</DialogTitle>
disabled={!NAME.test(slug) || create.isPending} <DialogDescription>
data-testid="create-flow" Flows are small on purpose. Name this one after what it does.
> </DialogDescription>
<Plus /> </DialogHeader>
Create <Input
</Button> value={name}
</form> placeholder="heating"
aria-label="New flow name"
autoComplete="off"
data-testid="new-flow-name"
onChange={(event) => setName(event.target.value)}
/>
<DialogFooter>
<Button
type="submit"
disabled={!NAME.test(slug) || create.isPending}
data-testid="create-flow"
>
Create flow
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div> </div>
{flows.length === 0 ? ( {flows.length === 0 ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{needle {needle
? "No flow matches that." ? "No flow matches that."
: "No flows yet. Name one above to start."} : "No flows yet. Create one to start."}
</p> </p>
) : ( ) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
+1
View File
@@ -19,6 +19,7 @@ test.afterAll(async ({ browser }) => {
test("a draft stays off the engine until it is published", async ({ page }) => { test("a draft stays off the engine until it is published", async ({ page }) => {
await page.goto("/flows") await page.goto("/flows")
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill(flowName) await page.getByTestId("new-flow-name").fill(flowName)
await page.getByTestId("create-flow").click() await page.getByTestId("create-flow").click()
await page.waitForURL(`/flows/${flowName}`) await page.waitForURL(`/flows/${flowName}`)
+1
View File
@@ -73,6 +73,7 @@ test("a flow can be created, wired up, and comes back after a reload", async ({
await page.goto("/flows") await page.goto("/flows")
// Create a flow of our own so the test does not lean on existing data. // Create a flow of our own so the test does not lean on existing data.
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill(flowName) await page.getByTestId("new-flow-name").fill(flowName)
await page.getByTestId("create-flow").click() await page.getByTestId("create-flow").click()
await page.waitForURL(`/flows/${flowName}`) await page.waitForURL(`/flows/${flowName}`)