Compare commits
6 Commits
feat/uploa
...
feat/folde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189b5ddf3a | ||
|
|
0a5459d7d8 | ||
|
|
5b8ebbf2f9 | ||
|
|
5e3e9b79e8 | ||
|
|
aa6f73c8c1 | ||
|
|
953c3148ee |
9
packages/next/src/utilities/getRouteWithoutAdmin.ts
Normal file
9
packages/next/src/utilities/getRouteWithoutAdmin.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const getRouteWithoutAdmin = ({
|
||||
adminRoute,
|
||||
route,
|
||||
}: {
|
||||
adminRoute: string
|
||||
route: string
|
||||
}): string => {
|
||||
return adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
|
||||
}
|
||||
18
packages/next/src/utilities/getVisisbleEntities.ts
Normal file
18
packages/next/src/utilities/getVisisbleEntities.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { PayloadRequest, VisibleEntities } from 'payload'
|
||||
|
||||
import { isEntityHidden } from 'payload'
|
||||
|
||||
export function getVisibleEntities({ req }: { req: PayloadRequest }): VisibleEntities {
|
||||
return {
|
||||
collections: req.payload.config.collections
|
||||
.map(({ slug, admin: { hidden } }) =>
|
||||
!isEntityHidden({ hidden, user: req.user }) ? slug : null,
|
||||
)
|
||||
.filter(Boolean),
|
||||
globals: req.payload.config.globals
|
||||
.map(({ slug, admin: { hidden } }) =>
|
||||
!isEntityHidden({ hidden, user: req.user }) ? slug : null,
|
||||
)
|
||||
.filter(Boolean),
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import type {
|
||||
Payload,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { getRouteWithoutAdmin, isAdminRoute } from './shared.js'
|
||||
|
||||
type Args = {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
defaultIDType: Payload['db']['defaultIDType']
|
||||
payload?: Payload
|
||||
route: string
|
||||
}
|
||||
|
||||
type RouteInfo = {
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
collectionSlug?: string
|
||||
docID?: number | string
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
globalSlug?: string
|
||||
}
|
||||
|
||||
export function getRouteInfo({
|
||||
adminRoute,
|
||||
config,
|
||||
defaultIDType,
|
||||
payload,
|
||||
route,
|
||||
}: Args): RouteInfo {
|
||||
if (isAdminRoute({ adminRoute, config, route })) {
|
||||
const routeWithoutAdmin = getRouteWithoutAdmin({ adminRoute, route })
|
||||
const routeSegments = routeWithoutAdmin.split('/').filter(Boolean)
|
||||
const [entityType, entitySlug, segment3, segment4] = routeSegments
|
||||
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
||||
const globalSlug = entityType === 'globals' ? entitySlug : undefined
|
||||
|
||||
let collectionConfig: SanitizedCollectionConfig | undefined
|
||||
let globalConfig: SanitizedGlobalConfig | undefined
|
||||
let idType = defaultIDType
|
||||
|
||||
if (collectionSlug) {
|
||||
collectionConfig = payload.collections?.[collectionSlug]?.config
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
globalConfig = config.globals.find((global) => global.slug === globalSlug)
|
||||
}
|
||||
|
||||
// If the collection is using a custom ID, we need to determine its type
|
||||
if (collectionConfig && payload) {
|
||||
if (payload.collections?.[collectionSlug]?.customIDType) {
|
||||
idType = payload.collections?.[collectionSlug].customIDType
|
||||
}
|
||||
}
|
||||
|
||||
let docID: number | string | undefined
|
||||
|
||||
if (collectionSlug) {
|
||||
if (segment3 === 'trash' && segment4) {
|
||||
// /collections/:slug/trash/:id
|
||||
docID = idType === 'number' ? Number(segment4) : segment4
|
||||
} else if (segment3 && segment3 !== 'create') {
|
||||
// /collections/:slug/:id
|
||||
docID = idType === 'number' ? Number(segment3) : segment3
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
collectionConfig,
|
||||
collectionSlug,
|
||||
docID,
|
||||
globalConfig,
|
||||
globalSlug,
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { InitPageResult, VisibleEntities } from 'payload'
|
||||
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { isEntityHidden } from 'payload'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
import type { Args } from './types.js'
|
||||
|
||||
import { initReq } from '../initReq.js'
|
||||
import { getRouteInfo } from './handleAdminPage.js'
|
||||
import { handleAuthRedirect } from './handleAuthRedirect.js'
|
||||
import { isCustomAdminView } from './isCustomAdminView.js'
|
||||
import { isPublicAdminRoute } from './shared.js'
|
||||
|
||||
export const initPage = async ({
|
||||
config: configPromise,
|
||||
importMap,
|
||||
route,
|
||||
routeParams = {},
|
||||
searchParams,
|
||||
useLayoutReq,
|
||||
}: Args): Promise<InitPageResult> => {
|
||||
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
|
||||
|
||||
const {
|
||||
cookies,
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
req: { payload },
|
||||
} = await initReq({
|
||||
configPromise,
|
||||
importMap,
|
||||
key: useLayoutReq ? 'RootLayout' : 'initPage',
|
||||
overrides: {
|
||||
fallbackLocale: false,
|
||||
req: {
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
routeParams,
|
||||
},
|
||||
urlSuffix: `${route}${searchParams ? queryString : ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin: adminRoute },
|
||||
} = payload.config
|
||||
|
||||
const languageOptions = Object.entries(payload.config.i18n.supportedLanguages || {}).reduce(
|
||||
(acc, [language, languageConfig]) => {
|
||||
if (Object.keys(payload.config.i18n.supportedLanguages).includes(language)) {
|
||||
acc.push({
|
||||
label: languageConfig.translations.general.thisLanguage,
|
||||
value: language,
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const visibleEntities: VisibleEntities = {
|
||||
collections: collections
|
||||
.map(({ slug, admin: { hidden } }) =>
|
||||
!isEntityHidden({ hidden, user: req.user }) ? slug : null,
|
||||
)
|
||||
.filter(Boolean),
|
||||
globals: globals
|
||||
.map(({ slug, admin: { hidden } }) =>
|
||||
!isEntityHidden({ hidden, user: req.user }) ? slug : null,
|
||||
)
|
||||
.filter(Boolean),
|
||||
}
|
||||
|
||||
let redirectTo = null
|
||||
|
||||
if (
|
||||
!permissions.canAccessAdmin &&
|
||||
!isPublicAdminRoute({ adminRoute, config: payload.config, route }) &&
|
||||
!isCustomAdminView({ adminRoute, config: payload.config, route })
|
||||
) {
|
||||
redirectTo = handleAuthRedirect({
|
||||
config: payload.config,
|
||||
route,
|
||||
searchParams,
|
||||
user: req.user,
|
||||
})
|
||||
}
|
||||
|
||||
const { collectionConfig, collectionSlug, docID, globalConfig, globalSlug } = getRouteInfo({
|
||||
adminRoute,
|
||||
config: payload.config,
|
||||
defaultIDType: payload.db.defaultIDType,
|
||||
payload,
|
||||
route,
|
||||
})
|
||||
|
||||
if ((collectionSlug && !collectionConfig) || (globalSlug && !globalConfig)) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return {
|
||||
collectionConfig,
|
||||
cookies,
|
||||
docID,
|
||||
globalConfig,
|
||||
languageOptions,
|
||||
locale,
|
||||
permissions,
|
||||
redirectTo,
|
||||
req,
|
||||
translations: req.i18n.translations,
|
||||
visibleEntities,
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
export type Args = {
|
||||
/**
|
||||
* Your sanitized Payload config.
|
||||
* If unresolved, this function will await the promise.
|
||||
*/
|
||||
config: Promise<SanitizedConfig> | SanitizedConfig
|
||||
importMap: ImportMap
|
||||
/**
|
||||
* If true, redirects unauthenticated users to the admin login page.
|
||||
* If a string is provided, the user will be redirected to that specific URL.
|
||||
*/
|
||||
redirectUnauthenticatedUser?: boolean | string
|
||||
/**
|
||||
* The current route, i.e. `/admin/collections/posts`.
|
||||
*/
|
||||
route: string
|
||||
/**
|
||||
* The route parameters of the current route
|
||||
*
|
||||
* @example `{ collection: 'posts', id: "post-id" }`.
|
||||
*/
|
||||
routeParams?: { [key: string]: string }
|
||||
/**
|
||||
* The search parameters of the current route provided to all pages in Next.js.
|
||||
*/
|
||||
searchParams: { [key: string]: string | string[] | undefined }
|
||||
/**
|
||||
* If `useLayoutReq` is `true`, this page will use the cached `req` created by the root layout
|
||||
* instead of creating a new one.
|
||||
*
|
||||
* This improves performance for pages that are able to share the same `req` as the root layout,
|
||||
* as permissions do not need to be re-calculated.
|
||||
*
|
||||
* If the page has unique query and url params that need to be part of the `req` object, or if you
|
||||
* need permissions calculation to respect those you should not use this property.
|
||||
*/
|
||||
useLayoutReq?: boolean
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdminViewConfig, PayloadRequest, SanitizedConfig } from 'payload'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { getRouteWithoutAdmin } from './shared.js'
|
||||
import { getRouteWithoutAdmin } from './getRouteWithoutAdmin.js'
|
||||
|
||||
/**
|
||||
* Returns an array of views marked with 'public: true' in the config
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { getRouteWithoutAdmin } from './getRouteWithoutAdmin.js'
|
||||
|
||||
// Routes that require admin authentication
|
||||
const publicAdminRoutes: (keyof Pick<
|
||||
SanitizedConfig['admin']['routes'],
|
||||
@@ -15,17 +17,6 @@ const publicAdminRoutes: (keyof Pick<
|
||||
'reset',
|
||||
]
|
||||
|
||||
export const isAdminRoute = ({
|
||||
adminRoute,
|
||||
route,
|
||||
}: {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
route: string
|
||||
}): boolean => {
|
||||
return route.startsWith(adminRoute)
|
||||
}
|
||||
|
||||
export const isPublicAdminRoute = ({
|
||||
adminRoute,
|
||||
config,
|
||||
@@ -50,13 +41,3 @@ export const isPublicAdminRoute = ({
|
||||
|
||||
return isPublicAdminRoute
|
||||
}
|
||||
|
||||
export const getRouteWithoutAdmin = ({
|
||||
adminRoute,
|
||||
route,
|
||||
}: {
|
||||
adminRoute: string
|
||||
route: string
|
||||
}): string => {
|
||||
return adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { I18n } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { AdminViewServerProps, ImportMap, SanitizedConfig } from 'payload'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getVisibleEntities } from '../../utilities/getVisisbleEntities.js'
|
||||
import { initReq } from '../../utilities/initReq.js'
|
||||
import { NotFoundClient } from './index.client.js'
|
||||
|
||||
export const generateNotFoundViewMetadata = async ({
|
||||
@@ -27,12 +28,6 @@ export const generateNotFoundViewMetadata = async ({
|
||||
}
|
||||
}
|
||||
|
||||
export type GenerateViewMetadata = (args: {
|
||||
config: SanitizedConfig
|
||||
i18n: I18n
|
||||
params?: { [key: string]: string | string[] }
|
||||
}) => Promise<Metadata>
|
||||
|
||||
export const NotFoundPage = async ({
|
||||
config: configPromise,
|
||||
importMap,
|
||||
@@ -52,31 +47,45 @@ export const NotFoundPage = async ({
|
||||
const { routes: { admin: adminRoute } = {} } = config
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
const initPageResult = await initPage({
|
||||
config,
|
||||
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
|
||||
const {
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
req: { payload },
|
||||
} = await initReq({
|
||||
configPromise: config,
|
||||
importMap,
|
||||
redirectUnauthenticatedUser: true,
|
||||
route: formatAdminURL({ adminRoute, path: '/not-found' }),
|
||||
searchParams,
|
||||
useLayoutReq: true,
|
||||
key: 'RootLayout',
|
||||
overrides: {
|
||||
fallbackLocale: false,
|
||||
req: {
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
},
|
||||
urlSuffix: `${formatAdminURL({ adminRoute, path: '/not-found' })}${searchParams ? queryString : ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
const params = await paramsPromise
|
||||
|
||||
if (!initPageResult.req.user || !initPageResult.permissions.canAccessAdmin) {
|
||||
if (!req.user || !permissions.canAccessAdmin) {
|
||||
return <NotFoundClient />
|
||||
}
|
||||
|
||||
const params = await paramsPromise
|
||||
const visibleEntities = getVisibleEntities({ req })
|
||||
|
||||
return (
|
||||
<DefaultTemplate
|
||||
i18n={initPageResult.req.i18n}
|
||||
locale={initPageResult.locale}
|
||||
i18n={req.i18n}
|
||||
locale={locale}
|
||||
params={params}
|
||||
payload={initPageResult.req.payload}
|
||||
permissions={initPageResult.permissions}
|
||||
payload={payload}
|
||||
permissions={permissions}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult.req.user}
|
||||
visibleEntities={initPageResult.visibleEntities}
|
||||
user={req.user}
|
||||
visibleEntities={visibleEntities}
|
||||
>
|
||||
<NotFoundClient />
|
||||
</DefaultTemplate>
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
EditConfig,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedGlobalConfig,
|
||||
ServerPropsFromView,
|
||||
} from 'payload'
|
||||
|
||||
export function getViewActions({
|
||||
@@ -12,23 +11,21 @@ export function getViewActions({
|
||||
}: {
|
||||
editConfig: EditConfig
|
||||
viewKey: keyof EditConfig
|
||||
}): CustomComponent[] | undefined {
|
||||
}): CustomComponent[] {
|
||||
if (editConfig && viewKey in editConfig && 'actions' in editConfig[viewKey]) {
|
||||
return editConfig[viewKey].actions
|
||||
return editConfig[viewKey].actions ?? []
|
||||
}
|
||||
|
||||
return undefined
|
||||
return []
|
||||
}
|
||||
|
||||
export function attachViewActions({
|
||||
export function getSubViewActions({
|
||||
collectionOrGlobal,
|
||||
serverProps,
|
||||
viewKeyArg,
|
||||
}: {
|
||||
collectionOrGlobal: SanitizedCollectionConfig | SanitizedGlobalConfig
|
||||
serverProps: ServerPropsFromView
|
||||
viewKeyArg?: keyof EditConfig
|
||||
}) {
|
||||
}): CustomComponent[] {
|
||||
if (collectionOrGlobal?.admin?.components?.views?.edit) {
|
||||
let viewKey = viewKeyArg || 'default'
|
||||
if ('root' in collectionOrGlobal.admin.components.views.edit) {
|
||||
@@ -40,8 +37,8 @@ export function attachViewActions({
|
||||
viewKey,
|
||||
})
|
||||
|
||||
if (actions) {
|
||||
serverProps.viewActions = serverProps.viewActions.concat(actions)
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import type {
|
||||
AdminViewServerProps,
|
||||
CollectionSlug,
|
||||
CustomComponent,
|
||||
DocumentSubViewTypes,
|
||||
ImportMap,
|
||||
Payload,
|
||||
PayloadComponent,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
ServerPropsFromView,
|
||||
SanitizedGlobalConfig,
|
||||
ViewTypes,
|
||||
} from 'payload'
|
||||
import type React from 'react'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
|
||||
import type { initPage } from '../../utilities/initPage/index.js'
|
||||
import { parseDocumentID } from 'payload'
|
||||
import { formatAdminURL, isNumber } from 'payload/shared'
|
||||
|
||||
import { Account } from '../Account/index.js'
|
||||
import { BrowseByFolder } from '../BrowseByFolder/index.js'
|
||||
@@ -28,7 +29,7 @@ import { LogoutInactivity, LogoutView } from '../Logout/index.js'
|
||||
import { ResetPassword, resetPasswordBaseClass } from '../ResetPassword/index.js'
|
||||
import { UnauthorizedView } from '../Unauthorized/index.js'
|
||||
import { Verify, verifyBaseClass } from '../Verify/index.js'
|
||||
import { attachViewActions, getViewActions } from './attachViewActions.js'
|
||||
import { getSubViewActions, getViewActions } from './attachViewActions.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
import { getDocumentViewInfo } from './getDocumentViewInfo.js'
|
||||
import { isPathMatchingRoute } from './isPathMatchingRoute.js'
|
||||
@@ -62,59 +63,59 @@ const oneSegmentViews: OneSegmentViews = {
|
||||
unauthorized: UnauthorizedView,
|
||||
}
|
||||
|
||||
type GetRouteDataResult = {
|
||||
browseByFolderSlugs: CollectionSlug[]
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
DefaultView: ViewFromConfig
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
routeParams: {
|
||||
collection?: string
|
||||
folderCollection?: string
|
||||
folderID?: number | string
|
||||
global?: string
|
||||
id?: number | string
|
||||
token?: string
|
||||
versionID?: number | string
|
||||
}
|
||||
templateClassName: string
|
||||
templateType: 'default' | 'minimal'
|
||||
viewActions?: CustomComponent[]
|
||||
viewType?: ViewTypes
|
||||
}
|
||||
|
||||
type GetRouteDataArgs = {
|
||||
adminRoute: string
|
||||
config: SanitizedConfig
|
||||
collectionConfig?: SanitizedCollectionConfig
|
||||
currentRoute: string
|
||||
importMap: ImportMap
|
||||
globalConfig?: SanitizedGlobalConfig
|
||||
payload: Payload
|
||||
replaceListWithFolders?: boolean
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
segments: string[]
|
||||
}
|
||||
|
||||
type GetRouteDataResult = {
|
||||
browseByFolderSlugs: CollectionSlug[]
|
||||
DefaultView: ViewFromConfig
|
||||
documentSubViewType?: DocumentSubViewTypes
|
||||
folderID?: string
|
||||
initPageOptions: Parameters<typeof initPage>[0]
|
||||
serverProps: ServerPropsFromView
|
||||
templateClassName: string
|
||||
templateType: 'default' | 'minimal'
|
||||
viewType?: ViewTypes
|
||||
}
|
||||
|
||||
export const getRouteData = ({
|
||||
adminRoute,
|
||||
config,
|
||||
collectionConfig,
|
||||
currentRoute,
|
||||
importMap,
|
||||
searchParams,
|
||||
globalConfig,
|
||||
payload,
|
||||
replaceListWithFolders = false,
|
||||
segments,
|
||||
}: GetRouteDataArgs): GetRouteDataResult => {
|
||||
const { config } = payload
|
||||
let ViewToRender: ViewFromConfig = null
|
||||
let templateClassName: string
|
||||
let templateType: 'default' | 'minimal' | undefined
|
||||
let documentSubViewType: DocumentSubViewTypes
|
||||
let viewType: ViewTypes
|
||||
let folderID: string
|
||||
|
||||
const initPageOptions: Parameters<typeof initPage>[0] = {
|
||||
config,
|
||||
importMap,
|
||||
route: currentRoute,
|
||||
routeParams: {},
|
||||
searchParams,
|
||||
}
|
||||
const routeParams: GetRouteDataResult['routeParams'] = {}
|
||||
|
||||
const [segmentOne, segmentTwo, segmentThree, segmentFour, segmentFive, segmentSix] = segments
|
||||
|
||||
const isGlobal = segmentOne === 'globals'
|
||||
const isCollection = segmentOne === 'collections'
|
||||
let matchedCollection: SanitizedConfig['collections'][number] = undefined
|
||||
let matchedGlobal: SanitizedConfig['globals'][number] = undefined
|
||||
|
||||
const isBrowseByFolderEnabled = config.folders && config.folders.browseByFolder
|
||||
const browseByFolderSlugs =
|
||||
(isBrowseByFolderEnabled &&
|
||||
@@ -126,19 +127,7 @@ export const getRouteData = ({
|
||||
}, [])) ||
|
||||
[]
|
||||
|
||||
const serverProps: ServerPropsFromView = {
|
||||
viewActions: config?.admin?.components?.actions || [],
|
||||
}
|
||||
|
||||
if (isCollection) {
|
||||
matchedCollection = config.collections.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps.collectionConfig = matchedCollection
|
||||
}
|
||||
|
||||
if (isGlobal) {
|
||||
matchedGlobal = config.globals.find(({ slug }) => slug === segmentTwo)
|
||||
serverProps.globalConfig = matchedGlobal
|
||||
}
|
||||
const viewActions: CustomComponent[] = [...(config?.admin?.components?.actions || [])]
|
||||
|
||||
switch (segments.length) {
|
||||
case 0: {
|
||||
@@ -214,7 +203,7 @@ export const getRouteData = ({
|
||||
`/${segmentOne}` === config.admin.routes.browseByFolder
|
||||
) {
|
||||
// --> /browse-by-folder/:folderID
|
||||
initPageOptions.routeParams.folderID = folderID
|
||||
routeParams.folderID = segmentTwo
|
||||
|
||||
ViewToRender = {
|
||||
Component: oneSegmentViews.browseByFolder,
|
||||
@@ -222,24 +211,32 @@ export const getRouteData = ({
|
||||
templateClassName = baseClasses.folders
|
||||
templateType = 'default'
|
||||
viewType = 'folders'
|
||||
folderID = segmentTwo
|
||||
} else if (isCollection && matchedCollection) {
|
||||
// --> /collections/:collectionSlug
|
||||
initPageOptions.routeParams.collection = matchedCollection.slug
|
||||
} else if (collectionConfig) {
|
||||
// --> /collections/:collectionSlug'
|
||||
routeParams.collection = collectionConfig.slug
|
||||
|
||||
ViewToRender = {
|
||||
Component: ListView,
|
||||
if (replaceListWithFolders) {
|
||||
ViewToRender = {
|
||||
Component: CollectionFolderView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-folders`
|
||||
templateType = 'default'
|
||||
viewType = 'collection-folders'
|
||||
} else {
|
||||
ViewToRender = {
|
||||
Component: ListView,
|
||||
}
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
viewType = 'list'
|
||||
}
|
||||
|
||||
templateClassName = `${segmentTwo}-list`
|
||||
templateType = 'default'
|
||||
viewType = 'list'
|
||||
serverProps.viewActions = serverProps.viewActions.concat(
|
||||
matchedCollection.admin.components?.views?.list?.actions,
|
||||
)
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
viewActions.push(...(collectionConfig.admin.components?.views?.list?.actions || []))
|
||||
} else if (globalConfig) {
|
||||
// --> /globals/:globalSlug
|
||||
initPageOptions.routeParams.global = matchedGlobal.slug
|
||||
routeParams.global = globalConfig.slug
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -250,9 +247,9 @@ export const getRouteData = ({
|
||||
viewType = 'document'
|
||||
|
||||
// add default view actions
|
||||
serverProps.viewActions = serverProps.viewActions.concat(
|
||||
getViewActions({
|
||||
editConfig: matchedGlobal.admin?.components?.views?.edit,
|
||||
viewActions.push(
|
||||
...getViewActions({
|
||||
editConfig: globalConfig.admin?.components?.views?.edit,
|
||||
viewKey: 'default',
|
||||
}),
|
||||
)
|
||||
@@ -262,7 +259,8 @@ export const getRouteData = ({
|
||||
default:
|
||||
if (segmentTwo === 'verify') {
|
||||
// --> /:collectionSlug/verify/:token
|
||||
initPageOptions.routeParams.collection = segmentOne
|
||||
routeParams.collection = segmentOne
|
||||
routeParams.token = segmentThree
|
||||
|
||||
ViewToRender = {
|
||||
Component: Verify,
|
||||
@@ -271,8 +269,8 @@ export const getRouteData = ({
|
||||
templateClassName = 'verify'
|
||||
templateType = 'minimal'
|
||||
viewType = 'verify'
|
||||
} else if (isCollection && matchedCollection) {
|
||||
initPageOptions.routeParams.collection = matchedCollection.slug
|
||||
} else if (collectionConfig) {
|
||||
routeParams.collection = collectionConfig.slug
|
||||
|
||||
if (segmentThree === 'trash' && typeof segmentFour === 'string') {
|
||||
// --> /collections/:collectionSlug/trash/:id (read-only)
|
||||
@@ -280,8 +278,8 @@ export const getRouteData = ({
|
||||
// --> /collections/:collectionSlug/trash/:id/preview
|
||||
// --> /collections/:collectionSlug/trash/:id/versions
|
||||
// --> /collections/:collectionSlug/trash/:id/versions/:versionID
|
||||
initPageOptions.routeParams.id = segmentFour
|
||||
initPageOptions.routeParams.versionID = segmentSix
|
||||
routeParams.id = segmentFour
|
||||
routeParams.versionID = segmentSix
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -294,11 +292,12 @@ export const getRouteData = ({
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
viewActions.push(
|
||||
...getSubViewActions({
|
||||
collectionOrGlobal: collectionConfig,
|
||||
viewKeyArg: documentSubViewType,
|
||||
}),
|
||||
)
|
||||
} else if (segmentThree === 'trash') {
|
||||
// --> /collections/:collectionSlug/trash
|
||||
ViewToRender = {
|
||||
@@ -309,61 +308,60 @@ export const getRouteData = ({
|
||||
templateType = 'default'
|
||||
viewType = 'trash'
|
||||
|
||||
serverProps.viewActions = serverProps.viewActions.concat(
|
||||
matchedCollection.admin.components?.views?.list?.actions ?? [],
|
||||
)
|
||||
} else if (
|
||||
config.folders &&
|
||||
segmentThree === config.folders.slug &&
|
||||
matchedCollection.folders
|
||||
) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
||||
initPageOptions.routeParams.folderCollection = segmentThree
|
||||
initPageOptions.routeParams.folderID = segmentFour
|
||||
|
||||
ViewToRender = {
|
||||
Component: CollectionFolderView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-folders`
|
||||
templateType = 'default'
|
||||
viewType = 'collection-folders'
|
||||
folderID = segmentFour
|
||||
viewActions.push(...(collectionConfig.admin.components?.views?.list?.actions || []))
|
||||
} else {
|
||||
// Collection Edit Views
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
initPageOptions.routeParams.id = segmentThree
|
||||
initPageOptions.routeParams.versionID = segmentFive
|
||||
if (config.folders && segmentThree === config.folders.slug && collectionConfig.folders) {
|
||||
// Collection Folder Views
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug
|
||||
// --> /collections/:collectionSlug/:folderCollectionSlug/:folderID
|
||||
routeParams.folderCollection = segmentThree
|
||||
routeParams.folderID = segmentFour
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
ViewToRender = {
|
||||
Component: CollectionFolderView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-folders`
|
||||
templateType = 'default'
|
||||
viewType = 'collection-folders'
|
||||
|
||||
viewActions.push(...(collectionConfig.admin.components?.views?.list?.actions || []))
|
||||
} else {
|
||||
// Collection Edit Views
|
||||
// --> /collections/:collectionSlug/create
|
||||
// --> /collections/:collectionSlug/:id
|
||||
// --> /collections/:collectionSlug/:id/api
|
||||
// --> /collections/:collectionSlug/:id/versions
|
||||
// --> /collections/:collectionSlug/:id/versions/:versionID
|
||||
routeParams.id = segmentThree === 'create' ? undefined : segmentThree
|
||||
routeParams.versionID = segmentFive
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
viewActions.push(
|
||||
...getSubViewActions({
|
||||
collectionOrGlobal: collectionConfig,
|
||||
viewKeyArg: documentSubViewType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
templateClassName = `collection-default-edit`
|
||||
templateType = 'default'
|
||||
|
||||
const viewInfo = getDocumentViewInfo([segmentFour, segmentFive])
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedCollection,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
}
|
||||
} else if (isGlobal && matchedGlobal) {
|
||||
} else if (globalConfig) {
|
||||
// Global Edit Views
|
||||
// --> /globals/:globalSlug/versions
|
||||
// --> /globals/:globalSlug/versions/:versionID
|
||||
// --> /globals/:globalSlug/api
|
||||
initPageOptions.routeParams.global = matchedGlobal.slug
|
||||
initPageOptions.routeParams.versionID = segmentFour
|
||||
routeParams.global = globalConfig.slug
|
||||
routeParams.versionID = segmentFour
|
||||
|
||||
ViewToRender = {
|
||||
Component: DocumentView,
|
||||
@@ -376,11 +374,12 @@ export const getRouteData = ({
|
||||
viewType = viewInfo.viewType
|
||||
documentSubViewType = viewInfo.documentSubViewType
|
||||
|
||||
attachViewActions({
|
||||
collectionOrGlobal: matchedGlobal,
|
||||
serverProps,
|
||||
viewKeyArg: documentSubViewType,
|
||||
})
|
||||
viewActions.push(
|
||||
...getSubViewActions({
|
||||
collectionOrGlobal: globalConfig,
|
||||
viewKeyArg: documentSubViewType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -389,17 +388,53 @@ export const getRouteData = ({
|
||||
ViewToRender = getCustomViewByRoute({ config, currentRoute })?.view
|
||||
}
|
||||
|
||||
serverProps.viewActions.reverse()
|
||||
if (collectionConfig) {
|
||||
if (routeParams.id) {
|
||||
routeParams.id = parseDocumentID({
|
||||
id: routeParams.id,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
if (routeParams.versionID) {
|
||||
routeParams.versionID = parseDocumentID({
|
||||
id: routeParams.versionID,
|
||||
collectionSlug: collectionConfig.slug,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (config.folders && routeParams.folderID) {
|
||||
routeParams.folderID = parseDocumentID({
|
||||
id: routeParams.folderID,
|
||||
collectionSlug: config.folders.slug,
|
||||
payload,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalConfig && routeParams.versionID) {
|
||||
routeParams.versionID =
|
||||
payload.db.defaultIDType === 'number' && isNumber(routeParams.versionID)
|
||||
? Number(routeParams.versionID)
|
||||
: routeParams.versionID
|
||||
}
|
||||
|
||||
if (viewActions.length) {
|
||||
viewActions.reverse()
|
||||
}
|
||||
|
||||
return {
|
||||
browseByFolderSlugs,
|
||||
collectionConfig,
|
||||
DefaultView: ViewToRender,
|
||||
documentSubViewType,
|
||||
folderID,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
globalConfig,
|
||||
routeParams,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions: viewActions.length ? viewActions : undefined,
|
||||
viewType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type {
|
||||
AdminViewClientProps,
|
||||
AdminViewServerPropsOnly,
|
||||
ImportMap,
|
||||
SanitizedCollectionConfig,
|
||||
SanitizedConfig,
|
||||
SanitizedGlobalConfig,
|
||||
} from 'payload'
|
||||
|
||||
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
|
||||
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import {
|
||||
type AdminViewClientProps,
|
||||
type AdminViewServerPropsOnly,
|
||||
type ImportMap,
|
||||
parseDocumentID,
|
||||
type SanitizedConfig,
|
||||
} from 'payload'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import * as qs from 'qs-esm'
|
||||
import React from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
import { MinimalTemplate } from '../../templates/Minimal/index.js'
|
||||
import { initPage } from '../../utilities/initPage/index.js'
|
||||
import { getPreferences } from '../../utilities/getPreferences.js'
|
||||
import { getVisibleEntities } from '../../utilities/getVisisbleEntities.js'
|
||||
import { handleAuthRedirect } from '../../utilities/handleAuthRedirect.js'
|
||||
import { initReq } from '../../utilities/initReq.js'
|
||||
import { isCustomAdminView } from '../../utilities/isCustomAdminView.js'
|
||||
import { isPublicAdminRoute } from '../../utilities/isPublicAdminRoute.js'
|
||||
import { getCustomViewByRoute } from './getCustomViewByRoute.js'
|
||||
import { getRouteData } from './getRouteData.js'
|
||||
|
||||
@@ -60,32 +67,103 @@ export const RootPage = async ({
|
||||
})
|
||||
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
const isCollectionRoute = segments[0] === 'collections'
|
||||
const isGlobalRoute = segments[0] === 'globals'
|
||||
let collectionConfig: SanitizedCollectionConfig = undefined
|
||||
let globalConfig: SanitizedGlobalConfig = undefined
|
||||
|
||||
const searchParams = await searchParamsPromise
|
||||
|
||||
// Redirect `${adminRoute}/collections` to `${adminRoute}`
|
||||
if (segments.length === 1 && segments[0] === 'collections') {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/collections',
|
||||
})
|
||||
if (isCollectionRoute) {
|
||||
if (segments.length === 1) {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/collections',
|
||||
})
|
||||
|
||||
// Only redirect if there's NO custom view configured for /collections
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
// Only redirect if there's NO custom view configured for /collections
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
if (segments[1]) {
|
||||
collectionConfig = config.collections.find(({ slug }) => slug === segments[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect `${adminRoute}/globals` to `${adminRoute}`
|
||||
if (segments.length === 1 && segments[0] === 'globals') {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/globals',
|
||||
})
|
||||
if (isGlobalRoute) {
|
||||
if (segments.length === 1) {
|
||||
const { viewKey } = getCustomViewByRoute({
|
||||
config,
|
||||
currentRoute: '/globals',
|
||||
})
|
||||
|
||||
// Only redirect if there's NO custom view configured for /globals
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
// Only redirect if there's NO custom view configured for /globals
|
||||
if (!viewKey) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
if (segments[1]) {
|
||||
globalConfig = config.globals.find(({ slug }) => slug === segments[1])
|
||||
}
|
||||
}
|
||||
|
||||
if ((isCollectionRoute && !collectionConfig) || (isGlobalRoute && !globalConfig)) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}`
|
||||
const {
|
||||
cookies,
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
req: { payload },
|
||||
} = await initReq({
|
||||
configPromise: config,
|
||||
importMap,
|
||||
key: 'initPage',
|
||||
overrides: {
|
||||
fallbackLocale: false,
|
||||
req: {
|
||||
query: qs.parse(queryString, {
|
||||
depth: 10,
|
||||
ignoreQueryPrefix: true,
|
||||
}),
|
||||
},
|
||||
urlSuffix: `${currentRoute}${searchParams ? queryString : ''}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
!permissions.canAccessAdmin &&
|
||||
!isPublicAdminRoute({ adminRoute, config: payload.config, route: currentRoute }) &&
|
||||
!isCustomAdminView({ adminRoute, config: payload.config, route: currentRoute })
|
||||
) {
|
||||
redirect(
|
||||
handleAuthRedirect({
|
||||
config: payload.config,
|
||||
route: currentRoute,
|
||||
searchParams,
|
||||
user: req.user,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
let replaceListWithFolders = false
|
||||
|
||||
if (collectionConfig && segments.length === 2) {
|
||||
if (config.folders && collectionConfig.folders && segments[1] !== config.folders.slug) {
|
||||
const prefs = await getPreferences<{
|
||||
listViewType: string
|
||||
}>(`collection-${collectionConfig.slug}`, req.payload, req.user.id, config.admin.user)
|
||||
if (prefs?.value.listViewType && prefs.value.listViewType === 'folders') {
|
||||
replaceListWithFolders = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,29 +171,30 @@ export const RootPage = async ({
|
||||
browseByFolderSlugs,
|
||||
DefaultView,
|
||||
documentSubViewType,
|
||||
folderID: folderIDParam,
|
||||
initPageOptions,
|
||||
serverProps,
|
||||
routeParams,
|
||||
templateClassName,
|
||||
templateType,
|
||||
viewActions,
|
||||
viewType,
|
||||
} = getRouteData({
|
||||
adminRoute,
|
||||
config,
|
||||
collectionConfig,
|
||||
currentRoute,
|
||||
importMap,
|
||||
globalConfig,
|
||||
payload,
|
||||
replaceListWithFolders,
|
||||
searchParams,
|
||||
segments,
|
||||
})
|
||||
|
||||
const initPageResult = await initPage(initPageOptions)
|
||||
req.routeParams = routeParams
|
||||
|
||||
const dbHasUser =
|
||||
initPageResult.req.user ||
|
||||
(await initPageResult?.req.payload.db
|
||||
req.user ||
|
||||
(await req.payload.db
|
||||
.findOne({
|
||||
collection: userSlug,
|
||||
req: initPageResult?.req,
|
||||
req,
|
||||
})
|
||||
?.then((doc) => !!doc))
|
||||
|
||||
@@ -124,7 +203,7 @@ export const RootPage = async ({
|
||||
* The current route did not match any default views or custom route views.
|
||||
*/
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent) {
|
||||
if (initPageResult?.req?.user) {
|
||||
if (req?.user) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
@@ -133,27 +212,21 @@ export const RootPage = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof initPageResult?.redirectTo === 'string') {
|
||||
redirect(initPageResult.redirectTo)
|
||||
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
||||
|
||||
const usersCollection = config.collections.find(({ slug }) => slug === userSlug)
|
||||
const disableLocalStrategy = usersCollection?.auth?.disableLocalStrategy
|
||||
|
||||
if (disableLocalStrategy && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (initPageResult) {
|
||||
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
||||
if (!dbHasUser && currentRoute !== createFirstUserRoute && !disableLocalStrategy) {
|
||||
redirect(createFirstUserRoute)
|
||||
}
|
||||
|
||||
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
||||
const disableLocalStrategy = collectionConfig?.auth?.disableLocalStrategy
|
||||
|
||||
if (disableLocalStrategy && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (!dbHasUser && currentRoute !== createFirstUserRoute && !disableLocalStrategy) {
|
||||
redirect(createFirstUserRoute)
|
||||
}
|
||||
|
||||
if (dbHasUser && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
if (dbHasUser && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (!DefaultView?.Component && !DefaultView?.payloadComponent && !dbHasUser) {
|
||||
@@ -162,18 +235,13 @@ export const RootPage = async ({
|
||||
|
||||
const clientConfig = getClientConfig({
|
||||
config,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
i18n: req.i18n,
|
||||
importMap,
|
||||
})
|
||||
|
||||
const payload = initPageResult?.req.payload
|
||||
const folderID = payload.config.folders
|
||||
? parseDocumentID({
|
||||
id: folderIDParam,
|
||||
collectionSlug: payload.config.folders.slug,
|
||||
payload,
|
||||
})
|
||||
: undefined
|
||||
const visibleEntities = getVisibleEntities({ req })
|
||||
|
||||
const folderID = routeParams.folderID
|
||||
|
||||
const RenderedView = RenderServerComponent({
|
||||
clientProps: {
|
||||
@@ -186,16 +254,41 @@ export const RootPage = async ({
|
||||
Fallback: DefaultView.Component,
|
||||
importMap,
|
||||
serverProps: {
|
||||
...serverProps,
|
||||
clientConfig,
|
||||
docID: initPageResult?.docID,
|
||||
collectionConfig,
|
||||
docID: routeParams.id,
|
||||
folderID,
|
||||
i18n: initPageResult?.req.i18n,
|
||||
globalConfig,
|
||||
i18n: req.i18n,
|
||||
importMap,
|
||||
initPageResult,
|
||||
initPageResult: {
|
||||
collectionConfig,
|
||||
cookies,
|
||||
docID: routeParams.id,
|
||||
globalConfig,
|
||||
languageOptions: Object.entries(req.payload.config.i18n.supportedLanguages || {}).reduce(
|
||||
(acc, [language, languageConfig]) => {
|
||||
if (Object.keys(req.payload.config.i18n.supportedLanguages).includes(language)) {
|
||||
acc.push({
|
||||
label: languageConfig.translations.general.thisLanguage,
|
||||
value: language,
|
||||
})
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
[],
|
||||
),
|
||||
locale,
|
||||
permissions,
|
||||
req,
|
||||
translations: req.i18n.translations,
|
||||
visibleEntities,
|
||||
},
|
||||
params,
|
||||
payload: initPageResult?.req.payload,
|
||||
payload: req.payload,
|
||||
searchParams,
|
||||
viewActions,
|
||||
} satisfies AdminViewServerPropsOnly,
|
||||
})
|
||||
|
||||
@@ -207,25 +300,25 @@ export const RootPage = async ({
|
||||
)}
|
||||
{templateType === 'default' && (
|
||||
<DefaultTemplate
|
||||
collectionSlug={initPageResult?.collectionConfig?.slug}
|
||||
docID={initPageResult?.docID}
|
||||
collectionSlug={collectionConfig?.slug}
|
||||
docID={routeParams.id}
|
||||
documentSubViewType={documentSubViewType}
|
||||
globalSlug={initPageResult?.globalConfig?.slug}
|
||||
i18n={initPageResult?.req.i18n}
|
||||
locale={initPageResult?.locale}
|
||||
globalSlug={globalConfig?.slug}
|
||||
i18n={req.i18n}
|
||||
locale={locale}
|
||||
params={params}
|
||||
payload={initPageResult?.req.payload}
|
||||
permissions={initPageResult?.permissions}
|
||||
req={initPageResult?.req}
|
||||
payload={req.payload}
|
||||
permissions={permissions}
|
||||
req={req}
|
||||
searchParams={searchParams}
|
||||
user={initPageResult?.req.user}
|
||||
viewActions={serverProps.viewActions}
|
||||
user={req.user}
|
||||
viewActions={viewActions}
|
||||
viewType={viewType}
|
||||
visibleEntities={{
|
||||
// The reason we are not passing in initPageResult.visibleEntities directly is due to a "Cannot assign to read only property of object '#<Object>" error introduced in React 19
|
||||
// which this caused as soon as initPageResult.visibleEntities is passed in
|
||||
collections: initPageResult?.visibleEntities?.collections,
|
||||
globals: initPageResult?.visibleEntities?.globals,
|
||||
collections: visibleEntities?.collections,
|
||||
globals: visibleEntities?.globals,
|
||||
}}
|
||||
>
|
||||
{RenderedView}
|
||||
|
||||
@@ -40,12 +40,14 @@ export type AdminViewClientProps = {
|
||||
|
||||
export type AdminViewServerPropsOnly = {
|
||||
readonly clientConfig: ClientConfig
|
||||
readonly collectionConfig?: SanitizedCollectionConfig
|
||||
readonly disableActions?: boolean
|
||||
/**
|
||||
* @todo remove `docID` here as it is already contained in `initPageResult`
|
||||
*/
|
||||
readonly docID?: number | string
|
||||
readonly folderID?: number | string
|
||||
readonly globalConfig?: SanitizedGlobalConfig
|
||||
readonly importMap: ImportMap
|
||||
readonly initialData?: Data
|
||||
readonly initPageResult: InitPageResult
|
||||
@@ -54,6 +56,7 @@ export type AdminViewServerPropsOnly = {
|
||||
readonly redirectAfterDelete?: boolean
|
||||
readonly redirectAfterDuplicate?: boolean
|
||||
readonly redirectAfterRestore?: boolean
|
||||
readonly viewActions?: CustomComponent[]
|
||||
} & ServerProps
|
||||
|
||||
export type AdminViewServerProps = AdminViewClientProps & AdminViewServerPropsOnly
|
||||
|
||||
@@ -39,6 +39,7 @@ export type CollectionPreferences = {
|
||||
editViewType?: 'default' | 'live-preview'
|
||||
groupBy?: string
|
||||
limit?: number
|
||||
listViewType?: 'folders' | 'list'
|
||||
preset?: DefaultDocumentIDType
|
||||
sort?: string
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
&__account {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@layer payload-default {
|
||||
.list-pills {
|
||||
.default-list-view-tabs {
|
||||
display: flex;
|
||||
gap: calc(var(--base) * 0.5);
|
||||
}
|
||||
126
packages/ui/src/elements/DefaultListViewTabs/index.tsx
Normal file
126
packages/ui/src/elements/DefaultListViewTabs/index.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ClientConfig, ViewTypes } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React from 'react'
|
||||
|
||||
import { usePreferences } from '../../providers/Preferences/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'default-list-view-tabs'
|
||||
|
||||
type DefaultListViewTabsProps = {
|
||||
collectionConfig: ClientCollectionConfig
|
||||
config: ClientConfig
|
||||
onChange?: (viewType: ViewTypes) => void
|
||||
viewType?: ViewTypes
|
||||
}
|
||||
|
||||
export const DefaultListViewTabs: React.FC<DefaultListViewTabsProps> = ({
|
||||
collectionConfig,
|
||||
config,
|
||||
onChange,
|
||||
viewType,
|
||||
}) => {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { setPreference } = usePreferences()
|
||||
const router = useRouter()
|
||||
const isTrashEnabled = collectionConfig.trash
|
||||
const isFoldersEnabled = collectionConfig.folders && config.folders
|
||||
|
||||
if (!isTrashEnabled && !isFoldersEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleViewChange = async (newViewType: ViewTypes) => {
|
||||
if (onChange) {
|
||||
onChange(newViewType)
|
||||
}
|
||||
|
||||
if (newViewType === 'list' || newViewType === 'folders') {
|
||||
await setPreference(`collection-${collectionConfig.slug}`, {
|
||||
listViewType: newViewType,
|
||||
})
|
||||
}
|
||||
|
||||
let path: `/${string}` = `/collections/${collectionConfig.slug}`
|
||||
switch (newViewType) {
|
||||
case 'folders':
|
||||
if (config.folders) {
|
||||
path = `/collections/${collectionConfig.slug}/${config.folders.slug}`
|
||||
}
|
||||
break
|
||||
case 'trash':
|
||||
path = `/collections/${collectionConfig.slug}/trash`
|
||||
break
|
||||
}
|
||||
|
||||
const url = formatAdminURL({
|
||||
adminRoute: config.routes.admin,
|
||||
path,
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
|
||||
router.push(url)
|
||||
}
|
||||
|
||||
const allButtonLabel = `${t('general:all')} ${getTranslation(collectionConfig?.labels?.plural, i18n)}`
|
||||
const allButtonId = allButtonLabel.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
className={[`${baseClass}__button`, viewType === 'list' && `${baseClass}__button--active`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
disabled={viewType === 'list'}
|
||||
el="button"
|
||||
id={allButtonId}
|
||||
onClick={() => handleViewChange('list')}
|
||||
>
|
||||
{t('general:all')} {getTranslation(collectionConfig?.labels?.plural, i18n)}
|
||||
</Button>
|
||||
|
||||
{isFoldersEnabled && (
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
viewType === 'folders' && `${baseClass}__button--active`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
disabled={viewType === 'folders'}
|
||||
el="button"
|
||||
onClick={() => handleViewChange('folders')}
|
||||
>
|
||||
{t('folder:byFolder')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isTrashEnabled && (
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
viewType === 'trash' && `${baseClass}__button--active`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
disabled={viewType === 'trash'}
|
||||
el="button"
|
||||
id="trash-view-pill"
|
||||
onClick={() => handleViewChange('trash')}
|
||||
>
|
||||
{t('general:trash')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ViewTypes } from 'payload'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'list-pills'
|
||||
|
||||
type ByFolderPillProps = {
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
readonly folderCollectionSlug: string
|
||||
readonly viewType: ViewTypes
|
||||
}
|
||||
|
||||
export function ByFolderPill({
|
||||
collectionConfig,
|
||||
folderCollectionSlug,
|
||||
viewType,
|
||||
}: ByFolderPillProps) {
|
||||
const { t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
|
||||
if (!folderCollectionSlug) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
className={[
|
||||
`${baseClass}__button`,
|
||||
viewType === 'folders' && `${baseClass}__button--active`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
disabled={viewType === 'folders'}
|
||||
el={viewType === 'list' || viewType === 'trash' ? 'link' : 'div'}
|
||||
to={formatAdminURL({
|
||||
adminRoute: config.routes.admin,
|
||||
path: `/collections/${collectionConfig.slug}/${folderCollectionSlug}`,
|
||||
serverURL: config.serverURL,
|
||||
})}
|
||||
>
|
||||
{t('folder:byFolder')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ViewTypes } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'list-pills'
|
||||
|
||||
type DefaultListPillProps = {
|
||||
readonly collectionConfig: ClientCollectionConfig
|
||||
readonly viewType: ViewTypes
|
||||
}
|
||||
|
||||
export function DefaultListPill({ collectionConfig, viewType }: DefaultListPillProps) {
|
||||
const { i18n, t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
|
||||
const buttonLabel = `${t('general:all')} ${getTranslation(collectionConfig?.labels?.plural, i18n)}`
|
||||
const buttonId = buttonLabel.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
className={[`${baseClass}__button`, viewType === 'list' && `${baseClass}__button--active`]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
disabled={viewType === 'list'}
|
||||
el={viewType === 'folders' || viewType === 'trash' ? 'link' : 'div'}
|
||||
id={buttonId}
|
||||
to={formatAdminURL({
|
||||
adminRoute: config.routes.admin,
|
||||
path: `/collections/${collectionConfig.slug}`,
|
||||
serverURL: config.serverURL,
|
||||
})}
|
||||
>
|
||||
{t('general:all')} {getTranslation(collectionConfig?.labels?.plural, i18n)}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { ClientCollectionConfig, ViewTypes } from 'payload'
|
||||
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
|
||||
export function TrashPill({
|
||||
collectionConfig,
|
||||
viewType,
|
||||
}: {
|
||||
collectionConfig: ClientCollectionConfig
|
||||
readonly viewType: ViewTypes
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { config } = useConfig()
|
||||
|
||||
if (!collectionConfig.trash) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
buttonStyle="tab"
|
||||
disabled={viewType === 'trash'}
|
||||
el={viewType === 'list' || viewType === 'folders' ? 'link' : 'div'}
|
||||
id="trash-view-pill"
|
||||
key="trash-view-pill"
|
||||
to={formatAdminURL({
|
||||
adminRoute: config.routes.admin,
|
||||
path: `/collections/${collectionConfig.slug}/trash`,
|
||||
serverURL: config.serverURL,
|
||||
})}
|
||||
>
|
||||
{t('general:trash')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { useRouter } from 'next/navigation.js'
|
||||
import { formatAdminURL } from 'payload/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultListViewTabs } from '../../elements/DefaultListViewTabs/index.js'
|
||||
import { DroppableBreadcrumb } from '../../elements/FolderView/Breadcrumbs/index.js'
|
||||
import { ColoredFolderIcon } from '../../elements/FolderView/ColoredFolderIcon/index.js'
|
||||
import { CurrentFolderActions } from '../../elements/FolderView/CurrentFolderActions/index.js'
|
||||
@@ -21,9 +22,6 @@ import {
|
||||
ListBulkUploadButton,
|
||||
ListCreateNewDocInFolderButton,
|
||||
} from '../../elements/ListHeader/TitleActions/index.js'
|
||||
import { ByFolderPill } from '../../elements/ListHeaderTabs/ByFolderPill.js'
|
||||
import { DefaultListPill } from '../../elements/ListHeaderTabs/DefaultListPill.js'
|
||||
import { TrashPill } from '../../elements/ListHeaderTabs/TrashPill.js'
|
||||
import { NoListResults } from '../../elements/NoListResults/index.js'
|
||||
import { SearchBar } from '../../elements/SearchBar/index.js'
|
||||
import { useStepNav } from '../../elements/StepNav/index.js'
|
||||
@@ -274,28 +272,12 @@ function CollectionFolderViewInContext(props: CollectionFolderViewInContextProps
|
||||
key="list-selection"
|
||||
/>
|
||||
),
|
||||
config.folders && collectionConfig.folders && (
|
||||
<Fragment key="list-header-folder-view-buttons">
|
||||
<DefaultListPill
|
||||
collectionConfig={collectionConfig}
|
||||
key="list-header-default-button"
|
||||
viewType="folders"
|
||||
/>
|
||||
<ByFolderPill
|
||||
collectionConfig={collectionConfig}
|
||||
folderCollectionSlug={folderCollectionSlug}
|
||||
key="list-header-by-folder-button"
|
||||
viewType="folders"
|
||||
/>
|
||||
</Fragment>
|
||||
),
|
||||
collectionConfig.trash && (
|
||||
<TrashPill
|
||||
collectionConfig={collectionConfig}
|
||||
key="list-header-trash-button"
|
||||
viewType="folders"
|
||||
/>
|
||||
),
|
||||
<DefaultListViewTabs
|
||||
collectionConfig={collectionConfig}
|
||||
config={config}
|
||||
key="default-list-actions"
|
||||
viewType="folders"
|
||||
/>,
|
||||
].filter(Boolean)}
|
||||
AfterListHeaderContent={Description}
|
||||
title={getTranslation(labels?.plural, i18n)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getTranslation } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import { CloseModalButton } from '../../../elements/CloseModalButton/index.js'
|
||||
import { DefaultListViewTabs } from '../../../elements/DefaultListViewTabs/index.js'
|
||||
import { useListDrawerContext } from '../../../elements/ListDrawer/Provider.js'
|
||||
import { DrawerRelationshipSelect } from '../../../elements/ListHeader/DrawerRelationshipSelect/index.js'
|
||||
import { ListDrawerCreateNewDocButton } from '../../../elements/ListHeader/DrawerTitleActions/index.js'
|
||||
@@ -14,13 +15,10 @@ import {
|
||||
ListCreateNewButton,
|
||||
ListEmptyTrashButton,
|
||||
} from '../../../elements/ListHeader/TitleActions/index.js'
|
||||
import { ByFolderPill } from '../../../elements/ListHeaderTabs/ByFolderPill.js'
|
||||
import { DefaultListPill } from '../../../elements/ListHeaderTabs/DefaultListPill.js'
|
||||
import './index.scss'
|
||||
import { TrashPill } from '../../../elements/ListHeaderTabs/TrashPill.js'
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
import { useListQuery } from '../../../providers/ListQuery/index.js'
|
||||
import { ListSelection } from '../ListSelection/index.js'
|
||||
import './index.scss'
|
||||
|
||||
const drawerBaseClass = 'list-drawer'
|
||||
|
||||
@@ -119,28 +117,12 @@ export const CollectionListHeader: React.FC<ListHeaderProps> = ({
|
||||
viewType={viewType}
|
||||
/>
|
||||
),
|
||||
((collectionConfig.folders && config.folders) || isTrashEnabled) && (
|
||||
<DefaultListPill
|
||||
collectionConfig={collectionConfig}
|
||||
key="list-header-default-button"
|
||||
viewType={viewType}
|
||||
/>
|
||||
),
|
||||
collectionConfig.folders && config.folders && (
|
||||
<ByFolderPill
|
||||
collectionConfig={collectionConfig}
|
||||
folderCollectionSlug={config.folders.slug}
|
||||
key="list-header-by-folder-button"
|
||||
viewType={viewType}
|
||||
/>
|
||||
),
|
||||
isTrashEnabled && (
|
||||
<TrashPill
|
||||
collectionConfig={collectionConfig}
|
||||
key="list-header-trash-button"
|
||||
viewType={viewType}
|
||||
/>
|
||||
),
|
||||
<DefaultListViewTabs
|
||||
collectionConfig={collectionConfig}
|
||||
config={config}
|
||||
key="default-list-actions"
|
||||
viewType={viewType}
|
||||
/>,
|
||||
].filter(Boolean)}
|
||||
AfterListHeaderContent={Description}
|
||||
className={className}
|
||||
|
||||
@@ -367,11 +367,11 @@ test.describe('Folders', () => {
|
||||
})
|
||||
|
||||
test('should show By Folder button', async () => {
|
||||
const folderButton = page.locator('.list-pills__button', { hasText: 'By Folder' })
|
||||
const folderButton = page.locator('.default-list-view-tabs__button', { hasText: 'By Folder' })
|
||||
await expect(folderButton).toBeVisible()
|
||||
})
|
||||
test('should navigate to By Folder view', async () => {
|
||||
const folderButton = page.locator('.list-pills__button', { hasText: 'By Folder' })
|
||||
const folderButton = page.locator('.default-list-view-tabs__button', { hasText: 'By Folder' })
|
||||
await folderButton.click()
|
||||
await expect(page).toHaveURL(`${serverURL}/admin/collections/posts/payload-folders`)
|
||||
const foldersTitle = page.locator('.collection-folder-list', { hasText: 'Folders' })
|
||||
|
||||
Reference in New Issue
Block a user