Skip to content

Conversation

JarrodMFlesch
Copy link
Contributor

@JarrodMFlesch JarrodMFlesch commented Dec 17, 2024

You can enable folders under the folders key in the root config, here is the type:

folders?: {
  /**
   * An array of functions to be ran when the folder collection is initialized
   * This allows plugins to modify the collection configuration
   */
  collectionOverrides?: (({
    collection,
  }: {
    collection: CollectionConfig
  }) => CollectionConfig | Promise<CollectionConfig>)[]
  /**
   * Ability to view hidden fields and collections related to folders
   *
   * @default false
   */
  debug?: boolean
  /**
   * The Folder field name
   *
   * @default "folder"
   */
  fieldName?: string
  /**
   * Slug for the folder collection
   *
   * @default "payload-folders"
   */
  slug?: string
}

To enable a collection, in the collection config you will need to add:

admin: {
  folders: true
}

@JarrodMFlesch JarrodMFlesch changed the title Feat/folders feat: folder enabled collections Dec 17, 2024
@himanshu-ntml
Copy link

himanshu-ntml commented Dec 23, 2024

Excited for the folders! Thank you so much, everyone. You all are rocking it!

@@ -115,6 +115,14 @@ export const renderDocumentSlots: (args: {
}
}

if (collectionConfig.upload && collectionConfig?.admin?.components?.edit?.Upload) {
Copy link
Contributor

@rilrom rilrom Jan 11, 2025

Choose a reason for hiding this comment

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

collectionConfig.upload throws a type error for globals. It needs to be collectionConfig?.upload.

I discovered this while testing #9925.

@RobinSCU
Copy link

RobinSCU commented Feb 7, 2025

Hey @JarrodMFlesch,
just wanted ask if there is possibility the Beta test that feature. We are currently switch from an older CMS to Payload an one big Topic is the Media Management. I think we have something like 5000 images and because of that folders would be amazing 😄

@nachomglz
Copy link
Contributor

hey, is there an ETA for "non-enterprise" users?

@JarrodMFlesch JarrodMFlesch merged commit 00667fa into main May 22, 2025
76 checks passed
@JarrodMFlesch JarrodMFlesch deleted the feat/folders branch May 22, 2025 14:04
Copy link
Contributor

🚀 This is included in version v3.39.0

@JesperWe
Copy link
Contributor

JesperWe commented May 22, 2025

@JarrodMFlesch I have a config with

  admin: {
    ...
    routes: {
      unauthorized: '/../unauthorized',
    },

on pnpm build I get

Linting and checking validity of types.

Failed to compile.

./src/payload.config.ts:87:5
Type error: Property 'browseByFolder' is missing in type '{ unauthorized: "/../unauthorized"; }' but required in type '{ account?: `/${string}` | undefined; browseByFolder: `/${string}`; createFirstUser?: `/${string}` | undefined; forgot?: `/${string}` | undefined; inactivity?: `/${string}` | undefined; login?: `/${string}` | undefined; logout?: `/${string}` | undefined; reset?: `/${string}` | undefined; unauthorized?: `/${string}` ...'.

It feels like browseByFolder should be optional as well?

@JesperWe
Copy link
Contributor

Another question: Is it by design that the folder structure is common between Collections?
It feels counterintuitive to me.

@jmcapra
Copy link

jmcapra commented May 22, 2025

Hi Payload team, exciting release! Have just started testing this out on a fresh install of the website template, something seems to be triggering:

TypeError: r.value._folderOrDocumentTitle.toLowerCase is not a function
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20183:48)
    at Array.filter (<anonymous>)
    at sl (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20182:14)
    at ev.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20209:28)
    at mountStateImpl (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7271:24)
    at mountState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7292:22)
    at Object.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:23291:18)
    at exports.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react/cjs/react.development.js:1231:34)
    at ev (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20208:125)

Reprod:

  • Website template
  • folders: {} in global config
  • folders: true in Posts collection
  • Navigate to folder view and create folder "ABC"
  • Create nested folder "123"
  • Create a document in "123"
  • Without saving/publishing the document, click the "123" folder icon in the header
  • The error should show, and return any time you try to view the "123" folder ("ABC")
  • This bug occurs whether or not another document already exists in that folder. It does not occur if you save the document first, return to the edit screen and click the folder icon (this correctly shows the "move to another folder" modal).

Hope this helps! Loving the feature otherwise, only other comment would be something already discussed prior - it is pretty confusing returning to the default List view on the link trail from the edit page. Could a partial solution be to allow forcing the folder view in the collection config, then only ever show the folder trail for documents in that collection?

@immotus
Copy link

immotus commented May 23, 2025

Can someone tell me why folders: true in a collection setting is already marked for deprecation upon initial release?

    /**
     * Enables folders for this collection
     * @deprecated this property will move out of `admin` in the next patch
     */
    folders?: CollectionFoldersConfiguration;

What is the correct way of enabling it for a collection, then?

@tarikkavaz
Copy link

Works like a charm. Thanks to everyone who made this possible.

@JarrodMFlesch
Copy link
Contributor Author

Can someone tell me why folders: true in a collection setting is already marked for deprecation upon initial release?

    /**
     * Enables folders for this collection
     * @deprecated this property will move out of `admin` in the next patch
     */
    folders?: CollectionFoldersConfiguration;

What is the correct way of enabling it for a collection, then?

In the next release it will not be under admin it will be top level on the collection config.

@JarrodMFlesch
Copy link
Contributor Author

Hi Payload team, exciting release! Have just started testing this out on a fresh install of the website template, something seems to be triggering:

TypeError: r.value._folderOrDocumentTitle.toLowerCase is not a function
    at eval (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20183:48)
    at Array.filter (<anonymous>)
    at sl (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20182:14)
    at ev.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20209:28)
    at mountStateImpl (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7271:24)
    at mountState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:7292:22)
    at Object.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react-dom/cjs/react-dom-client.development.js:23291:18)
    at exports.useState (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/[email protected][email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/react/cjs/react.development.js:1231:34)
    at ev (webpack-internal:///(app-pages-browser)/./node_modules/.pnpm/@[email protected]_@[email protected][email protected][email protected][email protected]_r_nqk3l6wzra53uqquzjzfldeehu/node_modules/@payloadcms/ui/dist/exports/client/index.js:20208:125)

Reprod:

  • Website template
  • folders: {} in global config
  • folders: true in Posts collection
  • Navigate to folder view and create folder "ABC"
  • Create nested folder "123"
  • Create a document in "123"
  • Without saving/publishing the document, click the "123" folder icon in the header
  • The error should show, and return any time you try to view the "123" folder ("ABC")
  • This bug occurs whether or not another document already exists in that folder. It does not occur if you save the document first, return to the edit screen and click the folder icon (this correctly shows the "move to another folder" modal).

Hope this helps! Loving the feature otherwise, only other comment would be something already discussed prior - it is pretty confusing returning to the default List view on the link trail from the edit page. Could a partial solution be to allow forcing the folder view in the collection config, then only ever show the folder trail for documents in that collection?

Thanks for reporting, will have a fix for this in the next release - its Postgres specific since ids are numbers by default.

@immotus
Copy link

immotus commented May 23, 2025

Another question: Is it by design that the folder structure is common between Collections? It feels counterintuitive to me.

I totally agree. It is such a typical case that you want to have logically completely different groupings between collections.

@JarrodMFlesch
Copy link
Contributor Author

Another question: Is it by design that the folder structure is common between Collections? It feels counterintuitive to me.

I totally agree. It is such a typical case that you want to have logically completely different groupings between collections.

We are talking through how to support this internally, we agree!

@JarrodMFlesch
Copy link
Contributor Author

@JarrodMFlesch I have a config with

  admin: {
    ...
    routes: {
      unauthorized: '/../unauthorized',
    },

on pnpm build I get

Linting and checking validity of types.

Failed to compile.

./src/payload.config.ts:87:5
Type error: Property 'browseByFolder' is missing in type '{ unauthorized: "/../unauthorized"; }' but required in type '{ account?: `/${string}` | undefined; browseByFolder: `/${string}`; createFirstUser?: `/${string}` | undefined; forgot?: `/${string}` | undefined; inactivity?: `/${string}` | undefined; login?: `/${string}` | undefined; logout?: `/${string}` | undefined; reset?: `/${string}` | undefined; unauthorized?: `/${string}` ...'.

It feels like browseByFolder should be optional as well?

In the next release this will be fixed, thanks for reporting!

@immotus
Copy link

immotus commented May 24, 2025

Another counterintuitive behaviour of the Folders is that when you browse a collection By Folder, create a new folder, navigate into it, and then click Bulk Upload, one would expect that all these bulk-uploaded images would land in the currently selected folder, but the fact is that they don't. If you click Create Document within a folder, then the newly uploaded image gets the folder assigned correctly, but not for the bulk upload.

@immotus
Copy link

immotus commented May 24, 2025

It would also be great if the list view of a folder-enabled upload collection looked like it is in the All Media mode, i.e. with image previews, selectable columns, and filters.

@Nick-PDS
Copy link

Can this view be compatible in drawers? It sucks being restricted to the table view when selecting from a related, upload enabled, collection with folders enabled.

AlessioGr added a commit that referenced this pull request May 28, 2025
#10030 adjusted the default `Pill` component size but forgot to set the
column selector pill sizes to small

## Before

![Screenshot 2025-05-27 at 14 34
31@2x](https://github.com/user-attachments/assets/0f7d44e7-343a-4542-9bc5-830f4bd2bd96)

## After

![Screenshot 2025-05-27 at 14 34
25@2x](https://github.com/user-attachments/assets/33f65fb7-130a-405b-820f-e31259b4f950)
@nedjamez
Copy link

I needed a way to automatically upload new media files to the nested current month folder (YYY/MM), if no folder is set. This is the default behaviour in WordPress, and I wanted to replicated it. This is what worked for me, if anyone needs it or has a better way.

//// src/collections/Media.ts
// Hook to set the default folder to the current year/month folder
const setDefaultYearMonthFolder: CollectionBeforeValidateHook = async ({ 
req, data, operation, context }) => {
  // Only run on create operations for media uploads
  if (operation === 'create' && data && !data.folder && req?.payload) {
    try {
      const now = new Date();
      const currentYear = now.getFullYear().toString();
      const currentMonth = (now.getMonth() + 1).toString().padStart(2, '0'); // getMonth() is 0-indexed
      
      // First, find or create the year folder
      let yearFolder = await findFolderByName(req.payload, currentYear);
      
      if (!yearFolder) {
        // Create the year folder if it doesn't exist
        yearFolder = await req.payload.create({
          collection: 'payload-folders', // Use the correct collection name for folders
          data: {
            name: currentYear,
          },
        });
      }
      
      // Then, find or create the month folder as a child of the year folder
      let monthFolder = await findFolderByNameAndParent(
        req.payload, 
        currentMonth, 
        yearFolder.id
      );
      
      if (!monthFolder) {
        // Create the month folder if it doesn't exist
        monthFolder = await req.payload.create({
          collection: 'payload-folders', // Use the correct collection name for folders
          data: {
            name: currentMonth,
            // Payload's folder structure uses 'parent' field 
            folder: yearFolder.id,
          },
        });
      }
      
      // Set the default folder to the month folder
      if (monthFolder?.id && data) {
        data.folder = monthFolder.id;
      }
    } catch (error) {
      console.error('Error setting default year/month folder:', error);
    }
  }
  
  return data;
};

// Helper function to find a folder by name
async function findFolderByName(payload, name) {
  const folders = await payload.find({
    collection: 'payload-folders', // Use the correct collection name for folders
    where: {
      name: {
        equals: name,
      },
      folder: {
        exists: false,
      },
    },
  });
  
  return folders?.docs?.[0] || null;
}

// Helper function to find a folder by name and parent ID
async function findFolderByNameAndParent(payload, name, parentId) {
  const folders = await payload.find({
    collection: 'payload-folders', // Use the correct collection name for folders
    where: {
      name: {
        equals: name,
      },
      folder: {
        equals: parentId,
      },
    },
  });
  
  return folders?.docs?.[0] || null;
};


export const Media: CollectionConfig = {
  slug: 'media',
  access: {
    create: authenticated,
    delete: authenticated,
    read: anyone,
    update: authenticated,
  },
  hooks: {
    beforeValidate: [setDefaultYearMonthFolder],
////...... rest of file.

paulpopus added a commit that referenced this pull request Jun 3, 2025
…g to support I18nClient too (#12576)

In one of the versions we've changed the type of the argument from
`I18n<any, any>` to `I18n<unknown, unknown>` and this has caused some
issues with TS resolving the type compatibility in the `formatDate`
utility so it no longer supports `I18nClient`.

This type change happened in
#10030
@JesperWe
Copy link
Contributor

Heads up: There is a RFC Discussion opened around the Global/Collection scoping of folders at #12729

jessrynkar added a commit that referenced this pull request Jun 12, 2025
### What?
Fixes inconsistent `pill` sizes across the Admin Panel.

### How?
Pills without a specified size default to **medium**. In the folders
[PR](#10030), additional
padding was to the medium size. As a result, any pills without an
explicit size now appear larger than intended.

This PR fixes that by updating any pills that should be small to
explicitly set `size="small"`.

Fixes #12752
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.