xtablo-source/.planning/phases/16-tablo-detail/16-04-PLAN.md
Arthur Belleville 965ec5e5ce
docs(16): create phase 16 tablo detail plan — 4 plans, 4 waves
Phase 16 delivers DETAIL-01/02/03/04: header restyling, kanban
tasks-section layout with server-side etape grouping, and files
table component. Ends with a browser verify checkpoint.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-16 23:20:49 +02:00

16 KiB
Raw Blame History

phase plan type wave depends_on files_modified autonomous requirements must_haves
16-tablo-detail 04 execute 4
16-03
backend/templates/files.templ
false
DETAIL-04
truths artifacts key_links
Files section uses .overview-section / .overview-section-heading layout with h3 'Files' on left and Upload file button on right
File list uses @ui.Table with Filename / Size / Uploaded / Actions columns
Each file row renders as <tr class='file-row-zone'> with Download and Delete @ui.IconButton actions
FileDeleteConfirmFragment outer element is <tr class='file-row-zone'> (matching element type for outerHTML swap)
Empty state uses @ui.EmptyState with Title 'No files yet' and Description 'Upload your first file to get started.'
All 9 existing file handler tests pass unchanged
Browser walkthrough confirms: files tab shows correct table layout, delete confirm swap works, empty state renders correctly
path provides contains
backend/templates/files.templ Restyled FilesTabFragment, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr @ui.Table(ui.TableProps{
path provides exports
backend/templates/files_templ.go Generated Go from restyled files.templ
from to via pattern
backend/templates/files.templ FileListRow backend/templates/files.templ FileDeleteConfirmFragment hx-target='closest .file-row-zone' hx-swap='outerHTML' class="file-row-zone"
from to via pattern
backend/templates/files.templ FilesTabFragment backend/internal/web/ui/table.templ @ui.Table @ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(...)}) ui.Table
Restyle the files section (DETAIL-04): replace raw `
    `/`
  • ` layout with `@ui.Table`, add `.overview-section-heading` header, introduce `@ui.EmptyState`, and convert `FileListRow` and `FileDeleteConfirmFragment` to use `` as the outer element so the existing HTMX outerHTML delete swap continues to work. Ends with a browser verification checkpoint.

    Purpose: Deliver DETAIL-04 — files section uses the table component with consistent row actions. Output: Restyled files.templ; all 9 file tests pass; browser checkpoint approves files tab visual result.

    <execution_context> @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md @/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md </execution_context>

    @.planning/ROADMAP.md @.planning/phases/16-tablo-detail/16-CONTEXT.md @.planning/phases/16-tablo-detail/16-RESEARCH.md @.planning/phases/16-tablo-detail/16-PATTERNS.md @.planning/phases/16-tablo-detail/16-UI-SPEC.md @.planning/phases/16-tablo-detail/16-03-SUMMARY.md

    From backend/internal/web/ui/table.templ (verified in RESEARCH.md): type TableProps struct { Head templ.Component Body templ.Component } Usage: @ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(tabloID, files)})

    From backend/internal/web/ui/empty_state.templ (verified): @ui.EmptyState(ui.EmptyStateProps{Title: "No files yet", Description: "Upload your first file to get started."})

    From backend/internal/web/ui/variants.go: ui.ButtonVariantDefault, ui.ButtonToneSolid, ui.SizeMD ui.IconButtonVariantNeutral, ui.IconButtonVariantDanger, ui.IconButtonToneGhost

    Current files.templ structure (must read file to confirm): FilesTabFragment: renders

      with FileListRow
    • items or FileListEmpty() FileListRow:
    • — must become FileDeleteConfirmFragment:
      — must become FileListEmpty: standalone empty state component — no longer called; @ui.EmptyState used instead FileUploadForm: existing form component — triggered from new "Upload file" button via HTMX

      HTMX delete flow (Pitfall 2 — CRITICAL):

      • FileListRow renders inside @ui.Table's
      • Trash IconButton: hx-get="/tablos/{tabloID}/files/{fileID}/delete-confirm" hx-target="closest .file-row-zone" hx-swap="outerHTML"
      • FileDeleteConfirmFragment must render (same element type for outerHTML swap)
      • Both use colspan="4" in the confirm to span all 4 columns

      Files table columns: Filename / Size / Uploaded / Actions (4 columns)

      formatBytes helper: check if it already exists in files.templ or files_helpers.go (grep for formatBytes); if not in scope, use a simple inline format (file.SizeBytes/1024 + "KB" or similar)

      File upload button triggers existing FileUploadForm via HTMX to a #file-upload-slot: "Upload file" button → hx-get="/tablos/{tabloID}/files/upload-form" hx-target="#file-upload-slot" hx-swap="innerHTML" div id="file-upload-slot" (empty initially, receives the form fragment)

      Download icon button: wrap in tag (download is a navigation, not a form POST): @ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})

      Task 1: Restyle FilesTabFragment, FileListRow, and FileDeleteConfirmFragment backend/templates/files.templ - backend/templates/files.templ (read the FULL file before editing — check FilesTabFragment, FileListRow, FileDeleteConfirmFragment, FileListEmpty structures; find formatBytes helper location; confirm all HTMX attributes on existing delete confirm flow) - backend/internal/web/ui/table.templ (read to confirm TableProps signature and how Head/Body components are rendered — specifically whether @ui.Table renders a full or just the body; affects how fileTableHead and fileTableBody must be structured) - backend/internal/web/ui/empty_state.templ (read to confirm EmptyStateProps fields) - .planning/phases/16-tablo-detail/16-PATTERNS.md (section "backend/templates/files.templ" — FilesTabFragment new structure, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr) Edit `backend/templates/files.templ` to make the following changes. Read the file before starting.
      1. RESTYLE FilesTabFragment: replace the current body with:
         - Outer: `div class="overview-section"`
         - Header: `div class="overview-section-heading"` containing h3 "Files" on the left and `@ui.Button(ui.ButtonProps{Label: "Upload file", Variant: ui.ButtonVariantDefault, Tone: ui.ButtonToneSolid, Size: ui.SizeMD, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tablo.ID.String()}/files/upload-form", "hx-target": "#file-upload-slot", "hx-swap": "innerHTML"}})` on the right
         - `div id="file-upload-slot"` (empty; receives FileUploadForm on button click)
         - Conditional: if len(files) == 0 → `@ui.EmptyState(ui.EmptyStateProps{Title: "No files yet", Description: "Upload your first file to get started."})` else → `@ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(tablo.ID, files)})`
      
      2. ADD fileTableHead private templ function (lowercase — not exported):
         `templ fileTableHead()` rendering `<tr><th>Filename</th><th>Size</th><th>Uploaded</th><th>Actions</th></tr>`
      
      3. ADD fileTableBody private templ function:
         `templ fileTableBody(tabloID uuid.UUID, files []sqlc.TabloFile)` rendering `for _, f := range files { @FileListRow(tabloID, f) }`
      
      4. RESTYLE FileListRow: change the outer element from `<li class="file-row-zone" id="file-{id}">` to `<tr class="file-row-zone" id="file-{id}">`. Inside the `<tr>`:
         - `<td>{ file.Filename }</td>`
         - `<td>{ formatBytes(file.SizeBytes) }</td>` (use existing formatBytes helper; if not found, format as strconv.FormatInt(file.SizeBytes/1024, 10) + " KB" inline)
         - `<td>if file.CreatedAt.Valid { file.CreatedAt.Time.Format("2006-01-02") }</td>`
         - `<td>` containing: `<a href="/tablos/{tabloID}/files/{fileID}/download" aria-label="Download {file.Filename}">@ui.IconButton(ui.IconButtonProps{Label: "Download file", Icon: "download", Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, Type: "button"})</a>` followed by `@ui.IconButton(ui.IconButtonProps{Label: "Delete file", Icon: "trash", Variant: ui.IconButtonVariantDanger, Tone: ui.IconButtonToneGhost, Type: "button", Attrs: templ.Attributes{"hx-get": "/tablos/{tabloID}/files/{fileID}/delete-confirm", "hx-target": "closest .file-row-zone", "hx-swap": "outerHTML"}})`
      
      5. RESTYLE FileDeleteConfirmFragment: change outer element from `<div class="file-row-zone" id="file-{id}">` to `<tr class="file-row-zone" id="file-{id}">`. Wrap existing confirm dialog content in a single `<td colspan="4">`. Preserve all HTMX confirm/cancel button attributes exactly — do not change the confirm POST URL or cancel reload URL.
      
      6. PRESERVE FileListEmpty function body unchanged (it is no longer called from FilesTabFragment but may be referenced elsewhere; keep it in the file).
      
      7. ADD `"github.com/google/uuid"` to the import block if not already present (needed for fileTableBody parameter type).
      
      After editing, run: `just generate` to regenerate `files_templ.go`.
      
      grep -c "@ui.Table" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "@ui.EmptyState" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "class=\"file-row-zone\"" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && grep -c "overview-section-heading" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/templates/files.templ && cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source && go build ./backend/... && go test ./backend/internal/web/... -run TestFilesTab -count=1 - `files.templ` contains `@ui.Table(ui.TableProps{` - `files.templ` contains `@ui.EmptyState(ui.EmptyStateProps{Title: "No files yet"` - `files.templ` contains `class="overview-section-heading"` - `files.templ` contains `class="file-row-zone"` on a `` element (not `
    • ` or `
      `) - `files.templ` FileDeleteConfirmFragment outer element is `
    • ` - `files.templ` contains `Icon: "download"` in FileListRow actions column - `go build ./backend/...` exits 0 - `go test ./backend/internal/web/... -run TestFilesTab -count=1` exits 0 (all file tests pass) - `go test ./backend/internal/web/... -count=1` exits 0 (no regressions) FilesTabFragment uses @ui.Table and @ui.EmptyState; FileListRow and FileDeleteConfirmFragment use tr; all file tests pass. Plans 0104 together deliver the complete Phase 16 restyling: - Plan 01: download + chat icons in UIIcon; CSS Sections 1925 in app.css - Plan 02: Tablo detail header (project-card-top), metadata row, tab nav (design token classes), desc in overview tab, EtapeStrip removed from TasksTabFragment - Plan 03: Kanban board with tasks-section layout, etape-grouped task rows, KanbanBoard/Column restyled, handlers updated - Plan 04 (just completed): Files section with @ui.Table, @ui.EmptyState, download + delete IconButtons Start the dev server: `cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && just dev` (or `air`)
      Navigate to a tablo detail page (e.g. http://localhost:3000/tablos/{some-id}).
      
      Check these items:
      
      **Header (DETAIL-01):**
      1. Header shows a colored circle avatar with the first letter of the tablo title
      2. Title is inline-editable (click to edit, save with Enter or blur)
      3. Action controls appear on the right: Discussion link with chat icon, Invite Member button, trash icon button for Delete
      4. No hardcoded purple (#804EEC) hex appears as a visible style artifact — brand purple shows only via CSS tokens
      
      **Metadata row:**
      5. Created date appears with a calendar icon and formatted date
      6. A purple badge labeled "In progress" appears
      7. A progress bar track (gray) appears with a thin colored fill
      
      **Tab nav:**
      8. All 5 tabs (Overview, Tasks, Files, Discussion, Events) appear in a clean horizontal row
      9. The active tab has a purple underline and bold text; inactive tabs are muted gray
      10. Clicking a tab loads its content via HTMX
      
      **Overview tab (DETAIL-01):**
      11. Description zone appears inside the Overview tab (not above the tab nav)
      12. Click description → edit form appears; save returns to display
      
      **Tasks tab (DETAIL-02 + DETAIL-03):**
      13. Three kanban columns (Todo / In Progress / Done) appear side by side
      14. Each column has a section header with status label, task count badge, and "Add task" button
      15. Tasks within each column appear as row-style cards with a round checkbox + task title + trash icon
      16. If etapes are defined: tasks are grouped by etape with a colored dot sub-heading; unassigned tasks appear at the bottom under "No etape"
      17. Drag-and-drop reorder still works (drag a task between columns)
      18. No EtapeStrip filter pills appear anywhere in the tasks tab
      
      **Files tab (DETAIL-04):**
      19. Files section header shows "Files" on the left and "Upload file" button on the right
      20. Clicking "Upload file" reveals the upload form inline
      21. If files exist: they appear in a clean table with Filename / Size / Uploaded / Actions columns
      22. Each file row has a download icon and a trash icon
      23. Clicking trash → delete confirm replaces the row; confirm/cancel work
      24. If no files: the empty state with "No files yet" appears
      
      If any item above is broken or looks wrong, describe the specific issue.
      
      Type "approved" if all 24 items pass, or describe specific issues to fix.

      <threat_model>

      Trust Boundaries

      Boundary Description
      Handler → FilesTabFragment files []sqlc.TabloFile from authenticated DB query; tabloID from URL path validated by loadOwnedTablo
      HTMX outerHTML swap delete confirm fragment swaps the .file-row-zone tr; hx-target="closest .file-row-zone" is client-driven but the server validates ownership before returning the confirm fragment

      STRIDE Threat Register

      Threat ID Category Component Disposition Mitigation Plan
      T-16-04-01 Tampering FileListRow download href accept Download URL /tablos/{id}/files/{fileID}/download is an authenticated route; file ownership validated server-side; no user-controlled URL injection (template uses templ.SafeURL)
      T-16-04-02 Information Disclosure file.Filename in table accept Filename is user-supplied data returned to the same authenticated user who uploaded it; no cross-user exposure possible given ownership model
      T-16-04-03 Tampering FileDeleteConfirmFragment tr element accept HTMX outerHTML swap of tr is client-side presentation only; server validates delete ownership at POST handler; confirm fragment is GET-only (no side effect)
      </threat_model>
      ```bash cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source go test ./backend/internal/web/... -count=1 ``` All existing file tests pass. Browser checkpoint approved.

      Final phase gate (run with DB):

      TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1
      

      <success_criteria>

      • files.templ FilesTabFragment uses .overview-section + .overview-section-heading + @ui.Table + @ui.EmptyState
      • FileListRow renders <tr class="file-row-zone"> with Download + Delete @ui.IconButton
      • FileDeleteConfirmFragment renders <tr class="file-row-zone"> with <td colspan="4">
      • go test ./backend/internal/web/... -count=1 passes (all 9 file tests unchanged)
      • Browser checkpoint approved: all 24 visual/interaction items verified
      • TEST_DATABASE_URL=... go test ./... -count=1 passes (full suite green) </success_criteria>
      After completion, create `.planning/phases/16-tablo-detail/16-04-SUMMARY.md` using the summary template.