From 5964e5e7b634e8821e933c95d22eb0c3ae77a79f Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 11:48:27 -0500 Subject: [PATCH 1/8] Add design doc for site edit popover UI (issue #31) Co-Authored-By: Claude Opus 4.6 --- .../2026-02-20-site-edit-popover-design.md | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/plans/2026-02-20-site-edit-popover-design.md diff --git a/docs/plans/2026-02-20-site-edit-popover-design.md b/docs/plans/2026-02-20-site-edit-popover-design.md new file mode 100644 index 0000000..05fe46a --- /dev/null +++ b/docs/plans/2026-02-20-site-edit-popover-design.md @@ -0,0 +1,53 @@ +# Site Edit Popover Design + +**Issue:** #31 — Site edit UI should be its own page (popover) +**Date:** 2026-02-20 + +## Problem + +The inline site edit UI replaces card content in-place, which feels awkward. Both add and edit flows should use a centered popover dialog instead. + +## Design + +### New component: `SiteFormDialog` + +A single client component using the native HTML `` element, shared by both add and edit flows. + +**Props:** +- `mode: "add" | "edit"` +- `siteId?: string` (edit only) +- `name?: string` (edit only, pre-fills input) +- `url?: string` (edit only, pre-fills input) +- `trigger: ReactNode` — the button/element that opens the dialog + +**Behavior:** +- Clicking the trigger calls `dialogRef.current.showModal()` +- Dialog renders centered with semi-transparent backdrop +- Form contains: Name input, URL input, action buttons +- Add mode: "Add" and "Cancel" buttons +- Edit mode: "Save", "Cancel", and "Delete" buttons +- Cancel button and Escape key dismiss the dialog +- Backdrop click does NOT close the dialog +- On successful save/add: dialog closes, server action revalidates paths +- Delete: clicking Delete shows inline confirmation ("Are you sure? Yes / No") replacing the Delete button. "Yes" executes deletion. + +### Changes to existing components + +**Status page (`page.tsx`):** +- Replace `` with `` wrapping a pencil icon trigger +- Replace `` inline form — dashed card becomes a simple trigger for `` + +**Site detail page (`sites/[id]/page.tsx`):** +- Replace pencil icon `` with `` wrapping a full "Edit" button (text, not just icon) + +**Components to delete/simplify:** +- `EditSiteButton.tsx` — delete entirely, replaced by `SiteFormDialog` +- `AddSiteCard.tsx` — simplify to just the dashed card trigger (no inline form logic) + +### Server actions + +No changes — existing `addSite`, `editSite`, `deleteSite` in `actions.ts` work as-is. + +### E2E tests + +Update selectors in `e2e/sites-crud.spec.ts` to interact with the dialog instead of inline forms. Test dialog open/close, Escape key, and delete confirmation flow. From d94dddc676f8659e0fd488ea11a73852c0f9eb98 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 11:50:22 -0500 Subject: [PATCH 2/8] Add implementation plan for site edit popover (issue #31) Co-Authored-By: Claude Opus 4.6 --- .../2026-02-20-site-edit-popover-plan.md | 627 ++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100644 docs/plans/2026-02-20-site-edit-popover-plan.md diff --git a/docs/plans/2026-02-20-site-edit-popover-plan.md b/docs/plans/2026-02-20-site-edit-popover-plan.md new file mode 100644 index 0000000..bf6b99f --- /dev/null +++ b/docs/plans/2026-02-20-site-edit-popover-plan.md @@ -0,0 +1,627 @@ +# Site Edit Popover Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace inline site edit/add UI with a centered `` popover, shared by both add and edit flows. + +**Architecture:** Single new `SiteFormDialog` client component wraps a native ``. It accepts a `trigger` ReactNode that opens the dialog on click. The form inside calls existing server actions (`addSite`, `editSite`, `deleteSite`) unchanged. Delete has an inline confirmation step. + +**Tech Stack:** Next.js 15 App Router, React `useRef`/`useState`, native HTML ``, existing server actions. + +**Design doc:** `docs/plans/2026-02-20-site-edit-popover-design.md` + +**Branch:** Create `site-edit-popover` from `main`. + +--- + +### Task 1: Create `SiteFormDialog` component + +**Files:** +- Create: `src/components/SiteFormDialog.tsx` + +**Step 1: Create the component** + +```tsx +"use client" + +import { useRef, useState } from "react" +import { addSite, editSite, deleteSite } from "@/app/sites/actions" + +export default function SiteFormDialog({ + mode, + siteId, + name, + url, + trigger, +}: { + mode: "add" | "edit" + siteId?: string + name?: string + url?: string + trigger: React.ReactNode +}) { + const dialogRef = useRef(null) + const [confirmingDelete, setConfirmingDelete] = useState(false) + + function open() { + setConfirmingDelete(false) + dialogRef.current?.showModal() + } + + function close() { + dialogRef.current?.close() + setConfirmingDelete(false) + } + + return ( + <> + + {trigger} + + { + // Native dialog fires cancel on Escape — allow it + // But prevent backdrop click from closing + }} + onClick={(e) => { + // Clicks on the backdrop hit the dialog element itself + // Don't close — user preference + if (e.target === dialogRef.current) { + e.preventDefault() + } + }} + > +
+

+ {mode === "add" ? "Add site" : "Edit site"} +

+
{ + if (mode === "add") { + await addSite(formData) + } else { + await editSite(siteId!, formData) + } + close() + }} + className="flex flex-col gap-3" + > +
+ + +
+
+ + +
+
+ + + {mode === "edit" && !confirmingDelete && ( + + )} + {mode === "edit" && confirmingDelete && ( + + Delete? + + + + )} +
+
+
+
+ + ) +} +``` + +**Step 2: Commit** + +```bash +git add src/components/SiteFormDialog.tsx +git commit -m "feat: add SiteFormDialog component with native dialog element" +``` + +--- + +### Task 2: Simplify `AddSiteCard` to use `SiteFormDialog` + +**Files:** +- Modify: `src/components/AddSiteCard.tsx` (full rewrite) + +**Step 1: Replace AddSiteCard with a simple trigger card that wraps SiteFormDialog** + +Replace the entire file with: + +```tsx +import SiteFormDialog from "@/components/SiteFormDialog" + +export default function AddSiteCard() { + return ( + + + + + Add site + + } + /> + ) +} +``` + +Note: `AddSiteCard` is no longer a client component — it doesn't use `useState` anymore. `SiteFormDialog` handles all the state. Remove the `"use client"` directive. + +**Step 2: Commit** + +```bash +git add src/components/AddSiteCard.tsx +git commit -m "refactor: simplify AddSiteCard to use SiteFormDialog" +``` + +--- + +### Task 3: Update status page to use `SiteFormDialog` instead of `EditSiteButton` + +**Files:** +- Modify: `src/app/page.tsx` + +**Step 1: Replace import and usage** + +- Replace `import EditSiteButton from "@/components/EditSiteButton"` with `import SiteFormDialog from "@/components/SiteFormDialog"` +- Replace the `` block (lines 186-194) with: + +```tsx +{isAdmin && ( +
+ + + + + + + } + /> +
+)} +``` + +**Step 2: Commit** + +```bash +git add src/app/page.tsx +git commit -m "refactor: use SiteFormDialog on status page instead of inline edit" +``` + +--- + +### Task 4: Update site detail page with full "Edit" button using `SiteFormDialog` + +**Files:** +- Modify: `src/app/sites/[id]/page.tsx` + +**Step 1: Replace import and usage** + +- Replace `import EditSiteButton from "@/components/EditSiteButton"` with `import SiteFormDialog from "@/components/SiteFormDialog"` +- Replace the `{user && (...)}` block (lines 94-98) with: + +```tsx +{user && ( +
+ + Edit + + } + /> +
+)} +``` + +**Step 2: Commit** + +```bash +git add src/app/sites/[id]/page.tsx +git commit -m "refactor: use SiteFormDialog with Edit button on site detail page" +``` + +--- + +### Task 5: Delete `EditSiteButton.tsx` + +**Files:** +- Delete: `src/components/EditSiteButton.tsx` + +**Step 1: Delete the file** + +```bash +git rm src/components/EditSiteButton.tsx +``` + +**Step 2: Verify no remaining imports** + +```bash +grep -r "EditSiteButton" src/ +``` + +Expected: no results. + +**Step 3: Commit** + +```bash +git commit -m "chore: remove EditSiteButton, replaced by SiteFormDialog" +``` + +--- + +### Task 6: Run unit tests + +**Step 1: Run vitest** + +```bash +npx vitest run +``` + +Expected: All 88 tests pass. The server action unit tests (`sites-crud.test.ts`) should be unaffected since we didn't change `actions.ts`. + +--- + +### Task 7: Update e2e tests for dialog-based UI + +**Files:** +- Modify: `e2e/sites-crud.spec.ts` + +**Step 1: Rewrite the e2e tests** + +The key changes: +- "Add site" click now opens a dialog, not an inline form. Use `page.getByRole("dialog")` to scope interactions. +- Edit pencil icon on status page now opens a dialog. +- Site detail page now has an "Edit" button (text), not a pencil icon. +- Delete now has a confirmation step (click Delete, then click Yes). +- Add test for Escape key dismissal. + +Replace the full file: + +```ts +import { test, expect } from "./fixtures"; + +test.afterEach(async ({ adminClient }) => { + // Restore renamed site if needed + const { data: sites } = await adminClient.from("sites").select("id, name"); + const renamed = sites?.find((s) => s.name === "Renamed Site"); + if (renamed) { + await adminClient + .from("sites") + .update({ name: "E2E Test Site" }) + .eq("id", renamed.id); + } + + // Clean up any extra sites created during tests (keep the seeded one) + for (const site of sites ?? []) { + if (site.name !== "E2E Test Site" && site.name !== "Renamed Site") { + await adminClient.from("incidents").delete().eq("site_id", site.id); + await adminClient.from("checks").delete().eq("site_id", site.id); + await adminClient.from("sites").delete().eq("id", site.id); + } + } +}); + +test("shows Add site card when authenticated", async ({ page }) => { + await page.goto("/"); + await expect(page.getByText("Add site")).toBeVisible(); +}); + +test("adds a new site", async ({ page }) => { + await page.goto("/"); + await page.getByText("Add site").click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + await dialog.getByLabel("Name").fill("New Test Site"); + await dialog.getByLabel("URL").fill("https://example.com/test"); + await dialog.getByRole("button", { name: "Add" }).click(); + + await expect(dialog).not.toBeVisible(); + await expect(page.getByText("New Test Site")).toBeVisible(); + await expect(page.getByText("https://example.com/test")).toBeVisible(); +}); + +test("cancels adding a site", async ({ page }) => { + await page.goto("/"); + await page.getByText("Add site").click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + await dialog.getByRole("button", { name: "Cancel" }).click(); + await expect(dialog).not.toBeVisible(); + await expect(page.getByText("Add site")).toBeVisible(); +}); + +test("dismisses dialog with Escape key", async ({ page }) => { + await page.goto("/"); + await page.getByText("Add site").click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible(); +}); + +test("edits a site name", async ({ page }) => { + await page.goto("/"); + + // Hover the site card to reveal the edit button, click it + const siteCard = page.locator(".group").filter({ hasText: "E2E Test Site" }); + await siteCard.hover(); + await siteCard.getByTitle("Edit site").click({ force: true }); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + const nameInput = dialog.getByLabel("Name"); + await nameInput.clear(); + await nameInput.fill("Renamed Site"); + await dialog.getByRole("button", { name: "Save" }).click(); + + await expect(dialog).not.toBeVisible(); + await expect(page.getByText("Renamed Site")).toBeVisible(); +}); + +test("site detail Edit button opens edit dialog", async ({ page, testData }) => { + await page.goto(`/sites/${testData.siteId}`); + await page.getByRole("button", { name: "Edit" }).click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(dialog.getByLabel("Name")).toBeVisible(); + await expect(dialog.getByLabel("URL")).toBeVisible(); + await expect(dialog.getByRole("button", { name: "Save" })).toBeVisible(); +}); + +test("deletes a site with confirmation", async ({ page, adminClient }) => { + // First create a site to delete + await adminClient + .from("sites") + .insert({ name: "Site To Delete", url: "https://example.com/delete" }); + + await page.goto("/"); + await expect(page.getByText("Site To Delete")).toBeVisible(); + + // Hover and click edit + const siteCard = page.locator(".group").filter({ hasText: "Site To Delete" }); + await siteCard.hover(); + await siteCard.getByTitle("Edit site").click({ force: true }); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + // Click Delete — should show confirmation + await dialog.getByRole("button", { name: "Delete" }).click(); + await expect(dialog.getByText("Delete?")).toBeVisible(); + + // Confirm deletion + await dialog.getByRole("button", { name: "Yes" }).click(); + + await expect(dialog).not.toBeVisible(); + await expect(page.getByText("Site To Delete")).not.toBeVisible(); +}); +``` + +**Step 2: Commit** + +```bash +git add e2e/sites-crud.spec.ts +git commit -m "test: update e2e tests for dialog-based site edit UI" +``` + +--- + +### Task 8: Run all tests + +**Step 1: Run unit tests** + +```bash +npx vitest run +``` + +Expected: All pass. + +**Step 2: Run e2e tests** + +```bash +env-cmd -f .env.test npx playwright test +``` + +Expected: All pass. If any fail, debug and fix before proceeding. + +**Step 3: Final commit if any fixes were needed** + +--- + +### Task 9: Create PR + +```bash +gh pr create --title "Replace inline site edit with dialog popover" --body "$(cat <<'EOF' +## Summary +- New `SiteFormDialog` component using native `` for both add and edit flows +- Centered popover with semi-transparent backdrop (non-dismissive on backdrop click) +- Delete now requires confirmation (Delete → "Delete? Yes / No") +- Site detail page uses full "Edit" button instead of pencil icon +- Removed `EditSiteButton` component entirely, simplified `AddSiteCard` + +Closes #31 + +## Test plan +- [ ] Unit tests pass (server actions unchanged) +- [ ] E2E: add site via dialog +- [ ] E2E: edit site via dialog (status page pencil icon) +- [ ] E2E: edit site via dialog (site detail Edit button) +- [ ] E2E: delete site with confirmation +- [ ] E2E: cancel/Escape dismisses dialog +- [ ] E2E: backdrop click does NOT dismiss + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` From d17489078ae6a7bdb2dcb54b27f4cd511d6379e2 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 11:54:15 -0500 Subject: [PATCH 3/8] feat: add SiteFormDialog component with native dialog element Co-Authored-By: Claude Opus 4.6 --- src/components/SiteFormDialog.tsx | 190 ++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/components/SiteFormDialog.tsx diff --git a/src/components/SiteFormDialog.tsx b/src/components/SiteFormDialog.tsx new file mode 100644 index 0000000..3fd7fd1 --- /dev/null +++ b/src/components/SiteFormDialog.tsx @@ -0,0 +1,190 @@ +"use client" + +import { useRef, useState } from "react" +import { addSite, editSite, deleteSite } from "@/app/sites/actions" + +export default function SiteFormDialog({ + mode, + siteId, + name, + url, + trigger, +}: { + mode: "add" | "edit" + siteId?: string + name?: string + url?: string + trigger: React.ReactNode +}) { + const dialogRef = useRef(null) + const [confirmingDelete, setConfirmingDelete] = useState(false) + + function open() { + setConfirmingDelete(false) + dialogRef.current?.showModal() + } + + function close() { + dialogRef.current?.close() + setConfirmingDelete(false) + } + + return ( + <> + + {trigger} + + { + // Clicks on the backdrop hit the dialog element itself. + // Don't close — user preference is Cancel/Escape only. + if (e.target === dialogRef.current) { + e.preventDefault() + } + }} + > +
+

+ {mode === "add" ? "Add site" : "Edit site"} +

+
{ + if (mode === "add") { + await addSite(formData) + } else { + await editSite(siteId!, formData) + } + close() + }} + className="flex flex-col gap-3" + > +
+ + +
+
+ + +
+
+ + + {mode === "edit" && !confirmingDelete && ( + + )} + {mode === "edit" && confirmingDelete && ( + + Delete? + + + + )} +
+
+
+
+ + ) +} From 1bb18fc81e7f2b19c744fde52ec153e216b30f87 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 11:55:14 -0500 Subject: [PATCH 4/8] refactor: replace inline edit UI with SiteFormDialog popover - Simplify AddSiteCard to just a trigger for the dialog - Status page pencil icon opens dialog instead of inline edit - Site detail page uses full Edit button opening dialog - Delete EditSiteButton.tsx (replaced by SiteFormDialog) Co-Authored-By: Claude Opus 4.6 --- src/app/page.tsx | 34 ++++++++- src/app/sites/[id]/page.tsx | 21 ++++- src/components/AddSiteCard.tsx | 123 +++++++----------------------- src/components/EditSiteButton.tsx | 122 ----------------------------- 4 files changed, 77 insertions(+), 223 deletions(-) delete mode 100644 src/components/EditSiteButton.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index ae293ba..7a6ab38 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,7 +3,7 @@ import { createClient } from "@/lib/supabase/server" import type { Site, Check, Incident } from "@/lib/supabase/types" import { formatTimeAgo, formatDuration } from "@/lib/format" import AddSiteCard from "@/components/AddSiteCard" -import EditSiteButton from "@/components/EditSiteButton" +import SiteFormDialog from "@/components/SiteFormDialog" import RealtimeStatusPage from "@/components/RealtimeStatusPage" export const revalidate = 0 @@ -184,11 +184,39 @@ export default async function StatusPage() { {isAdmin && ( -
- + + + + + + + } />
)} diff --git a/src/app/sites/[id]/page.tsx b/src/app/sites/[id]/page.tsx index 532910e..d9c50c9 100644 --- a/src/app/sites/[id]/page.tsx +++ b/src/app/sites/[id]/page.tsx @@ -3,7 +3,7 @@ import { notFound } from "next/navigation" import { createClient } from "@/lib/supabase/server" import type { Site, Check, Incident } from "@/lib/supabase/types" import { formatTimeAgo } from "@/lib/format" -import EditSiteButton from "@/components/EditSiteButton" +import SiteFormDialog from "@/components/SiteFormDialog" export const revalidate = 0 @@ -93,7 +93,24 @@ export default async function SiteDetailPage({ {user && (
- + + Edit + + } + />
)} diff --git a/src/components/AddSiteCard.tsx b/src/components/AddSiteCard.tsx index 348cfb4..4b9c029 100644 --- a/src/components/AddSiteCard.tsx +++ b/src/components/AddSiteCard.tsx @@ -1,104 +1,35 @@ -"use client" - -import { useState } from "react" -import { addSite } from "@/app/sites/actions" +import SiteFormDialog from "@/components/SiteFormDialog" export default function AddSiteCard() { - const [isOpen, setIsOpen] = useState(false) - - if (!isOpen) { - return ( - - ) - } - return ( -
-
{ - await addSite(formData) - setIsOpen(false) - }} - className="flex flex-col gap-2" - > - - -
- - -
-
-
+ + + Add site + + } + /> ) } diff --git a/src/components/EditSiteButton.tsx b/src/components/EditSiteButton.tsx deleted file mode 100644 index f042ecd..0000000 --- a/src/components/EditSiteButton.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client" - -import { useState } from "react" -import { editSite, deleteSite } from "@/app/sites/actions" - -export default function EditSiteButton({ - siteId, - name, - url, -}: { - siteId: string - name: string - url: string -}) { - const [isEditing, setIsEditing] = useState(false) - - if (!isEditing) { - return ( - - ) - } - - return ( -
-
- - -
- - - -
-
-
- ) -} From bc23d680d1059ad8c972b496986ec9d3529503f5 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 14:17:35 -0500 Subject: [PATCH 5/8] test: update e2e tests for dialog-based site edit UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dialog selectors instead of inline form selectors - Test Escape key dismissal - Test delete confirmation flow (Delete → Yes) - Site detail uses Edit button instead of pencil icon Co-Authored-By: Claude Opus 4.6 --- e2e/sites-crud.spec.ts | 66 ++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/e2e/sites-crud.spec.ts b/e2e/sites-crud.spec.ts index 2ccb282..19c0b48 100644 --- a/e2e/sites-crud.spec.ts +++ b/e2e/sites-crud.spec.ts @@ -30,10 +30,14 @@ test("adds a new site", async ({ page }) => { await page.goto("/"); await page.getByText("Add site").click(); - await page.getByPlaceholder("Site name").fill("New Test Site"); - await page.getByPlaceholder("https://example.com").fill("https://example.com/test"); - await page.getByRole("button", { name: "Add" }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await dialog.getByLabel("Name").fill("New Test Site"); + await dialog.getByLabel("URL").fill("https://example.com/test"); + await dialog.getByRole("button", { name: "Add" }).click(); + + await expect(dialog).not.toBeVisible(); await expect(page.getByText("New Test Site")).toBeVisible(); await expect(page.getByText("https://example.com/test")).toBeVisible(); }); @@ -42,41 +46,57 @@ test("cancels adding a site", async ({ page }) => { await page.goto("/"); await page.getByText("Add site").click(); - await expect(page.getByPlaceholder("Site name")).toBeVisible(); - await page.getByRole("button", { name: "Cancel" }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); - await expect(page.getByPlaceholder("Site name")).not.toBeVisible(); + await dialog.getByRole("button", { name: "Cancel" }).click(); + await expect(dialog).not.toBeVisible(); await expect(page.getByText("Add site")).toBeVisible(); }); +test("dismisses dialog with Escape key", async ({ page }) => { + await page.goto("/"); + await page.getByText("Add site").click(); + + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + await page.keyboard.press("Escape"); + await expect(dialog).not.toBeVisible(); +}); + test("edits a site name", async ({ page }) => { await page.goto("/"); - // Hover the site card to reveal the edit button + // Hover the site card to reveal the edit button, click it const siteCard = page.locator(".group").filter({ hasText: "E2E Test Site" }); await siteCard.hover(); await siteCard.getByTitle("Edit site").click({ force: true }); - // Clear and fill the name field - const nameInput = page.locator('input[name="name"]'); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + const nameInput = dialog.getByLabel("Name"); await nameInput.clear(); await nameInput.fill("Renamed Site"); - await page.getByRole("button", { name: "Save" }).click(); + await dialog.getByRole("button", { name: "Save" }).click(); + await expect(dialog).not.toBeVisible(); await expect(page.getByText("Renamed Site")).toBeVisible(); }); -test("site detail Edit button opens edit form", async ({ page, testData }) => { +test("site detail Edit button opens edit dialog", async ({ page, testData }) => { await page.goto(`/sites/${testData.siteId}`); - await page.getByTitle("Edit site").click(); + await page.getByRole("button", { name: "Edit" }).click(); - // Should show the inline edit form with name and URL fields - await expect(page.locator('input[name="name"]')).toBeVisible(); - await expect(page.locator('input[name="url"]')).toBeVisible(); - await expect(page.getByRole("button", { name: "Save" })).toBeVisible(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + await expect(dialog.getByLabel("Name")).toBeVisible(); + await expect(dialog.getByLabel("URL")).toBeVisible(); + await expect(dialog.getByRole("button", { name: "Save" })).toBeVisible(); }); -test("deletes a site", async ({ page, adminClient }) => { +test("deletes a site with confirmation", async ({ page, adminClient }) => { // First create a site to delete await adminClient .from("sites") @@ -90,8 +110,16 @@ test("deletes a site", async ({ page, adminClient }) => { await siteCard.hover(); await siteCard.getByTitle("Edit site").click({ force: true }); - // Click delete - await page.getByRole("button", { name: "Delete" }).click(); + const dialog = page.getByRole("dialog"); + await expect(dialog).toBeVisible(); + + // Click Delete — should show confirmation + await dialog.getByRole("button", { name: "Delete" }).click(); + await expect(dialog.getByText("Delete?")).toBeVisible(); + + // Confirm deletion + await dialog.getByRole("button", { name: "Yes" }).click(); + await expect(dialog).not.toBeVisible(); await expect(page.getByText("Site To Delete")).not.toBeVisible(); }); From e213b479017ef9e9a498071ba934b67f9275b4e9 Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 14:29:50 -0500 Subject: [PATCH 6/8] fix: use unique ids in SiteFormDialog, fix e2e test selectors - useId() for label/input associations to avoid id collisions - Use getByRole('button', { name: 'Add site' }) to avoid ambiguity with dialog title Co-Authored-By: Claude Opus 4.6 --- e2e/sites-crud.spec.ts | 10 +++++----- src/components/SiteFormDialog.tsx | 11 ++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/e2e/sites-crud.spec.ts b/e2e/sites-crud.spec.ts index 19c0b48..9035f3e 100644 --- a/e2e/sites-crud.spec.ts +++ b/e2e/sites-crud.spec.ts @@ -23,12 +23,12 @@ test.afterEach(async ({ adminClient }) => { test("shows Add site card when authenticated", async ({ page }) => { await page.goto("/"); - await expect(page.getByText("Add site")).toBeVisible(); + await expect(page.getByRole("button", { name: "Add site" })).toBeVisible(); }); test("adds a new site", async ({ page }) => { await page.goto("/"); - await page.getByText("Add site").click(); + await page.getByRole("button", { name: "Add site" }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); @@ -44,19 +44,19 @@ test("adds a new site", async ({ page }) => { test("cancels adding a site", async ({ page }) => { await page.goto("/"); - await page.getByText("Add site").click(); + await page.getByRole("button", { name: "Add site" }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); await dialog.getByRole("button", { name: "Cancel" }).click(); await expect(dialog).not.toBeVisible(); - await expect(page.getByText("Add site")).toBeVisible(); + await expect(page.getByRole("button", { name: "Add site" })).toBeVisible(); }); test("dismisses dialog with Escape key", async ({ page }) => { await page.goto("/"); - await page.getByText("Add site").click(); + await page.getByRole("button", { name: "Add site" }).click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible(); diff --git a/src/components/SiteFormDialog.tsx b/src/components/SiteFormDialog.tsx index 3fd7fd1..d752953 100644 --- a/src/components/SiteFormDialog.tsx +++ b/src/components/SiteFormDialog.tsx @@ -1,6 +1,6 @@ "use client" -import { useRef, useState } from "react" +import { useId, useRef, useState } from "react" import { addSite, editSite, deleteSite } from "@/app/sites/actions" export default function SiteFormDialog({ @@ -16,6 +16,7 @@ export default function SiteFormDialog({ url?: string trigger: React.ReactNode }) { + const id = useId() const dialogRef = useRef(null) const [confirmingDelete, setConfirmingDelete] = useState(false) @@ -71,14 +72,14 @@ export default function SiteFormDialog({ >
Date: Sat, 21 Feb 2026 00:14:55 -0500 Subject: [PATCH 7/8] Trigger redeploy with Preview env vars Co-Authored-By: Claude Opus 4.6 From f6c9eaf3377093d397f24b2c26739e003722d3ba Mon Sep 17 00:00:00 2001 From: Jason Crawford Date: Fri, 20 Feb 2026 21:34:02 -0800 Subject: [PATCH 8/8] fix: center dialog and improve Edit button contrast Co-Authored-By: Claude Opus 4.6 --- src/app/sites/[id]/page.tsx | 5 ++--- src/components/SiteFormDialog.tsx | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/sites/[id]/page.tsx b/src/app/sites/[id]/page.tsx index d9c50c9..833f938 100644 --- a/src/app/sites/[id]/page.tsx +++ b/src/app/sites/[id]/page.tsx @@ -102,9 +102,8 @@ export default async function SiteDetailPage({