Skip to content

Add the media library screen - #2601

Merged
pournasserian merged 2 commits into
v2from
2595-media-library
Oct 19, 2026
Merged

pournasserian merged 2 commits into
v2from
2595-media-library

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Closes #2595

What

The media library screen of F-MED (#2427), MED-01's UI: upload, browse, organize and where used. It runs on the merged IMediaLibrary (#2543) and IWhereUsed.ForMediaAsync (#2494), and uses the desk's layout and conventions (#2422).

  • Host (Media/MediaEndpoints.cs, next to the audit export endpoint):
    • POST /media/upload takes a multipart form with the anti-forgery token. It calls UploadAsync and redirects to the new asset with ?uploaded=true.
      • A refused upload redirects back to /media with the error codes and the typed fields, as the audit export does.
      • The request size limit is Media__MaxBytes plus 1 MB for the form, so the library's own limit decides first.
      • A form with only a file binds no fields, and the endpoint treats that as empty metadata.
    • GET /media/{id}/file?version=n streams the bytes to a signed-in person. Images are inline for the previews, and every other type is an attachment. Each response has X-Content-Type-Options: nosniff and Cache-Control: private, no-store. The library already refuses types a browser runs as a document (MediaTypes).
  • Admin:
    • MediaUploadForm, MediaMetadataForm and their shared MediaMetadataFields. Tags are typed comma-separated, and a blank alt text or caption means none.
    • /media:
      • Folder links with subfolders, and tag links.
      • An upload card that shows each refused field's problem next to it and keeps what was typed.
      • The assets newest first. Each row has a decorative image preview, the file name linked to the asset, type, folder, tags, size, Published, and No alt text for an image without it (MED-02). The rows rove like the desk's.
    • /media/{id}:
      • The preview with the asset's alt text, its version and whether it is published, and a download link.
      • A details form that saves the next version from the version the page showed. A form from an older version is refused with the current details shown.
      • Where used: each entry version that references the asset, with its paths.
    • A Media link in the header.
  • Accessibility: the axe WCAG 2.1 AA check from Keyboard-first batch review with roving focus, Playwright tests and WCAG checks #2582 now covers /media and an asset page in both directions and themes.

Why

  • Endpoints for files, pages for everything else. A Razor page cannot read a multipart upload or answer with bytes, so the host takes those two requests, as it does for the audit export. The pages stay static and post ordinary forms.
  • Upload calls the library directly, for now. Put media in change sets and add the media tools #2546 puts uploads and metadata changes into change sets with the uploader as initiator. Until it lands, the screen does what the merged library offers. When it lands, the endpoint's UploadAsync and the page's UpdateAsync pass the person's change set target, like the other writes.
  • Previews are the image itself, scaled by CSS. Server-side thumbnails are image processing, which F-MED leaves out of M1.
  • No paging yet. IMediaLibrary.ListAsync returns every match. The table is fine at M1's scale, and paging belongs in the library's query once one is needed.
  • Not in this PR: change set operations for uploads (Put media in change sets and add the media tools #2546), soft delete and restore (Soft delete and restore media assets #2553), and attaching an asset to a field from the library (generated forms, F-FRM F-FRM: Generated forms as the human fallback editor #2426).

Tests

bUnit (MediaViewsTests):

  • The library lists newest first, with previews only for images, decorative preview alt text, No alt text and Published badges, folder and tag links, the current filter and roving rows. A folder in the address becomes the query.
  • The upload form posts multipart to the endpoint and accepts the allowed types. A refused upload shows its problems next to the fields and keeps the typed values.
  • The asset page shows the preview with its alt text, the details form at the latest version and where it is used. An image without alt text says so.
  • Tags split on commas, a refused upload returns its codes and fields, and blank texts mean none. Logical utilities only.

Integration, host:

  • An upload lands on the asset. Its bytes come back with the declared type, inline and nosniff, and an unknown asset's file is 404.
  • A bad folder, bytes that do not match the declared type, and a post with no file are each sent back with their codes and shown next to their fields, and nothing is stored.
  • The library filters by folder, subfolders and tag, newest first, and marks the one image without alt text.
  • Saving details stores version 2, a form from version 1 is refused with the current details, and an invalid tag is reported.
  • Where used lists the product page that references the asset, and an unused asset says so.
  • Anonymous requests to the pages and the file go to sign-in, and an upload without the anti-forgery token answers 400 and stores nothing.

Browser: the axe check passes on /media and an asset page.

dotnet build -warnaserror and the full suite with the browser tests pass locally against a postgres:17 container as well as SQLite. No new package and no new project reference.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

@pournasserian pournasserian added this to the M1 Governance core milestone Oct 19, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:media Media library (MED) labels Oct 19, 2026
@pournasserian
pournasserian merged commit 4d120e8 into v2 Oct 19, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2595-media-library branch October 19, 2026 07:17
pournasserian added a commit that referenced this pull request Oct 19, 2026
… the library (#2651)

Closes #2629

## What

Follow-ups from the independent review of #2601 (F-MED, #2427).

- **Base version.** The asset page's hidden `BaseVersion` comes from the
form being shown. A form refused for its own problems keeps the version
it started from, so its next save is refused as a conflict instead of
overwriting a change saved meanwhile.
- **Refused uploads no longer filter the library.**
- The return query uses `upload-error`, `upload-folder`, `upload-tags`,
`upload-alt` and `upload-caption`, which no list filter reads.
- A field whose escaped text is over 1500 characters isn't carried back.
`upload-dropped` names it, and the form says to type it again, so the
redirect stays well inside the server's request line limit.
  - Empty fields are left out of the return query.
- **`sub` and `uploaded` are read as text.** `sub` takes `1` or `true`.
A value that isn't a flag counts as false instead of failing the page
with a 500. `uploaded` has the same bug as `sub`, so it is fixed here
too.
- **Where-used rows link to the item's audit history.** An entity type's
items link as `entity` and other items as `entry`, as the log names
them.
- **An upload that declares a body over the limit** gets the library's
"too large" message instead of a reset connection.
- `UseMediaUploadLimit` runs before the anti-forgery check. It reads and
discards the declared body, then redirects back to the library.
  - It applies to a signed-in person, with a body up to 1 GB.
- A chunked body that declares no length still meets the server's limit.

## Why

- **Discard the body, then answer.** A browser still sending its upload
treats an early answer as a failed request. Kestrel aborts a drain over
the endpoint's limit, so the limit is lifted for that request only.
- **Upload names of its own.** The upload form and the list share the
page, so names the list doesn't read can never filter it.

## Tests

- The media screen tests run on SQLite and PostgreSQL.
- An anonymous upload goes to sign-in.
- The #2629 overwrite scenario ends in the conflict message, and the
other person's change stays.
- After a refused upload, the library lists every asset.
- A refused SVG upload says its type can't be uploaded.
- A PDF downloads as an attachment, with its file name.
- `sub=1` lists subfolders, and `sub=yes` doesn't.
- `uploaded=abc` shows the page without the upload note.
- An upload declared over the limit comes back with the too-large
message and stores nothing.
- Where-used rows link to an entry's history, and to an entity's history
as `entity`, in bUnit.
- bUnit:
  - The upload's fields never filter the list.
  - A dropped field is named.
  - The `sub` flag takes 1 and true.
  - The return query drops a text too long to carry.

`SignedInPageTests` gives each page load a 30-second deadline that names
the page. That test hung once on PostgreSQL in CI on v2, and passed on
re-run, so the next failure says which page it was.

`dotnet build -warnaserror`, the unit and bUnit suite and the media
tests on SQLite and PostgreSQL pass locally. No new package and no new
project reference.

## Checklist

- [x] Linked to an issue a maintainer has confirmed
- [x] Targets `v2`
- [x] Tests or docs updated where behaviour changed
- [x] No tool-generated co-author trailers or footers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:media Media library (MED) type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Media library screen: upload, browse, organize and where used

1 participant