Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next/src/utilities/handleServerFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down
132 changes: 52 additions & 80 deletions packages/next/src/views/BrowseByFolder/buildView.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import type {
AdminViewServerProps,
BuildCollectionFolderViewResult,
FolderListViewClientProps,
FolderListViewServerPropsOnly,
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
Expand Down Expand Up @@ -56,18 +53,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) =>
Expand All @@ -79,62 +76,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<{
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the right way to do this, new ServerFunction perhaps?

sort?: string
viewPreference?: 'grid' | 'list'
}>({
key: 'browse-by-folder',
req: initPageResult.req,
value: {
sort: query?.sort as string,
},
})

const sortPreference = browseByFolderPreferences?.sort
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 (
Expand Down Expand Up @@ -172,38 +142,40 @@ 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,
disableBulkDelete,
disableBulkEdit,
enableRowSelections,
hasCreatePermissionCollectionSlugs,
selectedCollectionSlugs,
viewPreference: browseByFolderPreferences?.value?.viewPreference,
},
// Component:config.folders?.components?.views?.list?.Component,
// hasCreatePermissionCollectionSlugs,
// selectedCollectionSlugs,
activeCollectionFolderSlugs,
allCollectionFolderSlugs: browseByFolderSlugs,
allowCreateCollectionSlugs,
baseFolderPath: `/browse-by-folder`,
breadcrumbs,
documents,
folderFieldName: config.folders.fieldName,
folderID: resolvedFolderID || null,
FolderResultsComponent,
subfolders,
viewPreference,
} satisfies FolderListViewClientProps,
// Component:config.folders?.components?.views?.BrowseByFolders?.Component,
Fallback: DefaultBrowseByFolderView,
importMap: payload.importMap,
serverProps,
})}
</FolderProvider>
</>
),
}
}
Loading
Loading