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" })
if (!(await page.getByTestId("dashboard-card").count())) {
await page.getByTestId("new-dashboard").click()
await page.getByTestId("new-dashboard-name").fill("panel")
await page.getByTestId("create-dashboard").click()
await page.waitForURL(/\/dashboards\/.+/, { timeout: 15000 })
@@ -113,6 +114,7 @@ async function captureFlows(page, dir) {
if (await page.getByTestId("flow-card").count()) {
await page.getByTestId("flow-card").first().click()
} else {
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill("first_flow")
await page.getByTestId("create-flow").click()
}
@@ -9,6 +9,15 @@ import {
dashboardsQueryOptions,
} from "@/components/Dashboard/queries"
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 useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
@@ -24,12 +33,14 @@ function Dashboards() {
const { showErrorToast } = useCustomToast()
const [name, setName] = useState("")
const [search, setSearch] = useState("")
const [dialogOpen, setDialogOpen] = useState(false)
const create = useMutation({
mutationFn: (dashboard: string) =>
DashboardsService.createDashboard({ name: dashboard }),
onSuccess: (created) => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
setDialogOpen(false)
navigate({
to: "/dashboards/$name",
params: { name: created.name },
@@ -67,37 +78,54 @@ function Dashboards() {
data-testid="search-dashboards"
onChange={(event) => setSearch(event.target.value)}
/>
<form
className="flex items-center gap-2"
onSubmit={(event) => {
event.preventDefault()
if (slug) create.mutate(slug)
}}
>
<Input
value={name}
placeholder="New dashboard"
aria-label="New dashboard name"
data-testid="new-dashboard-name"
onChange={(event) => setName(event.target.value)}
/>
<Button
type="submit"
variant="secondary"
disabled={!slug || create.isPending}
data-testid="create-dashboard"
>
<Plus />
Create
</Button>
</form>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" data-testid="new-dashboard">
<Plus />
Create
</Button>
</DialogTrigger>
<DialogContent>
<form
className="grid gap-4"
onSubmit={(event) => {
event.preventDefault()
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
value={name}
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>
{dashboards.length === 0 ? (
<p className="text-sm text-muted-foreground">
{needle
? "No dashboard matches that."
: "No dashboards yet. Name one above to start."}
: "No dashboards yet. Create one to start."}
</p>
) : (
<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 { flowKeys, flowsQueryOptions } from "@/components/Flow/queries"
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 useCustomToast from "@/hooks/useCustomToast"
import { handleError } from "@/utils"
@@ -25,6 +34,7 @@ function Flows() {
const { showErrorToast } = useCustomToast()
const [name, setName] = useState("")
const [search, setSearch] = useState("")
const [dialogOpen, setDialogOpen] = useState(false)
const create = useMutation({
mutationFn: (flow: string) =>
@@ -34,6 +44,7 @@ function Flows() {
}),
onSuccess: (_saved, flow) => {
queryClient.invalidateQueries({ queryKey: flowKeys.all })
setDialogOpen(false)
navigate({ to: "/flows/$flowName", params: { flowName: flow } })
},
onError: handleError.bind(showErrorToast),
@@ -67,37 +78,54 @@ function Flows() {
data-testid="search-flows"
onChange={(event) => setSearch(event.target.value)}
/>
<form
className="flex items-center gap-2"
onSubmit={(event) => {
event.preventDefault()
if (NAME.test(slug)) create.mutate(slug)
}}
>
<Input
value={name}
placeholder="New flow"
aria-label="New flow name"
data-testid="new-flow-name"
onChange={(event) => setName(event.target.value)}
/>
<Button
type="submit"
variant="secondary"
disabled={!NAME.test(slug) || create.isPending}
data-testid="create-flow"
>
<Plus />
Create
</Button>
</form>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary" data-testid="new-flow">
<Plus />
Create
</Button>
</DialogTrigger>
<DialogContent>
<form
className="grid gap-4"
onSubmit={(event) => {
event.preventDefault()
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
value={name}
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>
{flows.length === 0 ? (
<p className="text-sm text-muted-foreground">
{needle
? "No flow matches that."
: "No flows yet. Name one above to start."}
: "No flows yet. Create one to start."}
</p>
) : (
<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 }) => {
await page.goto("/flows")
await page.getByTestId("new-flow").click()
await page.getByTestId("new-flow-name").fill(flowName)
await page.getByTestId("create-flow").click()
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")
// 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("create-flow").click()
await page.waitForURL(`/flows/${flowName}`)