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)}
/> />
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" data-testid="new-dashboard">
<Plus />
Create
</Button>
</DialogTrigger>
<DialogContent>
<form <form
className="flex items-center gap-2" className="grid gap-4"
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault() event.preventDefault()
if (slug) create.mutate(slug) if (slug) create.mutate(slug)
}} }}
> >
<DialogHeader>
<DialogTitle>New dashboard</DialogTitle>
<DialogDescription>
Name it after the panel it will hang on, or what it shows.
</DialogDescription>
</DialogHeader>
<Input <Input
value={name} value={name}
placeholder="New dashboard" placeholder="kitchen"
aria-label="New dashboard name" aria-label="New dashboard name"
autoComplete="off"
data-testid="new-dashboard-name" data-testid="new-dashboard-name"
onChange={(event) => setName(event.target.value)} onChange={(event) => setName(event.target.value)}
/> />
<DialogFooter>
<Button <Button
type="submit" type="submit"
variant="secondary"
disabled={!slug || create.isPending} disabled={!slug || create.isPending}
data-testid="create-dashboard" data-testid="create-dashboard"
> >
<Plus /> Create dashboard
Create
</Button> </Button>
</DialogFooter>
</form> </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">
+34 -6
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)}
/> />
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" data-testid="new-flow">
<Plus />
Create
</Button>
</DialogTrigger>
<DialogContent>
<form <form
className="flex items-center gap-2" className="grid gap-4"
onSubmit={(event) => { onSubmit={(event) => {
event.preventDefault() event.preventDefault()
if (NAME.test(slug)) create.mutate(slug) if (NAME.test(slug)) create.mutate(slug)
}} }}
> >
<DialogHeader>
<DialogTitle>New flow</DialogTitle>
<DialogDescription>
Flows are small on purpose. Name this one after what it does.
</DialogDescription>
</DialogHeader>
<Input <Input
value={name} value={name}
placeholder="New flow" placeholder="heating"
aria-label="New flow name" aria-label="New flow name"
autoComplete="off"
data-testid="new-flow-name" data-testid="new-flow-name"
onChange={(event) => setName(event.target.value)} onChange={(event) => setName(event.target.value)}
/> />
<DialogFooter>
<Button <Button
type="submit" type="submit"
variant="secondary"
disabled={!NAME.test(slug) || create.isPending} disabled={!NAME.test(slug) || create.isPending}
data-testid="create-flow" data-testid="create-flow"
> >
<Plus /> Create flow
Create
</Button> </Button>
</DialogFooter>
</form> </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}`)