Skip to content
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
dc1ea7f
feat: improve sanitization for folders
JarrodMFlesch Jun 11, 2025
caddc9c
feat(ui): supports collection scoped folders
JarrodMFlesch Jun 12, 2025
368c7b1
set available collection automatically when possible
JarrodMFlesch Jun 12, 2025
b868c49
fix auto assigning collections
JarrodMFlesch Jun 13, 2025
eb2c45e
adds custom select component for assignedTo, prevents drag, adds tags…
JarrodMFlesch Jun 13, 2025
109774e
chore: improve dx
JarrodMFlesch Jun 13, 2025
df8b59f
adds validation hook when changing assignedCollections on folder
JarrodMFlesch Jun 13, 2025
17b0c19
adds int tests, fixes assignedCollections when child folders do not p…
JarrodMFlesch Jun 14, 2025
fb7dd79
passing e2e
JarrodMFlesch Jun 16, 2025
e289056
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 16, 2025
5ad270e
chore: adds folders e2e in CI matrix
JarrodMFlesch Jun 16, 2025
5deac4c
adds folders to turbo test suite
JarrodMFlesch Jun 16, 2025
6339ee4
import FolderFileTable in RSC from exports
JarrodMFlesch Jun 17, 2025
7fb82fe
import ItemCardGrid in RSC from exports
JarrodMFlesch Jun 17, 2025
01d1fc9
fix lint issue
JarrodMFlesch Jun 17, 2025
7523fef
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 23, 2025
0a09603
chore: adds description for folderType field
JarrodMFlesch Jun 23, 2025
4ef3ef1
rename assignedCollections to folderType
JarrodMFlesch Jun 23, 2025
f4787be
add folderTypeDescription clientKey
JarrodMFlesch Jun 23, 2025
bb118f5
pluralize folderType subtext on cards
JarrodMFlesch Jun 23, 2025
95987ee
fixes validation checking on child folders
JarrodMFlesch Jun 23, 2025
abafc45
optional folderType field
JarrodMFlesch Jun 23, 2025
5cea4ea
adds flag to disable collection scoping
JarrodMFlesch Jun 23, 2025
1143e97
rm console log
JarrodMFlesch Jun 23, 2025
066bc44
unassigned folders should appear for all folder collections
JarrodMFlesch Jun 23, 2025
3b6173f
ability to move scoped folders into unscoped folders
JarrodMFlesch Jun 23, 2025
1ec58da
fix selectReactSelectOptions helper fn
JarrodMFlesch Jun 24, 2025
f362606
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 24, 2025
20cca81
fix import change from main
JarrodMFlesch Jun 24, 2025
26e0532
fix selected/disabled styles
JarrodMFlesch Jun 24, 2025
8529906
add folderType display in table
JarrodMFlesch Jun 24, 2025
7490e7e
lint
JarrodMFlesch Jun 24, 2025
7878660
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 24, 2025
734c8e0
rename AssignedCollections component
JarrodMFlesch Jun 24, 2025
758f258
rm console log
JarrodMFlesch Jun 26, 2025
b09e08c
default folderType value should be inherited for nested folder creation
JarrodMFlesch Jun 26, 2025
77338e0
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 26, 2025
46cbed5
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 26, 2025
96d82c8
adjust where the folders collection gets added
JarrodMFlesch Jun 26, 2025
b21f91e
fix dependent folders test
JarrodMFlesch Jun 26, 2025
74a7c71
scoping fixes
JarrodMFlesch Jun 27, 2025
2d871d1
feat: pre-populate folderType based on parent folder if child folder
JarrodMFlesch Jun 27, 2025
2a6692f
ensure child folders cannot be empty if parent folder has folderType set
JarrodMFlesch Jun 27, 2025
d6a7d7d
fix logic
JarrodMFlesch Jun 27, 2025
6dac056
fix tests
JarrodMFlesch Jun 27, 2025
38261a7
remove unused import in test helper
JarrodMFlesch Jun 27, 2025
0ca33d0
rename enableCollectionScoping to collectionSpecific
JarrodMFlesch Jun 27, 2025
9426653
fix: prevent adding scope to a folder if it contains documents outsid…
JarrodMFlesch Jun 27, 2025
835c52a
passing folder tests
JarrodMFlesch Jun 27, 2025
6f6f625
adds test and fix for scoped subfolders
JarrodMFlesch Jun 27, 2025
b4193fa
adds test and fix for scoping
JarrodMFlesch Jun 27, 2025
8869281
Merge branch 'main' into feat/folders-by-collection
JarrodMFlesch Jun 27, 2025
d696b60
fix: unique draggable ids, inherit folderType correctly
JarrodMFlesch Jun 29, 2025
ff0ee6f
fix: scroll on card drag
JarrodMFlesch Jun 30, 2025
52db3a9
fix folder tests
JarrodMFlesch Jun 30, 2025
77b1014
keyboard actions
JarrodMFlesch Jun 30, 2025
6a7d3c8
adjust how keyboard selections are handled
JarrodMFlesch Jun 30, 2025
67af92c
fix ctrl click and drag
JarrodMFlesch Jul 8, 2025
03b50b1
chore: adds req to ensureSafeCollectionsChange internal find
JarrodMFlesch Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- folders
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
Expand Down Expand Up @@ -438,6 +439,7 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- folders
- hooks
- lexical__collections__Lexical__e2e__main
- lexical__collections__Lexical__e2e__blocks
Expand Down
88 changes: 69 additions & 19 deletions packages/next/src/views/BrowseByFolder/buildView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,45 @@ export const buildBrowseByFolderView = async (
throw new Error('not-found')
}

const browseByFolderSlugs = browseByFolderSlugsFromArgs.filter(
const foldersSlug = config.folders.slug

/**
* All visiible folder enabled collection slugs that the user has read permissions for.
*/
const allowReadCollectionSlugs = browseByFolderSlugsFromArgs.filter(
(collectionSlug) =>
permissions?.collections?.[collectionSlug]?.read &&
visibleEntities.collections.includes(collectionSlug),
)

const query = queryFromArgs || queryFromReq
const activeCollectionFolderSlugs: string[] =
Array.isArray(query?.relationTo) && query.relationTo.length
? query.relationTo.filter(
(slug) =>
browseByFolderSlugs.includes(slug) || (config.folders && slug === config.folders.slug),
)
: [...browseByFolderSlugs, config.folders.slug]
const query =
queryFromArgs ||
((queryFromReq
? {
...queryFromReq,
relationTo:
typeof queryFromReq?.relationTo === 'string'
? JSON.parse(queryFromReq.relationTo)
: undefined,
}
: {}) as ListQuery)

/**
* If a folderID is provided and the relationTo query param exists,
* we filter the collection slugs to only those that are allowed to be read.
*
* If no folderID is provided, only folders should be active and displayed (the root view).
*/
let collectionsToDisplay: string[] = []
if (folderID && Array.isArray(query?.relationTo)) {
collectionsToDisplay = query.relationTo.filter(
(slug) => allowReadCollectionSlugs.includes(slug) || slug === foldersSlug,
)
} else if (folderID) {
collectionsToDisplay = [...allowReadCollectionSlugs, foldersSlug]
} else {
collectionsToDisplay = [foldersSlug]
}

const {
routes: { admin: adminRoute },
Expand All @@ -93,14 +118,15 @@ export const buildBrowseByFolderView = async (
},
})

const sortPreference: FolderSortKeys = browseByFolderPreferences?.sort || '_folderOrDocumentTitle'
const sortPreference: FolderSortKeys = browseByFolderPreferences?.sort || 'name'
const viewPreference = browseByFolderPreferences?.viewPreference || 'grid'

const { breadcrumbs, documents, FolderResultsComponent, subfolders } =
const { breadcrumbs, documents, folderAssignedCollections, FolderResultsComponent, subfolders } =
await getFolderResultsComponentAndData({
activeCollectionSlugs: activeCollectionFolderSlugs,
browseByFolder: false,
browseByFolder: true,
collectionsToDisplay,
displayAs: viewPreference,
folderAssignedCollections: collectionsToDisplay.filter((slug) => slug !== foldersSlug) || [],
folderID,
req: initPageResult.req,
sort: sortPreference,
Expand Down Expand Up @@ -142,10 +168,33 @@ export const buildBrowseByFolderView = async (
// serverProps,
// })

// documents cannot be created without a parent folder in this view
const allowCreateCollectionSlugs = resolvedFolderID
? [config.folders.slug, ...browseByFolderSlugs]
: [config.folders.slug]
// Filter down allCollectionFolderSlugs by the ones the current folder is assingned to
const allAvailableCollectionSlugs =
folderID && Array.isArray(folderAssignedCollections) && folderAssignedCollections.length
? allowReadCollectionSlugs.filter((slug) => folderAssignedCollections.includes(slug))
: allowReadCollectionSlugs

// Filter down activeCollectionFolderSlugs by the ones the current folder is assingned to
const availableActiveCollectionFolderSlugs = collectionsToDisplay.filter((slug) => {
if (slug === foldersSlug) {
return permissions?.collections?.[foldersSlug]?.read
} else {
return !folderAssignedCollections || folderAssignedCollections.includes(slug)
}
})

// Documents cannot be created without a parent folder in this view
const allowCreateCollectionSlugs = (
resolvedFolderID ? [foldersSlug, ...allAvailableCollectionSlugs] : [foldersSlug]
).filter((collectionSlug) => {
if (collectionSlug === foldersSlug) {
return permissions?.collections?.[foldersSlug]?.create
}
return (
permissions?.collections?.[collectionSlug]?.create &&
visibleEntities.collections.includes(collectionSlug)
)
})

return {
View: (
Expand All @@ -154,15 +203,16 @@ export const buildBrowseByFolderView = async (
{RenderServerComponent({
clientProps: {
// ...folderViewSlots,
activeCollectionFolderSlugs,
allCollectionFolderSlugs: browseByFolderSlugs,
activeCollectionFolderSlugs: availableActiveCollectionFolderSlugs,
allCollectionFolderSlugs: allAvailableCollectionSlugs,
allowCreateCollectionSlugs,
baseFolderPath: `/browse-by-folder`,
breadcrumbs,
disableBulkDelete,
disableBulkEdit,
documents,
enableRowSelections,
folderAssignedCollections,
folderFieldName: config.folders.fieldName,
folderID: resolvedFolderID || null,
FolderResultsComponent,
Expand Down
28 changes: 17 additions & 11 deletions packages/next/src/views/CollectionFolders/buildView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,23 +97,28 @@ export const buildCollectionFolderView = async (
},
})

const sortPreference: FolderSortKeys =
collectionFolderPreferences?.sort || '_folderOrDocumentTitle'
const sortPreference: FolderSortKeys = collectionFolderPreferences?.sort || 'name'
const viewPreference = collectionFolderPreferences?.viewPreference || 'grid'

const {
routes: { admin: adminRoute },
} = config

const { breadcrumbs, documents, FolderResultsComponent, subfolders } =
await getFolderResultsComponentAndData({
activeCollectionSlugs: [config.folders.slug, collectionSlug],
browseByFolder: false,
displayAs: viewPreference,
folderID,
req: initPageResult.req,
sort: sortPreference,
})
const {
breadcrumbs,
documents,
folderAssignedCollections,
FolderResultsComponent,
subfolders,
} = await getFolderResultsComponentAndData({
browseByFolder: false,
collectionsToDisplay: [config.folders.slug, collectionSlug],
displayAs: viewPreference,
folderAssignedCollections: [collectionSlug],
folderID,
req: initPageResult.req,
sort: sortPreference,
})

const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id

Expand Down Expand Up @@ -182,6 +187,7 @@ export const buildCollectionFolderView = async (
disableBulkEdit,
documents,
enableRowSelections,
folderAssignedCollections,
folderFieldName: config.folders.fieldName,
folderID: resolvedFolderID || null,
FolderResultsComponent,
Expand Down
32 changes: 29 additions & 3 deletions packages/payload/src/admin/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ImportMap } from '../../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../../config/types.js'
import type { PaginatedDocs } from '../../database/types.js'
import type { CollectionSlug, ColumnPreference } from '../../index.js'
import type { CollectionSlug, ColumnPreference, FolderSortKeys } from '../../index.js'
import type { PayloadRequest, Sort, Where } from '../../types/index.js'
import type { ColumnsFromURL } from '../../utilities/transformColumnPreferences.js'

Expand Down Expand Up @@ -78,10 +78,36 @@ export type BuildCollectionFolderViewResult = {
}

export type GetFolderResultsComponentAndDataArgs = {
activeCollectionSlugs: CollectionSlug[]
/**
* If true and no folderID is provided, only folders will be returned.
* If false, the results will include documents from the active collections.
*/
browseByFolder: boolean
/**
* Used to filter document types to include in the results/display.
*
* i.e. ['folders', 'posts'] will only include folders and posts in the results.
*
* collectionsToQuery?
*/
collectionsToDisplay: CollectionSlug[]
/**
* Used to determine how the results should be displayed.
*/
displayAs: 'grid' | 'list'
/**
* Used to filter folders by the collections they are assigned to.
*
* i.e. ['posts'] will only include folders that are assigned to the posts collections.
*/
folderAssignedCollections: CollectionSlug[]
/**
* The ID of the folder to filter results by.
*/
folderID: number | string | undefined
req: PayloadRequest
sort: string
/**
* The sort order for the results.
*/
sort: FolderSortKeys
}
1 change: 1 addition & 0 deletions packages/payload/src/admin/views/folderList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type FolderListViewClientProps = {
disableBulkEdit?: boolean
documents: FolderOrDocument[]
enableRowSelections?: boolean
folderAssignedCollections?: SanitizedCollectionConfig['slug'][]
folderFieldName: string
folderID: null | number | string
FolderResultsComponent: React.ReactNode
Expand Down
17 changes: 10 additions & 7 deletions packages/payload/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,17 @@ export const addDefaultsToConfig = (config: Config): Config => {
...(config.auth || {}),
}

const hasFolderCollections = config.collections.some((collection) => Boolean(collection.folders))
if (hasFolderCollections) {
if (
config.folders !== false &&
config.collections.some((collection) => Boolean(collection.folders))
) {
config.folders = {
slug: foldersSlug,
browseByFolder: true,
debug: false,
fieldName: parentFolderFieldName,
...(config.folders || {}),
slug: config.folders?.slug ?? foldersSlug,
browseByFolder: config.folders?.browseByFolder ?? true,
collectionOverrides: config.folders?.collectionOverrides || undefined,
collectionSpecific: config.folders?.collectionSpecific ?? true,
debug: config.folders?.debug ?? false,
fieldName: config.folders?.fieldName ?? parentFolderFieldName,
}
} else {
config.folders = false
Expand Down
43 changes: 33 additions & 10 deletions packages/payload/src/config/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AcceptedLanguages } from '@payloadcms/translations'
import { en } from '@payloadcms/translations/languages/en'
import { deepMergeSimple } from '@payloadcms/translations/utilities'

import type { CollectionSlug, GlobalSlug, SanitizedCollectionConfig } from '../index.js'
import type { SanitizedJobsConfig } from '../queues/config/types/index.js'
import type {
Config,
Expand All @@ -18,15 +19,10 @@ import { sanitizeCollection } from '../collections/config/sanitize.js'
import { migrationsCollection } from '../database/migrations/migrationsCollection.js'
import { DuplicateCollection, InvalidConfiguration } from '../errors/index.js'
import { defaultTimezones } from '../fields/baseFields/timezone/defaultTimezones.js'
import { addFolderCollections } from '../folders/addFolderCollections.js'
import { addFolderCollection } from '../folders/addFolderCollection.js'
import { addFolderFieldToCollection } from '../folders/addFolderFieldToCollection.js'
import { sanitizeGlobal } from '../globals/config/sanitize.js'
import {
baseBlockFields,
type CollectionSlug,
formatLabels,
type GlobalSlug,
sanitizeFields,
} from '../index.js'
import { baseBlockFields, formatLabels, sanitizeFields } from '../index.js'
import {
getLockedDocumentsCollection,
lockedDocumentsCollectionSlug,
Expand Down Expand Up @@ -191,15 +187,17 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC

const collectionSlugs = new Set<CollectionSlug>()

await addFolderCollections(config as unknown as Config)

const validRelationships = [
...(config.collections?.map((c) => c.slug) ?? []),
jobsCollectionSlug,
lockedDocumentsCollectionSlug,
preferencesCollectionSlug,
]

if (config.folders !== false) {
validRelationships.push(config.folders!.slug)
}

/**
* Blocks sanitization needs to happen before collections, as collection/global join field sanitization needs config.blocks
* to be populated with the sanitized blocks
Expand Down Expand Up @@ -236,6 +234,8 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
}
}

const folderEnabledCollections: SanitizedCollectionConfig[] = []

for (let i = 0; i < config.collections!.length; i++) {
if (collectionSlugs.has(config.collections![i]!.slug)) {
throw new DuplicateCollection('slug', config.collections![i]!.slug)
Expand All @@ -257,12 +257,25 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
}
}

if (config.folders !== false && config.collections![i]!.folders) {
addFolderFieldToCollection({
collection: config.collections![i]!,
collectionSpecific: config.folders!.collectionSpecific,
folderFieldName: config.folders!.fieldName,
folderSlug: config.folders!.slug,
})
}

config.collections![i] = await sanitizeCollection(
config as unknown as Config,
config.collections![i]!,
richTextSanitizationPromises,
validRelationships,
)

if (config.folders !== false && config.collections![i]!.folders) {
folderEnabledCollections.push(config.collections![i]!)
}
}

if (config.globals!.length > 0) {
Expand Down Expand Up @@ -332,6 +345,16 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
configWithDefaults.collections!.push(sanitizedJobsCollection)
}

if (config.folders !== false && folderEnabledCollections.length) {
await addFolderCollection({
collectionSpecific: config.folders!.collectionSpecific,
config: config as unknown as Config,
folderEnabledCollections,
richTextSanitizationPromises,
validRelationships,
})
}

configWithDefaults.collections!.push(
await sanitizeCollection(
config as unknown as Config,
Expand Down
Loading
Loading