From 6b695355c38eeb8d986a9da511e4cf319a263a9e Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Mon, 7 Apr 2025 14:14:51 -0400 Subject: [PATCH] wires up create new polymorphic list view button --- packages/payload/src/config/types.ts | 6 + .../src/folders/addFolderCollections.ts | 8 +- .../src/folders/createFolderCollection.ts | 12 +- .../folders/endpoints/populateFolderData.ts | 3 +- .../hooks/deleteSubfoldersAfterDelete.ts | 6 +- .../folders/utils/buildFolderBreadcrumbs.ts | 4 +- .../src/folders/utils/getFolderData.ts | 3 - .../src/folders/utils/getFolderDocuments.ts | 6 +- .../src/folders/utils/getFolderSubfolders.ts | 10 +- .../src/elements/CloseModalButton/index.scss | 24 ++++ .../src/elements/CloseModalButton/index.tsx | 25 ++++ .../ui/src/elements/ListControls/index.tsx | 2 +- .../ListDrawerCreateNewDocButton.tsx | 31 +++++ .../ListHeader/DrawerTitleActions/index.tsx | 36 +---- .../TitleActions/ListBulkUploadButton.tsx | 82 ++++++++++++ .../TitleActions/ListCreateNewDocButton.tsx | 42 ++++++ .../ListCreateNewDocInFolderButton.tsx | 119 +++++++++++++++++ .../ListHeader/TitleActions/index.tsx | 69 +--------- packages/ui/src/providers/Folders/index.tsx | 18 ++- .../CollectionFolder/ListHeader/index.scss | 86 ------------ .../CollectionFolder/ListHeader/index.tsx | 124 ------------------ .../CollectionFolder/ListSelection/index.tsx | 3 +- .../ui/src/views/CollectionFolder/index.tsx | 112 +++++++++------- .../CollectionFolder/renderListTable.tsx | 2 +- .../ui/src/views/List/ListHeader/index.scss | 25 ---- .../ui/src/views/List/ListHeader/index.tsx | 74 ++++++----- packages/ui/src/views/List/index.tsx | 17 +-- .../collections/UploadRestricted/e2e.spec.ts | 2 +- test/helpers/e2e/toggleListDrawer.ts | 2 +- 29 files changed, 486 insertions(+), 467 deletions(-) create mode 100644 packages/ui/src/elements/CloseModalButton/index.scss create mode 100644 packages/ui/src/elements/CloseModalButton/index.tsx create mode 100644 packages/ui/src/elements/ListHeader/DrawerTitleActions/ListDrawerCreateNewDocButton.tsx create mode 100644 packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx create mode 100644 packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocButton.tsx create mode 100644 packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocInFolderButton.tsx delete mode 100644 packages/ui/src/views/CollectionFolder/ListHeader/index.scss delete mode 100644 packages/ui/src/views/CollectionFolder/ListHeader/index.tsx diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 2a2508bfef..51f3fb7c7c 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -1009,6 +1009,12 @@ export type Config = { * @default false */ enabled?: boolean + /** + * Slug for the folder collection + * + * @default "_folders" + */ + slug?: string } /** * @see https://payloadcms.com/docs/configuration/globals#global-configs diff --git a/packages/payload/src/folders/addFolderCollections.ts b/packages/payload/src/folders/addFolderCollections.ts index ba48dfcaae..155d899fa5 100644 --- a/packages/payload/src/folders/addFolderCollections.ts +++ b/packages/payload/src/folders/addFolderCollections.ts @@ -14,12 +14,13 @@ export async function addFolderCollections(config: NonNullable): Promise if (config.folders?.enabled) { const enabledCollectionSlugs: CollectionSlug[] = [] const debug = Boolean(config.folders?.debug) + config.folders.slug = config.folders.slug || foldersSlug for (let i = 0; i < config.collections.length; i++) { const collection = config.collections[i] if (config.folders.collections[collection.slug]) { if (collection) { - addFieldsToCollection({ collection, debug }) + addFieldsToCollection({ collection, debug, folderSlug: config.folders.slug }) enabledCollectionSlugs.push(collection.slug) } } @@ -27,6 +28,7 @@ export async function addFolderCollections(config: NonNullable): Promise if (enabledCollectionSlugs.length) { let folderCollection = createFolderCollection({ + slug: config.folders.slug, collectionSlugs: enabledCollectionSlugs, debug, }) @@ -47,9 +49,11 @@ export async function addFolderCollections(config: NonNullable): Promise function addFieldsToCollection({ collection, debug, + folderSlug, }: { collection: CollectionConfig debug?: boolean + folderSlug: CollectionSlug }): void { let useAsTitle = collection.admin?.useAsTitle || (collection.upload ? 'filename' : 'id') const titleField = collection.fields.find((field): field is TextField => { @@ -66,7 +70,7 @@ function addFieldsToCollection({ type: 'relationship', hidden: !debug, index: true, - relationTo: foldersSlug, + relationTo: folderSlug, }, { name: '_folderSearch', diff --git a/packages/payload/src/folders/createFolderCollection.ts b/packages/payload/src/folders/createFolderCollection.ts index d64e7d0b65..1668998837 100644 --- a/packages/payload/src/folders/createFolderCollection.ts +++ b/packages/payload/src/folders/createFolderCollection.ts @@ -1,6 +1,6 @@ import type { CollectionConfig } from '../collections/config/types.js' -import { foldersSlug, parentFolderFieldName } from './constants.js' +import { parentFolderFieldName } from './constants.js' import { populateFolderDataEndpoint } from './endpoints/populateFolderData.js' import { createBaseFolderSearchField } from './fields/folderSearch.js' import { deleteSubfoldersAfterDelete } from './hooks/deleteSubfoldersAfterDelete.js' @@ -9,12 +9,14 @@ import { dissasociateAfterDelete } from './hooks/dissasociateAfterDelete.js' type CreateFolderCollectionArgs = { collectionSlugs: string[] debug?: boolean + slug: string } export const createFolderCollection = ({ + slug, collectionSlugs, debug, }: CreateFolderCollectionArgs): CollectionConfig => ({ - slug: foldersSlug, + slug, admin: { hidden: !debug, useAsTitle: 'name', @@ -34,7 +36,7 @@ export const createFolderCollection = ({ hidden: !debug, }, index: true, - relationTo: foldersSlug, + relationTo: slug, }, { name: 'documentsAndFolders', @@ -42,7 +44,7 @@ export const createFolderCollection = ({ admin: { hidden: !debug, }, - collection: ['_folders', ...collectionSlugs], + collection: [slug, ...collectionSlugs], hasMany: true, on: parentFolderFieldName, }, @@ -54,7 +56,7 @@ export const createFolderCollection = ({ collectionSlugs, parentFolderFieldName, }), - deleteSubfoldersAfterDelete({ parentFolderFieldName }), + deleteSubfoldersAfterDelete({ folderSlug: slug, parentFolderFieldName }), ], }, labels: { diff --git a/packages/payload/src/folders/endpoints/populateFolderData.ts b/packages/payload/src/folders/endpoints/populateFolderData.ts index c417086915..698ebe2bab 100644 --- a/packages/payload/src/folders/endpoints/populateFolderData.ts +++ b/packages/payload/src/folders/endpoints/populateFolderData.ts @@ -3,7 +3,6 @@ import * as qs from 'qs-esm' import type { Endpoint, Where } from '../../index.js' -import { foldersSlug } from '../constants.js' import { getFolderData } from '../utils/getFolderData.js' export const populateFolderDataEndpoint: Endpoint = { @@ -19,7 +18,7 @@ export const populateFolderDataEndpoint: Endpoint = { ) } - const folderCollection = Boolean(req.payload.collections?.[foldersSlug]) + const folderCollection = Boolean(req.payload.collections?.[req.payload.config.folders.slug]) if (!folderCollection) { return Response.json( diff --git a/packages/payload/src/folders/hooks/deleteSubfoldersAfterDelete.ts b/packages/payload/src/folders/hooks/deleteSubfoldersAfterDelete.ts index ad1b35bee7..61663e1644 100644 --- a/packages/payload/src/folders/hooks/deleteSubfoldersAfterDelete.ts +++ b/packages/payload/src/folders/hooks/deleteSubfoldersAfterDelete.ts @@ -1,16 +1,16 @@ import type { CollectionAfterDeleteHook } from '../../index.js' -import { foldersSlug } from '../constants.js' - type Args = { + folderSlug: string parentFolderFieldName: string } export const deleteSubfoldersAfterDelete = ({ + folderSlug, parentFolderFieldName, }: Args): CollectionAfterDeleteHook => { return async ({ id, req }) => { await req.payload.delete({ - collection: foldersSlug, + collection: folderSlug, req, where: { [parentFolderFieldName]: { diff --git a/packages/payload/src/folders/utils/buildFolderBreadcrumbs.ts b/packages/payload/src/folders/utils/buildFolderBreadcrumbs.ts index b484c6eb90..46be025317 100644 --- a/packages/payload/src/folders/utils/buildFolderBreadcrumbs.ts +++ b/packages/payload/src/folders/utils/buildFolderBreadcrumbs.ts @@ -3,7 +3,7 @@ import type { User } from '../../index.js' import type { Payload } from '../../types/index.js' import type { FolderBreadcrumb, FolderInterface } from '../types.js' -import { foldersSlug, parentFolderFieldName } from '../constants.js' +import { parentFolderFieldName } from '../constants.js' type BuildFolderBreadcrumbsArgs = { breadcrumbs?: FolderBreadcrumb[] folderID?: number | string @@ -22,7 +22,7 @@ export const buildFolderBreadcrumbs = async ({ }: BuildFolderBreadcrumbsArgs): Promise => { if (folderID) { const folderQuery = (await payload.find({ - collection: foldersSlug, + collection: payload.config.folders.slug, depth: 0, limit: 1, overrideAccess: false, diff --git a/packages/payload/src/folders/utils/getFolderData.ts b/packages/payload/src/folders/utils/getFolderData.ts index 68564775e8..78a9b143b6 100644 --- a/packages/payload/src/folders/utils/getFolderData.ts +++ b/packages/payload/src/folders/utils/getFolderData.ts @@ -11,7 +11,6 @@ type Args = { docSort?: string docWhere?: Where folderID?: number | string - folderSlug?: CollectionSlug locale?: string payload: Payload user?: User @@ -22,7 +21,6 @@ export const getFolderData = async ({ docSort, docWhere, folderID: folderIDArg, - folderSlug = '_folders', locale, payload, user, @@ -47,7 +45,6 @@ export const getFolderData = async ({ const documents = getFolderDocuments({ collectionSlugs, folderID, - folderSlug, locale, payload, sort: docSort, diff --git a/packages/payload/src/folders/utils/getFolderDocuments.ts b/packages/payload/src/folders/utils/getFolderDocuments.ts index 92b92b7866..4ec3d3bf04 100644 --- a/packages/payload/src/folders/utils/getFolderDocuments.ts +++ b/packages/payload/src/folders/utils/getFolderDocuments.ts @@ -6,7 +6,6 @@ import { combineWhereConstraints } from '../../utilities/combineWhereConstraints export async function getFolderDocuments({ collectionSlugs, folderID, - folderSlug, locale, payload, sort, @@ -15,7 +14,6 @@ export async function getFolderDocuments({ }: { collectionSlugs: CollectionSlug[] folderID?: number | string - folderSlug: CollectionSlug locale?: string payload: Payload sort?: string @@ -31,7 +29,7 @@ export async function getFolderDocuments({ const parentFolderID = folderID ? parseDocumentID({ id: folderID, - collectionSlug: folderSlug, + collectionSlug: payload.config.folders.slug, payload, }) : undefined @@ -61,7 +59,7 @@ export async function getFolderDocuments({ } else if (parentFolderID) { // polymorphic queries must have parent folders const currentFolderQuery = await payload.find({ - collection: folderSlug, + collection: payload.config.folders.slug, joins: { documentsAndFolders: { limit: 100_000, diff --git a/packages/payload/src/folders/utils/getFolderSubfolders.ts b/packages/payload/src/folders/utils/getFolderSubfolders.ts index 5f1c196b3c..59ed14a39c 100644 --- a/packages/payload/src/folders/utils/getFolderSubfolders.ts +++ b/packages/payload/src/folders/utils/getFolderSubfolders.ts @@ -2,8 +2,6 @@ import type { PaginatedDocs, User } from '../../index.js' import type { Payload } from '../../types/index.js' import type { FolderInterface } from '../types.js' -import { foldersSlug } from '../constants.js' - type GetSubfoldersArgs = { folderID?: number | string payload: Payload @@ -21,14 +19,14 @@ export const getFolderSubfolders = async ({ > => { if (folderID) { const subfolderDocs = (await payload.find({ - collection: foldersSlug, + collection: payload.config.folders.slug, joins: { documentsAndFolders: { limit: 100_000, sort: 'name', where: { relationTo: { - equals: foldersSlug, + equals: payload.config.folders.slug, }, }, }, @@ -47,7 +45,7 @@ export const getFolderSubfolders = async ({ } const orphanedFolders = (await payload.find({ - collection: foldersSlug, + collection: payload.config.folders.slug, limit: 0, overrideAccess: false, sort: 'name', @@ -60,7 +58,7 @@ export const getFolderSubfolders = async ({ })) as PaginatedDocs const orphanedDocsWithRelation = orphanedFolders?.docs.map((folder) => ({ - relationTo: foldersSlug, + relationTo: payload.config.folders.slug, value: folder, })) diff --git a/packages/ui/src/elements/CloseModalButton/index.scss b/packages/ui/src/elements/CloseModalButton/index.scss new file mode 100644 index 0000000000..bf789cadd0 --- /dev/null +++ b/packages/ui/src/elements/CloseModalButton/index.scss @@ -0,0 +1,24 @@ +.close-modal-button { + flex-shrink: 0; + border: 0; + background-color: transparent; + padding: 0; + margin: 0; + cursor: pointer; + overflow: hidden; + width: base(1); + height: base(1); + + svg { + width: base(2); + height: base(2); + position: relative; + inset-inline-start: base(-0.5); + top: base(-0.5); + + .stroke { + stroke-width: 2px; + vector-effect: non-scaling-stroke; + } + } +} diff --git a/packages/ui/src/elements/CloseModalButton/index.tsx b/packages/ui/src/elements/CloseModalButton/index.tsx new file mode 100644 index 0000000000..21d97d0834 --- /dev/null +++ b/packages/ui/src/elements/CloseModalButton/index.tsx @@ -0,0 +1,25 @@ +import { useModal } from '@faceless-ui/modal' + +import { XIcon } from '../../icons/X/index.js' +import { useTranslation } from '../../providers/Translation/index.js' + +const baseClass = 'close-modal-button' + +export function CloseModalButton({ slug, className }: { className?: string; slug: string }) { + const { closeModal } = useModal() + const { t } = useTranslation() + + return ( + + ) +} diff --git a/packages/ui/src/elements/ListControls/index.tsx b/packages/ui/src/elements/ListControls/index.tsx index 282d61fcb4..6d7dcf40c1 100644 --- a/packages/ui/src/elements/ListControls/index.tsx +++ b/packages/ui/src/elements/ListControls/index.tsx @@ -36,7 +36,7 @@ export const ListControls: React.FC = (props) => { beforeActions, collectionConfig, collectionSlug, - disableQueryPresets, + disableQueryPresets = true, enableColumns = true, enableFilters = true, enableSort = false, diff --git a/packages/ui/src/elements/ListHeader/DrawerTitleActions/ListDrawerCreateNewDocButton.tsx b/packages/ui/src/elements/ListHeader/DrawerTitleActions/ListDrawerCreateNewDocButton.tsx new file mode 100644 index 0000000000..d3baafb371 --- /dev/null +++ b/packages/ui/src/elements/ListHeader/DrawerTitleActions/ListDrawerCreateNewDocButton.tsx @@ -0,0 +1,31 @@ +'use client' + +import { useTranslation } from '../../../providers/Translation/index.js' +import { useListDrawerContext } from '../../ListDrawer/Provider.js' +import { Pill } from '../../Pill/index.js' + +const baseClass = 'list-header' + +type DefaultDrawerTitleActionsProps = { + hasCreatePermission: boolean +} + +export function ListDrawerCreateNewDocButton({ + hasCreatePermission, +}: DefaultDrawerTitleActionsProps) { + const { DocumentDrawerToggler } = useListDrawerContext() + const { t } = useTranslation() + + if (!hasCreatePermission) { + return null + } + + return ( + + {t('general:createNew')} + + ) +} diff --git a/packages/ui/src/elements/ListHeader/DrawerTitleActions/index.tsx b/packages/ui/src/elements/ListHeader/DrawerTitleActions/index.tsx index 113a1ec77c..fdd7964902 100644 --- a/packages/ui/src/elements/ListHeader/DrawerTitleActions/index.tsx +++ b/packages/ui/src/elements/ListHeader/DrawerTitleActions/index.tsx @@ -1,35 +1 @@ -'use client' - -import type { TFunction } from '@payloadcms/translations' - -import { useListDrawerContext } from '../../ListDrawer/Provider.js' -import { Pill } from '../../Pill/index.js' - -const baseClass = 'list-header' - -type DefaultDrawerTitleActionsProps = { - hasCreatePermission: boolean - t: TFunction -} - -export const DefaultDrawerTitleActions = ({ - hasCreatePermission, - t, -}: DefaultDrawerTitleActionsProps): React.ReactNode[] => { - const Actions: React.ReactNode[] = [] - - const { DocumentDrawerToggler } = useListDrawerContext() - - if (hasCreatePermission) { - Actions.push( - - {t('general:createNew')} - , - ) - } - - return Actions -} +export { ListDrawerCreateNewDocButton } from './ListDrawerCreateNewDocButton.js' diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx new file mode 100644 index 0000000000..a01940b772 --- /dev/null +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListBulkUploadButton.tsx @@ -0,0 +1,82 @@ +'use client' +import type { CollectionSlug } from 'payload' + +import { useModal } from '@faceless-ui/modal' +import { useRouter } from 'next/navigation.js' +import React from 'react' + +import { useBulkUpload } from '../../../elements/BulkUpload/index.js' +import { useTranslation } from '../../../providers/Translation/index.js' +import { Button } from '../../Button/index.js' + +export function ListBulkUploadButton({ + collectionSlug, + hasCreatePermission, + isBulkUploadEnabled, + onBulkUploadSuccess, + openBulkUpload: openBulkUploadFromProps, +}: { + collectionSlug: CollectionSlug + hasCreatePermission: boolean + isBulkUploadEnabled: boolean + onBulkUploadSuccess?: () => void + /** + * @deprecated This prop will be removed in the next major version. + * + * Prefer using `onBulkUploadSuccess` + */ + openBulkUpload?: () => void +}) { + const { + drawerSlug: bulkUploadDrawerSlug, + setCollectionSlug, + setCurrentActivePath, + setOnSuccess, + } = useBulkUpload() + const { t } = useTranslation() + const { openModal } = useModal() + const router = useRouter() + + const openBulkUpload = React.useCallback(() => { + if (typeof openBulkUploadFromProps === 'function') { + openBulkUploadFromProps() + } else { + setCollectionSlug(collectionSlug) + setCurrentActivePath(collectionSlug) + openModal(bulkUploadDrawerSlug) + setOnSuccess(collectionSlug, () => { + if (typeof onBulkUploadSuccess === 'function') { + onBulkUploadSuccess() + } else { + router.refresh() + } + }) + } + }, [ + router, + collectionSlug, + bulkUploadDrawerSlug, + openModal, + setCollectionSlug, + setCurrentActivePath, + setOnSuccess, + onBulkUploadSuccess, + openBulkUploadFromProps, + ]) + + if (!hasCreatePermission || !isBulkUploadEnabled) { + return null + } + + return ( + + ) +} diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocButton.tsx new file mode 100644 index 0000000000..9d59424e17 --- /dev/null +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocButton.tsx @@ -0,0 +1,42 @@ +'use client' +import type { ClientCollectionConfig } from 'payload' + +import { getTranslation } from '@payloadcms/translations' +import React from 'react' + +import { useTranslation } from '../../../providers/Translation/index.js' +import { Button } from '../../Button/index.js' + +const baseClass = 'list-create-new-doc' + +export function ListCreateNewButton({ + collectionConfig, + hasCreatePermission, + newDocumentURL, +}: { + collectionConfig: ClientCollectionConfig + hasCreatePermission: boolean + newDocumentURL: string +}) { + const { i18n, t } = useTranslation() + + if (!hasCreatePermission) { + return null + } + + return ( + + ) +} diff --git a/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocInFolderButton.tsx b/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocInFolderButton.tsx new file mode 100644 index 0000000000..faa47554c4 --- /dev/null +++ b/packages/ui/src/elements/ListHeader/TitleActions/ListCreateNewDocInFolderButton.tsx @@ -0,0 +1,119 @@ +'use client' + +import type { CollectionSlug } from 'payload' + +import { useModal } from '@faceless-ui/modal' +import { getTranslation } from '@payloadcms/translations' +import React from 'react' + +import { useConfig } from '../../../providers/Config/index.js' +import { useFolder } from '../../../providers/Folders/index.js' +import { useTranslation } from '../../../providers/Translation/index.js' +import { Button } from '../../Button/index.js' +import { DocumentDrawer } from '../../DocumentDrawer/index.js' +import { NewFolderDrawer } from '../../FolderView/Drawers/NewFolder/index.js' +import { Popup, PopupList } from '../../Popup/index.js' + +const newFolderDrawerSlug = 'create-new-folder' +const newDocInFolderDrawerSlug = 'create-new-document-with-folder' +const baseClass = 'create-new-doc-in-folder' + +export function ListCreateNewDocInFolderButton({ + collectionSlugs, +}: { + collectionSlugs: CollectionSlug[] +}) { + const { i18n, t } = useTranslation() + const { closeModal, openModal } = useModal() + const { config } = useConfig() + const { folderCollectionConfig, folderID, populateFolderData, setSubfolders } = useFolder() + const [createCollectionSlug, setCreateCollectionSlug] = React.useState() + + const onNewFolderCreate = React.useCallback( + async (doc) => { + setSubfolders((prev) => { + return [ + ...prev, + { + relationTo: config.folders.slug, + value: doc, + }, + ] + }) + await populateFolderData({ folderID }) + closeModal(newFolderDrawerSlug) + }, + [config.folders.slug, folderID, populateFolderData, setSubfolders, closeModal], + ) + + return ( + + + {t('general:createNew')} + + } + buttonType="default" + className={`${baseClass}__action-popup`} + > + + { + openModal(newFolderDrawerSlug) + }} + > + {getTranslation(folderCollectionConfig.labels.singular, i18n)} + + {config.collections.map((collection, index) => { + if ( + config.folders.collections[collection.slug] && + collectionSlugs.includes(collection.slug) + ) { + const label = + typeof collection.labels.singular === 'string' + ? collection.labels.singular + : collection.slug + return ( + { + setCreateCollectionSlug(collection.slug) + openModal(newDocInFolderDrawerSlug) + }} + > + {label} + + ) + } + return null + })} + + + + { + if (operation === 'create') { + closeModal(newDocInFolderDrawerSlug) + setCreateCollectionSlug(undefined) + void populateFolderData({ folderID }) + } + }} + redirectAfterCreate={false} + /> + + + + ) +} diff --git a/packages/ui/src/elements/ListHeader/TitleActions/index.tsx b/packages/ui/src/elements/ListHeader/TitleActions/index.tsx index 27153c3415..8989634388 100644 --- a/packages/ui/src/elements/ListHeader/TitleActions/index.tsx +++ b/packages/ui/src/elements/ListHeader/TitleActions/index.tsx @@ -1,66 +1,3 @@ -'use client' - -import type { I18nClient, TFunction } from '@payloadcms/translations' -import type { ClientCollectionConfig } from 'payload' - -import { getTranslation } from '@payloadcms/translations' - -import { Button } from '../../Button/index.js' - -const baseClass = 'list-header' - -type DefaultTitleActionsProps = { - collectionConfig: ClientCollectionConfig - hasCreatePermission: boolean - i18n: I18nClient - isBulkUploadEnabled: boolean - newDocumentURL: string - openBulkUpload: () => void - t: TFunction -} - -export const DefaultTitleActions = ({ - collectionConfig, - hasCreatePermission, - i18n, - isBulkUploadEnabled, - newDocumentURL, - openBulkUpload, - t, -}: DefaultTitleActionsProps): React.ReactNode[] => { - const Actions: React.ReactNode[] = [] - - if (hasCreatePermission) { - Actions.push( - , - ) - } - - if (hasCreatePermission && isBulkUploadEnabled) { - Actions.push( - , - ) - } - - return Actions -} +export { ListBulkUploadButton } from './ListBulkUploadButton.js' +export { ListCreateNewButton } from './ListCreateNewDocButton.js' +export { ListCreateNewDocInFolderButton } from './ListCreateNewDocInFolderButton.js' diff --git a/packages/ui/src/providers/Folders/index.tsx b/packages/ui/src/providers/Folders/index.tsx index a7bb57a199..35d17d6fa0 100644 --- a/packages/ui/src/providers/Folders/index.tsx +++ b/packages/ui/src/providers/Folders/index.tsx @@ -1,6 +1,6 @@ 'use client' -import type { CollectionSlug } from 'payload' +import type { ClientCollectionConfig, CollectionSlug } from 'payload' import { useRouter, useSearchParams } from 'next/navigation.js' import { @@ -40,6 +40,7 @@ export type FolderContextValue = { deleteCurrentFolder: () => Promise documents?: GetFolderDataResult['documents'] focusedRowIndex: number + folderCollectionConfig: ClientCollectionConfig folderCollectionSlug: string folderID?: number | string getSelectedItems?: () => PolymorphicRelationshipValue[] @@ -72,6 +73,7 @@ const Context = React.createContext({ deleteCurrentFolder: () => Promise.resolve(undefined), documents: [], focusedRowIndex: -1, + folderCollectionConfig: null, folderCollectionSlug: '', folderID: undefined, getSelectedItems: () => [], @@ -98,7 +100,6 @@ export type FolderProviderData = { folderID?: number | string subfolders?: GetFolderDataResult['subfolders'] } & GetFolderDataResult -const folderCollectionSlug = '_folders' type Props = { readonly allowMultiSelection?: boolean readonly children: React.ReactNode @@ -120,6 +121,10 @@ export function FolderProvider({ const { refineListData } = useListQuery() const { startRouteTransition } = useRouteTransition() const { clearRouteCache } = useRouteCache() + const [folderCollectionConfig] = React.useState(() => + config.collections.find((collection) => collection.slug === config.folders.slug), + ) + const folderCollectionSlug = folderCollectionConfig.slug const [isDragging, setIsDragging] = React.useState(false) const [activeFolderID, setActiveFolderID] = React.useState( @@ -197,7 +202,7 @@ export function FolderProvider({ setLoadingStatus('error') } }, - [routes.api, serverURL, searchParams], + [routes.api, serverURL, searchParams, folderCollectionSlug], ) const setNewActiveFolderID: FolderContextValue['setFolderID'] = React.useCallback( @@ -485,7 +490,7 @@ export function FolderProvider({ toast.success(t('general:deletedSuccessfully')) }, - [routes.api, serverURL, t, clearRouteCache], + [routes.api, serverURL, t, clearRouteCache, folderCollectionSlug], ) const deleteCurrentFolder = React.useCallback(async () => { @@ -496,7 +501,7 @@ export function FolderProvider({ credentials: 'include', method: 'DELETE', }) - }, [activeFolderID, routes.api, serverURL]) + }, [activeFolderID, routes.api, serverURL, folderCollectionSlug]) const moveToFolder: FolderContextValue['moveToFolder'] = React.useCallback( async (args) => { @@ -598,7 +603,7 @@ export function FolderProvider({ clearSelections() clearRouteCache() }, - [t, clearSelections, clearRouteCache, serverURL, routes.api], + [t, clearSelections, clearRouteCache, serverURL, routes.api, folderCollectionSlug], ) React.useEffect(() => { @@ -629,6 +634,7 @@ export function FolderProvider({ deleteCurrentFolder, documents, focusedRowIndex, + folderCollectionConfig, folderCollectionSlug, folderID: activeFolderID, getSelectedItems, diff --git a/packages/ui/src/views/CollectionFolder/ListHeader/index.scss b/packages/ui/src/views/CollectionFolder/ListHeader/index.scss deleted file mode 100644 index 5de51d1f35..0000000000 --- a/packages/ui/src/views/CollectionFolder/ListHeader/index.scss +++ /dev/null @@ -1,86 +0,0 @@ -@import '../../../scss/styles.scss'; - -@layer payload-default { - .list-header { - &__folder-view-buttons { - display: flex; - gap: calc(var(--base) * 0.5); - margin-left: calc(var(--base) * 0.5); - } - } - .list-drawer { - .list-header__title { - @extend %h2; - } - - .doc-drawer__toggler.list-header__create-new-button { - background: transparent; - border: 0; - padding: 0; - cursor: pointer; - color: inherit; - border-radius: var(--style-radius-s); - - &:hover .pill { - background: var(--theme-elevation-250); - } - - &:focus:not(:focus-visible), - &:focus-within:not(:focus-visible) { - outline: none; - } - - &:focus-visible { - outline: var(--accessibility-outline); - outline-offset: var(--accessibility-outline-offset); - } - - &:disabled { - pointer-events: none; - } - - .pill { - cursor: inherit; - } - } - - &__close-drawer-button { - flex-shrink: 0; - border: 0; - background-color: transparent; - padding: 0; - margin: 0; - cursor: pointer; - overflow: hidden; - width: base(1); - height: base(1); - - svg { - width: base(2); - height: base(2); - position: relative; - inset-inline-start: base(-0.5); - top: base(-0.5); - - .stroke { - stroke-width: 2px; - vector-effect: non-scaling-stroke; - } - } - } - - &__select-collection-wrap { - margin-top: base(1); - } - - @include mid-break { - .collection-list__header { - margin-bottom: base(0.5); - } - - &__select-collection-wrap { - margin-top: calc(var(--base) / 2); - } - } - } -} diff --git a/packages/ui/src/views/CollectionFolder/ListHeader/index.tsx b/packages/ui/src/views/CollectionFolder/ListHeader/index.tsx deleted file mode 100644 index f206a6cab8..0000000000 --- a/packages/ui/src/views/CollectionFolder/ListHeader/index.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import type { I18nClient, TFunction } from '@payloadcms/translations' -import type { ClientCollectionConfig } from 'payload' - -import { useModal } from '@faceless-ui/modal' -import { getTranslation } from '@payloadcms/translations' -import { formatAdminURL } from 'payload/shared' -import React from 'react' - -import { Button } from '../../../elements/Button/index.js' -import { useListDrawerContext } from '../../../elements/ListDrawer/Provider.js' -import { ListFolderPills } from '../../../elements/ListFolderPills/index.js' -import { DrawerRelationshipSelect } from '../../../elements/ListHeader/DrawerRelationshipSelect/index.js' -import { DefaultDrawerTitleActions } from '../../../elements/ListHeader/DrawerTitleActions/index.js' -import { CollectionListHeader } from '../../../elements/ListHeader/index.js' -import { DefaultTitleActions } from '../../../elements/ListHeader/TitleActions/index.js' -import { XIcon } from '../../../icons/X/index.js' -import { useConfig } from '../../../providers/Config/index.js' -import './index.scss' -import { ListSelection } from '../ListSelection/index.js' - -const drawerBaseClass = 'list-drawer' -const baseClass = 'list-header' -export type ListHeaderProps = { - Actions?: React.ReactNode[] - className?: string - collectionConfig: ClientCollectionConfig - Description?: React.ReactNode - disableBulkDelete?: boolean - disableBulkEdit?: boolean - hasCreatePermission: boolean - i18n: I18nClient - isBulkUploadEnabled: boolean - newDocumentURL: string - openBulkUpload: () => void - smallBreak: boolean - t: TFunction - TitleActions?: React.ReactNode[] - viewType?: 'folders' | 'list' -} - -export const ListHeader: React.FC = ({ - className, - collectionConfig, - Description, - disableBulkDelete, - disableBulkEdit, - hasCreatePermission, - i18n, - isBulkUploadEnabled, - newDocumentURL, - openBulkUpload, - smallBreak, - t, - viewType, -}) => { - const { config, getEntityConfig } = useConfig() - const { drawerSlug, isInDrawer, selectedOption } = useListDrawerContext() - const { closeModal } = useModal() - - if (isInDrawer) { - return ( - { - closeModal(drawerSlug) - }} - type="button" - > - - , - ]} - AfterListHeaderContent={ - <> - {Description} - {} - - } - className={`${drawerBaseClass}__header`} - collectionConfig={getEntityConfig({ collectionSlug: selectedOption.value })} - TitleActions={DefaultDrawerTitleActions({ - hasCreatePermission, - t, - }).filter(Boolean)} - /> - ) - } - - return ( - - ), - Object.keys(config.folders.collections).includes(collectionConfig.slug) && ( - - ), - ].filter(Boolean)} - AfterListHeaderContent={Description} - className={className} - collectionConfig={collectionConfig} - TitleActions={DefaultTitleActions({ - collectionConfig, - hasCreatePermission, - i18n, - isBulkUploadEnabled, - newDocumentURL, - openBulkUpload, - t, - }).filter(Boolean)} - /> - ) -} diff --git a/packages/ui/src/views/CollectionFolder/ListSelection/index.tsx b/packages/ui/src/views/CollectionFolder/ListSelection/index.tsx index aa635d1268..57c5b09ffd 100644 --- a/packages/ui/src/views/CollectionFolder/ListSelection/index.tsx +++ b/packages/ui/src/views/CollectionFolder/ListSelection/index.tsx @@ -62,7 +62,8 @@ export const ListSelection: React.FC = ({ const count = items.length const singleNonFolderCollectionSelected = - Object.keys(groupedSelections).length === 1 && Object.keys(groupedSelections)[0] !== '_folders' + Object.keys(groupedSelections).length === 1 && + Object.keys(groupedSelections)[0] !== config.folders.slug const collectionConfig = singleNonFolderCollectionSelected ? config.collections.find((collection) => { return collection.slug === Object.keys(groupedSelections)[0] diff --git a/packages/ui/src/views/CollectionFolder/index.tsx b/packages/ui/src/views/CollectionFolder/index.tsx index 1e087a1cd3..4618d2089d 100644 --- a/packages/ui/src/views/CollectionFolder/index.tsx +++ b/packages/ui/src/views/CollectionFolder/index.tsx @@ -5,22 +5,26 @@ import type { FolderListViewClientProps } from 'payload' import { useDndMonitor } from '@dnd-kit/core' import { getTranslation } from '@payloadcms/translations' -import { useRouter } from 'next/navigation.js' import React, { Fragment, useEffect, useState } from 'react' -import { useBulkUpload } from '../../elements/BulkUpload/index.js' import { Button } from '../../elements/Button/index.js' +import { CloseModalButton } from '../../elements/CloseModalButton/index.js' import { DroppableBreadcrumb } from '../../elements/FolderView/Breadcrumbs/index.js' import { DragOverlaySelection } from '../../elements/FolderView/DragOverlaySelection/index.js' import { Gutter } from '../../elements/Gutter/index.js' import { ListControls } from '../../elements/ListControls/index.js' import { useListDrawerContext } from '../../elements/ListDrawer/Provider.js' +import { ListFolderPills } from '../../elements/ListFolderPills/index.js' +import { CollectionListHeader } from '../../elements/ListHeader/index.js' +import { + ListBulkUploadButton, + ListCreateNewDocInFolderButton, +} from '../../elements/ListHeader/TitleActions/index.js' import { useModal } from '../../elements/Modal/index.js' -import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' +import { Pill } from '../../elements/Pill/index.js' import { SelectMany } from '../../elements/SelectMany/index.js' import { useStepNav } from '../../elements/StepNav/index.js' import { RelationshipProvider } from '../../elements/Table/RelationshipProvider/index.js' -import { ViewDescription } from '../../elements/ViewDescription/index.js' import { FolderIcon } from '../../icons/Folder/index.js' import { useConfig } from '../../providers/Config/index.js' import { useEditDepth } from '../../providers/EditDepth/index.js' @@ -28,10 +32,11 @@ import { useFolder } from '../../providers/Folders/index.js' import { TableColumnsProvider } from '../../providers/TableColumns/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { useWindowInfo } from '../../providers/WindowInfo/index.js' -import { ListHeader } from './ListHeader/index.js' +import { ListSelection } from './ListSelection/index.js' import './index.scss' const baseClass = 'collection-folder-list' +const drawerBaseClass = 'collection-folder-list-drawer' export function DefaultCollectionFolderView(props: FolderListViewClientProps) { const { @@ -59,8 +64,11 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) { const { allowCreate, createNewDrawerSlug, + DocumentDrawerToggler, drawerSlug: listDrawerSlug, + isInDrawer, onBulkSelect, + selectedOption, } = useListDrawerContext() const hasCreatePermission = @@ -75,7 +83,6 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) { }, [InitialTable]) const { getEntityConfig } = useConfig() - const router = useRouter() const { breadcrumbs, @@ -89,8 +96,6 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) { subfolders, } = useFolder() const { openModal } = useModal() - const { setCollectionSlug, setCurrentActivePath, setOnSuccess } = useBulkUpload() - const { drawerSlug: bulkUploadDrawerSlug } = useBulkUpload() const collectionConfig = getEntityConfig({ collectionSlug }) @@ -100,8 +105,6 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) { const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload - const isInDrawer = Boolean(listDrawerSlug) - const { i18n, t } = useTranslation() const drawerDepth = useEditDepth() @@ -112,21 +115,6 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) { breakpoints: { s: smallBreak }, } = useWindowInfo() - const openBulkUpload = React.useCallback(() => { - setCollectionSlug(collectionSlug) - setCurrentActivePath(collectionSlug) - openModal(bulkUploadDrawerSlug) - setOnSuccess(collectionSlug, () => router.refresh()) - }, [ - router, - collectionSlug, - bulkUploadDrawerSlug, - openModal, - setCollectionSlug, - setCurrentActivePath, - setOnSuccess, - ]) - useEffect(() => { if (!drawerDepth) { setStepNav([ @@ -201,32 +189,53 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
{BeforeFolderList} - - - } - /> -
- } - disableBulkDelete={disableBulkDelete} - disableBulkEdit={disableBulkEdit} - hasCreatePermission={hasCreatePermission} - i18n={i18n} - isBulkUploadEnabled={isBulkUploadEnabled && !upload.hideFileInputOnCreate} - newDocumentURL={newDocumentURL} - openBulkUpload={openBulkUpload} - smallBreak={smallBreak} - t={t} - viewType="folders" - /> + {isInDrawer ? ( + ]} + AfterListHeaderContent={Description} + className={`${drawerBaseClass}__header`} + collectionConfig={getEntityConfig({ collectionSlug: selectedOption.value })} + TitleActions={[ + hasCreatePermission && ( + + {t('general:createNew')} + + ), + ].filter(Boolean)} + /> + ) : ( + + ), + , + ].filter(Boolean)} + AfterListHeaderContent={Description} + collectionConfig={collectionConfig} + TitleActions={[ + ListCreateNewDocInFolderButton({ + collectionSlugs: [collectionSlug], + }), + ListBulkUploadButton({ + collectionSlug, + hasCreatePermission, + isBulkUploadEnabled, + }), + ].filter(Boolean)} + /> + )} void + /** @deprecated This prop will be removed in the next major version. + * + * Opening of the bulk upload modal is handled internally. + * + * Prefer `onBulkUploadSuccess` usage to handle the success of the bulk upload. + */ openBulkUpload: () => void smallBreak: boolean t: TFunction @@ -48,6 +55,7 @@ export const ListHeader: React.FC = ({ i18n, isBulkUploadEnabled, newDocumentURL, + onBulkUploadSuccess, openBulkUpload, smallBreak, t, @@ -55,23 +63,16 @@ export const ListHeader: React.FC = ({ }) => { const { config, getEntityConfig } = useConfig() const { drawerSlug, isInDrawer, selectedOption } = useListDrawerContext() - const { closeModal } = useModal() if (isInDrawer) { return ( { - closeModal(drawerSlug) - }} - type="button" - > - - , + slug={drawerSlug} + />, ]} AfterListHeaderContent={ <> @@ -81,10 +82,11 @@ export const ListHeader: React.FC = ({ } className={`${drawerBaseClass}__header`} collectionConfig={getEntityConfig({ collectionSlug: selectedOption.value })} - TitleActions={DefaultDrawerTitleActions({ - hasCreatePermission, - t, - }).filter(Boolean)} + TitleActions={[ + ListDrawerCreateNewDocButton({ + hasCreatePermission, + }), + ].filter(Boolean)} /> ) } @@ -112,15 +114,23 @@ export const ListHeader: React.FC = ({ AfterListHeaderContent={Description} className={className} collectionConfig={collectionConfig} - TitleActions={DefaultTitleActions({ - collectionConfig, - hasCreatePermission, - i18n, - isBulkUploadEnabled, - newDocumentURL, - openBulkUpload, - t, - }).filter(Boolean)} + TitleActions={[ + hasCreatePermission && + ListCreateNewButton({ + collectionConfig, + hasCreatePermission, + newDocumentURL, + }), + hasCreatePermission && + isBulkUploadEnabled && + ListBulkUploadButton({ + collectionSlug: collectionConfig.slug, + hasCreatePermission, + isBulkUploadEnabled, + onBulkUploadSuccess, + openBulkUpload, + }), + ].filter(Boolean)} /> ) } diff --git a/packages/ui/src/views/List/index.tsx b/packages/ui/src/views/List/index.tsx index b16d4d108f..2129c7cd8e 100644 --- a/packages/ui/src/views/List/index.tsx +++ b/packages/ui/src/views/List/index.tsx @@ -59,12 +59,7 @@ export function DefaultListView(props: ListViewClientProps) { const [Table, setTable] = useState(InitialTable) - const { - allowCreate, - createNewDrawerSlug, - drawerSlug: listDrawerSlug, - onBulkSelect, - } = useListDrawerContext() + const { allowCreate, createNewDrawerSlug, isInDrawer, onBulkSelect } = useListDrawerContext() const hasCreatePermission = allowCreate !== undefined @@ -91,8 +86,12 @@ export function DefaultListView(props: ListViewClientProps) { } = useListQuery() const { openModal } = useModal() - const { setCollectionSlug, setCurrentActivePath, setOnSuccess } = useBulkUpload() - const { drawerSlug: bulkUploadDrawerSlug } = useBulkUpload() + const { + drawerSlug: bulkUploadDrawerSlug, + setCollectionSlug, + setCurrentActivePath, + setOnSuccess, + } = useBulkUpload() const collectionConfig = getEntityConfig({ collectionSlug }) @@ -102,8 +101,6 @@ export function DefaultListView(props: ListViewClientProps) { const isBulkUploadEnabled = isUploadCollection && collectionConfig.upload.bulkUpload - const isInDrawer = Boolean(listDrawerSlug) - const { i18n, t } = useTranslation() const { setStepNav } = useStepNav() diff --git a/test/fields/collections/UploadRestricted/e2e.spec.ts b/test/fields/collections/UploadRestricted/e2e.spec.ts index 8238d8510b..7943401cf7 100644 --- a/test/fields/collections/UploadRestricted/e2e.spec.ts +++ b/test/fields/collections/UploadRestricted/e2e.spec.ts @@ -90,7 +90,7 @@ describe('Upload with restrictions', () => { .locator('.list-drawer__header') .locator('button', { hasText: 'Create New' }) await expect(createNewHeader).toBeVisible() - await page.locator('.list-drawer__header-close').click() + await page.locator('.list-drawer__header .close-modal-button').click() await expect(drawer).toBeHidden() const fieldWithAllowCreateFalse = page.locator('#field-uploadWithAllowCreateFalse') await expect(fieldWithAllowCreateFalse).toBeVisible() diff --git a/test/helpers/e2e/toggleListDrawer.ts b/test/helpers/e2e/toggleListDrawer.ts index 5be807f098..61d518e4d6 100644 --- a/test/helpers/e2e/toggleListDrawer.ts +++ b/test/helpers/e2e/toggleListDrawer.ts @@ -9,6 +9,6 @@ export const closeListDrawer = async ({ drawerSelector?: string page: Page }): Promise => { - await page.locator('[id^=list-drawer_1_] .list-drawer__header-close').click() + await page.locator('[id^=list-drawer_1_] .list-drawer__header .close-modal-button').click() await expect(page.locator(drawerSelector)).not.toBeVisible() }