diff --git a/backend/templates/files.templ b/backend/templates/files.templ index 9bc02bb..3a6d6c4 100644 --- a/backend/templates/files.templ +++ b/backend/templates/files.templ @@ -6,26 +6,57 @@ import ( "github.com/google/uuid" ) -// FilesTabFragment renders the upload form followed by the file list. -// Called by TabloFilesTabHandler for HTMX requests and embedded in TabloDetailPage -// #tab-content for the "files" tab. +// FilesTabFragment renders the files section with an overview-section layout, +// an "Upload file" button, and either a table of files or an empty state. templ FilesTabFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string) { -
- @FileUploadForm(tablo.ID, csrfToken, "") -
- if len(files) == 0 { - @FileListEmpty() - } else { - - } +
+
+

Files

+ @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", + }, + })
+
+ 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), + }) + }
} +// fileTableHead renders the row for the files table. +templ fileTableHead() { + + Filename + Size + Uploaded + Actions + +} + +// fileTableBody renders the rows for the files table. +templ fileTableBody(tabloID uuid.UUID, files []sqlc.TabloFile) { + for _, f := range files { + @FileListRow(tabloID, f) + } +} + // FileUploadForm renders the multipart upload form. // hx-encoding="multipart/form-data" is required for HTMX multipart submissions. // If uploadErr is non-empty, a red error message is shown above the form. @@ -66,112 +97,141 @@ templ FileUploadForm(tabloID uuid.UUID, csrfToken string, uploadErr string) { } -// FileListRow renders a single file row showing filename, human-readable size, and date. -// Download link points to GET /tablos/{tabloID}/files/{file.ID}/download (wired in Plan 03). -// Delete confirm link uses HTMX outerHTML swap on .file-row-zone (Plan 03). +// FileListRow renders a single file row as a for use inside @ui.Table. +// HTMX outerHTML swap on .file-row-zone works because both FileListRow and +// FileDeleteConfirmFragment use . templ FileListRow(tabloID uuid.UUID, file sqlc.TabloFile) { -
  • -
    -
    -

    { file.Filename }

    -

    - { formatBytes(file.SizeBytes) } - if file.CreatedAt.Valid { - { file.CreatedAt.Time.Format("2006-01-02 15:04") } - } -

    -
    -
    - Download - -
    -
    -
  • + + { file.Filename } + { formatBytes(file.SizeBytes) } + + if file.CreatedAt.Valid { + { file.CreatedAt.Time.Format("2006-01-02") } + } + + + + @ui.IconButton(ui.IconButtonProps{ + Label: "Download file", + Icon: "download", + Variant: ui.IconButtonVariantNeutral, + Tone: ui.IconButtonToneGhost, + Type: "button", + }) + + @ui.IconButton(ui.IconButtonProps{ + Label: "Delete file", + Icon: "trash", + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + tabloID.String() + "/files/" + file.ID.String() + "/delete-confirm", + "hx-target": "closest .file-row-zone", + "hx-swap": "outerHTML", + }, + }) + + } // FileDeleteConfirmFragment renders the inline delete confirmation for a single file row. -// Mirrors TaskDeleteConfirmFragment exactly: outer wrapper reuses the .file-row-zone id -// so HTMX outerHTML swap replaces the row in place (T-05-03-05, FILE-05). +// Outer element MUST be to match FileListRow for HTMX outerHTML swap. +// colspan="4" spans all 4 columns (Filename / Size / Uploaded / Actions). templ FileDeleteConfirmFragment(tabloID uuid.UUID, file sqlc.TabloFile, csrfToken string) { -
    -
    -

    Delete file?

    -

    { file.Filename }

    -

    This cannot be undone.

    -
    -
    - @ui.CSRFField(csrfToken) + + +
    +

    Delete file?

    +

    { file.Filename }

    +

    This cannot be undone.

    +
    + + @ui.CSRFField(csrfToken) + @ui.Button(ui.ButtonProps{ + Label: "Yes, delete", + Variant: ui.ButtonVariantDanger, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "submit", + Attrs: templ.Attributes{ + "aria-label": "Confirm delete file", + }, + }) + @ui.Button(ui.ButtonProps{ - Label: "Yes, delete", - Variant: ui.ButtonVariantDanger, - Tone: ui.ButtonToneSolid, + Label: "Keep file", + Variant: ui.ButtonVariantNeutral, + Tone: ui.ButtonToneSoft, Size: ui.SizeMD, - Type: "submit", + Type: "button", Attrs: templ.Attributes{ - "aria-label": "Confirm delete file", + "hx-get": "/tablos/" + tabloID.String() + "/files", + "hx-target": "#tab-content", + "hx-swap": "innerHTML", + "aria-label": "Keep file", }, }) - - @ui.Button(ui.ButtonProps{ - Label: "Keep file", - Variant: ui.ButtonVariantNeutral, - Tone: ui.ButtonToneSoft, - Size: ui.SizeMD, - Type: "button", - Attrs: templ.Attributes{ - "hx-get": "/tablos/" + tabloID.String() + "/files", - "hx-target": "#tab-content", - "hx-swap": "innerHTML", - "aria-label": "Keep file", - }, - }) +
    -
    -
    + + } // FileListEmpty renders the empty-state message when no files are attached. +// Retained for backward compatibility; FilesTabFragment now uses @ui.EmptyState instead. templ FileListEmpty() {

    No files attached yet.

    } -// FileRowGone renders an empty zone div with the file's id so HTMX outerHTML -// swap removes the row from the DOM after a successful delete (Plan 03, FILE-05). +// FileRowGone renders an empty zone tr with the file's id so HTMX outerHTML +// swap removes the row from the DOM after a successful delete (FILE-05). // Same pattern as TaskCardGone. templ FileRowGone(fileID uuid.UUID) { -
    + } // UploadErrorFragment re-renders FilesTabFragment with the error message set. // Used by FileUploadHandler on size violation (returns 422). templ UploadErrorFragment(tablo sqlc.Tablo, files []sqlc.TabloFile, csrfToken string, errMsg string) { -
    - @FileUploadForm(tablo.ID, csrfToken, errMsg) -
    - if len(files) == 0 { - @FileListEmpty() - } else { -
      - for _, f := range files { - @FileListRow(tablo.ID, f) - } -
    - } +
    +
    +

    Files

    + @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", + }, + })
    +
    + @FileUploadForm(tablo.ID, csrfToken, errMsg) +
    + 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), + }) + }
    }