From 337f6188da838a6ee9dd5e47b6858d5dbc507ce0 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:22:26 -0400 Subject: [PATCH] feat: optionally exclude collection documents from appearing in browse-by-folder (#12654) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds configurations for browse-by-folder document results. This PR **does NOT** allow for filtering out folders on a per collection basis. That will be addressed in a future PR 👍 ### Disable browse-by-folder all together ```ts type RootFoldersConfiguration = { /** * If true, the browse by folder view will be enabled * * @default true */ browseByFolder?: boolean // ...rest of type } ``` ### Remove document types from appearing in the browse by folder view ```ts type CollectionFoldersConfiguration = | boolean | { /** * If true, the collection documents will be included in the browse by folder view * * @default true */ browseByFolder?: boolean } ``` ### Misc Fixes https://github.com/payloadcms/payload/issues/12631 where adding folders.collectionOverrides was being set on the client config - it should be omitted. Fixes an issue where `baseListFilters` were not being respected. --- docs/folders/overview.mdx | 17 +- .../next/src/elements/Nav/index.client.tsx | 13 +- .../src/views/BrowseByFolder/buildView.tsx | 79 +++- .../src/views/CollectionFolders/buildView.tsx | 70 ++-- packages/next/src/views/Root/getRouteData.ts | 29 +- packages/next/src/views/Root/index.tsx | 16 +- packages/next/src/views/Root/metadata.ts | 2 +- packages/payload/src/admin/views/index.ts | 2 +- .../src/collections/config/sanitize.ts | 8 + .../payload/src/collections/config/types.ts | 5 +- packages/payload/src/config/client.ts | 12 +- packages/payload/src/config/defaults.ts | 20 +- packages/payload/src/config/types.ts | 2 +- packages/payload/src/exports/shared.ts | 1 + .../src/folders/addFolderCollections.ts | 2 +- .../folders/endpoints/populateFolderData.ts | 103 +++++- .../src/folders/hooks/reparentChildFolder.ts | 12 +- packages/payload/src/folders/types.ts | 15 +- .../utils/buildFolderWhereConstraints.ts | 65 ++++ .../src/folders/utils/getFolderBreadcrumbs.ts | 4 +- .../src/folders/utils/getFolderData.ts | 32 +- .../utils/getFoldersAndDocumentsFromJoin.ts | 45 +-- .../src/folders/utils/getOrphanedDocs.ts | 31 +- .../elements/BulkUpload/EditForm/index.tsx | 3 +- .../src/elements/DocumentControls/index.tsx | 7 +- .../elements/FolderView/Cell/index.client.tsx | 18 +- .../elements/FolderView/Cell/index.server.tsx | 5 + .../FolderView/CollectionTypePill/index.tsx | 5 +- .../FolderView/CurrentFolderActions/index.tsx | 3 + .../FolderView/Drawers/MoveToFolder/index.tsx | 13 +- .../FolderView/Field/index.server.tsx | 4 + .../FolderView/MoveDocToFolder/index.tsx | 42 ++- .../ui/src/elements/ListFolderPills/index.tsx | 17 +- .../ListCreateNewDocInFolderButton.tsx | 4 +- packages/ui/src/providers/Folders/index.tsx | 23 +- .../ui/src/views/BrowseByFolder/index.tsx | 20 +- .../CollectionFolder/ListSelection/index.tsx | 6 +- .../ui/src/views/CollectionFolder/index.tsx | 34 +- .../ui/src/views/List/ListHeader/index.tsx | 3 +- test/eslint.config.js | 3 + test/folders-browse-by-disabled/README.md | 0 .../collections/Posts/index.ts | 17 + test/folders-browse-by-disabled/config.ts | 49 +++ test/folders-browse-by-disabled/e2e.spec.ts | 44 +++ test/folders-browse-by-disabled/int.spec.ts | 195 ++++++++++ .../payload-types.ts | 340 ++++++++++++++++++ test/folders-browse-by-disabled/shared.ts | 1 + .../tsconfig.eslint.json | 13 + .../collections/OmittedFromBrowseBy/index.ts | 23 ++ test/folders/config.ts | 10 +- test/folders/e2e.spec.ts | 234 +++++------- test/folders/payload-types.ts | 33 +- test/folders/shared.ts | 1 + test/helpers/folders/clickFolderCard.ts | 27 ++ test/helpers/folders/createFolder.ts | 42 +++ test/helpers/folders/createFolderFromDoc.ts | 27 ++ .../expectNoResultsAndCreateFolderButton.ts | 12 + .../folders/selectFolderAndConfirmMove.ts | 22 ++ .../selectFolderAndConfirmMoveFromList.ts | 31 ++ 59 files changed, 1549 insertions(+), 367 deletions(-) create mode 100644 packages/payload/src/folders/utils/buildFolderWhereConstraints.ts create mode 100644 test/folders-browse-by-disabled/README.md create mode 100644 test/folders-browse-by-disabled/collections/Posts/index.ts create mode 100644 test/folders-browse-by-disabled/config.ts create mode 100644 test/folders-browse-by-disabled/e2e.spec.ts create mode 100644 test/folders-browse-by-disabled/int.spec.ts create mode 100644 test/folders-browse-by-disabled/payload-types.ts create mode 100644 test/folders-browse-by-disabled/shared.ts create mode 100644 test/folders-browse-by-disabled/tsconfig.eslint.json create mode 100644 test/folders/collections/OmittedFromBrowseBy/index.ts create mode 100644 test/helpers/folders/clickFolderCard.ts create mode 100644 test/helpers/folders/createFolder.ts create mode 100644 test/helpers/folders/createFolderFromDoc.ts create mode 100644 test/helpers/folders/expectNoResultsAndCreateFolderButton.ts create mode 100644 test/helpers/folders/selectFolderAndConfirmMove.ts create mode 100644 test/helpers/folders/selectFolderAndConfirmMoveFromList.ts diff --git a/docs/folders/overview.mdx b/docs/folders/overview.mdx index 8d1307ee9..35c0d744a 100644 --- a/docs/folders/overview.mdx +++ b/docs/folders/overview.mdx @@ -23,6 +23,12 @@ On the payload config, you can configure the following settings under the `folde // Type definition type RootFoldersConfiguration = { + /** + * If true, the browse by folder view will be enabled + * + * @default true + */ + browseByFolder?: boolean /** * An array of functions to be ran when the folder collection is initialized * This allows plugins to modify the collection configuration @@ -82,7 +88,16 @@ To enable folders on a collection, you need to set the `admin.folders` property ```ts // Type definition -type CollectionFoldersConfiguration = boolean +type CollectionFoldersConfiguration = + | boolean + | { + /** + * If true, the collection will be included in the browse by folder view + * + * @default true + */ + browseByFolder?: boolean + } ``` ```ts diff --git a/packages/next/src/elements/Nav/index.client.tsx b/packages/next/src/elements/Nav/index.client.tsx index aff575704..7ba321342 100644 --- a/packages/next/src/elements/Nav/index.client.tsx +++ b/packages/next/src/elements/Nav/index.client.tsx @@ -23,20 +23,11 @@ export const DefaultNavClient: React.FC<{ admin: { routes: { browseByFolder: foldersRoute }, }, - collections, + folders, routes: { admin: adminRoute }, }, } = useConfig() - const [folderCollectionSlugs] = React.useState(() => { - return collections.reduce((acc, collection) => { - if (collection.folders) { - acc.push(collection.slug) - } - return acc - }, []) - }) - const { i18n } = useTranslation() const folderURL = formatAdminURL({ @@ -48,7 +39,7 @@ export const DefaultNavClient: React.FC<{ return ( - {folderCollectionSlugs.length > 0 && } + {folders && folders.browseByFolder && } {groups.map(({ entities, label }, key) => { return ( diff --git a/packages/next/src/views/BrowseByFolder/buildView.tsx b/packages/next/src/views/BrowseByFolder/buildView.tsx index 19ed8a4e4..fa7d64e19 100644 --- a/packages/next/src/views/BrowseByFolder/buildView.tsx +++ b/packages/next/src/views/BrowseByFolder/buildView.tsx @@ -3,6 +3,7 @@ import type { BuildCollectionFolderViewResult, FolderListViewServerPropsOnly, ListQuery, + Where, } from 'payload' import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui' @@ -10,6 +11,7 @@ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerCompo import { formatAdminURL } from '@payloadcms/ui/shared' import { redirect } from 'next/navigation.js' import { getFolderData } from 'payload' +import { buildFolderWhereConstraints } from 'payload/shared' import React from 'react' import { getPreferences } from '../../utilities/getPreferences.js' @@ -29,10 +31,10 @@ export const buildBrowseByFolderView = async ( args: BuildFolderViewArgs, ): Promise => { const { + browseByFolderSlugs: browseByFolderSlugsFromArgs = [], disableBulkDelete, disableBulkEdit, enableRowSelections, - folderCollectionSlugs, folderID, initPageResult, isInDrawer, @@ -54,30 +56,83 @@ export const buildBrowseByFolderView = async ( visibleEntities, } = initPageResult - const collections = folderCollectionSlugs.filter( + const browseByFolderSlugs = browseByFolderSlugsFromArgs.filter( (collectionSlug) => permissions?.collections?.[collectionSlug]?.read && visibleEntities.collections.includes(collectionSlug), ) - if (!collections.length) { + if (config.folders === false || config.folders.browseByFolder === false) { throw new Error('not-found') } const query = queryFromArgs || queryFromReq const selectedCollectionSlugs: string[] = Array.isArray(query?.relationTo) && query.relationTo.length - ? query.relationTo - : [...folderCollectionSlugs, config.folders.slug] + ? query.relationTo.filter( + (slug) => + browseByFolderSlugs.includes(slug) || (config.folders && slug === config.folders.slug), + ) + : [...browseByFolderSlugs, config.folders.slug] const { routes: { admin: adminRoute }, } = config + const folderCollectionConfig = payload.collections[config.folders.slug].config + + const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>( + 'browse-by-folder', + payload, + user.id, + user.collection, + ) + + let documentWhere: undefined | Where = undefined + let folderWhere: undefined | Where = undefined + // if folderID, dont make a documentWhere since it only queries root folders + for (const collectionSlug of selectedCollectionSlugs) { + if (collectionSlug === config.folders.slug) { + const folderCollectionConstraints = await buildFolderWhereConstraints({ + collectionConfig: folderCollectionConfig, + folderID, + localeCode: fullLocale?.code, + req: initPageResult.req, + search: typeof query?.search === 'string' ? query.search : undefined, + }) + + if (folderCollectionConstraints) { + folderWhere = folderCollectionConstraints + } + } else if (folderID) { + if (!documentWhere) { + documentWhere = { + or: [], + } + } + + const collectionConfig = payload.collections[collectionSlug].config + if (collectionConfig.folders && collectionConfig.folders.browseByFolder === true) { + const collectionConstraints = await buildFolderWhereConstraints({ + collectionConfig, + folderID, + localeCode: fullLocale?.code, + req: initPageResult.req, + search: typeof query?.search === 'string' ? query.search : undefined, + }) + + if (collectionConstraints) { + documentWhere.or.push(collectionConstraints) + } + } + } + } + const { breadcrumbs, documents, subfolders } = await getFolderData({ + documentWhere, folderID, + folderWhere, req: initPageResult.req, - search: query?.search as string, }) const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id @@ -96,13 +151,6 @@ export const buildBrowseByFolderView = async ( ) } - const browseByFolderPreferences = await getPreferences<{ viewPreference: string }>( - 'browse-by-folder', - payload, - user.id, - user.collection, - ) - const serverProps: Omit = { documents, i18n, @@ -125,7 +173,7 @@ export const buildBrowseByFolderView = async ( // documents cannot be created without a parent folder in this view const hasCreatePermissionCollectionSlugs = folderID - ? [config.folders.slug, ...folderCollectionSlugs] + ? [config.folders.slug, ...browseByFolderSlugs] : [config.folders.slug] return { @@ -134,7 +182,8 @@ export const buildBrowseByFolderView = async ( breadcrumbs={breadcrumbs} documents={documents} filteredCollectionSlugs={selectedCollectionSlugs} - folderCollectionSlugs={folderCollectionSlugs} + folderCollectionSlugs={browseByFolderSlugs} + folderFieldName={config.folders.fieldName} folderID={folderID} subfolders={subfolders} > diff --git a/packages/next/src/views/CollectionFolders/buildView.tsx b/packages/next/src/views/CollectionFolders/buildView.tsx index 5cbd842ac..167f4f428 100644 --- a/packages/next/src/views/CollectionFolders/buildView.tsx +++ b/packages/next/src/views/CollectionFolders/buildView.tsx @@ -8,9 +8,10 @@ import type { import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' -import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared' +import { formatAdminURL } from '@payloadcms/ui/shared' import { redirect } from 'next/navigation.js' -import { getFolderData, parseDocumentID } from 'payload' +import { getFolderData } from 'payload' +import { buildFolderWhereConstraints } from 'payload/shared' import React from 'react' import { getPreferences } from '../../utilities/getPreferences.js' @@ -37,7 +38,6 @@ export const buildCollectionFolderView = async ( disableBulkDelete, disableBulkEdit, enableRowSelections, - folderCollectionSlugs, folderID, initPageResult, isInDrawer, @@ -69,12 +69,12 @@ export const buildCollectionFolderView = async ( if (collectionConfig) { const query = queryFromArgs || queryFromReq - const collectionFolderPreferences = await getPreferences<{ viewPreference: string }>( - `${collectionSlug}-collection-folder`, - payload, - user.id, - user.collection, - ) + const collectionFolderPreferences = await getPreferences<{ + sort?: string + viewPreference: string + }>(`${collectionSlug}-collection-folder`, payload, user.id, user.collection) + + const sortPreference = collectionFolderPreferences?.value.sort const { routes: { admin: adminRoute }, @@ -82,38 +82,45 @@ export const buildCollectionFolderView = async ( if ( (!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) || - !folderCollectionSlugs.includes(collectionSlug) + !config.folders ) { throw new Error('not-found') } - const whereConstraints = [ - mergeListSearchAndWhere({ - collectionConfig, - search: typeof query?.search === 'string' ? query.search : undefined, - where: (query?.where as Where) || undefined, - }), - ] + let folderWhere: undefined | Where + const folderCollectionConfig = payload.collections[config.folders.slug].config + const folderCollectionConstraints = await buildFolderWhereConstraints({ + collectionConfig: folderCollectionConfig, + folderID, + localeCode: fullLocale?.code, + req: initPageResult.req, + search: typeof query?.search === 'string' ? query.search : undefined, + sort: sortPreference, + }) - if (folderID) { - whereConstraints.push({ - [config.folders.fieldName]: { - equals: parseDocumentID({ id: folderID, collectionSlug, payload }), - }, - }) - } else { - whereConstraints.push({ - [config.folders.fieldName]: { - exists: false, - }, - }) + if (folderCollectionConstraints) { + folderWhere = folderCollectionConstraints + } + + let documentWhere: undefined | Where + const collectionConstraints = await buildFolderWhereConstraints({ + collectionConfig, + folderID, + localeCode: fullLocale?.code, + req: initPageResult.req, + search: typeof query?.search === 'string' ? query.search : undefined, + sort: sortPreference, + }) + if (collectionConstraints) { + documentWhere = collectionConstraints } const { breadcrumbs, documents, subfolders } = await getFolderData({ collectionSlug, + documentWhere, folderID, + folderWhere, req: initPageResult.req, - search: query?.search as string, }) const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id @@ -175,7 +182,8 @@ export const buildCollectionFolderView = async ( breadcrumbs={breadcrumbs} collectionSlug={collectionSlug} documents={documents} - folderCollectionSlugs={folderCollectionSlugs} + folderCollectionSlugs={[collectionSlug]} + folderFieldName={config.folders.fieldName} folderID={folderID} search={search} subfolders={subfolders} diff --git a/packages/next/src/views/Root/getRouteData.ts b/packages/next/src/views/Root/getRouteData.ts index 01ab6e3f6..868078045 100644 --- a/packages/next/src/views/Root/getRouteData.ts +++ b/packages/next/src/views/Root/getRouteData.ts @@ -73,9 +73,9 @@ type GetRouteDataArgs = { } type GetRouteDataResult = { + browseByFolderSlugs: CollectionSlug[] DefaultView: ViewFromConfig documentSubViewType?: DocumentSubViewTypes - folderCollectionSlugs: CollectionSlug[] folderID?: string initPageOptions: Parameters[0] serverProps: ServerPropsFromView @@ -113,12 +113,16 @@ export const getRouteData = ({ let matchedCollection: SanitizedConfig['collections'][number] = undefined let matchedGlobal: SanitizedConfig['globals'][number] = undefined - const folderCollectionSlugs = config.collections.reduce((acc, { slug, folders }) => { - if (folders) { - return [...acc, slug] - } - return acc - }, []) + const isBrowseByFolderEnabled = config.folders && config.folders.browseByFolder + const browseByFolderSlugs = + (isBrowseByFolderEnabled && + config.collections.reduce((acc, { slug, folders }) => { + if (folders && folders.browseByFolder) { + return [...acc, slug] + } + return acc + }, [])) || + [] const serverProps: ServerPropsFromView = { viewActions: config?.admin?.components?.actions || [], @@ -187,7 +191,7 @@ export const getRouteData = ({ viewType = 'account' } - if (folderCollectionSlugs.length && viewKey === 'browseByFolder') { + if (isBrowseByFolderEnabled && viewKey === 'browseByFolder') { templateType = 'default' viewType = 'folders' } @@ -204,7 +208,7 @@ export const getRouteData = ({ templateType = 'minimal' viewType = 'reset' } else if ( - folderCollectionSlugs.length && + isBrowseByFolderEnabled && `/${segmentOne}` === config.admin.routes.browseByFolder ) { // --> /browse-by-folder/:folderID @@ -260,10 +264,7 @@ export const getRouteData = ({ templateType = 'minimal' viewType = 'verify' } else if (isCollection && matchedCollection) { - if ( - segmentThree === config.folders.slug && - folderCollectionSlugs.includes(matchedCollection.slug) - ) { + if (config.folders && segmentThree === config.folders.slug && matchedCollection.folders) { // Collection Folder Views // --> /collections/:collectionSlug/:folderCollectionSlug // --> /collections/:collectionSlug/:folderCollectionSlug/:folderID @@ -333,9 +334,9 @@ export const getRouteData = ({ serverProps.viewActions.reverse() return { + browseByFolderSlugs, DefaultView: ViewToRender, documentSubViewType, - folderCollectionSlugs, folderID, initPageOptions, serverProps, diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx index c4838dcf4..be6a4c956 100644 --- a/packages/next/src/views/Root/index.tsx +++ b/packages/next/src/views/Root/index.tsx @@ -63,9 +63,9 @@ export const RootPage = async ({ const searchParams = await searchParamsPromise const { + browseByFolderSlugs, DefaultView, documentSubViewType, - folderCollectionSlugs, folderID: folderIDParam, initPageOptions, serverProps, @@ -140,17 +140,19 @@ export const RootPage = async ({ }) const payload = initPageResult?.req.payload - const folderID = parseDocumentID({ - id: folderIDParam, - collectionSlug: payload.config.folders.slug, - payload, - }) + const folderID = payload.config.folders + ? parseDocumentID({ + id: folderIDParam, + collectionSlug: payload.config.folders.slug, + payload, + }) + : undefined const RenderedView = RenderServerComponent({ clientProps: { + browseByFolderSlugs, clientConfig, documentSubViewType, - folderCollectionSlugs, viewType, } satisfies AdminViewClientProps, Component: DefaultView.payloadComponent, diff --git a/packages/next/src/views/Root/metadata.ts b/packages/next/src/views/Root/metadata.ts index d08560b32..110b28818 100644 --- a/packages/next/src/views/Root/metadata.ts +++ b/packages/next/src/views/Root/metadata.ts @@ -129,7 +129,7 @@ export const generatePageMetadata = async ({ // --> /:collectionSlug/verify/:token meta = await generateVerifyViewMetadata({ config, i18n }) } else if (isCollection) { - if (segmentThree === config.folders.slug) { + if (config.folders && segmentThree === config.folders.slug) { if (folderCollectionSlugs.includes(collectionConfig.slug)) { // Collection Folder Views // --> /collections/:collectionSlug/:folderCollectionSlug diff --git a/packages/payload/src/admin/views/index.ts b/packages/payload/src/admin/views/index.ts index 31c935bc3..9b0af4d63 100644 --- a/packages/payload/src/admin/views/index.ts +++ b/packages/payload/src/admin/views/index.ts @@ -29,9 +29,9 @@ export type AdminViewConfig = { } export type AdminViewClientProps = { + browseByFolderSlugs?: SanitizedCollectionConfig['slug'][] clientConfig: ClientConfig documentSubViewType?: DocumentSubViewTypes - folderCollectionSlugs?: SanitizedCollectionConfig['slug'][] viewType: ViewTypes } diff --git a/packages/payload/src/collections/config/sanitize.ts b/packages/payload/src/collections/config/sanitize.ts index c38d50a90..b73e150cd 100644 --- a/packages/payload/src/collections/config/sanitize.ts +++ b/packages/payload/src/collections/config/sanitize.ts @@ -176,6 +176,14 @@ export const sanitizeCollection = async ( } } + if (sanitized.folders === true) { + sanitized.folders = { + browseByFolder: true, + } + } else if (sanitized.folders) { + sanitized.folders.browseByFolder = sanitized.folders.browseByFolder ?? true + } + if (sanitized.upload) { if (sanitized.upload === true) { sanitized.upload = {} diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 93a814f17..ec68e8a35 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -452,7 +452,7 @@ export type CollectionConfig = { /** * Enables folders for this collection */ - folders?: CollectionFoldersConfiguration + folders?: boolean | CollectionFoldersConfiguration /** * Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks */ @@ -602,7 +602,7 @@ export type SanitizedJoins = { export interface SanitizedCollectionConfig extends Omit< DeepRequired, - 'admin' | 'auth' | 'endpoints' | 'fields' | 'slug' | 'upload' | 'versions' + 'admin' | 'auth' | 'endpoints' | 'fields' | 'folders' | 'slug' | 'upload' | 'versions' > { admin: CollectionAdminOptions auth: Auth @@ -616,6 +616,7 @@ export interface SanitizedCollectionConfig /** * Object of collections to join 'Join Fields object keyed by collection */ + folders: CollectionFoldersConfiguration | false joins: SanitizedJoins /** diff --git a/packages/payload/src/config/client.ts b/packages/payload/src/config/client.ts index 76592446b..618c3bbc1 100644 --- a/packages/payload/src/config/client.ts +++ b/packages/payload/src/config/client.ts @@ -144,6 +144,17 @@ export const createClientConfig = ({ importMap, }) break + case 'folders': + if (config.folders) { + clientConfig.folders = { + slug: config.folders.slug, + browseByFolder: config.folders.browseByFolder, + debug: config.folders.debug, + fieldName: config.folders.fieldName, + } + } + break + case 'globals': ;(clientConfig.globals as ClientGlobalConfig[]) = createClientGlobalConfigs({ defaultIDType: config.db.defaultIDType, @@ -152,7 +163,6 @@ export const createClientConfig = ({ importMap, }) break - case 'localization': if (typeof config.localization === 'object' && config.localization) { clientConfig.localization = {} diff --git a/packages/payload/src/config/defaults.ts b/packages/payload/src/config/defaults.ts index 96ca03478..ad55b814e 100644 --- a/packages/payload/src/config/defaults.ts +++ b/packages/payload/src/config/defaults.ts @@ -112,13 +112,6 @@ export const addDefaultsToConfig = (config: Config): Config => { }, } - config.folders = { - slug: foldersSlug, - debug: false, - fieldName: parentFolderFieldName, - ...(config.folders || {}), - } - config.bin = config.bin ?? [] config.collections = config.collections ?? [] config.cookiePrefix = config.cookiePrefix ?? 'payload' @@ -169,5 +162,18 @@ export const addDefaultsToConfig = (config: Config): Config => { ...(config.auth || {}), } + const hasFolderCollections = config.collections.some((collection) => Boolean(collection.folders)) + if (hasFolderCollections) { + config.folders = { + slug: foldersSlug, + browseByFolder: true, + debug: false, + fieldName: parentFolderFieldName, + ...(config.folders || {}), + } + } else { + config.folders = false + } + return config } diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 25563956a..1bcb96a00 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -991,7 +991,7 @@ export type Config = { * Options for folder view within the admin panel * @experimental this feature may change in minor versions until it is fully stable */ - folders?: RootFoldersConfiguration + folders?: false | RootFoldersConfiguration /** * @see https://payloadcms.com/docs/configuration/globals#global-configs */ diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index 6d8c469e4..d25c9360a 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -50,6 +50,7 @@ export type { Subfolder, } from '../folders/types.js' +export { buildFolderWhereConstraints } from '../folders/utils/buildFolderWhereConstraints.js' export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js' export { validOperators, validOperatorSet } from '../types/constants.js' diff --git a/packages/payload/src/folders/addFolderCollections.ts b/packages/payload/src/folders/addFolderCollections.ts index f232e845e..deb932319 100644 --- a/packages/payload/src/folders/addFolderCollections.ts +++ b/packages/payload/src/folders/addFolderCollections.ts @@ -4,7 +4,7 @@ import type { CollectionSlug } from '../index.js' import { createFolderCollection } from './createFolderCollection.js' export async function addFolderCollections(config: NonNullable): Promise { - if (!config.collections) { + if (!config.collections || !config.folders) { return } diff --git a/packages/payload/src/folders/endpoints/populateFolderData.ts b/packages/payload/src/folders/endpoints/populateFolderData.ts index 3784c0666..9347602a9 100644 --- a/packages/payload/src/folders/endpoints/populateFolderData.ts +++ b/packages/payload/src/folders/endpoints/populateFolderData.ts @@ -1,7 +1,8 @@ import httpStatus from 'http-status' -import type { Endpoint } from '../../index.js' +import type { Endpoint, Where } from '../../index.js' +import { buildFolderWhereConstraints } from '../utils/buildFolderWhereConstraints.js' import { getFolderData } from '../utils/getFolderData.js' export const populateFolderDataEndpoint: Endpoint = { @@ -17,9 +18,12 @@ export const populateFolderDataEndpoint: Endpoint = { ) } - const folderCollection = Boolean(req.payload.collections?.[req.payload.config.folders.slug]) - - if (!folderCollection) { + if ( + !( + req.payload.config.folders && + Boolean(req.payload.collections?.[req.payload.config.folders.slug]) + ) + ) { return Response.json( { message: 'Folders are not configured', @@ -30,13 +34,100 @@ export const populateFolderDataEndpoint: Endpoint = { ) } - const data = await getFolderData({ - collectionSlug: req.searchParams?.get('collectionSlug') || undefined, + // if collectionSlug exists, we need to create constraints for that _specific collection_ and the folder collection + // if collectionSlug does not exist, we need to create constraints for _all folder enabled collections_ and the folder collection + let documentWhere: undefined | Where + let folderWhere: undefined | Where + const collectionSlug = req.searchParams?.get('collectionSlug') + + if (collectionSlug) { + const collectionConfig = req.payload.collections?.[collectionSlug]?.config + + if (!collectionConfig) { + return Response.json( + { + message: `Collection with slug "${collectionSlug}" not found`, + }, + { + status: httpStatus.NOT_FOUND, + }, + ) + } + + const collectionConstraints = await buildFolderWhereConstraints({ + collectionConfig, + folderID: req.searchParams?.get('folderID') || undefined, + localeCode: typeof req?.locale === 'string' ? req.locale : undefined, + req, + search: req.searchParams?.get('search') || undefined, + sort: req.searchParams?.get('sort') || undefined, + }) + + if (collectionConstraints) { + documentWhere = collectionConstraints + } + } else { + // loop over all folder enabled collections and build constraints for each + for (const collectionSlug of Object.keys(req.payload.collections)) { + const collectionConfig = req.payload.collections[collectionSlug]?.config + + if (collectionConfig?.folders) { + const collectionConstraints = await buildFolderWhereConstraints({ + collectionConfig, + folderID: req.searchParams?.get('folderID') || undefined, + localeCode: typeof req?.locale === 'string' ? req.locale : undefined, + req, + search: req.searchParams?.get('search') || undefined, + }) + + if (collectionConstraints) { + if (!documentWhere) { + documentWhere = { or: [] } + } + if (!Array.isArray(documentWhere.or)) { + documentWhere.or = [documentWhere] + } else if (Array.isArray(documentWhere.or)) { + documentWhere.or.push(collectionConstraints) + } + } + } + } + } + + const folderCollectionConfig = + req.payload.collections?.[req.payload.config.folders.slug]?.config + + if (!folderCollectionConfig) { + return Response.json( + { + message: 'Folder collection not found', + }, + { + status: httpStatus.NOT_FOUND, + }, + ) + } + + const folderConstraints = await buildFolderWhereConstraints({ + collectionConfig: folderCollectionConfig, folderID: req.searchParams?.get('folderID') || undefined, + localeCode: typeof req?.locale === 'string' ? req.locale : undefined, req, search: req.searchParams?.get('search') || undefined, }) + if (folderConstraints) { + folderWhere = folderConstraints + } + + const data = await getFolderData({ + collectionSlug: req.searchParams?.get('collectionSlug') || undefined, + documentWhere: documentWhere ? documentWhere : undefined, + folderID: req.searchParams?.get('folderID') || undefined, + folderWhere, + req, + }) + return Response.json(data) }, method: 'get', diff --git a/packages/payload/src/folders/hooks/reparentChildFolder.ts b/packages/payload/src/folders/hooks/reparentChildFolder.ts index 832dad5f4..dc8ee3ca4 100644 --- a/packages/payload/src/folders/hooks/reparentChildFolder.ts +++ b/packages/payload/src/folders/hooks/reparentChildFolder.ts @@ -3,6 +3,7 @@ import type { CollectionAfterChangeHook, Payload } from '../../index.js' import { extractID } from '../../utilities/extractID.js' type Args = { + folderCollectionSlug: string folderFieldName: string folderID: number | string parentIDToFind: number | string @@ -14,6 +15,7 @@ type Args = { * recursively checking upwards through the folder hierarchy. */ async function isChildOfFolder({ + folderCollectionSlug, folderFieldName, folderID, parentIDToFind, @@ -21,7 +23,7 @@ async function isChildOfFolder({ }: Args): Promise { const parentFolder = await payload.findByID({ id: folderID, - collection: payload.config.folders.slug, + collection: folderCollectionSlug, }) const parentFolderID = parentFolder[folderFieldName] @@ -39,6 +41,7 @@ async function isChildOfFolder({ } return isChildOfFolder({ + folderCollectionSlug, folderFieldName, folderID: parentFolderID, parentIDToFind, @@ -71,10 +74,15 @@ export const reparentChildFolder = ({ folderFieldName: string }): CollectionAfterChangeHook => { return async ({ doc, previousDoc, req }) => { - if (previousDoc[folderFieldName] !== doc[folderFieldName] && doc[folderFieldName]) { + if ( + previousDoc[folderFieldName] !== doc[folderFieldName] && + doc[folderFieldName] && + req.payload.config.folders + ) { const newParentFolderID = extractID(doc[folderFieldName]) const isMovingToChild = newParentFolderID ? await isChildOfFolder({ + folderCollectionSlug: req.payload.config.folders.slug, folderFieldName, folderID: newParentFolderID, parentIDToFind: doc.id, diff --git a/packages/payload/src/folders/types.ts b/packages/payload/src/folders/types.ts index 2584e65c4..579836d2f 100644 --- a/packages/payload/src/folders/types.ts +++ b/packages/payload/src/folders/types.ts @@ -70,6 +70,12 @@ export type GetFolderDataResult = { } export type RootFoldersConfiguration = { + /** + * If true, the browse by folder view will be enabled + * + * @default true + */ + browseByFolder?: boolean /** * An array of functions to be ran when the folder collection is initialized * This allows plugins to modify the collection configuration @@ -99,4 +105,11 @@ export type RootFoldersConfiguration = { slug?: string } -export type CollectionFoldersConfiguration = boolean +export type CollectionFoldersConfiguration = { + /** + * If true, the collection will be included in the browse by folder view + * + * @default true + */ + browseByFolder?: boolean +} diff --git a/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts b/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts new file mode 100644 index 000000000..667b91ab7 --- /dev/null +++ b/packages/payload/src/folders/utils/buildFolderWhereConstraints.ts @@ -0,0 +1,65 @@ +import type { SanitizedCollectionConfig } from '../../collections/config/types.js' +import type { PayloadRequest, Where } from '../../types/index.js' + +import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js' +import { mergeListSearchAndWhere } from '../../utilities/mergeListSearchAndWhere.js' + +type Args = { + collectionConfig: SanitizedCollectionConfig + folderID?: number | string + localeCode?: string + req: PayloadRequest + search?: string + sort?: string +} +export async function buildFolderWhereConstraints({ + collectionConfig, + folderID, + localeCode, + req, + search = '', + sort, +}: Args): Promise { + const constraints: Where[] = [ + mergeListSearchAndWhere({ + collectionConfig, + search, + // where // cannot have where since fields in folders and collection will differ + }), + ] + + if (typeof collectionConfig.admin?.baseListFilter === 'function') { + const baseListFilterConstraint = await collectionConfig.admin.baseListFilter({ + limit: 0, + locale: localeCode, + page: 1, + req, + sort: + sort || + (typeof collectionConfig.defaultSort === 'string' ? collectionConfig.defaultSort : 'id'), + }) + + if (baseListFilterConstraint) { + constraints.push(baseListFilterConstraint) + } + } + + if (folderID) { + // build folder join where constraints + constraints.push({ + relationTo: { + equals: collectionConfig.slug, + }, + }) + } + + const filteredConstraints = constraints.filter(Boolean) + + if (filteredConstraints.length > 1) { + return combineWhereConstraints(filteredConstraints) + } else if (filteredConstraints.length === 1) { + return filteredConstraints[0] + } + + return undefined +} diff --git a/packages/payload/src/folders/utils/getFolderBreadcrumbs.ts b/packages/payload/src/folders/utils/getFolderBreadcrumbs.ts index e5f4644ce..c2cb4c097 100644 --- a/packages/payload/src/folders/utils/getFolderBreadcrumbs.ts +++ b/packages/payload/src/folders/utils/getFolderBreadcrumbs.ts @@ -16,8 +16,8 @@ export const getFolderBreadcrumbs = async ({ req, }: GetFolderBreadcrumbsArgs): Promise => { const { payload, user } = req - const folderFieldName: string = payload.config.folders.fieldName - if (folderID) { + if (folderID && payload.config.folders) { + const folderFieldName: string = payload.config.folders.fieldName const folderQuery = await payload.find({ collection: payload.config.folders.slug, depth: 0, diff --git a/packages/payload/src/folders/utils/getFolderData.ts b/packages/payload/src/folders/utils/getFolderData.ts index 14408cf14..d5efa40ef 100644 --- a/packages/payload/src/folders/utils/getFolderData.ts +++ b/packages/payload/src/folders/utils/getFolderData.ts @@ -1,5 +1,5 @@ import type { CollectionSlug } from '../../index.js' -import type { PayloadRequest } from '../../types/index.js' +import type { PayloadRequest, Where } from '../../types/index.js' import type { GetFolderDataResult } from '../types.js' import { parseDocumentID } from '../../index.js' @@ -14,27 +14,38 @@ type Args = { * @example 'posts' */ collectionSlug?: CollectionSlug + /** + * Optional where clause to filter documents by + * @default undefined + */ + documentWhere?: Where /** * The ID of the folder to query documents from * @default undefined */ folderID?: number | string - req: PayloadRequest - /** - * Search term to filter documents by - only applicable IF `collectionSlug` exists and NO `folderID` is provided + /** Optional where clause to filter subfolders by + * @default undefined */ - search?: string + folderWhere?: Where + req: PayloadRequest } /** * Query for documents, subfolders and breadcrumbs for a given folder */ export const getFolderData = async ({ collectionSlug, + documentWhere, folderID: _folderID, + folderWhere, req, - search, }: Args): Promise => { const { payload } = req + + if (payload.config.folders === false) { + throw new Error('Folders are not enabled') + } + const parentFolderID = parseDocumentID({ id: _folderID, collectionSlug: payload.config.folders.slug, @@ -49,7 +60,8 @@ export const getFolderData = async ({ if (parentFolderID) { // subfolders and documents are queried together const documentAndSubfolderPromise = queryDocumentsAndFoldersFromJoin({ - collectionSlug, + documentWhere, + folderWhere, parentFolderID, req, }) @@ -67,14 +79,16 @@ export const getFolderData = async ({ // subfolders and documents are queried separately const subfoldersPromise = getOrphanedDocs({ collectionSlug: payload.config.folders.slug, + folderFieldName: payload.config.folders.fieldName, req, - search, + where: folderWhere, }) const documentsPromise = collectionSlug ? getOrphanedDocs({ collectionSlug, + folderFieldName: payload.config.folders.fieldName, req, - search, + where: documentWhere, }) : Promise.resolve([]) const [breadcrumbs, subfolders, documents] = await Promise.all([ diff --git a/packages/payload/src/folders/utils/getFoldersAndDocumentsFromJoin.ts b/packages/payload/src/folders/utils/getFoldersAndDocumentsFromJoin.ts index 8884ac24b..ea3ef47af 100644 --- a/packages/payload/src/folders/utils/getFoldersAndDocumentsFromJoin.ts +++ b/packages/payload/src/folders/utils/getFoldersAndDocumentsFromJoin.ts @@ -1,8 +1,9 @@ import type { PaginatedDocs } from '../../database/types.js' -import type { CollectionSlug } from '../../index.js' -import type { Document, PayloadRequest } from '../../types/index.js' +import type { Document, PayloadRequest, Where } from '../../types/index.js' import type { FolderOrDocument } from '../types.js' +import { APIError } from '../../errors/APIError.js' +import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js' import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js' type QueryDocumentsAndFoldersResults = { @@ -10,40 +11,37 @@ type QueryDocumentsAndFoldersResults = { subfolders: FolderOrDocument[] } type QueryDocumentsAndFoldersArgs = { - collectionSlug?: CollectionSlug + /** + * Optional where clause to filter documents by + * @default undefined + */ + documentWhere?: Where + /** Optional where clause to filter subfolders by + * @default undefined + */ + folderWhere?: Where parentFolderID: number | string req: PayloadRequest } export async function queryDocumentsAndFoldersFromJoin({ - collectionSlug, + documentWhere, + folderWhere, parentFolderID, req, }: QueryDocumentsAndFoldersArgs): Promise { const { payload, user } = req - const folderCollectionSlugs: string[] = payload.config.collections.reduce( - (acc, collection) => { - if (collection?.folders) { - acc.push(collection.slug) - } - return acc - }, - [], - ) + + if (payload.config.folders === false) { + throw new APIError('Folders are not enabled', 500) + } const subfolderDoc = (await payload.find({ collection: payload.config.folders.slug, joins: { documentsAndFolders: { - limit: 100_000, + limit: 100_000_000, sort: 'name', - where: { - relationTo: { - in: [ - payload.config.folders.slug, - ...(collectionSlug ? [collectionSlug] : folderCollectionSlugs), - ], - }, - }, + where: combineWhereConstraints([folderWhere, documentWhere], 'or'), }, }, limit: 1, @@ -61,6 +59,9 @@ export async function queryDocumentsAndFoldersFromJoin({ const results: QueryDocumentsAndFoldersResults = childrenDocs.reduce( (acc: QueryDocumentsAndFoldersResults, doc: Document) => { + if (!payload.config.folders) { + return acc + } const { relationTo, value } = doc const item = formatFolderOrDocumentItem({ folderFieldName: payload.config.folders.fieldName, diff --git a/packages/payload/src/folders/utils/getOrphanedDocs.ts b/packages/payload/src/folders/utils/getOrphanedDocs.ts index 126077fea..cea65c127 100644 --- a/packages/payload/src/folders/utils/getOrphanedDocs.ts +++ b/packages/payload/src/folders/utils/getOrphanedDocs.ts @@ -1,42 +1,41 @@ import type { CollectionSlug, PayloadRequest, Where } from '../../index.js' import type { FolderOrDocument } from '../types.js' +import { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js' import { formatFolderOrDocumentItem } from './formatFolderOrDocumentItem.js' type Args = { collectionSlug: CollectionSlug + folderFieldName: string req: PayloadRequest - search?: string + /** + * Optional where clause to filter documents by + * @default undefined + */ + where?: Where } export async function getOrphanedDocs({ collectionSlug, + folderFieldName, req, - search, + where, }: Args): Promise { const { payload, user } = req - let whereConstraints: Where = { + const noParentFolderConstraint: Where = { or: [ { - [payload.config.folders.fieldName]: { + [folderFieldName]: { exists: false, }, }, { - [payload.config.folders.fieldName]: { + [folderFieldName]: { equals: null, }, }, ], } - if (collectionSlug && search && payload.collections[collectionSlug]?.config.admin?.useAsTitle) { - whereConstraints = { - [payload.collections[collectionSlug].config.admin.useAsTitle]: { - like: search, - }, - } - } - const orphanedFolders = await payload.find({ collection: collectionSlug, limit: 0, @@ -44,13 +43,15 @@ export async function getOrphanedDocs({ req, sort: payload.collections[collectionSlug]?.config.admin.useAsTitle, user, - where: whereConstraints, + where: where + ? combineWhereConstraints([noParentFolderConstraint, where]) + : noParentFolderConstraint, }) return ( orphanedFolders?.docs.map((doc) => formatFolderOrDocumentItem({ - folderFieldName: payload.config.folders.fieldName, + folderFieldName, isUpload: Boolean(payload.collections[collectionSlug]?.config.upload), relationTo: collectionSlug, useAsTitle: payload.collections[collectionSlug]?.config.admin.useAsTitle, diff --git a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx index bb830a707..0fe87a64f 100644 --- a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx +++ b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx @@ -169,12 +169,13 @@ export function EditForm({ diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index 1685f95d0..599fca2ed 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -171,7 +171,12 @@ export const DocumentControls: React.FC<{ {showLockedMetaIcon && ( )} - {showFolderMetaIcon && } + {showFolderMetaIcon && config.folders && ( + + )} ) : null} diff --git a/packages/ui/src/elements/FolderView/Cell/index.client.tsx b/packages/ui/src/elements/FolderView/Cell/index.client.tsx index f351c4020..420c636ba 100644 --- a/packages/ui/src/elements/FolderView/Cell/index.client.tsx +++ b/packages/ui/src/elements/FolderView/Cell/index.client.tsx @@ -9,16 +9,18 @@ import React, { useEffect } from 'react' import { MoveDocToFolderButton, useConfig, useTranslation } from '../../../exports/client/index.js' type Props = { - collectionSlug: string - data: Data - docTitle: string - folderFieldName: string + readonly collectionSlug: string + readonly data: Data + readonly docTitle: string + readonly folderCollectionSlug: string + readonly folderFieldName: string } export const FolderTableCellClient = ({ collectionSlug, data, docTitle, + folderCollectionSlug, folderFieldName, }: Props) => { const docID = data.id @@ -54,14 +56,14 @@ export const FolderTableCellClient = ({ console.error('Error moving document to folder', error) } }, - [config.routes.api, collectionSlug, docID, t], + [config.routes.api, collectionSlug, docID, folderFieldName, t], ) useEffect(() => { const loadFolderName = async () => { try { const req = await fetch( - `${config.routes.api}/${config.folders.slug}${intialFolderID ? `/${intialFolderID}` : ''}`, + `${config.routes.api}/${folderCollectionSlug}${intialFolderID ? `/${intialFolderID}` : ''}`, { credentials: 'include', headers: { @@ -83,7 +85,7 @@ export const FolderTableCellClient = ({ void loadFolderName() hasLoadedFolderName.current = true } - }, []) + }, [config.routes.api, folderCollectionSlug, intialFolderID, t]) return ( { (props.collectionConfig.upload ? props.rowData?.filename : props.rowData?.title) || props.rowData.id + if (!props.payload.config.folders) { + return null + } + return ( ) diff --git a/packages/ui/src/elements/FolderView/CollectionTypePill/index.tsx b/packages/ui/src/elements/FolderView/CollectionTypePill/index.tsx index ef461b025..132283658 100644 --- a/packages/ui/src/elements/FolderView/CollectionTypePill/index.tsx +++ b/packages/ui/src/elements/FolderView/CollectionTypePill/index.tsx @@ -13,14 +13,15 @@ import './index.scss' const baseClass = 'collection-type' export function CollectionTypePill() { - const { filterItems, folderCollectionSlug, visibleCollectionSlugs } = useFolder() + const { filterItems, folderCollectionSlug, folderCollectionSlugs, visibleCollectionSlugs } = + useFolder() const { i18n, t } = useTranslation() const { config, getEntityConfig } = useConfig() const [allCollectionOptions] = React.useState(() => { return config.collections.reduce( (acc, collection) => { - if (collection.folders) { + if (collection.folders && folderCollectionSlugs.includes(collection.slug)) { acc.push({ label: getTranslation(collection.labels?.plural, i18n), value: collection.slug, diff --git a/packages/ui/src/elements/FolderView/CurrentFolderActions/index.tsx b/packages/ui/src/elements/FolderView/CurrentFolderActions/index.tsx index aed31b79a..168750091 100644 --- a/packages/ui/src/elements/FolderView/CurrentFolderActions/index.tsx +++ b/packages/ui/src/elements/FolderView/CurrentFolderActions/index.tsx @@ -28,6 +28,7 @@ export function CurrentFolderActions({ className }: Props) { currentFolder, folderCollectionConfig, folderCollectionSlug, + folderFieldName, folderID, moveToFolder, renameFolder, @@ -84,6 +85,8 @@ export function CurrentFolderActions({ className }: Props) { ([]) const [documents, setDocuments] = React.useState([]) @@ -92,7 +90,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) { try { const folderDataReq = await fetch( - `${serverURL}${routes.api}/${folderCollectionSlug}/populate-folder-data${props.fromFolderID ? `?folderID=${props.fromFolderID}` : ''}`, + `${serverURL}${routes.api}/${props.folderCollectionSlug}/populate-folder-data${props.fromFolderID ? `?folderID=${props.fromFolderID}` : ''}`, { credentials: 'include', headers: { @@ -122,7 +120,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) { if (!hasLoaded) { void onLoad() } - }, [folderCollectionSlug, routes.api, serverURL, hasLoaded, props.fromFolderID]) + }, [props.folderCollectionSlug, routes.api, serverURL, hasLoaded, props.fromFolderID]) if (!hasLoaded) { return @@ -134,6 +132,7 @@ function LoadFolderData(props: MoveToFolderDrawerProps) { breadcrumbs={breadcrumbs} documents={documents} folderCollectionSlugs={[]} + folderFieldName={props.folderFieldName} folderID={props.fromFolderID} subfolders={subfolders} > diff --git a/packages/ui/src/elements/FolderView/Field/index.server.tsx b/packages/ui/src/elements/FolderView/Field/index.server.tsx index 057430da2..42da4dd07 100644 --- a/packages/ui/src/elements/FolderView/Field/index.server.tsx +++ b/packages/ui/src/elements/FolderView/Field/index.server.tsx @@ -7,9 +7,13 @@ import './index.scss' const baseClass = 'folder-edit-field' export const FolderEditField = (props: RelationshipFieldServerProps) => { + if (props.payload.config.folders === false) { + return null + } return ( ) diff --git a/packages/ui/src/elements/FolderView/MoveDocToFolder/index.tsx b/packages/ui/src/elements/FolderView/MoveDocToFolder/index.tsx index c29f0e60b..1b20c1321 100644 --- a/packages/ui/src/elements/FolderView/MoveDocToFolder/index.tsx +++ b/packages/ui/src/elements/FolderView/MoveDocToFolder/index.tsx @@ -27,11 +27,13 @@ const baseClass = 'move-doc-to-folder' export function MoveDocToFolder({ buttonProps, className = '', + folderCollectionSlug, folderFieldName, }: { - buttonProps?: Partial - className?: string - folderFieldName: string + readonly buttonProps?: Partial + readonly className?: string + readonly folderCollectionSlug: string + readonly folderFieldName: string }) { const { t } = useTranslation() const dispatchField = useFormFields(([_, dispatch]) => dispatch) @@ -49,7 +51,7 @@ export function MoveDocToFolder({ React.useEffect(() => { async function fetchFolderLabel() { if (fromFolderID && (typeof fromFolderID === 'string' || typeof fromFolderID === 'number')) { - const response = await fetch(`${config.routes.api}/${config.folders.slug}/${fromFolderID}`) + const response = await fetch(`${config.routes.api}/${folderCollectionSlug}/${fromFolderID}`) const folderData = await response.json() setFromFolderName(folderData?.name || t('folder:noFolder')) } else { @@ -58,7 +60,7 @@ export function MoveDocToFolder({ } void fetchFolderLabel() - }, [config.folders.slug, config.routes.api, fromFolderID, t]) + }, [folderCollectionSlug, config.routes.api, fromFolderID, t]) return ( - className?: string - collectionSlug: string - docData: FolderOrDocument['value'] - docID: number | string - docTitle?: string - fromFolderID?: number | string - fromFolderName: string - modalSlug: string - onConfirm?: (args: { id: number | string; name: string }) => Promise | void - skipConfirmModal?: boolean + readonly buttonProps?: Partial + readonly className?: string + readonly collectionSlug: string + readonly docData: FolderOrDocument['value'] + readonly docID: number | string + readonly docTitle?: string + readonly folderCollectionSlug: string + readonly folderFieldName: string + readonly fromFolderID?: number | string + readonly fromFolderName: string + readonly modalSlug: string + readonly onConfirm?: (args: { id: number | string; name: string }) => Promise | void + readonly skipConfirmModal?: boolean } /** @@ -110,6 +116,8 @@ export const MoveDocToFolderButton = ({ docData, docID, docTitle, + folderCollectionSlug, + folderFieldName, fromFolderID, fromFolderName, modalSlug, @@ -143,6 +151,8 @@ export const MoveDocToFolderButton = ({