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>
16 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 16-tablo-detail | 04 | execute | 4 |
|
|
false |
|
|
- `/`
- ` 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.mdFrom 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.
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 `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`.- ` or `
` - `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 01–04 together deliver the complete Phase 16 restyling: - Plan 01: download + chat icons in UIIcon; CSS Sections 19–25 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`)`) - `files.templ` FileDeleteConfirmFragment outer element is `
Type "approved" if all 24 items pass, or describe specific issues to fix.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.<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
```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.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> Final phase gate (run with DB):
TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1<success_criteria>
files.templFilesTabFragment uses.overview-section+.overview-section-heading+@ui.Table+@ui.EmptyStateFileListRowrenders<tr class="file-row-zone">with Download + Delete@ui.IconButtonFileDeleteConfirmFragmentrenders<tr class="file-row-zone">with<td colspan="4">go test ./backend/internal/web/... -count=1passes (all 9 file tests unchanged)- Browser checkpoint approved: all 24 visual/interaction items verified
TEST_DATABASE_URL=... go test ./... -count=1passes (full suite green) </success_criteria>