Skip to content

Add tenant-scoped folder organization to Media (#307)#1147

Open
rustynwac wants to merge 1 commit into
mainfrom
claude/media-folders-307
Open

Add tenant-scoped folder organization to Media (#307)#1147
rustynwac wants to merge 1 commit into
mainfrom
claude/media-folders-307

Conversation

@rustynwac

@rustynwac rustynwac commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Enables Payload's native folder support on the Media collection so editors can organize assets into folders in the admin (the SAC use case in #307: Bios/, BlogImages/, etc.).

The catch is tenant isolation. Payload auto-generates a single shared folder collection (payload-folders) that has no notion of tenancy and isn't covered by our tenant list filter — so simply flipping the feature on would let every avalanche center see and reassign every other center's folders. This PR folds that generated collection into the app's existing home-grown multi-tenancy so folders are strictly per-tenant.

Related Issues

Fixes #307

Key Changes

  • src/collections/Media/index.tsfolders: true.
  • src/folders/index.ts (new) — the root folders config. Uses the (experimental) collectionOverrides hook to inject into the generated folder collection:
    • a required tenant relationship (via the shared tenantField()),
    • the filterByTenant base list filter (the folder-browsing view applies baseListFilter, so this scopes folder browsing/selection to the selected tenant),
    • tenant-aware RBAC via accessByTenantRole('media') — folders exist to organize media, so media permissions govern them (replaces Payload's permissive default of "any logged-in user").
  • src/collections/Tenants/hooks/deprovisionBeforeDelete.ts — add the folder slug to TENANT_SCOPED_COLLECTIONS. The folder→tenant FK is NOT NULL with ON DELETE set null, so without this, deleting a tenant would FK-error; deleting the folders first avoids it.
  • src/endpoints/seed/upsert.ts — exclude payload-folders from the seed upsert helper's collection union (folders have no contentHash field and are never seeded through it).
  • Migration add_media_folders + regenerated payload-types.ts.

The root folders config is marked experimental upstream ("may change in minor versions") — worth noting for future Payload upgrades.

How to test

  1. pnpm seed then open localhost:3000/admin, Media → Browse by Folder.
  2. Select a tenant, create folders, and move media into them.
  3. Switch the tenant selector to another center and confirm the first center's folders are not visible or selectable.
  4. Deleting a tenant removes its folders with no FK error.

Automated:

  • pnpm test — 528 passing, incl. new __tests__/server/folders.server.test.ts (verifies the override injects the tenant field, base list filter, and RBAC, and that the folder slug is registered for deprovisioning).
  • pnpm tsc, pnpm lint, pnpm fallow:audit — clean.
  • pnpm seed:standalone completes.
  • I also ran a throwaway Local API script against a seeded DB to confirm at runtime: folders create per-tenant; a tenant-scoped user sees only its own tenant's folders and cannot read another tenant's folder by id; media can be assigned to a folder; and delete-by-tenant of folders runs with no FK error.

Migration Explanation

20260709_144026_add_media_folders is additive: it creates the payload_folders (+ payload_folders_folder_type) tables and adds nullable folder_id columns to media and payload_locked_documents_rels. No data is dropped or rewritten in up(). pnpm migrate:check flags only false positives — ON DELETE clauses inside new FOREIGN KEY definitions and additive ALTER TABLE ADD COLUMN (same keyword-heuristic warnings as prior accepted migrations like add_galleries). The down() uses Payload's standard SQLite table-recreation pattern to reverse the added columns.

Future enhancements / Questions

  • Enabling folders on Documents is the obvious follow-up (separate issue).
  • Auto-sorting existing media into folders is out of scope.

🤖 Generated with Claude Code

https://claude.ai/code/session_016s7MJjeP8Mr3KRiRA22WwJ


Generated by Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Enable Payload's native folder support on the Media collection so editors
can organize assets into folders in the admin. Payload auto-generates a
single shared folder collection with no notion of tenancy, so enabling the
feature naively would let every avalanche center see and reassign every
other center's folders.

Fold the generated folder collection into the app's home-grown multi-tenancy
via the (experimental) root `folders.collectionOverrides` hook: a required
`tenant` relationship, the `filterByTenant` base list filter, and tenant-aware
RBAC keyed on the `media` permission. Register the folder slug with the tenant
deprovision hook so folders cascade-delete with their tenant (the folder
tenant FK is NOT NULL, so orphaning would otherwise FK-error on delete).

- Media: `folders: true`
- New `src/folders` module: slug + tenant-scoping overrides
- Deprovision: add folder slug to TENANT_SCOPED_COLLECTIONS
- Seed upsert: exclude the folder slug (no contentHash field)
- Migration add_media_folders + regenerated types

Admin-only; no frontend or public-read change. Folders are not enabled on
any other collection and existing media is not auto-sorted.

Fixes #307

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016s7MJjeP8Mr3KRiRA22WwJ
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Migration Safety Check

Found 5 potential issues:

20260709_144026_add_media_folders.ts

Warning (line 10): DELETE keyword detected - review for data loss

FOREIGN KEY (\`parent_id\`) REFERENCES \`payload_folders\`(\`id\`) ON UPDATE no action ON DELETE cascade

Warning (line 26): DELETE keyword detected - review for data loss

FOREIGN KEY (\`folder_id\`) REFERENCES \`payload_folders\`(\`id\`) ON UPDATE no action ON DELETE set null,

Warning (line 27): DELETE keyword detected - review for data loss

FOREIGN KEY (\`tenant_id\`) REFERENCES \`tenants\`(\`id\`) ON UPDATE no action ON DELETE set null

Warning (line 43): ALTER keyword detected - review for data loss

await db.run(sql`ALTER TABLE \`media\` ADD \`folder_id\` integer REFERENCES payload_folders(id);`)

Warning (line 46): ALTER keyword detected - review for data loss

sql`ALTER TABLE \`payload_locked_documents_rels\` ADD \`payload_folders_id\` integer REFERENCES payload_folders(id);`,

Review these patterns and add backup/restore logic if needed. See docs/migration-safety.md for guidance.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Preview deployment: https://claudexmedia-folders-307.preview.avy-fx.org

@rustynwac

Copy link
Copy Markdown
Contributor Author

Lots of weird things in here that dont seem right:

Folder in admin panel:
image

I cannot seem to nest folders

Folder looks empty:
image

When media says it's in a folder:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Folder structure for Media

2 participants