feat(ui): moves folder rendering from the client to the server (#12710)

This commit is contained in:
Jarrod Flesch
2025-06-10 11:56:28 -04:00
committed by GitHub
parent 9d2817e647
commit a43d1a685f
29 changed files with 1073 additions and 1373 deletions

View File

@@ -3,6 +3,7 @@ import type { ServerFunction, ServerFunctionHandler } from 'payload'
import { copyDataFromLocaleHandler } from '@payloadcms/ui/rsc'
import { buildFormStateHandler } from '@payloadcms/ui/utilities/buildFormState'
import { buildTableStateHandler } from '@payloadcms/ui/utilities/buildTableState'
import { getFolderResultsComponentAndDataHandler } from '@payloadcms/ui/utilities/getFolderResultsComponentAndData'
import { schedulePublishHandler } from '@payloadcms/ui/utilities/schedulePublishHandler'
import { renderDocumentHandler } from '../views/Document/handleServerFunction.js'
@@ -28,6 +29,8 @@ export const handleServerFunctions: ServerFunctionHandler = async (args) => {
const serverFunctions = {
'copy-data-from-locale': copyDataFromLocaleHandler as any as ServerFunction,
'form-state': buildFormStateHandler as any as ServerFunction,
'get-folder-results-component-and-data':
getFolderResultsComponentAndDataHandler as any as ServerFunction,
'render-document': renderDocumentHandler as any as ServerFunction,
'render-document-slots': renderDocumentSlotsHandler as any as ServerFunction,
'render-list': renderListHandler as any as ServerFunction,

View File

@@ -1,21 +1,19 @@
import type {
AdminViewServerProps,
BuildCollectionFolderViewResult,
FolderListViewClientProps,
FolderListViewServerPropsOnly,
FolderSortKeys,
ListQuery,
Where,
} from 'payload'
import { DefaultBrowseByFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { DefaultBrowseByFolderView, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { getFolderResultsComponentAndData, upsertPreferences } from '@payloadcms/ui/rsc'
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'
export type BuildFolderViewArgs = {
customCellProps?: Record<string, any>
disableBulkDelete?: boolean
@@ -56,18 +54,18 @@ export const buildBrowseByFolderView = async (
visibleEntities,
} = initPageResult
if (config.folders === false || config.folders.browseByFolder === false) {
throw new Error('not-found')
}
const browseByFolderSlugs = browseByFolderSlugsFromArgs.filter(
(collectionSlug) =>
permissions?.collections?.[collectionSlug]?.read &&
visibleEntities.collections.includes(collectionSlug),
)
if (config.folders === false || config.folders.browseByFolder === false) {
throw new Error('not-found')
}
const query = queryFromArgs || queryFromReq
const selectedCollectionSlugs: string[] =
const activeCollectionFolderSlugs: string[] =
Array.isArray(query?.relationTo) && query.relationTo.length
? query.relationTo.filter(
(slug) =>
@@ -79,62 +77,35 @@ export const buildBrowseByFolderView = async (
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,
/**
* @todo: find a pattern to avoid setting preferences on hard navigation, i.e. direct links, page refresh, etc.
* This will ensure that prefs are only updated when explicitly set by the user
* This could potentially be done by injecting a `sessionID` into the params and comparing it against a session cookie
*/
const browseByFolderPreferences = await upsertPreferences<{
sort?: FolderSortKeys
viewPreference?: 'grid' | 'list'
}>({
key: 'browse-by-folder',
req: initPageResult.req,
value: {
sort: query?.sort as FolderSortKeys,
},
})
const sortPreference: FolderSortKeys = browseByFolderPreferences?.sort || '_folderOrDocumentTitle'
const viewPreference = browseByFolderPreferences?.viewPreference || 'grid'
const { breadcrumbs, documents, FolderResultsComponent, subfolders } =
await getFolderResultsComponentAndData({
activeCollectionSlugs: activeCollectionFolderSlugs,
browseByFolder: false,
displayAs: viewPreference,
folderID,
req: initPageResult.req,
sort: sortPreference,
})
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
if (
@@ -172,38 +143,39 @@ export const buildBrowseByFolderView = async (
// })
// documents cannot be created without a parent folder in this view
const hasCreatePermissionCollectionSlugs = folderID
const allowCreateCollectionSlugs = resolvedFolderID
? [config.folders.slug, ...browseByFolderSlugs]
: [config.folders.slug]
return {
View: (
<FolderProvider
breadcrumbs={breadcrumbs}
documents={documents}
filteredCollectionSlugs={selectedCollectionSlugs}
folderCollectionSlugs={browseByFolderSlugs}
folderFieldName={config.folders.fieldName}
folderID={folderID}
subfolders={subfolders}
>
<>
<HydrateAuthProvider permissions={permissions} />
{RenderServerComponent({
clientProps: {
// ...folderViewSlots,
activeCollectionFolderSlugs,
allCollectionFolderSlugs: browseByFolderSlugs,
allowCreateCollectionSlugs,
baseFolderPath: `/browse-by-folder`,
breadcrumbs,
disableBulkDelete,
disableBulkEdit,
documents,
enableRowSelections,
hasCreatePermissionCollectionSlugs,
selectedCollectionSlugs,
viewPreference: browseByFolderPreferences?.value?.viewPreference,
},
// Component:config.folders?.components?.views?.list?.Component,
folderFieldName: config.folders.fieldName,
folderID: resolvedFolderID || null,
FolderResultsComponent,
sort: sortPreference,
subfolders,
viewPreference,
} satisfies FolderListViewClientProps,
// Component:config.folders?.components?.views?.BrowseByFolders?.Component,
Fallback: DefaultBrowseByFolderView,
importMap: payload.importMap,
serverProps,
})}
</FolderProvider>
</>
),
}
}

View File

@@ -1,21 +1,19 @@
import type {
AdminViewServerProps,
BuildCollectionFolderViewResult,
FolderListViewClientProps,
FolderListViewServerPropsOnly,
FolderSortKeys,
ListQuery,
Where,
} from 'payload'
import { DefaultCollectionFolderView, FolderProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { DefaultCollectionFolderView, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { getFolderResultsComponentAndData, upsertPreferences } from '@payloadcms/ui/rsc'
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'
// import { renderFolderViewSlots } from './renderFolderViewSlots.js'
export type BuildCollectionFolderViewStateArgs = {
@@ -62,24 +60,18 @@ export const buildCollectionFolderView = async (
visibleEntities,
} = initPageResult
if (!permissions?.collections?.[collectionSlug]?.read) {
if (!config.folders) {
throw new Error('not-found')
}
if (
!permissions?.collections?.[collectionSlug]?.read ||
!permissions?.collections?.[config.folders.slug].read
) {
throw new Error('not-found')
}
if (collectionConfig) {
const query = queryFromArgs || queryFromReq
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 },
} = config
if (
(!visibleEntities.collections.includes(collectionSlug) && !overrideEntityVisibility) ||
!config.folders
@@ -87,41 +79,41 @@ export const buildCollectionFolderView = async (
throw new Error('not-found')
}
let folderWhere: undefined | Where
const folderCollectionConfig = payload.collections[config.folders.slug].config
const folderCollectionConstraints = await buildFolderWhereConstraints({
collectionConfig: folderCollectionConfig,
folderID,
localeCode: fullLocale?.code,
const query = queryFromArgs || queryFromReq
/**
* @todo: find a pattern to avoid setting preferences on hard navigation, i.e. direct links, page refresh, etc.
* This will ensure that prefs are only updated when explicitly set by the user
* This could potentially be done by injecting a `sessionID` into the params and comparing it against a session cookie
*/
const collectionFolderPreferences = await upsertPreferences<{
sort?: FolderSortKeys
viewPreference?: 'grid' | 'list'
}>({
key: `${collectionSlug}-collection-folder`,
req: initPageResult.req,
search: typeof query?.search === 'string' ? query.search : undefined,
sort: sortPreference,
value: {
sort: query?.sort as FolderSortKeys,
},
})
if (folderCollectionConstraints) {
folderWhere = folderCollectionConstraints
}
const sortPreference: FolderSortKeys =
collectionFolderPreferences?.sort || '_folderOrDocumentTitle'
const viewPreference = collectionFolderPreferences?.viewPreference || 'grid'
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 {
routes: { admin: adminRoute },
} = config
const { breadcrumbs, documents, subfolders } = await getFolderData({
collectionSlug,
documentWhere,
folderID,
folderWhere,
req: initPageResult.req,
})
const { breadcrumbs, documents, FolderResultsComponent, subfolders } =
await getFolderResultsComponentAndData({
activeCollectionSlugs: [config.folders.slug, collectionSlug],
browseByFolder: false,
displayAs: viewPreference,
folderID,
req: initPageResult.req,
sort: sortPreference,
})
const resolvedFolderID = breadcrumbs[breadcrumbs.length - 1]?.id
@@ -139,13 +131,6 @@ export const buildCollectionFolderView = async (
)
}
const newDocumentURL = formatAdminURL({
adminRoute,
path: `/collections/${collectionSlug}/create`,
})
const hasCreatePermission = permissions?.collections?.[collectionSlug]?.create
const serverProps: FolderListViewServerPropsOnly = {
collectionConfig,
documents,
@@ -178,34 +163,39 @@ export const buildCollectionFolderView = async (
return {
View: (
<FolderProvider
breadcrumbs={breadcrumbs}
collectionSlug={collectionSlug}
documents={documents}
folderCollectionSlugs={[collectionSlug]}
folderFieldName={config.folders.fieldName}
folderID={folderID}
search={search}
subfolders={subfolders}
>
<>
<HydrateAuthProvider permissions={permissions} />
{RenderServerComponent({
clientProps: {
// ...folderViewSlots,
allCollectionFolderSlugs: [config.folders.slug, collectionSlug],
allowCreateCollectionSlugs: [
permissions?.collections?.[config.folders.slug]?.create
? config.folders.slug
: null,
permissions?.collections?.[collectionSlug]?.create ? collectionSlug : null,
].filter(Boolean),
baseFolderPath: `/collections/${collectionSlug}/${config.folders.slug}`,
breadcrumbs,
collectionSlug,
disableBulkDelete,
disableBulkEdit,
documents,
enableRowSelections,
hasCreatePermission,
newDocumentURL,
viewPreference: collectionFolderPreferences?.value?.viewPreference,
},
Component: collectionConfig?.admin?.components?.views?.list?.Component,
folderFieldName: config.folders.fieldName,
folderID: resolvedFolderID || null,
FolderResultsComponent,
search,
sort: sortPreference,
subfolders,
viewPreference,
} satisfies FolderListViewClientProps,
// Component: collectionConfig?.admin?.components?.views?.Folders?.Component,
Fallback: DefaultCollectionFolderView,
importMap: payload.importMap,
serverProps,
})}
</FolderProvider>
</>
),
}
}

View File

@@ -427,7 +427,7 @@ const PreviewView: React.FC<Props> = ({
: currentEditor !== user?.id) &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
// eslint-disable-next-line react-compiler/react-compiler
!documentLockStateRef.current?.hasShownLockedModal &&
!isLockExpired

View File

@@ -75,3 +75,12 @@ export type BuildTableStateArgs = {
export type BuildCollectionFolderViewResult = {
View: React.ReactNode
}
export type GetFolderResultsComponentAndDataArgs = {
activeCollectionSlugs: CollectionSlug[]
browseByFolder: boolean
displayAs: 'grid' | 'list'
folderID: number | string | undefined
req: PayloadRequest
sort: string
}

View File

@@ -572,6 +572,7 @@ export type {
BuildCollectionFolderViewResult,
BuildTableStateArgs,
DefaultServerFunctionArgs,
GetFolderResultsComponentAndDataArgs,
ListQuery,
ServerFunction,
ServerFunctionArgs,

View File

@@ -1,5 +1,5 @@
import type { ServerProps } from '../../config/types.js'
import type { FolderOrDocument } from '../../folders/types.js'
import type { FolderBreadcrumb, FolderOrDocument, FolderSortKeys } from '../../folders/types.js'
import type { SanitizedCollectionConfig } from '../../index.js'
export type FolderListViewSlots = {
AfterFolderList?: React.ReactNode
@@ -8,7 +8,6 @@ export type FolderListViewSlots = {
BeforeFolderListTable?: React.ReactNode
Description?: React.ReactNode
listMenuItems?: React.ReactNode[]
Table: React.ReactNode
}
export type FolderListViewServerPropsOnly = {
@@ -20,13 +19,23 @@ export type FolderListViewServerPropsOnly = {
export type FolderListViewServerProps = FolderListViewClientProps & FolderListViewServerPropsOnly
export type FolderListViewClientProps = {
activeCollectionFolderSlugs?: SanitizedCollectionConfig['slug'][]
allCollectionFolderSlugs: SanitizedCollectionConfig['slug'][]
allowCreateCollectionSlugs: SanitizedCollectionConfig['slug'][]
baseFolderPath: `/${string}`
beforeActions?: React.ReactNode[]
collectionSlug: SanitizedCollectionConfig['slug']
breadcrumbs: FolderBreadcrumb[]
collectionSlug?: SanitizedCollectionConfig['slug']
disableBulkDelete?: boolean
disableBulkEdit?: boolean
documents: FolderOrDocument[]
enableRowSelections?: boolean
hasCreatePermission: boolean
newDocumentURL: string
folderFieldName: string
folderID: null | number | string
FolderResultsComponent: React.ReactNode
search?: string
sort?: FolderSortKeys
subfolders: FolderOrDocument[]
viewPreference: 'grid' | 'list'
} & FolderListViewSlots

View File

@@ -113,3 +113,10 @@ export type CollectionFoldersConfiguration = {
*/
browseByFolder?: boolean
}
type BaseFolderSortKeys = keyof Pick<
FolderOrDocument['value'],
'_folderOrDocumentTitle' | 'createdAt' | 'updatedAt'
>
export type FolderSortKeys = `-${BaseFolderSortKeys}` | BaseFolderSortKeys

View File

@@ -1398,6 +1398,7 @@ export type {
UploadFieldValidation,
UsernameFieldValidation,
} from './fields/validations.js'
export type { FolderSortKeys } from './folders/types.js'
export { getFolderData } from './folders/utils/getFolderData.js'
export {
type ClientGlobalConfig,

View File

@@ -56,6 +56,11 @@
"types": "./src/utilities/buildTableState.ts",
"default": "./src/utilities/buildTableState.ts"
},
"./utilities/getFolderResultsComponentAndData": {
"import": "./src/utilities/getFolderResultsComponentAndData.tsx",
"types": "./src/utilities/getFolderResultsComponentAndData.tsx",
"default": "./src/utilities/getFolderResultsComponentAndData.tsx"
},
"./utilities/getClientSchemaMap": {
"import": "./src/utilities/getClientSchemaMap.ts",
"types": "./src/utilities/getClientSchemaMap.ts",

View File

@@ -13,8 +13,12 @@ import './index.scss'
const baseClass = 'collection-type'
export function CollectionTypePill() {
const { filterItems, folderCollectionSlug, folderCollectionSlugs, visibleCollectionSlugs } =
useFolder()
const {
activeCollectionFolderSlugs: visibleCollectionSlugs,
allCollectionFolderSlugs: folderCollectionSlugs,
folderCollectionSlug,
refineFolderData,
} = useFolder()
const { i18n, t } = useTranslation()
const { config, getEntityConfig } = useConfig()
@@ -53,8 +57,8 @@ export function CollectionTypePill() {
</Button>
}
key="relation-to-selection-popup"
onChange={({ selectedValues }) => {
void filterItems({ relationTo: selectedValues })
onChange={({ selectedValues: relationTo }) => {
void refineFolderData({ query: { relationTo }, updateURL: true })
}}
options={allCollectionOptions}
selectedValues={visibleCollectionSlugs}

View File

@@ -1,11 +1,14 @@
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js'
import React from 'react'
import { toast } from 'sonner'
import { Dots } from '../../../icons/Dots/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useFolder } from '../../../providers/Folders/index.js'
import { useRouteCache } from '../../../providers/RouteCache/index.js'
import { useRouteTransition } from '../../../providers/RouteTransition/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { ConfirmationModal } from '../../ConfirmationModal/index.js'
import { useDocumentDrawer } from '../../DocumentDrawer/index.js'
@@ -22,6 +25,8 @@ type Props = {
className?: string
}
export function CurrentFolderActions({ className }: Props) {
const router = useRouter()
const { startRouteTransition } = useRouteTransition()
const {
breadcrumbs,
currentFolder,
@@ -29,15 +34,15 @@ export function CurrentFolderActions({ className }: Props) {
folderCollectionSlug,
folderFieldName,
folderID,
getFolderRoute,
moveToFolder,
renameFolder,
setFolderID,
} = useFolder()
const [FolderDocumentDrawer, , { closeDrawer: closeFolderDrawer, openDrawer: openFolderDrawer }] =
useDocumentDrawer({
id: folderID,
collectionSlug: folderCollectionSlug,
})
const { clearRouteCache } = useRouteCache()
const { config } = useConfig()
const { routes, serverURL } = config
const { closeModal, openModal } = useModal()
@@ -48,8 +53,19 @@ export function CurrentFolderActions({ className }: Props) {
credentials: 'include',
method: 'DELETE',
})
await setFolderID({ folderID: breadcrumbs[breadcrumbs.length - 2]?.id || null })
}, [breadcrumbs, folderCollectionSlug, folderID, routes.api, serverURL, setFolderID])
startRouteTransition(() => {
router.push(getFolderRoute(breadcrumbs[breadcrumbs.length - 2]?.id || null))
})
}, [
breadcrumbs,
folderCollectionSlug,
folderID,
getFolderRoute,
router,
serverURL,
routes.api,
startRouteTransition,
])
if (!folderID) {
return null
@@ -143,12 +159,9 @@ export function CurrentFolderActions({ className }: Props) {
/>
<FolderDocumentDrawer
onSave={(result) => {
renameFolder({
folderID: result.doc.id,
newName: result.doc[folderCollectionConfig.admin.useAsTitle],
})
onSave={() => {
closeFolderDrawer()
clearRouteCache()
}}
/>
</>

View File

@@ -1,5 +1,4 @@
import type { DocumentDrawerContextProps } from '../../../DocumentDrawer/Provider.js'
import { useRouteCache } from '../../../../providers/RouteCache/index.js'
import { useTranslation } from '../../../../providers/Translation/index.js'
import { useDocumentDrawer } from '../../../DocumentDrawer/index.js'
import { ListSelectionButton } from '../../../ListSelection/index.js'
@@ -7,9 +6,9 @@ import { ListSelectionButton } from '../../../ListSelection/index.js'
type EditFolderActionProps = {
folderCollectionSlug: string
id: number | string
onSave: DocumentDrawerContextProps['onSave']
}
export const EditFolderAction = ({ id, folderCollectionSlug, onSave }: EditFolderActionProps) => {
export const EditFolderAction = ({ id, folderCollectionSlug }: EditFolderActionProps) => {
const { clearRouteCache } = useRouteCache()
const { t } = useTranslation()
const [FolderDocumentDrawer, , { closeDrawer, openDrawer }] = useDocumentDrawer({
id,
@@ -27,9 +26,9 @@ export const EditFolderAction = ({ id, folderCollectionSlug, onSave }: EditFolde
</ListSelectionButton>
<FolderDocumentDrawer
onSave={async (args) => {
await onSave(args)
onSave={() => {
closeDrawer()
clearRouteCache()
}}
/>
</>

View File

@@ -1,20 +1,17 @@
'use client'
import type { CollectionSlug, Document } from 'payload'
import type {
FolderBreadcrumb,
FolderDocumentItemKey,
FolderOrDocument,
GetFolderDataResult,
} from 'payload/shared'
import type { FolderBreadcrumb, FolderOrDocument } from 'payload/shared'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import { formatFolderOrDocumentItem } from 'payload/shared'
import { extractID } from 'payload/shared'
import React from 'react'
import { useConfig } from '../../../../providers/Config/index.js'
import { useAuth } from '../../../../providers/Auth/index.js'
import { FolderProvider, useFolder } from '../../../../providers/Folders/index.js'
import { useRouteCache } from '../../../../providers/RouteCache/index.js'
import { useServerFunctions } from '../../../../providers/ServerFunctions/index.js'
import { useTranslation } from '../../../../providers/Translation/index.js'
import { Button } from '../../../Button/index.js'
import { ConfirmationModal } from '../../../ConfirmationModal/index.js'
@@ -28,7 +25,6 @@ import { NoListResults } from '../../../NoListResults/index.js'
import { Translation } from '../../../Translation/index.js'
import { FolderBreadcrumbs } from '../../Breadcrumbs/index.js'
import { ColoredFolderIcon } from '../../ColoredFolderIcon/index.js'
import { ItemCardGrid } from '../../ItemCardGrid/index.js'
import './index.scss'
const baseClass = 'move-folder-drawer'
@@ -59,6 +55,7 @@ export type MoveToFolderDrawerProps = {
id: null | number | string
name: null | string
}) => Promise<void> | void
readonly populateMoveToFolderDrawer?: (folderID: null | number | string) => Promise<void> | void
/**
* Set to `true` to skip the confirmation modal
* @default false
@@ -75,51 +72,49 @@ export function MoveItemsToFolderDrawer(props: MoveToFolderDrawerProps) {
}
function LoadFolderData(props: MoveToFolderDrawerProps) {
const {
config: { routes, serverURL },
} = useConfig()
const { permissions } = useAuth()
const [subfolders, setSubfolders] = React.useState<FolderOrDocument[]>([])
const [documents, setDocuments] = React.useState<FolderOrDocument[]>([])
const [breadcrumbs, setBreadcrumbs] = React.useState<FolderBreadcrumb[]>([])
const [FolderResultsComponent, setFolderResultsComponent] = React.useState<React.ReactNode>(null)
const [hasLoaded, setHasLoaded] = React.useState(false)
const [folderID, setFolderID] = React.useState<null | number | string>(props.fromFolderID || null)
const hasLoadedRef = React.useRef(false)
const { getFolderResultsComponentAndData } = useServerFunctions()
React.useEffect(() => {
const onLoad = async () => {
// call some endpoint to load the data
const populateMoveToFolderDrawer = React.useCallback(
async (folderIDToPopulate: null | number | string) => {
try {
const folderDataReq = await fetch(
`${serverURL}${routes.api}/${props.folderCollectionSlug}/populate-folder-data${props.fromFolderID ? `?folderID=${props.fromFolderID}` : ''}`,
{
credentials: 'include',
headers: {
'content-type': 'application/json',
},
},
)
const result = await getFolderResultsComponentAndData({
activeCollectionSlugs: [props.folderCollectionSlug],
browseByFolder: false,
displayAs: 'grid',
folderID: folderIDToPopulate,
sort: '_folderOrDocumentTitle',
})
if (folderDataReq.status === 200) {
const folderDataRes: GetFolderDataResult = await folderDataReq.json()
setBreadcrumbs(folderDataRes?.breadcrumbs || [])
setSubfolders(folderDataRes?.subfolders || [])
setDocuments(folderDataRes?.documents || [])
} else {
setBreadcrumbs([])
setSubfolders([])
setDocuments([])
}
setBreadcrumbs(result.breadcrumbs || [])
setSubfolders(result?.subfolders || [])
setDocuments(result?.documents || [])
setFolderResultsComponent(result.FolderResultsComponent || null)
setFolderID(folderIDToPopulate)
setHasLoaded(true)
} catch (e) {
// eslint-disable-next-line no-console
console.error(e)
setBreadcrumbs([])
setSubfolders([])
setDocuments([])
}
setHasLoaded(true)
}
hasLoadedRef.current = true
},
[getFolderResultsComponentAndData, props.folderCollectionSlug],
)
if (!hasLoaded) {
void onLoad()
React.useEffect(() => {
if (!hasLoadedRef.current) {
void populateMoveToFolderDrawer(props.fromFolderID)
}
}, [props.folderCollectionSlug, routes.api, serverURL, hasLoaded, props.fromFolderID])
}, [populateMoveToFolderDrawer, props.fromFolderID])
if (!hasLoaded) {
return <LoadingOverlay />
@@ -127,46 +122,58 @@ function LoadFolderData(props: MoveToFolderDrawerProps) {
return (
<FolderProvider
allCollectionFolderSlugs={[props.folderCollectionSlug]}
allowCreateCollectionSlugs={
permissions.collections[props.folderCollectionSlug]?.create
? [props.folderCollectionSlug]
: []
}
allowMultiSelection={false}
breadcrumbs={breadcrumbs}
documents={documents}
folderCollectionSlugs={[]}
folderFieldName={props.folderFieldName}
folderID={props.fromFolderID}
folderID={folderID}
FolderResultsComponent={FolderResultsComponent}
key={folderID}
onItemClick={async (item) => {
await populateMoveToFolderDrawer(item.value.id)
}}
subfolders={subfolders}
>
<Content {...props} />
<Content {...props} populateMoveToFolderDrawer={populateMoveToFolderDrawer} />
</FolderProvider>
)
}
function Content({
drawerSlug,
fromFolderID,
fromFolderName,
itemsToMove,
onConfirm,
populateMoveToFolderDrawer,
skipConfirmModal,
...props
}: MoveToFolderDrawerProps) {
const { closeModal, openModal } = useModal()
const { clearRouteCache } = useRouteCache()
const { closeModal, isModalOpen, openModal } = useModal()
const [count] = React.useState(() => itemsToMove.length)
const [folderAddedToUnderlyingFolder, setFolderAddedToUnderlyingFolder] = React.useState(false)
const { i18n, t } = useTranslation()
const {
addItems,
breadcrumbs,
folderCollectionConfig,
folderCollectionSlug,
folderFieldName,
folderID,
FolderResultsComponent,
getSelectedItems,
setFolderID,
subfolders,
} = useFolder()
const [FolderDocumentDrawer, , { closeDrawer: closeFolderDrawer, openDrawer: openFolderDrawer }] =
useDocumentDrawer({
collectionSlug: folderCollectionSlug,
})
const { getEntityConfig } = useConfig()
const getSelectedFolder = React.useCallback((): {
id: null | number | string
@@ -191,19 +198,19 @@ function Content({
}, [breadcrumbs, getSelectedItems])
const onCreateSuccess = React.useCallback(
({ collectionSlug, doc }: { collectionSlug: CollectionSlug; doc: Document }) => {
const collectionConfig = getEntityConfig({ collectionSlug })
void addItems([
formatFolderOrDocumentItem({
folderFieldName,
isUpload: Boolean(collectionConfig?.upload),
relationTo: collectionSlug,
useAsTitle: collectionConfig.admin.useAsTitle,
value: doc,
}),
])
async ({ collectionSlug, doc }: { collectionSlug: CollectionSlug; doc: Document }) => {
await populateMoveToFolderDrawer(folderID)
if (
collectionSlug === folderCollectionSlug &&
((doc?.folder && fromFolderID === extractID(doc?.folder)) ||
(!fromFolderID && !doc?.folder))
) {
// if the folder we created is in the same folder as the one we are moving from
// set variable so we can clear the route cache when we close the drawer
setFolderAddedToUnderlyingFolder(true)
}
},
[addItems, folderFieldName, getEntityConfig],
[populateMoveToFolderDrawer, folderID, fromFolderID, folderCollectionSlug],
)
const onConfirmMove = React.useCallback(() => {
@@ -212,6 +219,15 @@ function Content({
}
}, [getSelectedFolder, onConfirm])
React.useEffect(() => {
if (!isModalOpen(drawerSlug) && folderAddedToUnderlyingFolder) {
// if we added a folder to the underlying folder, clear the route cache
// so that the folder view will be reloaded with the new folder
setFolderAddedToUnderlyingFolder(false)
clearRouteCache()
}
}, [drawerSlug, isModalOpen, clearRouteCache, folderAddedToUnderlyingFolder])
return (
<>
<DrawerActionHeader
@@ -230,7 +246,7 @@ function Content({
<DrawerHeading
action={props.action}
count={count}
fromFolderName={props.fromFolderID ? fromFolderName : undefined}
fromFolderName={fromFolderID ? fromFolderName : undefined}
title={props.action === 'moveItemToFolder' ? props.title : undefined}
/>
}
@@ -249,7 +265,7 @@ function Content({
),
onClick: breadcrumbs.length
? () => {
void setFolderID({ folderID: null })
void populateMoveToFolderDrawer(null)
}
: undefined,
},
@@ -259,7 +275,7 @@ function Content({
onClick:
index !== breadcrumbs.length - 1
? () => {
void setFolderID({ folderID: crumb.id })
void populateMoveToFolderDrawer(crumb.id)
}
: undefined,
})),
@@ -284,12 +300,10 @@ function Content({
[folderFieldName]: folderID,
}}
onSave={(result) => {
if (typeof onCreateSuccess === 'function') {
void onCreateSuccess({
collectionSlug: folderCollectionConfig.slug,
doc: result.doc,
})
}
void onCreateSuccess({
collectionSlug: folderCollectionConfig.slug,
doc: result.doc,
})
closeFolderDrawer()
}}
redirectAfterCreate={false}
@@ -300,14 +314,7 @@ function Content({
<DrawerContentContainer className={`${baseClass}__body-section`}>
{subfolders.length > 0 ? (
<ItemCardGrid
disabledItemKeys={new Set(itemsToMove.map(({ itemKey }) => itemKey))}
items={subfolders}
selectedItemKeys={
new Set<FolderDocumentItemKey>([`${folderCollectionSlug}-${getSelectedFolder().id}`])
}
type="folder"
/>
FolderResultsComponent
) : (
<NoListResults
Actions={[

View File

@@ -1,10 +1,13 @@
'use client'
import type { FolderOrDocument } from 'payload/shared'
import { useDroppable } from '@dnd-kit/core'
import React from 'react'
import { DocumentIcon } from '../../../icons/Document/index.js'
import { ThreeDotsIcon } from '../../../icons/ThreeDots/index.js'
import { useFolder } from '../../../providers/Folders/index.js'
import { Popup } from '../../Popup/index.js'
import { Thumbnail } from '../../Thumbnail/index.js'
import { ColoredFolderIcon } from '../ColoredFolderIcon/index.js'
@@ -127,3 +130,41 @@ export function FolderFileCard({
</div>
)
}
type ContextCardProps = {
readonly className?: string
readonly index: number // todo: possibly remove
readonly item: FolderOrDocument
readonly type: 'file' | 'folder'
}
export function ContextFolderFileCard({ type, className, index, item }: ContextCardProps) {
const {
focusedRowIndex,
isDragging,
itemKeysToMove,
onItemClick,
onItemKeyPress,
selectedItemKeys,
} = useFolder()
const isSelected = selectedItemKeys.has(item.itemKey)
return (
<FolderFileCard
className={className}
disabled={(isDragging && isSelected) || itemKeysToMove.has(item.itemKey)}
id={item.value.id}
isFocused={focusedRowIndex === index}
isSelected={isSelected}
itemKey={item.itemKey}
onClick={(event) => {
void onItemClick({ event, index, item })
}}
onKeyDown={(event) => {
void onItemKeyPress({ event, index, item })
}}
previewUrl={item.value.url}
title={item.value._folderOrDocumentTitle}
type={type}
/>
)
}

View File

@@ -1,14 +1,12 @@
import type { I18nClient } from '@payloadcms/translations'
import type { FolderDocumentItemKey, FolderOrDocument } from 'payload/shared'
'use client'
import { getTranslation } from '@payloadcms/translations'
import { extractID } from 'payload/shared'
import React from 'react'
import type { FormatDateArgs } from '../../../utilities/formatDocTitle/formatDateTitle.js'
import { DocumentIcon } from '../../../icons/Document/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useFolder } from '../../../providers/Folders/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
import { formatDate } from '../../../utilities/formatDocTitle/formatDateTitle.js'
import { ColoredFolderIcon } from '../ColoredFolderIcon/index.js'
@@ -19,42 +17,21 @@ import './index.scss'
const baseClass = 'folder-file-table'
type Props = {
dateFormat: FormatDateArgs['pattern']
disabledItems?: Set<FolderDocumentItemKey>
documents: FolderOrDocument[]
focusedRowIndex: number
i18n: I18nClient
isMovingItems: boolean
onRowClick: (args: {
event: React.MouseEvent<Element>
index: number
item: FolderOrDocument
}) => void
onRowPress: (args: {
event: React.KeyboardEvent<Element>
index: number
item: FolderOrDocument
}) => void
selectedItems: Set<FolderDocumentItemKey>
showRelationCell?: boolean
subfolders: FolderOrDocument[]
}
export function FolderFileTable({
dateFormat,
disabledItems = new Set(),
documents,
focusedRowIndex,
i18n,
isMovingItems,
onRowClick,
onRowPress,
selectedItems,
showRelationCell = true,
subfolders,
}: Props) {
export function FolderFileTable({ showRelationCell = true }: Props) {
const {
documents,
focusedRowIndex,
isDragging,
onItemClick,
onItemKeyPress,
selectedItemKeys,
subfolders,
} = useFolder()
const { config } = useConfig()
const { t } = useTranslation()
const { i18n, t } = useTranslation()
const [relationToMap] = React.useState(() => {
const map: Record<string, string> = {}
@@ -109,7 +86,11 @@ export function FolderFileTable({
}
if ((name === 'createdAt' || name === 'updatedAt') && value[name]) {
cellValue = formatDate({ date: value[name], i18n, pattern: dateFormat })
cellValue = formatDate({
date: value[name],
i18n,
pattern: config.admin.dateFormat,
})
}
if (name === 'type') {
@@ -127,9 +108,7 @@ export function FolderFileTable({
return cellValue
}
})}
disabled={
(isMovingItems && selectedItems?.has(itemKey)) || disabledItems?.has(itemKey)
}
disabled={isDragging && selectedItemKeys?.has(itemKey)}
dragData={{
id: subfolderID,
type: 'folder',
@@ -137,19 +116,19 @@ export function FolderFileTable({
id={subfolderID}
isDroppable
isFocused={focusedRowIndex === rowIndex}
isSelected={selectedItems.has(itemKey)}
isSelecting={selectedItems.size > 0}
isSelected={selectedItemKeys.has(itemKey)}
isSelecting={selectedItemKeys.size > 0}
itemKey={itemKey}
key={`${rowIndex}-${itemKey}`}
onClick={(event) => {
void onRowClick({
void onItemClick({
event,
index: rowIndex,
item: subfolder,
})
}}
onKeyDown={(event) => {
void onRowPress({
void onItemKeyPress({
event,
index: rowIndex,
item: subfolder,
@@ -173,7 +152,11 @@ export function FolderFileTable({
}
if ((name === 'createdAt' || name === 'updatedAt') && value[name]) {
cellValue = formatDate({ date: value[name], i18n, pattern: dateFormat })
cellValue = formatDate({
date: value[name],
i18n,
pattern: config.admin.dateFormat,
})
}
if (name === 'type') {
@@ -191,26 +174,26 @@ export function FolderFileTable({
return cellValue
}
})}
disabled={isMovingItems || disabledItems?.has(itemKey)}
disabled={isDragging || selectedItemKeys?.has(itemKey)}
dragData={{
id: documentID,
type: 'document',
}}
id={documentID}
isFocused={focusedRowIndex === rowIndex}
isSelected={selectedItems.has(itemKey)}
isSelecting={selectedItems.size > 0}
isSelected={selectedItemKeys.has(itemKey)}
isSelecting={selectedItemKeys.size > 0}
itemKey={itemKey}
key={`${rowIndex}-${itemKey}`}
onClick={(event) => {
void onRowClick({
void onItemClick({
event,
index: rowIndex,
item: document,
})
}}
onKeyDown={(event) => {
void onRowPress({
void onItemKeyPress({
event,
index: rowIndex,
item: document,

View File

@@ -1,20 +1,16 @@
'use client'
import type { FolderDocumentItemKey, FolderOrDocument } from 'payload/shared'
import type { FolderOrDocument } from 'payload/shared'
import React from 'react'
import { useFolder } from '../../../providers/Folders/index.js'
import { FolderFileCard } from '../FolderFileCard/index.js'
import { ContextFolderFileCard } from '../FolderFileCard/index.js'
import './index.scss'
const baseClass = 'item-card-grid'
type ItemCardGridProps = {
disabledItemKeys?: Set<FolderDocumentItemKey>
items: FolderOrDocument[]
RenderActionGroup?: (args: { index: number; item: FolderOrDocument }) => React.ReactNode
selectedItemKeys: Set<FolderDocumentItemKey>
title?: string
} & (
| {
@@ -26,17 +22,7 @@ type ItemCardGridProps = {
type: 'folder'
}
)
export function ItemCardGrid({
type,
disabledItemKeys,
items,
RenderActionGroup,
selectedItemKeys,
subfolderCount,
title,
}: ItemCardGridProps) {
const { focusedRowIndex, isDragging, onItemClick, onItemKeyPress, selectedIndexes } = useFolder()
export function ItemCardGrid({ type, items, subfolderCount, title }: ItemCardGridProps) {
return (
<>
{title && <p className={`${baseClass}__title`}>{title}</p>}
@@ -45,38 +31,9 @@ export function ItemCardGrid({
? null
: items.map((item, _index) => {
const index = _index + (subfolderCount || 0)
const { itemKey, value } = item
const { itemKey } = item
return (
<FolderFileCard
disabled={
(isDragging && selectedItemKeys.has(itemKey)) || disabledItemKeys?.has(itemKey)
}
id={value.id}
isFocused={focusedRowIndex === index}
isSelected={selectedItemKeys.has(itemKey)}
itemKey={itemKey}
key={itemKey}
onClick={(event) => {
void onItemClick({ event, index, item })
}}
onKeyDown={(event) => {
void onItemKeyPress({ event, index, item })
}}
PopupActions={
RenderActionGroup
? RenderActionGroup({
index,
item,
})
: null
}
previewUrl={value?.url}
selectedCount={selectedIndexes.size}
title={value._folderOrDocumentTitle}
type={type}
/>
)
return <ContextFolderFileCard index={index} item={item} key={itemKey} type={type} />
})}
</div>
</>

View File

@@ -45,9 +45,12 @@ const orderOnOptions: {
},
]
export function SortByPill() {
const { documents, sortAndUpdateState, sortDirection, sortOn, subfolders } = useFolder()
const { refineFolderData, sort } = useFolder()
const { t } = useTranslation()
const [selectedSortOption] = sortOnOptions.filter(({ value }) => value === sortOn)
const sortDirection = sort.startsWith('-') ? 'desc' : 'asc'
const [selectedSortOption] = sortOnOptions.filter(
({ value }) => value === (sort.startsWith('-') ? sort.slice(1) : sort),
)
const [selectedOrderOption] = orderOnOptions.filter(({ value }) => value === sortDirection)
return (
@@ -73,13 +76,11 @@ export function SortByPill() {
active={selectedSortOption.value === value}
key={value}
onClick={() => {
sortAndUpdateState({
documentsToSort: documents,
sortOn: value as keyof Pick<
FolderOrDocument['value'],
'_folderOrDocumentTitle' | 'createdAt' | 'updatedAt'
>,
subfoldersToSort: subfolders,
refineFolderData({
query: {
sort: value,
},
updateURL: true,
})
close()
}}
@@ -97,11 +98,14 @@ export function SortByPill() {
className={`${baseClass}__order-option`}
key={value}
onClick={() => {
sortAndUpdateState({
documentsToSort: documents,
sortDirection: value,
subfoldersToSort: subfolders,
})
if (value === 'asc') {
refineFolderData({
query: {
sort: value === 'asc' ? `-${sort}` : sort,
},
updateURL: true,
})
}
close()
}}
>

View File

@@ -118,12 +118,12 @@ export function ListCreateNewDocInFolderButton({
initialData={{
[folderFieldName]: folderID,
}}
onSave={({ doc }) => {
closeModal(newDocInFolderDrawerSlug)
void onCreateSuccess({
onSave={async ({ doc }) => {
await onCreateSuccess({
collectionSlug: createCollectionSlug,
doc,
})
closeModal(newDocInFolderDrawerSlug)
}}
redirectAfterCreate={false}
/>
@@ -134,13 +134,11 @@ export function ListCreateNewDocInFolderButton({
initialData={{
[folderFieldName]: folderID,
}}
onSave={(result) => {
if (typeof onCreateSuccess === 'function') {
void onCreateSuccess({
collectionSlug: folderCollectionConfig.slug,
doc: result.doc,
})
}
onSave={async (result) => {
await onCreateSuccess({
collectionSlug: folderCollectionConfig.slug,
doc: result.doc,
})
closeFolderDrawer()
}}
redirectAfterCreate={false}

View File

@@ -4,6 +4,7 @@ export { FolderEditField } from '../../elements/FolderView/Field/index.server.js
export { File } from '../../graphics/File/index.js'
export { CheckIcon } from '../../icons/Check/index.js'
export { copyDataFromLocaleHandler } from '../../utilities/copyDataFromLocale.js'
export { getFolderResultsComponentAndData } from '../../utilities/getFolderResultsComponentAndData.js'
export { renderFilters, renderTable } from '../../utilities/renderTable.js'
export { resolveFilterOptions } from '../../utilities/resolveFilterOptions.js'
export { upsertPreferences } from '../../utilities/upsertPreferences.js'

View File

@@ -101,7 +101,8 @@ export const RelationshipInput: React.FC<RelationshipInputProps> = (props) => {
const valueRef = useRef(value)
// the line below seems odd
// eslint-disable-next-line react-compiler/react-compiler
valueRef.current = value
const [DocumentDrawer, , { isDrawerOpen, openDrawer }] = useDocumentDrawer({

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import type {
Data,
DocumentSlots,
ErrorResult,
GetFolderResultsComponentAndDataArgs,
Locale,
ServerFunctionClient,
} from 'payload'
@@ -13,6 +14,7 @@ import React, { createContext, useCallback } from 'react'
import type { buildFormStateHandler } from '../../utilities/buildFormState.js'
import type { buildTableStateHandler } from '../../utilities/buildTableState.js'
import type { CopyDataFromLocaleArgs } from '../../utilities/copyDataFromLocale.js'
import type { getFolderResultsComponentAndDataHandler } from '../../utilities/getFolderResultsComponentAndData.js'
import type {
schedulePublishHandler,
SchedulePublishHandlerArgs,
@@ -63,9 +65,16 @@ type GetDocumentSlots = (args: {
signal?: AbortSignal
}) => Promise<DocumentSlots>
type GetFolderResultsComponentAndDataClient = (
args: {
signal?: AbortSignal
} & Omit<GetFolderResultsComponentAndDataArgs, 'req'>,
) => ReturnType<typeof getFolderResultsComponentAndDataHandler>
type ServerFunctionsContextType = {
copyDataFromLocale: CopyDataFromLocaleClient
getDocumentSlots: GetDocumentSlots
getFolderResultsComponentAndData: GetFolderResultsComponentAndDataClient
getFormState: GetFormStateClient
getTableState: GetTableStateClient
renderDocument: RenderDocument
@@ -218,11 +227,32 @@ export const ServerFunctionsProvider: React.FC<{
[serverFunction],
)
const getFolderResultsComponentAndData = useCallback<GetFolderResultsComponentAndDataClient>(
async (args) => {
const { signal: remoteSignal, ...rest } = args || {}
try {
const result = (await serverFunction({
name: 'get-folder-results-component-and-data',
args: rest,
})) as Awaited<ReturnType<typeof getFolderResultsComponentAndDataHandler>>
if (!remoteSignal?.aborted) {
return result
}
} catch (_err) {
console.error(_err) // eslint-disable-line no-console
}
},
[serverFunction],
)
return (
<ServerFunctionsContext
value={{
copyDataFromLocale,
getDocumentSlots,
getFolderResultsComponentAndData,
getFormState,
getTableState,
renderDocument,

View File

@@ -100,7 +100,7 @@ export const TableColumnsProvider: React.FC<TableColumnsProviderProps> = ({
resetColumnsState,
setActiveColumns,
toggleColumn,
// eslint-disable-next-line react-compiler/react-compiler
...contextRef.current,
}}
>

View File

@@ -0,0 +1,170 @@
import type {
CollectionSlug,
ErrorResult,
GetFolderResultsComponentAndDataArgs,
Where,
} from 'payload'
import type { FolderBreadcrumb, FolderOrDocument } from 'payload/shared'
import { APIError, formatErrors, getFolderData } from 'payload'
import { buildFolderWhereConstraints } from 'payload/shared'
import { FolderFileTable } from '../elements/FolderView/FolderFileTable/index.js'
import { ItemCardGrid } from '../elements/FolderView/ItemCardGrid/index.js'
type GetFolderResultsComponentAndDataResult = {
breadcrumbs?: FolderBreadcrumb[]
documents?: FolderOrDocument[]
FolderResultsComponent: React.ReactNode
subfolders?: FolderOrDocument[]
}
type GetFolderResultsComponentAndDataErrorResult = {
breadcrumbs?: never
documents?: never
FolderResultsComponent?: never
subfolders?: never
} & (
| {
message: string
}
| ErrorResult
)
export const getFolderResultsComponentAndDataHandler = async (
args: GetFolderResultsComponentAndDataArgs,
): Promise<
GetFolderResultsComponentAndDataErrorResult | GetFolderResultsComponentAndDataResult
> => {
const { req } = args
try {
const res = await getFolderResultsComponentAndData(args)
return res
} catch (err) {
req.payload.logger.error({ err, msg: `There was an error building form state` })
if (err.message === 'Could not find field schema for given path') {
return {
message: err.message,
}
}
if (err.message === 'Unauthorized') {
return null
}
return formatErrors(err)
}
}
/**
* This function is responsible for fetching folder data, building the results component
* and returns the data and component together.
*
*
* Open ended questions:
* - If we rerender the results section, does the provider update?? I dont think so, if the provider is on the server.
* Maybe we should move the provider to the client.
*/
export const getFolderResultsComponentAndData = async ({
activeCollectionSlugs,
browseByFolder,
displayAs,
folderID = undefined,
req,
sort,
}: GetFolderResultsComponentAndDataArgs): Promise<GetFolderResultsComponentAndDataResult> => {
const { payload } = req
if (!payload.config.folders) {
throw new APIError('Folders are not enabled in the configuration.')
}
let collectionSlug: CollectionSlug | undefined = undefined
let documentWhere: undefined | Where = undefined
let folderWhere: undefined | Where = undefined
// todo(perf): - collect promises and resolve them in parallel
for (const activeCollectionSlug of activeCollectionSlugs) {
if (activeCollectionSlug === payload.config.folders.slug) {
const folderCollectionConstraints = await buildFolderWhereConstraints({
collectionConfig: payload.collections[activeCollectionSlug].config,
folderID,
localeCode: req?.locale,
req,
search: typeof req?.query?.search === 'string' ? req.query.search : undefined,
sort,
})
if (folderCollectionConstraints) {
folderWhere = folderCollectionConstraints
}
} else if ((browseByFolder && folderID) || !browseByFolder) {
if (!browseByFolder) {
collectionSlug = activeCollectionSlug
}
if (!documentWhere) {
documentWhere = {
or: [],
}
}
const collectionConstraints = await buildFolderWhereConstraints({
collectionConfig: payload.collections[activeCollectionSlug].config,
folderID,
localeCode: req?.locale,
req,
search: typeof req?.query?.search === 'string' ? req.query.search : undefined,
sort,
})
if (collectionConstraints) {
documentWhere.or.push(collectionConstraints)
}
}
}
const folderData = await getFolderData({
collectionSlug,
documentWhere,
folderID,
folderWhere,
req,
})
let FolderResultsComponent = null
if (displayAs === 'grid') {
FolderResultsComponent = (
<div>
{folderData.subfolders.length ? (
<>
<ItemCardGrid items={folderData.subfolders} title={'Folders'} type="folder" />
</>
) : null}
{folderData.documents.length ? (
<>
<ItemCardGrid
items={folderData.documents}
subfolderCount={folderData.subfolders.length}
title={'Documents'}
type="file"
/>
</>
) : null}
</div>
)
} else {
FolderResultsComponent = <FolderFileTable showRelationCell={browseByFolder} />
}
return {
breadcrumbs: folderData.breadcrumbs,
documents: folderData.documents,
FolderResultsComponent,
subfolders: folderData.subfolders,
}
}

View File

@@ -1,12 +1,11 @@
'use client'
import type { DragEndEvent } from '@dnd-kit/core'
import type { CollectionSlug, Document, FolderListViewClientProps } from 'payload'
import type { FolderDocumentItemKey } from 'payload/shared'
import type { FolderListViewClientProps } from 'payload'
import { useDndMonitor } from '@dnd-kit/core'
import { getTranslation } from '@payloadcms/translations'
import { formatFolderOrDocumentItem } from 'payload/shared'
import { useRouter } from 'next/navigation.js'
import React, { Fragment } from 'react'
import { DroppableBreadcrumb } from '../../elements/FolderView/Breadcrumbs/index.js'
@@ -26,9 +25,10 @@ import { SearchBar } from '../../elements/SearchBar/index.js'
import { useStepNav } from '../../elements/StepNav/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useFolder } from '../../providers/Folders/index.js'
import { FolderProvider, useFolder } from '../../providers/Folders/index.js'
import { usePreferences } from '../../providers/Preferences/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { useWindowInfo } from '../../providers/WindowInfo/index.js'
import { ListSelection } from '../CollectionFolder/ListSelection/index.js'
@@ -36,12 +36,54 @@ import './index.scss'
const baseClass = 'folder-list'
export function DefaultBrowseByFolderView(
props: {
hasCreatePermissionCollectionSlugs?: string[]
selectedCollectionSlugs?: string[]
} & FolderListViewClientProps,
) {
export function DefaultBrowseByFolderView({
activeCollectionFolderSlugs,
allCollectionFolderSlugs,
allowCreateCollectionSlugs,
baseFolderPath,
breadcrumbs,
documents,
folderFieldName,
folderID,
FolderResultsComponent,
search,
subfolders,
...restOfProps
}: FolderListViewClientProps) {
return (
<FolderProvider
activeCollectionFolderSlugs={activeCollectionFolderSlugs}
allCollectionFolderSlugs={allCollectionFolderSlugs}
allowCreateCollectionSlugs={allowCreateCollectionSlugs}
baseFolderPath={baseFolderPath}
breadcrumbs={breadcrumbs}
documents={documents}
folderFieldName={folderFieldName}
folderID={folderID}
FolderResultsComponent={FolderResultsComponent}
search={search}
subfolders={subfolders}
>
<BrowseByFolderViewInContext {...restOfProps} />
</FolderProvider>
)
}
type BrowseByFolderViewInContextProps = Omit<
FolderListViewClientProps,
| 'activeCollectionFolderSlugs'
| 'allCollectionFolderSlugs'
| 'allowCreateCollectionSlugs'
| 'baseFolderPath'
| 'breadcrumbs'
| 'documents'
| 'folderFieldName'
| 'folderID'
| 'FolderResultsComponent'
| 'subfolders'
>
function BrowseByFolderViewInContext(props: BrowseByFolderViewInContextProps) {
const {
AfterFolderList,
AfterFolderListTable,
@@ -50,40 +92,36 @@ export function DefaultBrowseByFolderView(
Description,
disableBulkDelete,
disableBulkEdit,
hasCreatePermissionCollectionSlugs,
viewPreference,
} = props
const { config, getEntityConfig } = useConfig()
const router = useRouter()
const { getEntityConfig } = useConfig()
const { i18n, t } = useTranslation()
const drawerDepth = useEditDepth()
const { setStepNav } = useStepNav()
const { startRouteTransition } = useRouteTransition()
const { clearRouteCache } = useRouteCache()
const {
breakpoints: { s: smallBreak },
} = useWindowInfo()
const { setPreference } = usePreferences()
const {
addItems,
activeCollectionFolderSlugs: visibleCollectionSlugs,
allowCreateCollectionSlugs,
breadcrumbs,
documents,
filterItems,
focusedRowIndex,
folderCollectionConfig,
folderFieldName,
folderID,
getFolderRoute,
getSelectedItems,
isDragging,
lastSelectedIndex,
moveToFolder,
onItemClick,
onItemKeyPress,
refineFolderData,
search,
selectedIndexes,
setFolderID,
selectedItemKeys,
setIsDragging,
subfolders,
visibleCollectionSlugs,
} = useFolder()
const [activeView, setActiveView] = React.useState<'grid' | 'list'>(viewPreference || 'grid')
@@ -129,35 +167,6 @@ export function DefaultBrowseByFolderView(
return `${acc}, ${getTranslation(collectionConfig.labels?.plural, i18n)}`
}, '')
const onCreateSuccess = React.useCallback(
({ collectionSlug, doc }: { collectionSlug: CollectionSlug; doc: Document }) => {
const collectionConfig = getEntityConfig({ collectionSlug })
void addItems([
formatFolderOrDocumentItem({
folderFieldName,
isUpload: Boolean(collectionConfig?.upload),
relationTo: collectionSlug,
useAsTitle: collectionConfig.admin.useAsTitle,
value: doc,
}),
])
},
[getEntityConfig, addItems, folderFieldName],
)
const selectedItemKeys = React.useMemo(() => {
return new Set(
getSelectedItems().reduce<FolderDocumentItemKey[]>((acc, item) => {
if (item) {
if (item.relationTo && item.value) {
acc.push(`${item.relationTo}-${item.value.id}`)
}
}
return acc
}, []),
)
}, [getSelectedItems])
const handleSetViewType = React.useCallback(
(view: 'grid' | 'list') => {
void setPreference('browse-by-folder', {
@@ -192,7 +201,9 @@ export function DefaultBrowseByFolderView(
id={null}
key="root"
onClick={() => {
void setFolderID({ folderID: null })
startRouteTransition(() => {
router.push(getFolderRoute(null))
})
}}
>
<ColoredFolderIcon />
@@ -211,7 +222,9 @@ export function DefaultBrowseByFolderView(
id={crumb.id}
key={crumb.id}
onClick={() => {
void setFolderID({ folderID: crumb.id })
startRouteTransition(() => {
router.push(getFolderRoute(crumb.id))
})
}}
>
{crumb.name}
@@ -221,7 +234,7 @@ export function DefaultBrowseByFolderView(
}),
])
}
}, [setStepNav, drawerDepth, i18n, breadcrumbs, setFolderID, t])
}, [breadcrumbs, drawerDepth, getFolderRoute, router, setStepNav, startRouteTransition, t])
return (
<Fragment>
@@ -242,13 +255,15 @@ export function DefaultBrowseByFolderView(
AfterListHeaderContent={Description}
title={listHeaderTitle}
TitleActions={[
<ListCreateNewDocInFolderButton
buttonLabel={t('general:createNew')}
collectionSlugs={hasCreatePermissionCollectionSlugs}
key="create-new-button"
onCreateSuccess={onCreateSuccess}
slugPrefix="create-document--header-pill"
/>,
allowCreateCollectionSlugs.length && (
<ListCreateNewDocInFolderButton
buttonLabel={t('general:createNew')}
collectionSlugs={allowCreateCollectionSlugs}
key="create-new-button"
onCreateSuccess={clearRouteCache}
slugPrefix="create-document--header-pill"
/>
),
].filter(Boolean)}
/>
<SearchBar
@@ -263,7 +278,7 @@ export function DefaultBrowseByFolderView(
<CurrentFolderActions key="current-folder-actions" />,
].filter(Boolean)}
label={searchPlaceholder}
onSearchChange={(search) => filterItems({ search })}
onSearchChange={(search) => refineFolderData({ query: { search }, updateURL: true })}
searchQueryParam={search}
/>
{BeforeFolderListTable}
@@ -273,12 +288,7 @@ export function DefaultBrowseByFolderView(
<div>
{subfolders.length ? (
<>
<ItemCardGrid
items={subfolders}
selectedItemKeys={selectedItemKeys}
title={'Folders'}
type="folder"
/>
<ItemCardGrid items={subfolders} title={'Folders'} type="folder" />
</>
) : null}
@@ -286,7 +296,6 @@ export function DefaultBrowseByFolderView(
<>
<ItemCardGrid
items={documents}
selectedItemKeys={selectedItemKeys}
subfolderCount={subfolders.length}
title={'Documents'}
type="file"
@@ -295,41 +304,35 @@ export function DefaultBrowseByFolderView(
) : null}
</div>
) : (
<FolderFileTable
dateFormat={config.admin.dateFormat}
documents={documents}
focusedRowIndex={focusedRowIndex}
i18n={i18n}
isMovingItems={isDragging}
onRowClick={onItemClick}
onRowPress={onItemKeyPress}
selectedItems={selectedItemKeys}
subfolders={subfolders}
/>
<FolderFileTable />
)}
</>
)}
{totalDocsAndSubfolders === 0 && (
<NoListResults
Actions={[
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${getTranslation(folderCollectionConfig.labels?.singular, i18n).toLowerCase()}`}
collectionSlugs={[folderCollectionConfig.slug]}
key="create-folder"
onCreateSuccess={onCreateSuccess}
slugPrefix="create-folder--no-results"
/>,
folderID && (
allowCreateCollectionSlugs.includes(folderCollectionConfig.slug) && (
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${t('general:document').toLowerCase()}`}
collectionSlugs={hasCreatePermissionCollectionSlugs.filter(
(slug) => slug !== folderCollectionConfig.slug,
)}
key="create-document"
onCreateSuccess={onCreateSuccess}
slugPrefix="create-document--no-results"
buttonLabel={`${t('general:create')} ${getTranslation(folderCollectionConfig.labels?.singular, i18n).toLowerCase()}`}
collectionSlugs={[folderCollectionConfig.slug]}
key="create-folder"
onCreateSuccess={clearRouteCache}
slugPrefix="create-folder--no-results"
/>
),
folderID &&
allowCreateCollectionSlugs.filter((slug) => slug !== folderCollectionConfig.slug)
.length > 0 && (
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${t('general:document').toLowerCase()}`}
collectionSlugs={allowCreateCollectionSlugs.filter(
(slug) => slug !== folderCollectionConfig.slug,
)}
key="create-document"
onCreateSuccess={clearRouteCache}
slugPrefix="create-document--no-results"
/>
),
].filter(Boolean)}
Message={
<p>
@@ -347,7 +350,7 @@ export function DefaultBrowseByFolderView(
<DragOverlaySelection
allItems={[...subfolders, ...documents]}
lastSelected={lastSelectedIndex}
selectedCount={selectedIndexes.size}
selectedCount={selectedItemKeys.size}
/>
</Fragment>
)

View File

@@ -1,7 +1,5 @@
'use client'
import type { FolderOrDocument } from 'payload/shared'
import { useModal } from '@faceless-ui/modal'
import { extractID } from 'payload/shared'
import React, { Fragment } from 'react'
@@ -16,6 +14,7 @@ import { PublishMany_v4 } from '../../../elements/PublishMany/index.js'
import { UnpublishMany_v4 } from '../../../elements/UnpublishMany/index.js'
import { useConfig } from '../../../providers/Config/index.js'
import { useFolder } from '../../../providers/Folders/index.js'
import { useRouteCache } from '../../../providers/RouteCache/index.js'
import { useTranslation } from '../../../providers/Translation/index.js'
const moveToFolderDrawerSlug = 'move-to-folder--list'
@@ -46,9 +45,8 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
folderID,
getSelectedItems,
moveToFolder,
removeItems,
renameFolder,
} = useFolder()
const { clearRouteCache } = useRouteCache()
const { config } = useConfig()
const { t } = useTranslation()
const { closeModal, openModal } = useModal()
@@ -121,12 +119,6 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
folderCollectionSlug={folderCollectionSlug}
id={groupedSelections[folderCollectionSlug].ids[0]}
key="edit-folder-action"
onSave={({ doc }) => {
renameFolder({
folderID: doc.id,
newName: doc[folderCollectionConfig.admin.useAsTitle],
})
}}
/>
),
count > 0 ? (
@@ -178,25 +170,9 @@ export const ListSelection: React.FC<ListSelectionProps> = ({
) : null,
!disableBulkDelete && (
<DeleteMany_v4
afterDelete={(groupedCollections) => {
const itemsToRemove = Object.entries(groupedCollections).reduce<FolderOrDocument[]>(
(acc, [slug, res]) => {
if (res.ids.length) {
res.ids.forEach((id) => {
acc.push({
itemKey: `${slug}-${id}`,
relationTo: slug,
value: {
id,
} as FolderOrDocument['value'],
})
})
}
return acc
},
[],
)
void removeItems(itemsToRemove)
afterDelete={() => {
clearRouteCache()
clearSelections()
}}
key="bulk-delete"
selections={groupedSelections}

View File

@@ -1,20 +1,18 @@
'use client'
import type { DragEndEvent } from '@dnd-kit/core'
import type { CollectionSlug, Document, FolderListViewClientProps } from 'payload'
import type { FolderDocumentItemKey } from 'payload/shared'
import type { FolderListViewClientProps } from 'payload'
import { useDndMonitor } from '@dnd-kit/core'
import { getTranslation } from '@payloadcms/translations'
import { formatFolderOrDocumentItem } from 'payload/shared'
import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment } from 'react'
import { DroppableBreadcrumb } from '../../elements/FolderView/Breadcrumbs/index.js'
import { ColoredFolderIcon } from '../../elements/FolderView/ColoredFolderIcon/index.js'
import { CurrentFolderActions } from '../../elements/FolderView/CurrentFolderActions/index.js'
import { DragOverlaySelection } from '../../elements/FolderView/DragOverlaySelection/index.js'
import { FolderFileTable } from '../../elements/FolderView/FolderFileTable/index.js'
import { ItemCardGrid } from '../../elements/FolderView/ItemCardGrid/index.js'
import { SortByPill } from '../../elements/FolderView/SortByPill/index.js'
import { ToggleViewButtons } from '../../elements/FolderView/ToggleViewButtons/index.js'
import { Gutter } from '../../elements/Gutter/index.js'
@@ -29,8 +27,10 @@ import { SearchBar } from '../../elements/SearchBar/index.js'
import { useStepNav } from '../../elements/StepNav/index.js'
import { useConfig } from '../../providers/Config/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useFolder } from '../../providers/Folders/index.js'
import { FolderProvider, useFolder } from '../../providers/Folders/index.js'
import { usePreferences } from '../../providers/Preferences/index.js'
import { useRouteCache } from '../../providers/RouteCache/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { useWindowInfo } from '../../providers/WindowInfo/index.js'
import { ListSelection } from './ListSelection/index.js'
@@ -38,7 +38,53 @@ import './index.scss'
const baseClass = 'collection-folder-list'
export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
export function DefaultCollectionFolderView({
allCollectionFolderSlugs: folderCollectionSlugs,
allowCreateCollectionSlugs,
baseFolderPath,
breadcrumbs,
documents,
folderFieldName,
folderID,
FolderResultsComponent,
search,
sort,
subfolders,
...restOfProps
}: FolderListViewClientProps) {
return (
<FolderProvider
allCollectionFolderSlugs={folderCollectionSlugs}
allowCreateCollectionSlugs={allowCreateCollectionSlugs}
baseFolderPath={baseFolderPath}
breadcrumbs={breadcrumbs}
documents={documents}
folderFieldName={folderFieldName}
folderID={folderID}
FolderResultsComponent={FolderResultsComponent}
search={search}
sort={sort}
subfolders={subfolders}
>
<CollectionFolderViewInContext {...restOfProps} />
</FolderProvider>
)
}
type CollectionFolderViewInContextProps = Omit<
FolderListViewClientProps,
| 'allCollectionFolderSlugs'
| 'allowCreateCollectionSlugs'
| 'baseFolderPath'
| 'breadcrumbs'
| 'documents'
| 'folderFieldName'
| 'folderID'
| 'FolderResultsComponent'
| 'subfolders'
>
function CollectionFolderViewInContext(props: CollectionFolderViewInContextProps) {
const {
AfterFolderList,
AfterFolderListTable,
@@ -48,7 +94,7 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
Description,
disableBulkDelete,
disableBulkEdit,
hasCreatePermission,
search,
viewPreference,
} = props
@@ -58,28 +104,24 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
const { setStepNav } = useStepNav()
const { setPreference } = usePreferences()
const {
addItems,
allowCreateCollectionSlugs,
breadcrumbs,
documents,
filterItems,
focusedRowIndex,
folderCollectionConfig,
folderCollectionSlug,
folderFieldName,
FolderResultsComponent,
getSelectedItems,
isDragging,
lastSelectedIndex,
moveToFolder,
onItemClick,
onItemKeyPress,
search,
selectedIndexes,
setFolderID,
refineFolderData,
selectedItemKeys,
setIsDragging,
subfolders,
} = useFolder()
const [activeView, setActiveView] = React.useState<'grid' | 'list'>(viewPreference || 'grid')
const router = useRouter()
const { startRouteTransition } = useRouteTransition()
const { clearRouteCache } = useRouteCache()
const collectionConfig = getEntityConfig({ collectionSlug })
@@ -98,52 +140,30 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
}
if (event.over.data.current.type === 'folder' && 'id' in event.over.data.current) {
await moveToFolder({
itemsToMove: getSelectedItems(),
toFolderID: event.over.data.current.id || null,
})
try {
await moveToFolder({
itemsToMove: getSelectedItems(),
toFolderID: event.over.data.current.id,
})
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error moving items:', error)
}
clearRouteCache()
}
},
[moveToFolder, getSelectedItems],
[moveToFolder, getSelectedItems, clearRouteCache],
)
const onCreateSuccess = React.useCallback(
({ collectionSlug, doc }: { collectionSlug: CollectionSlug; doc: Document }) => {
const collectionConfig = getEntityConfig({ collectionSlug })
void addItems([
formatFolderOrDocumentItem({
folderFieldName,
isUpload: Boolean(collectionConfig?.upload),
relationTo: collectionSlug,
useAsTitle: collectionConfig.admin.useAsTitle,
value: doc,
}),
])
},
[getEntityConfig, addItems, folderFieldName],
)
const selectedItemKeys = React.useMemo(() => {
return new Set(
getSelectedItems().reduce<FolderDocumentItemKey[]>((acc, item) => {
if (item) {
if (item.relationTo && item.value) {
acc.push(`${item.relationTo}-${item.value.id}`)
}
}
return acc
}, []),
)
}, [getSelectedItems])
const handleSetViewType = React.useCallback(
(view: 'grid' | 'list') => {
void setPreference(`${collectionSlug}-collection-folder`, {
async (view: 'grid' | 'list') => {
await setPreference(`${collectionSlug}-collection-folder`, {
viewPreference: view,
})
setActiveView(view)
clearRouteCache()
},
[collectionSlug, setPreference],
[collectionSlug, setPreference, clearRouteCache],
)
React.useEffect(() => {
@@ -170,7 +190,16 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
id={null}
key="root"
onClick={() => {
void setFolderID({ folderID: null })
startRouteTransition(() => {
if (config.folders) {
router.push(
formatAdminURL({
adminRoute: config.routes.admin,
path: `/collections/${collectionSlug}/${config.folders.slug}`,
}),
)
}
})
}}
>
<ColoredFolderIcon />
@@ -189,7 +218,16 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
id={crumb.id}
key={crumb.id}
onClick={() => {
void setFolderID({ folderID: crumb.id })
startRouteTransition(() => {
if (config.folders) {
router.push(
formatAdminURL({
adminRoute: config.routes.admin,
path: `/collections/${collectionSlug}/${config.folders.slug}/${crumb.id}`,
}),
)
}
})
}}
>
{crumb.name}
@@ -199,13 +237,25 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
}),
])
}
}, [setStepNav, labels, drawerDepth, i18n, breadcrumbs, setFolderID])
}, [
breadcrumbs,
collectionSlug,
config.folders,
config.routes.admin,
drawerDepth,
i18n,
labels?.plural,
router,
setStepNav,
startRouteTransition,
])
const totalDocsAndSubfolders = documents.length + subfolders.length
return (
<Fragment>
<DndEventListener onDragEnd={onDragEnd} setIsDragging={setIsDragging} />
<div className={`${baseClass} ${baseClass}--${collectionSlug}`}>
{BeforeFolderList}
<Gutter className={`${baseClass}__wrap`}>
@@ -230,18 +280,18 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
AfterListHeaderContent={Description}
title={getTranslation(labels?.plural, i18n)}
TitleActions={[
hasCreatePermission && (
allowCreateCollectionSlugs.length && (
<ListCreateNewDocInFolderButton
buttonLabel={t('general:createNew')}
collectionSlugs={[folderCollectionConfig.slug, collectionSlug]}
collectionSlugs={allowCreateCollectionSlugs}
key="create-new-button"
onCreateSuccess={onCreateSuccess}
onCreateSuccess={clearRouteCache}
slugPrefix="create-document--header-pill"
/>
),
<ListBulkUploadButton
collectionSlug={collectionSlug}
hasCreatePermission={hasCreatePermission}
hasCreatePermission={allowCreateCollectionSlugs.includes(collectionSlug)}
isBulkUploadEnabled={isBulkUploadEnabled}
key="bulk-upload-button"
/>,
@@ -251,7 +301,7 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
Actions={[
<SortByPill key="sort-by-pill" />,
<ToggleViewButtons
activeView={activeView}
activeView={viewPreference}
key="toggle-view-buttons"
setActiveView={handleSetViewType}
/>,
@@ -260,70 +310,32 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
label={t('general:searchBy', {
label: t('general:name'),
})}
onSearchChange={(search) => filterItems({ search })}
onSearchChange={(search) => refineFolderData({ query: { search }, updateURL: true })}
searchQueryParam={search}
/>
{BeforeFolderListTable}
{totalDocsAndSubfolders > 0 && (
<>
{activeView === 'grid' ? (
<div>
{subfolders.length ? (
<>
<ItemCardGrid
items={subfolders}
selectedItemKeys={selectedItemKeys}
title={'Folders'}
type="folder"
/>
</>
) : null}
{documents.length ? (
<>
<ItemCardGrid
items={documents}
selectedItemKeys={selectedItemKeys}
subfolderCount={subfolders.length}
title={'Documents'}
type="file"
/>
</>
) : null}
</div>
) : (
<FolderFileTable
dateFormat={config.admin.dateFormat}
documents={documents}
focusedRowIndex={focusedRowIndex}
i18n={i18n}
isMovingItems={isDragging}
onRowClick={onItemClick}
onRowPress={onItemKeyPress}
selectedItems={selectedItemKeys}
showRelationCell={false}
subfolders={subfolders}
/>
)}
</>
)}
{totalDocsAndSubfolders > 0 && FolderResultsComponent}
{totalDocsAndSubfolders === 0 && (
<NoListResults
Actions={[
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${getTranslation(folderCollectionConfig.labels?.singular, i18n).toLowerCase()}`}
collectionSlugs={[folderCollectionConfig.slug]}
key="create-folder"
onCreateSuccess={onCreateSuccess}
slugPrefix="create-folder--no-results"
/>,
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${t('general:document').toLowerCase()}`}
collectionSlugs={[collectionSlug]}
key="create-document"
onCreateSuccess={onCreateSuccess}
slugPrefix="create-document--no-results"
/>,
allowCreateCollectionSlugs.includes(folderCollectionSlug) && (
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${getTranslation(folderCollectionConfig.labels?.singular, i18n).toLowerCase()}`}
collectionSlugs={[folderCollectionConfig.slug]}
key="create-folder"
onCreateSuccess={clearRouteCache}
slugPrefix="create-folder--no-results"
/>
),
allowCreateCollectionSlugs.includes(collectionSlug) && (
<ListCreateNewDocInFolderButton
buttonLabel={`${t('general:create')} ${t('general:document').toLowerCase()}`}
collectionSlugs={[collectionSlug]}
key="create-document"
onCreateSuccess={clearRouteCache}
slugPrefix="create-document--no-results"
/>
),
].filter(Boolean)}
Message={
<p>
@@ -344,11 +356,12 @@ export function DefaultCollectionFolderView(props: FolderListViewClientProps) {
<DragOverlaySelection
allItems={[...subfolders, ...documents]}
lastSelected={lastSelectedIndex}
selectedCount={selectedIndexes.size}
selectedCount={selectedItemKeys.size}
/>
</Fragment>
)
}
function DndEventListener({ onDragEnd, setIsDragging }) {
useDndMonitor({
onDragCancel() {