Files
Viktor Petersson bb9da44d02 fix(server): restore multi-file (bulk) upload in the Add Asset modal (#3049)
* fix(server): restore multi-file (bulk) upload in the Add Asset modal

Multi-file upload (added in #2778 for the React UI) was lost in the
#2818 React→Alpine/HTMX rewrite: the file picker accepted a single
file only. The assets_upload endpoint already takes one file per
POST, so this is a client-only fix.

- Add `multiple` to the #add-file input.
- Drive the upload from uploadFiles() in home.ts: iterate the selected
  files and POST them sequentially (one XHR per file) against the
  existing single-file endpoint, with "X of N" progress. htmx's
  single-form submit would only ever send the first file, so the file
  tab is no longer htmx-managed; toasts are replayed from the server's
  HX-Trigger header by hand.
- Single-file uploads still flow through the same path unchanged.

Adds an integration test (test_add_multiple_uploads_at_once) that
selects two files in one go and asserts both persist.

Fixes #3045

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(server): don't clear upload state on closeModal mid-batch

Hiding the modal during an in-flight 'sending' upload cleared
uploadState, which disarmed uploadFiles()'s re-entry guard and let a
reopened modal start a second batch racing the first over the shared
progress/index fields. Only reset upload state when nothing is in
flight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(server): surface upload rejections instead of swallowing them

assets_upload refused invalid/empty uploads with messages.error +
HTTP 200, which the HTMX/XHR path drops silently (the partial carries
no toast header) — so the operator saw nothing and the batch uploader
counted the rejection as a success. Pass the rejection through
_asset_table_response(toast=('error', …)) so it rides the HX-Trigger
header on every transport.

Client side, uploadOne() now distinguishes 'ok' / 'rejected' (2xx +
error toast) / 'error' (transport): a rejected file surfaces its
server toast and the batch skips it and keeps going, while a true
transport failure aborts. Addresses Copilot review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(server): update uploadOne comment for the UploadResult return

The doc comment still described the old boolean return; it now
documents the ok / rejected / error tri-state. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(server): attribute single-file limit to the endpoint, not htmx

The comments said htmx "would only ever send the first file", but
htmx includes every selected file in the multipart body — the real
single-file constraint is assets_upload reading request.FILES.get.
Reword both comments. Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 06:45:38 +02:00
..