263 lines
16 KiB
Markdown
263 lines
16 KiB
Markdown
|
|
---
|
|||
|
|
phase: 16-tablo-detail
|
|||
|
|
plan: 04
|
|||
|
|
type: execute
|
|||
|
|
wave: 4
|
|||
|
|
depends_on:
|
|||
|
|
- 16-03
|
|||
|
|
files_modified:
|
|||
|
|
- backend/templates/files.templ
|
|||
|
|
autonomous: false
|
|||
|
|
requirements:
|
|||
|
|
- DETAIL-04
|
|||
|
|
|
|||
|
|
must_haves:
|
|||
|
|
truths:
|
|||
|
|
- "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"
|
|||
|
|
artifacts:
|
|||
|
|
- path: backend/templates/files.templ
|
|||
|
|
provides: Restyled FilesTabFragment, fileTableHead, fileTableBody, FileListRow as tr, FileDeleteConfirmFragment as tr
|
|||
|
|
contains: "@ui.Table(ui.TableProps{"
|
|||
|
|
- path: backend/templates/files_templ.go
|
|||
|
|
provides: Generated Go from restyled files.templ
|
|||
|
|
exports: []
|
|||
|
|
key_links:
|
|||
|
|
- from: backend/templates/files.templ FileListRow
|
|||
|
|
to: backend/templates/files.templ FileDeleteConfirmFragment
|
|||
|
|
via: "hx-target='closest .file-row-zone' hx-swap='outerHTML'"
|
|||
|
|
pattern: "class=\"file-row-zone\""
|
|||
|
|
- from: backend/templates/files.templ FilesTabFragment
|
|||
|
|
to: backend/internal/web/ui/table.templ @ui.Table
|
|||
|
|
via: "@ui.Table(ui.TableProps{Head: fileTableHead(), Body: fileTableBody(...)})"
|
|||
|
|
pattern: "ui.Table"
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
<objective>
|
|||
|
|
Restyle the files section (DETAIL-04): replace raw `<ul>`/`<li>` layout with `@ui.Table`, add `.overview-section-heading` header, introduce `@ui.EmptyState`, and convert `FileListRow` and `FileDeleteConfirmFragment` to use `<tr>` 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.
|
|||
|
|
</objective>
|
|||
|
|
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<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
|
|||
|
|
|
|||
|
|
<interfaces>
|
|||
|
|
<!-- Key contracts the executor needs. -->
|
|||
|
|
|
|||
|
|
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 <ul> with FileListRow <li> items or FileListEmpty()
|
|||
|
|
FileListRow: <li class="file-row-zone" id="file-{id}"> — must become <tr>
|
|||
|
|
FileDeleteConfirmFragment: <div class="file-row-zone" id="file-{id}"> — must become <tr>
|
|||
|
|
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 <tr class="file-row-zone" id="file-{id}"> inside @ui.Table's <tbody>
|
|||
|
|
- Trash IconButton: hx-get="/tablos/{tabloID}/files/{fileID}/delete-confirm" hx-target="closest .file-row-zone" hx-swap="outerHTML"
|
|||
|
|
- FileDeleteConfirmFragment must render <tr class="file-row-zone" id="file-{id}"> (same element type for outerHTML swap)
|
|||
|
|
- Both use colspan="4" in the confirm <td> 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 <a> tag (download is a navigation, not a form POST):
|
|||
|
|
<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>
|
|||
|
|
</interfaces>
|
|||
|
|
</context>
|
|||
|
|
|
|||
|
|
<tasks>
|
|||
|
|
|
|||
|
|
<task type="auto">
|
|||
|
|
<name>Task 1: Restyle FilesTabFragment, FileListRow, and FileDeleteConfirmFragment</name>
|
|||
|
|
<files>backend/templates/files.templ</files>
|
|||
|
|
<read_first>
|
|||
|
|
- 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 <table><thead><tbody> 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)
|
|||
|
|
</read_first>
|
|||
|
|
<action>
|
|||
|
|
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`.
|
|||
|
|
</action>
|
|||
|
|
<verify>
|
|||
|
|
<automated>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</automated>
|
|||
|
|
</verify>
|
|||
|
|
<acceptance_criteria>
|
|||
|
|
- `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 `<tr>` element (not `<li>` or `<div>`)
|
|||
|
|
- `files.templ` FileDeleteConfirmFragment outer element is `<tr class="file-row-zone"` with `<td colspan="4">`
|
|||
|
|
- `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)
|
|||
|
|
</acceptance_criteria>
|
|||
|
|
<done>FilesTabFragment uses @ui.Table and @ui.EmptyState; FileListRow and FileDeleteConfirmFragment use tr; all file tests pass.</done>
|
|||
|
|
</task>
|
|||
|
|
|
|||
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|||
|
|
<what-built>
|
|||
|
|
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
|
|||
|
|
</what-built>
|
|||
|
|
<how-to-verify>
|
|||
|
|
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.
|
|||
|
|
</how-to-verify>
|
|||
|
|
<resume-signal>Type "approved" if all 24 items pass, or describe specific issues to fix.</resume-signal>
|
|||
|
|
</task>
|
|||
|
|
|
|||
|
|
</tasks>
|
|||
|
|
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<verification>
|
|||
|
|
```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):
|
|||
|
|
```bash
|
|||
|
|
TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1
|
|||
|
|
```
|
|||
|
|
</verification>
|
|||
|
|
|
|||
|
|
<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>
|
|||
|
|
|
|||
|
|
<output>
|
|||
|
|
After completion, create `.planning/phases/16-tablo-detail/16-04-SUMMARY.md` using the summary template.
|
|||
|
|
</output>
|