Skip to content

feat(media): add flat folder UI - #2586

Merged
khoinguyenpham04 merged 4 commits into
feat/media-folders-apifrom
feat/media-folders-ui
Aug 27, 2026
Merged

feat(media): add flat folder UI#2586
khoinguyenpham04 merged 4 commits into
feat/media-folders-apifrom
feat/media-folders-ui

Conversation

@khoinguyenpham04

@khoinguyenpham04 khoinguyenpham04 commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds the focused admin UI for the flat Media Library folder foundation in #2584. Editors can create, rename, browse, and delete folders; permitted users can move local media through the Media Details Location control or by dragging a grid card or list row onto a visible folder; deleting a folder returns its media to the Main library without changing IDs, URLs, storage keys, or usage records.

The interface follows the stable Strapi Media Library baseline while using native Kumo components: compact folder rows, explicit link and edit actions, Back and breadcrumb navigation, one mixed folder/media table in list mode, bounded loading/retry/load-more states, responsive mobile actions, and direction-aware folder names. Folder navigation preserves relevant search, filter, provider, view, page-size, focus, and scroll state.

This is PR2 of the folders sequence and is stacked directly on #2584. It intentionally excludes nesting, counts, bulk selection or moves, upload-to-folder behavior, Media Picker changes, provider behavior, CLI commands, MCP commands, and plugin changes. Local uploads continue to enter the Main library.

Stack:

  1. feat(media): add numbered library pagination #2582 — numbered Media Library pagination
  2. feat(media): add flat folder API foundation #2584 — flat folder API foundation
  3. This PR — flat folder admin UI

Related media roadmap Discussions:

A folder-specific maintainer-approved Discussion was not found. Code review can proceed, but this feature must not merge until the folder scope is approved.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • Typecheck passes for the changed admin package
  • Type-aware lint passes with 0 diagnostics
  • Targeted component, router, API, database, and browser tests pass
  • All changed TypeScript and documentation files pass their formatters
  • I have added/updated tests for my changes
  • User-visible admin strings are wrapped for translation, RTL-safe, and no messages.po files are included
  • I have added and reviewed emdash and @emdash-cms/admin minor changesets
  • New features link to an approved Discussion: folder-specific approval is still needed

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Codex GPT-5.6, with GPT-5.6 Terra Thinking X High second-opinion reviews

Screenshots / test output

The UI was exercised end to end at 1512×982 and 320×800 in light/dark themes, English LTR, and Arabic RTL. The browser audit covered grid/list modes, responsive 4/3/2/1 folder breakpoints, create/rename/delete, Location, global folder search, Back/Forward and direct URLs, safe deletion, delayed pagination, loading/error/retry states, focus restoration, scroll preservation, and mixed-direction long names.

  • The full admin browser suite passed: 125 files and 1,578 tests.
  • 37 admin API-client and router tests passed.
  • 61 focused core folder, database, route, and client tests passed.
  • Targeted Media Library Playwright flows and axe WCAG 2.x AA audits passed.
  • Admin typecheck, type-aware lint, build, formatting, and diff checks passed.
  • Iterative Terra Thinking X High adversarial review converged with no findings.
  • CI will publish the branch preview for visual review.

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 4,314 lines across 34 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do d2c7bfa Aug 27 2026, 08:36 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs 5165284 Aug 26 2026, 05:11 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache d2c7bfa Aug 27 2026, 08:36 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-media-folders-ui.try.emdashcms.com, https://feat-media-folders-ui-emdash-playground.emdash-cms.workers.dev (commit d2c7bfa)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://9e13fd76.try.emdashcms.com, https://9e13fd76-emdash-playground.emdash-cms.workers.dev d2c7bfa 2026-08-27T08:35:49.210Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://e2353f31.try.emdashcms.com, https://e2353f31-emdash-playground.emdash-cms.workers.dev 5165284 2026-08-26T17:16:02.699Z Visit the dashboard ↗
  • Build: Failed ❌

View logs ↗
ede3130 2026-08-26T16:45:00.108Z View logs ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://44aae045.try.emdashcms.com, https://44aae045-emdash-playground.emdash-cms.workers.dev 7536852 2026-08-26T15:53:00.743Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://73783735.try.emdashcms.com, https://73783735-emdash-playground.emdash-cms.workers.dev 619f5ed 2026-08-26T14:47:07.093Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://127d5da3.try.emdashcms.com, https://127d5da3-emdash-playground.emdash-cms.workers.dev 905d25a 2026-08-25T15:41:32.442Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://900bd94d.try.emdashcms.com, https://900bd94d-emdash-playground.emdash-cms.workers.dev df4a2ad 2026-08-25T12:23:27.325Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://aabb040d.try.emdashcms.com, https://aabb040d-emdash-playground.emdash-cms.workers.dev 8f844f0 2026-08-25T11:49:16.794Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1113cd65.try.emdashcms.com, https://1113cd65-emdash-playground.emdash-cms.workers.dev b50fa03 2026-08-25T11:42:55.537Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://390bbb51.try.emdashcms.com, https://390bbb51-emdash-playground.emdash-cms.workers.dev eb0ee11 2026-08-24T11:31:33.249Z Visit the dashboard ↗

View all previews: View all previews ↗

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d2c7bfa

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Major
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Aug 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@2586

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@2586

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@2586

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@2586

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@2586

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@2586

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@2586

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@2586

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@2586

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@2586

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@2586

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@2586

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@2586

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@2586

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@2586

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@2586

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@2586

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@2586

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@2586

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@2586

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@2586

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@2586

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@2586

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@2586

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@2586

commit: d2c7bfa

@khoinguyenpham04
khoinguyenpham04 marked this pull request as ready for review August 25, 2026 10:38
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Aug 25, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR ships a solid flat-folder admin UI on top of the #2584 API foundation: bounded folder list/search, direct-folder URLs, create/rename/delete dialogs, a Location control in Media Details, keyboard focus handling, and RTL-aware folder names. The API surfaces, authorization checks, OpenAPI schemas, client types, and admin API functions look correct and consistent with AGENTS.md conventions (CSRF, envelope shape, SQL-safe LIKE/parameterization, Kumo components, Lingui strings, logical Tailwind).

However, the PR description and the actual diff contradict each other on drag-and-drop: the description lists drag-and-drop as intentionally excluded, while the code, changeset, and Media Library guide all implement and advertise drag-to-folder. That scope inconsistency is the most important thing to resolve before merge. I also found two concrete UI correctness issues (success move feedback is visually hidden; folder edit buttons ignore the canManageFolders permission flag) and a handful of smaller maintainability items. None of these are security or data-loss blockers, so the review is comment rather than request_changes while the scope/doc/code alignment is worked out.


Findings

  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:27

    The PR description explicitly says drag-and-drop is "intentionally excluded," but this import (and the DndContext usage around line 725) adds a full @dnd-kit/core drag-to-folder interaction. The same feature is advertised in the changeset and user guide. Pick one source of truth: if drag is out of scope, remove the dnd-kit code, the onMoveMedia/can-drag wiring, and the related tests; if it is in scope, update the PR description so reviewers and maintainers know what is being merged.

  • [needs fixing] .changeset/media-library-folder-ui.md:6

    The changeset advertises moving media "by dragging a media card or row onto a visible folder," which conflicts with the PR description's claim that drag-and-drop is intentionally excluded. A changeset is public documentation copied into the CHANGELOG, so its scope must match the PR it describes. Reconcile this with the implementation before the release note is published.

  • [needs fixing] docs/src/content/docs/guides/media-library.mdx:167

    The user guide tells readers to move files by dragging grid cards or list rows onto folders, but the PR description says drag-and-drop is excluded. The shipped docs should match both the code and the PR scope. Update this section once the drag-to-folder decision is finalized.

  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:600

    The success toast for moving a file wraps its description in sr-only, so the visible toast only shows "Moved" and hides the file name and destination folder. The failure toast right below uses a visible string description, and the design spec calls for complete names in both visible and audible feedback. Make the description visible.

    			toastManager.add({
    				title: t`Moved`,
    				description: t`File: ${item.filename}. Destination: ${folder.name}.`,
    				variant: "success",
    				timeout: 3000,
    			});
    
  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:990

    MediaFolderCard and MediaFolderListItem use canEdit={folderActionsAvailable} to decide whether to show the edit button, but folderActionsAvailable only checks that the callbacks are defined. It does not consult the canManageFolders permission flag, so a caller that passes callbacks while setting canManageFolders={false} would still render edit actions. Gate both components with the actual permission flag.

    							canEdit={canManageFolders && folderActionsAvailable}
    
  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:1178

    Same permission-gap as the grid folder card: MediaFolderListItem passes canEdit={folderActionsAvailable} without checking canManageFolders. Folder edit actions should require both the permission flag and the supplied callbacks.

    							canEdit={canManageFolders && folderActionsAvailable}
    
  • [suggestion] packages/admin/src/components/MediaFolderDialog.tsx:81

    Every server-side VALIDATION_ERROR is mapped to the same "Folder name must be between 1 and 200 characters" message. That is correct today because only length validation exists, but if the backend later rejects names for other reasons (invalid characters, normalization failure), the inline error will be wrong. Surface the server-supplied message when mutationError.message is present, or keep a generic fallback and reserve the length copy for local validation only.

  • [suggestion] packages/admin/src/router.tsx:832

    MediaPage defines local numeric constants for author/editor roles (ROLE_AUTHOR = 30, ROLE_EDITOR = 40) instead of importing the canonical Role enum/constants from @emdash-cms/auth. If those numeric values ever change, the folder management and media-move permission gates will drift. Use the exported auth package constants.

  • [suggestion] packages/admin/tests/components/MediaDetailPanel.test.tsx:130

    This test (through line ~153) asserts exact Tailwind class names like grid-cols-1, md:p-8, and inline padding style values. Class-string assertions are implementation details and will break on harmless refactors (different utility classes, design-token updates, Kumo changes). Rewrite against observable behavior: rendered dimensions via getBoundingClientRect(), computed CSS values, or responsive landmark/column semantics.

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review needs-rebase and removed review/needs-review No maintainer or bot review yet labels Aug 25, 2026
@khoinguyenpham04
khoinguyenpham04 force-pushed the feat/media-folders-ui branch 2 times, most recently from 8f844f0 to b493327 Compare August 25, 2026 11:58
@khoinguyenpham04

Copy link
Copy Markdown
Collaborator Author

Review follow-up:

  • Updated the PR description so drag-to-folder is explicitly in scope; the implementation, changeset, and guide now agree.
  • Kept the visible success toast as the intentionally concise Moved. The single Kumo live notification includes the file and destination in screen-reader-only detail, avoiding the long visual toast and duplicate announcements.
  • Folder edit actions are already permission-gated: folderActionsAvailable requires canManageFolders as well as all write callbacks before either grid or list edit controls render.
  • Kept localized client-side handling for VALIDATION_ERROR; server error strings are English-only and should not replace Lingui copy.
  • Kept the admin-local role constants to avoid introducing the existing @emdash-cms/auth circular dependency into the SPA bundle.
  • Left the class-level layout test suggestion for a separate test-refinement change; current responsive behavior is also covered by Playwright.

The stack has been restacked onto current main. The full admin browser suite passes (125 files, 1,578 tests), and the previously failing Cloudflare folder round-trip passes locally.

@khoinguyenpham04 khoinguyenpham04 added bot:review Trigger an emdashbot code review on this PR and removed needs-rebase labels Aug 25, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of #2586 after the first round. The PR description now matches the code: drag-and-drop into folders is explicitly in scope, so the earlier scope/doc contradiction is resolved. The folder-edit permission gap is also fixed (folder edit actions are now gated through folderActionsAvailable, which includes canManageFolders).

The implementation remains a solid flat-folder admin UI on the #2584 API: bounded folder list/search, direct-folder URLs, create/rename/delete dialogs, ownership-aware Location control, keyboard focus handling, and RTL-aware folder names. Core API/handler additions are consistent with EmDash conventions (CSRF, envelope shape, SQL-safe LIKE, migration uses FK ON DELETE SET NULL, route authorization uses the Permissions map).

Remaining blockers before merge:

  1. The success toast for drag-to-folder still hides the visible description in an sr-only span, so the toast body only shows "Moved". Same fix as noted in the first review.
  2. MediaDetailPanel.test.tsx still asserts exact Tailwind class names and inline style values (grid-cols-1, md:p-8, padding, etc.). Per AGENTS.md, those are implementation-detail assertions that will break on harmless refactors and should be rewritten against observable behavior.

Also carried forward/re-raised as suggestions: centralizing the hardcoded role constants and surfacing server-supplied validation messages instead of assuming them.

Note: The PR description still records that a folder-specific maintainer-approved Discussion has not been found; that remains a process gate before merge per AGENTS.md.


Findings

  • [needs fixing] packages/admin/src/components/MediaLibrary.tsx:600-603

    The success toast for moving a file wraps its description in sr-only, so the visible toast only shows "Moved" and hides the file name and destination folder. The failure toast right below uses a visible string description. Make the description visible.

    			toastManager.add({
    				title: t`Moved`,
    				description: t`File: ${item.filename}. Destination: ${folder.name}.`,
    				variant: "success",
    				timeout: 3000,
    			});
    
  • [needs fixing] packages/admin/tests/components/MediaDetailPanel.test.tsx:167-190

    This block asserts exact Tailwind class names and inline style values (grid-cols-1, md:grid-cols-2, md:p-8, padding, transitionProperty, etc.). Per AGENTS.md, class-string and style-string assertions are implementation details; they will break on harmless refactors and do not prove the dialog is usable. Rewrite against observable behavior: rendered dimensions via getBoundingClientRect(), roles/landmarks, and visible content rather than how the CSS is spelled.

  • [suggestion] packages/admin/src/components/MediaFolderDialog.tsx:84

    Every VALIDATION_ERROR is mapped to the same length message. If the backend later rejects a name for another reason (invalid characters, normalization failure), the inline error will be wrong. Surface mutationError.message when it is present, or keep a generic fallback and reserve the length copy for local validation only.

    	const fieldError =
    		mutationError instanceof ApiResponseError
    			? mutationError.code === "VALIDATION_ERROR"
    				? (mutationError.message || t`Folder name must be between 1 and 200 characters`)
    				: mutationError.code === "CONFLICT"
    					? t`A media folder with this name already exists`
    					: null
    			: null;
    
  • [suggestion] packages/admin/src/router.tsx:832-833

    These local numeric role constants duplicate the canonical Role enum values. If the auth package constants change, the folder management and media-move permission gates will drift. Use the exported @emdash-cms/auth Role values, or at least centralize the constants in the admin package so there is one source of truth.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 25, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Aug 25, 2026
@khoinguyenpham04 khoinguyenpham04 added the bot:review Trigger an emdashbot code review on this PR label Aug 25, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 25, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Aug 25, 2026
Comment thread docs/technical-specs/media-library-folder-drag-drop.md Outdated
Comment thread packages/admin/src/components/MediaLibrary.tsx Outdated
@khoinguyenpham04
khoinguyenpham04 merged commit 815553c into main Aug 27, 2026
47 checks passed
@khoinguyenpham04
khoinguyenpham04 deleted the feat/media-folders-ui branch August 27, 2026 09:33
@emdashbot emdashbot Bot mentioned this pull request Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants