this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add dashboard table view

+503 -75
+19 -72
app/(creator)/dashboard/page.tsx
··· 1 - import Link from "next/link"; 2 - import { ArrowRight, Plus } from "lucide-react"; 1 + import { Plus } from "lucide-react"; 3 2 3 + import { createFormAction } from "@/app/(creator)/actions"; 4 + import { DashboardFormBrowser } from "@/components/dashboard-form-browser"; 4 5 import { EmptyState } from "@/components/empty-state"; 5 - import { Badge } from "@/components/ui/badge"; 6 6 import { Button } from "@/components/ui/button"; 7 - import { Card } from "@/components/ui/card"; 8 7 import { getServerAuthSession } from "@/lib/auth"; 9 8 import { listFormsForOwner } from "@/lib/forms"; 10 - import { formatDate } from "@/lib/utils"; 11 - import { createFormAction } from "@/app/(creator)/actions"; 12 9 13 10 export default async function DashboardPage() { 14 11 const session = await getServerAuthSession(); 15 12 const forms = await listFormsForOwner(session!.user.id); 16 13 17 - return ( 14 + return forms.length === 0 ? ( 18 15 <div className="space-y-8"> 19 16 <section className="flex flex-col gap-4 border-b border-black/8 pb-6 lg:flex-row lg:items-end lg:justify-between"> 20 17 <div className="space-y-2"> ··· 32 29 </form> 33 30 </section> 34 31 35 - {forms.length === 0 ? ( 36 - <EmptyState 37 - eyebrow="No forms yet" 38 - title="Create your first form" 39 - description="New forms start as drafts so you can edit before publishing." 40 - action={ 41 - <form action={createFormAction}> 42 - <Button type="submit"> 43 - <Plus className="size-4" /> 44 - New form 45 - </Button> 46 - </form> 47 - } 48 - /> 49 - ) : ( 50 - <section className="grid gap-5 lg:grid-cols-2"> 51 - {forms.map((form) => ( 52 - <Card key={form.id} className="p-6"> 53 - <div className="flex items-start justify-between gap-4"> 54 - <div> 55 - <Badge className={form.status === "PUBLISHED" ? "rounded-full bg-[var(--accent-soft)] text-[#2f5d35]" : "rounded-full bg-black/5"}> 56 - {form.status === "PUBLISHED" ? "Published" : "Draft"} 57 - </Badge> 58 - <h2 className="mt-4 font-display text-3xl text-[var(--ink)]">{form.title}</h2> 59 - <p className="mt-3 max-w-xl text-sm leading-6 text-[var(--muted)]"> 60 - {form.description || "No description yet."} 61 - </p> 62 - </div> 63 - <Link href={`/forms/${form.id}/edit`}> 64 - <Button variant="secondary" size="sm"> 65 - Edit 66 - </Button> 67 - </Link> 68 - </div> 69 - 70 - <div className="mt-6 grid gap-3 text-sm text-[var(--muted)] sm:grid-cols-3"> 71 - <div> 72 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Share URL</p> 73 - <p className="mt-2 truncate">/{form.slug}</p> 74 - </div> 75 - <div> 76 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Responses</p> 77 - <p className="mt-2">{form.responseCount}</p> 78 - </div> 79 - <div> 80 - <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Updated</p> 81 - <p className="mt-2">{formatDate(form.updatedAt)}</p> 82 - </div> 83 - </div> 84 - 85 - <div className="mt-6 flex flex-wrap gap-3"> 86 - <Link href={`/forms/${form.id}/edit`}> 87 - <Button> 88 - Open builder 89 - <ArrowRight className="size-4" /> 90 - </Button> 91 - </Link> 92 - <Link href={`/forms/${form.id}/responses`}> 93 - <Button variant="secondary">View responses</Button> 94 - </Link> 95 - </div> 96 - </Card> 97 - ))} 98 - </section> 99 - )} 32 + <EmptyState 33 + eyebrow="No forms yet" 34 + title="Create your first form" 35 + description="New forms start as drafts so you can edit before publishing." 36 + action={ 37 + <form action={createFormAction}> 38 + <Button type="submit"> 39 + <Plus className="size-4" /> 40 + New form 41 + </Button> 42 + </form> 43 + } 44 + /> 100 45 </div> 46 + ) : ( 47 + <DashboardFormBrowser forms={forms} /> 101 48 ); 102 49 }
+3 -3
app/page.tsx
··· 11 11 const session = await getServerAuthSession(); 12 12 13 13 return ( 14 - <main className="mx-auto flex min-h-screen w-full max-w-4xl flex-col px-6 py-8 lg:px-10 lg:py-10"> 15 - <section className="flex flex-1 items-center py-16 lg:py-24"> 16 - <Card className="w-full p-8 lg:p-10"> 14 + <main className="mx-auto flex min-h-screen w-full max-w-3xl flex-col px-6 py-8 lg:px-10 lg:py-10"> 15 + <section className="flex flex-1 items-center justify-center py-16 lg:py-24"> 16 + <Card className="w-full max-w-3xl p-8 lg:p-10"> 17 17 <div className="max-w-2xl"> 18 18 <div className="flex items-center gap-4"> 19 19 <Image src="/sproute.png" alt="Lively Forms" width={72} height={72} priority className="size-18" />
+259
components/dashboard-form-browser.tsx
··· 1 + "use client"; 2 + 3 + import Link from "next/link"; 4 + import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 + import { ArrowDown, ArrowRight, ArrowUp, LayoutGrid, Plus, TableProperties } from "lucide-react"; 6 + import { useEffect, useMemo, useState } from "react"; 7 + 8 + import { createFormAction } from "@/app/(creator)/actions"; 9 + import { Badge } from "@/components/ui/badge"; 10 + import { Button } from "@/components/ui/button"; 11 + import { Card } from "@/components/ui/card"; 12 + import type { FormListItem } from "@/lib/forms"; 13 + import { cn, formatDate } from "@/lib/utils"; 14 + 15 + type DashboardView = "grid" | "table"; 16 + type SortKey = "title" | "status" | "responseCount" | "updatedAt"; 17 + type SortDirection = "asc" | "desc"; 18 + 19 + const statusOrder = { 20 + DRAFT: 0, 21 + PUBLISHED: 1, 22 + } as const; 23 + 24 + function compareText(left: string, right: string) { 25 + return left.localeCompare(right, undefined, { sensitivity: "base" }); 26 + } 27 + 28 + export function DashboardFormBrowser({ forms }: { forms: FormListItem[] }) { 29 + const router = useRouter(); 30 + const pathname = usePathname(); 31 + const searchParams = useSearchParams(); 32 + const initialView = searchParams.get("view") === "table" ? "table" : "grid"; 33 + 34 + const [view, setView] = useState<DashboardView>(initialView); 35 + const [sortKey, setSortKey] = useState<SortKey>("updatedAt"); 36 + const [sortDirection, setSortDirection] = useState<SortDirection>("desc"); 37 + 38 + useEffect(() => { 39 + setView(searchParams.get("view") === "table" ? "table" : "grid"); 40 + }, [searchParams]); 41 + 42 + function setDashboardView(nextView: DashboardView) { 43 + setView(nextView); 44 + 45 + const nextParams = new URLSearchParams(searchParams.toString()); 46 + 47 + if (nextView === "grid") { 48 + nextParams.delete("view"); 49 + } else { 50 + nextParams.set("view", nextView); 51 + } 52 + 53 + const nextQuery = nextParams.toString(); 54 + router.replace(nextQuery ? `${pathname}?${nextQuery}` : pathname, { scroll: false }); 55 + } 56 + 57 + const sortedForms = useMemo(() => { 58 + const next = [...forms]; 59 + 60 + next.sort((left, right) => { 61 + let comparison = 0; 62 + 63 + switch (sortKey) { 64 + case "title": 65 + comparison = compareText(left.title, right.title); 66 + break; 67 + case "status": 68 + comparison = statusOrder[left.status] - statusOrder[right.status]; 69 + break; 70 + case "responseCount": 71 + comparison = left.responseCount - right.responseCount; 72 + break; 73 + case "updatedAt": 74 + comparison = new Date(left.updatedAt).getTime() - new Date(right.updatedAt).getTime(); 75 + break; 76 + } 77 + 78 + if (comparison === 0) { 79 + comparison = compareText(left.title, right.title); 80 + } 81 + 82 + return sortDirection === "asc" ? comparison : -comparison; 83 + }); 84 + 85 + return next; 86 + }, [forms, sortDirection, sortKey]); 87 + 88 + function handleSort(nextKey: SortKey) { 89 + if (sortKey === nextKey) { 90 + setSortDirection((current) => (current === "asc" ? "desc" : "asc")); 91 + return; 92 + } 93 + 94 + setSortKey(nextKey); 95 + setSortDirection(nextKey === "updatedAt" || nextKey === "responseCount" ? "desc" : "asc"); 96 + } 97 + 98 + function renderSortIcon(key: SortKey) { 99 + if (sortKey !== key) { 100 + return <ArrowDown className="size-3.5 opacity-35" />; 101 + } 102 + 103 + return sortDirection === "asc" ? <ArrowUp className="size-3.5" /> : <ArrowDown className="size-3.5" />; 104 + } 105 + 106 + return ( 107 + <div className="space-y-8"> 108 + <section className="flex flex-col gap-4 border-b border-black/8 pb-6 lg:flex-row lg:items-end lg:justify-between"> 109 + <div className="space-y-2"> 110 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Dashboard</p> 111 + <h1 className="font-display text-4xl leading-tight text-[var(--ink)]">Your forms</h1> 112 + <p className="max-w-2xl text-sm leading-6 text-[var(--muted)]">Create a form, open a draft, or review responses.</p> 113 + </div> 114 + <div className="flex flex-col gap-3 sm:flex-row sm:items-center"> 115 + <div className="inline-flex h-12 items-center gap-1 rounded-xl border border-black/8 bg-white/70 p-1"> 116 + <button 117 + type="button" 118 + onClick={() => setDashboardView("grid")} 119 + className={cn( 120 + "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 121 + view === "grid" ? "bg-[var(--ink)] text-[var(--bg)]" : "text-[var(--muted)] hover:bg-black/5 hover:text-[var(--ink)]", 122 + )} 123 + aria-pressed={view === "grid"} 124 + > 125 + <LayoutGrid className="size-4" /> 126 + Grid 127 + </button> 128 + <button 129 + type="button" 130 + onClick={() => setDashboardView("table")} 131 + className={cn( 132 + "inline-flex h-full items-center gap-2 rounded-lg px-3 text-sm font-medium transition", 133 + view === "table" ? "bg-[var(--ink)] text-[var(--bg)]" : "text-[var(--muted)] hover:bg-black/5 hover:text-[var(--ink)]", 134 + )} 135 + aria-pressed={view === "table"} 136 + > 137 + <TableProperties className="size-4" /> 138 + Table 139 + </button> 140 + </div> 141 + <form action={createFormAction}> 142 + <Button size="lg" type="submit"> 143 + <Plus className="size-4" /> 144 + New form 145 + </Button> 146 + </form> 147 + </div> 148 + </section> 149 + 150 + {view === "grid" ? ( 151 + <section className="grid gap-5 lg:grid-cols-2"> 152 + {sortedForms.map((form) => ( 153 + <Card key={form.id} className="p-6"> 154 + <div className="flex items-start justify-between gap-4"> 155 + <div> 156 + <div className="flex flex-wrap items-center gap-3"> 157 + <h2 className="font-display text-3xl text-[var(--ink)]">{form.title}</h2> 158 + <Badge className={form.status === "PUBLISHED" ? "rounded-full bg-[var(--accent-soft)] text-[#2f5d35]" : "rounded-full bg-black/5"}> 159 + {form.status === "PUBLISHED" ? "Published" : "Draft"} 160 + </Badge> 161 + </div> 162 + <p className="mt-3 max-w-xl text-sm leading-6 text-[var(--muted)]">{form.description || "No description yet."}</p> 163 + </div> 164 + <Link href={`/forms/${form.id}/edit`}> 165 + <Button variant="secondary" size="sm"> 166 + Edit 167 + </Button> 168 + </Link> 169 + </div> 170 + 171 + <div className="mt-6 grid gap-3 text-sm text-[var(--muted)] sm:grid-cols-3"> 172 + <div> 173 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Share URL</p> 174 + <p className="mt-2 truncate">/{form.slug}</p> 175 + </div> 176 + <div> 177 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Responses</p> 178 + <p className="mt-2">{form.responseCount}</p> 179 + </div> 180 + <div> 181 + <p className="text-xs font-semibold uppercase tracking-[0.24em] text-[var(--accent)]">Updated</p> 182 + <p className="mt-2">{formatDate(form.updatedAt)}</p> 183 + </div> 184 + </div> 185 + 186 + <div className="mt-6 flex flex-wrap gap-3"> 187 + <Link href={`/forms/${form.id}/edit`}> 188 + <Button> 189 + Open builder 190 + <ArrowRight className="size-4" /> 191 + </Button> 192 + </Link> 193 + <Link href={`/forms/${form.id}/responses`}> 194 + <Button variant="secondary">View responses</Button> 195 + </Link> 196 + </div> 197 + </Card> 198 + ))} 199 + </section> 200 + ) : ( 201 + <Card className="overflow-hidden p-0"> 202 + <div className="overflow-x-auto"> 203 + <table className="min-w-full divide-y divide-black/8 text-left"> 204 + <thead className="bg-[#e6e5dd] border-b border-black/8 shadow-[inset_0_-1px_0_rgba(23,23,23,0.06)]"> 205 + <tr className="text-xs uppercase tracking-[0.2em] text-[var(--muted)]"> 206 + <th className="px-5 py-4"> 207 + <button type="button" onClick={() => handleSort("title")} className="inline-flex items-center gap-2 font-semibold transition hover:text-[var(--ink)]"> 208 + Title 209 + {renderSortIcon("title")} 210 + </button> 211 + </th> 212 + <th className="px-5 py-4"> 213 + <button type="button" onClick={() => handleSort("status")} className="inline-flex items-center gap-2 font-semibold transition hover:text-[var(--ink)]"> 214 + Status 215 + {renderSortIcon("status")} 216 + </button> 217 + </th> 218 + <th className="px-5 py-4"> 219 + <button type="button" onClick={() => handleSort("responseCount")} className="inline-flex items-center gap-2 font-semibold transition hover:text-[var(--ink)]"> 220 + Responses 221 + {renderSortIcon("responseCount")} 222 + </button> 223 + </th> 224 + <th className="px-5 py-4"> 225 + <button type="button" onClick={() => handleSort("updatedAt")} className="inline-flex items-center gap-2 font-semibold transition hover:text-[var(--ink)]"> 226 + Updated 227 + {renderSortIcon("updatedAt")} 228 + </button> 229 + </th> 230 + </tr> 231 + </thead> 232 + <tbody className="divide-y divide-black/8 bg-white/70 text-sm text-[var(--ink)]"> 233 + {sortedForms.map((form) => ( 234 + <tr key={form.id} className="align-top"> 235 + <td className="px-5 py-4"> 236 + <div className="min-w-[220px]"> 237 + <Link href={`/forms/${form.id}/edit`} className="font-medium text-[var(--ink)] transition hover:text-[var(--accent)]"> 238 + {form.title} 239 + </Link> 240 + <p className="mt-1 truncate text-sm text-[var(--muted)]">{form.description || "No description yet."}</p> 241 + </div> 242 + </td> 243 + <td className="px-5 py-4"> 244 + <Badge className={form.status === "PUBLISHED" ? "rounded-full bg-[var(--accent-soft)] text-[#2f5d35]" : "rounded-full bg-black/5"}> 245 + {form.status === "PUBLISHED" ? "Published" : "Draft"} 246 + </Badge> 247 + </td> 248 + <td className="px-5 py-4 text-[var(--muted)]">{form.responseCount}</td> 249 + <td className="px-5 py-4 text-[var(--muted)]">{formatDate(form.updatedAt)}</td> 250 + </tr> 251 + ))} 252 + </tbody> 253 + </table> 254 + </div> 255 + </Card> 256 + )} 257 + </div> 258 + ); 259 + }
+2
openspec/changes/archive/2026-04-08-dashboard-form-views/.openspec.yaml
··· 1 + schema: spec-driven 2 + created: 2026-04-08
+85
openspec/changes/archive/2026-04-08-dashboard-form-views/design.md
··· 1 + ## Context 2 + 3 + The creator dashboard currently renders forms in one card-based grid. That layout works well for visual browsing, but it is less efficient for creators with many forms who want denser scanning and quick comparison of status, response counts, and recency. 4 + 5 + The requested change is purely within the creator dashboard surface. The dashboard already receives the full form list needed for the table columns, so the change can be implemented without new backend endpoints if sorting and view switching are handled in the dashboard UI layer. 6 + 7 + ## Goals / Non-Goals 8 + 9 + **Goals:** 10 + - Support two dashboard list presentations: grid and table. 11 + - Make the existing card layout explicitly the grid view. 12 + - Allow creators to switch between grid and table views from the dashboard. 13 + - Allow sorting in table view by title, status, response count, and updated date. 14 + - Preserve existing core actions such as creating a form, opening the builder, and reviewing responses. 15 + 16 + **Non-Goals:** 17 + - Persisting the selected view across devices or sessions. 18 + - Server-side sorting or pagination. 19 + - Bulk actions across forms. 20 + - Replacing the existing grid layout. 21 + 22 + ## Decisions 23 + 24 + ### Implement view switching in the dashboard UI 25 + The dashboard should offer a simple grid/table toggle in the existing management header area. 26 + 27 + **Why this approach:** 28 + - Keeps the interaction local to the dashboard where it is used. 29 + - Avoids introducing unnecessary routing complexity for a presentational preference. 30 + - Makes it easy to preserve the existing grid view as the default. 31 + 32 + **Alternatives considered:** 33 + - Separate routes for grid and table views: adds URL complexity for a lightweight display preference. 34 + - Replace grid with table entirely: does not satisfy the request to keep both views. 35 + 36 + ### Use client-side sorting for table mode 37 + Sorting should be handled in the dashboard presentation layer using the already loaded form list. 38 + 39 + **Why this approach:** 40 + - Existing dashboard data already includes the needed fields. 41 + - Sorting is a view concern rather than a data-ownership concern. 42 + - Avoids new API parameters or server coordination for the initial feature. 43 + 44 + **Alternatives considered:** 45 + - Server-side sorting via query params: more scalable for very large datasets, but unnecessary for the current scope. 46 + 47 + ### Keep grid as the initial/default view 48 + The current dashboard experience should remain the starting point and simply be named grid. 49 + 50 + **Why this approach:** 51 + - Preserves familiarity for existing users. 52 + - Minimizes product surprise while still enabling the denser table workflow. 53 + 54 + **Alternatives considered:** 55 + - Default to table for everyone: improves density but changes the current experience abruptly. 56 + 57 + ### Reuse existing form actions in both views 58 + The table view should still let creators move directly into common workflows, especially opening the builder and viewing responses. 59 + 60 + **Why this approach:** 61 + - Prevents the table from becoming a read-only report. 62 + - Maintains parity between views so changing the layout does not hide essential actions. 63 + 64 + **Alternatives considered:** 65 + - Make the table informational only: reduces usefulness and forces creators back to grid for actions. 66 + 67 + ## Risks / Trade-offs 68 + 69 + - **Client-side sorting may be less scalable for very large form lists** → Mitigation: acceptable for current scope; revisit with server-side sorting if dataset size grows. 70 + - **Adding toggle and sorting controls can clutter the dashboard header** → Mitigation: keep controls compact and aligned with the current management-focused design. 71 + - **Two views can drift visually or functionally over time** → Mitigation: ensure both use the same underlying form data and expose the same critical actions. 72 + 73 + ## Migration Plan 74 + 75 + 1. Introduce dashboard view and sorting state in the dashboard UI. 76 + 2. Extract or add grid and table presentations over the same form list data. 77 + 3. Add sortable table headers for title, status, response count, and updated date. 78 + 4. Verify creators can switch views and still reach builder/response actions. 79 + 5. Confirm the default dashboard experience remains the current grid layout. 80 + 81 + ## Open Questions 82 + 83 + - Should the selected view be remembered in local storage or left as a per-page-session preference for now? 84 + - Should clicking a table row open the builder, or should actions remain button/link-based only? 85 + - Should status sorting use raw enum order or a product-specific order such as published before draft?
+26
openspec/changes/archive/2026-04-08-dashboard-form-views/proposal.md
··· 1 + ## Why 2 + 3 + The dashboard currently presents forms in a single card grid view. That works for browsing a small set of forms, but it becomes less efficient when creators want to scan many forms quickly, compare status and response counts, or sort by updated date. 4 + 5 + ## What Changes 6 + 7 + - Rename the existing dashboard layout to the "grid" view. 8 + - Add a second dashboard layout called "table". 9 + - Allow creators to switch between grid and table views from the dashboard. 10 + - Show table columns for form title, status, response count, and updated date. 11 + - Allow sorting in the table view by the available columns. 12 + - Preserve the existing management actions for opening the builder and reviewing responses. 13 + 14 + ## Capabilities 15 + 16 + ### New Capabilities 17 + - `dashboard-form-views`: creator dashboard supports switchable grid and table views for forms, with sortable columns in table mode. 18 + 19 + ### Modified Capabilities 20 + - None. 21 + 22 + ## Impact 23 + 24 + - Affected specs: `dashboard-form-views` 25 + - Affected code: creator dashboard page, form list presentation components, dashboard sorting/view state handling 26 + - Affected APIs: none required if sorting is implemented from existing server data in the dashboard page
+46
openspec/changes/archive/2026-04-08-dashboard-form-views/specs/dashboard-form-views/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Creator can switch between dashboard form views 4 + The system SHALL allow an authenticated creator to switch the dashboard form list between a grid view and a table view. 5 + 6 + #### Scenario: Creator opens the dashboard 7 + - **WHEN** an authenticated creator opens the dashboard with existing forms 8 + - **THEN** the system shows the forms in the default grid view and offers a control to switch to table view 9 + 10 + #### Scenario: Creator switches to table view 11 + - **WHEN** an authenticated creator selects the table view option on the dashboard 12 + - **THEN** the system displays the same forms in a table layout 13 + 14 + #### Scenario: Creator switches back to grid view 15 + - **WHEN** an authenticated creator selects the grid view option on the dashboard after using table view 16 + - **THEN** the system displays the forms in the grid layout again 17 + 18 + ### Requirement: Table view shows sortable form management columns 19 + The system SHALL present table columns for form title, status, response count, and updated date, and SHALL allow the creator to sort the table by each of those columns. 20 + 21 + #### Scenario: Creator views form data in table mode 22 + - **WHEN** an authenticated creator opens the dashboard table view 23 + - **THEN** the system shows each form with title, status, response count, and updated date columns 24 + 25 + #### Scenario: Creator sorts by updated date 26 + - **WHEN** an authenticated creator selects the updated date column in table view 27 + - **THEN** the system reorders the visible forms by updated date 28 + 29 + #### Scenario: Creator sorts by responses 30 + - **WHEN** an authenticated creator selects the response count column in table view 31 + - **THEN** the system reorders the visible forms by response count 32 + 33 + #### Scenario: Creator sorts by title or status 34 + - **WHEN** an authenticated creator selects the title or status column in table view 35 + - **THEN** the system reorders the visible forms by the selected column 36 + 37 + ### Requirement: Dashboard actions remain available in each view 38 + The system SHALL keep core dashboard management actions available in both grid and table views. 39 + 40 + #### Scenario: Creator opens a form from grid view 41 + - **WHEN** an authenticated creator uses a dashboard action in grid view 42 + - **THEN** the system allows direct navigation to the builder or responses for that form 43 + 44 + #### Scenario: Creator opens a form from table view 45 + - **WHEN** an authenticated creator uses a dashboard action in table view 46 + - **THEN** the system allows direct navigation to the builder or responses for that form
+17
openspec/changes/archive/2026-04-08-dashboard-form-views/tasks.md
··· 1 + ## 1. Dashboard view state and data preparation 2 + 3 + - [x] 1.1 Add dashboard UI state for switching between grid and table views 4 + - [x] 1.2 Add dashboard sorting state and derived sorted form data for title, status, response count, and updated date 5 + - [x] 1.3 Keep the current card layout as the default grid view over the shared form data 6 + 7 + ## 2. Table view implementation 8 + 9 + - [x] 2.1 Implement a dashboard table layout with columns for title, status, response count, and updated date 10 + - [x] 2.2 Add sortable controls to each supported table column 11 + - [x] 2.3 Expose core form actions in the table view so creators can open the builder and responses from each row 12 + 13 + ## 3. Dashboard polish and verification 14 + 15 + - [x] 3.1 Add compact view-switching controls that fit the existing dashboard management header 16 + - [x] 3.2 Verify empty-state and existing grid behavior remain intact while table view works for populated dashboards 17 + - [x] 3.3 Run build and relevant checks to confirm the dashboard view toggle and sorting work correctly
+46
openspec/specs/dashboard-form-views/spec.md
··· 1 + ## ADDED Requirements 2 + 3 + ### Requirement: Creator can switch between dashboard form views 4 + The system SHALL allow an authenticated creator to switch the dashboard form list between a grid view and a table view. 5 + 6 + #### Scenario: Creator opens the dashboard 7 + - **WHEN** an authenticated creator opens the dashboard with existing forms 8 + - **THEN** the system shows the forms in the default grid view and offers a control to switch to table view 9 + 10 + #### Scenario: Creator switches to table view 11 + - **WHEN** an authenticated creator selects the table view option on the dashboard 12 + - **THEN** the system displays the same forms in a table layout 13 + 14 + #### Scenario: Creator switches back to grid view 15 + - **WHEN** an authenticated creator selects the grid view option on the dashboard after using table view 16 + - **THEN** the system displays the forms in the grid layout again 17 + 18 + ### Requirement: Table view shows sortable form management columns 19 + The system SHALL present table columns for form title, status, response count, and updated date, and SHALL allow the creator to sort the table by each of those columns. 20 + 21 + #### Scenario: Creator views form data in table mode 22 + - **WHEN** an authenticated creator opens the dashboard table view 23 + - **THEN** the system shows each form with title, status, response count, and updated date columns 24 + 25 + #### Scenario: Creator sorts by updated date 26 + - **WHEN** an authenticated creator selects the updated date column in table view 27 + - **THEN** the system reorders the visible forms by updated date 28 + 29 + #### Scenario: Creator sorts by responses 30 + - **WHEN** an authenticated creator selects the response count column in table view 31 + - **THEN** the system reorders the visible forms by response count 32 + 33 + #### Scenario: Creator sorts by title or status 34 + - **WHEN** an authenticated creator selects the title or status column in table view 35 + - **THEN** the system reorders the visible forms by the selected column 36 + 37 + ### Requirement: Dashboard actions remain available in each view 38 + The system SHALL keep core dashboard management actions available in both grid and table views. 39 + 40 + #### Scenario: Creator opens a form from grid view 41 + - **WHEN** an authenticated creator uses a dashboard action in grid view 42 + - **THEN** the system allows direct navigation to the builder or responses for that form 43 + 44 + #### Scenario: Creator opens a form from table view 45 + - **WHEN** an authenticated creator uses a dashboard action in table view 46 + - **THEN** the system allows direct navigation to the builder or responses for that form