From a3d6879c55eac222b8ac5ffbdc7a741ae95f5233 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 29 Mar 2024 10:34:09 -0400 Subject: [PATCH 1/5] fix(ui): wraps nav with fragment --- packages/ui/src/elements/Nav/index.client.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/elements/Nav/index.client.tsx b/packages/ui/src/elements/Nav/index.client.tsx index 9d0124dba..951b9b62b 100644 --- a/packages/ui/src/elements/Nav/index.client.tsx +++ b/packages/ui/src/elements/Nav/index.client.tsx @@ -3,7 +3,7 @@ import { getTranslation } from '@payloadcms/translations' import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility' import LinkWithDefault from 'next/link.js' -import React from 'react' +import React, { Fragment } from 'react' import type { EntityToGroup } from '../../utilities/groupNavItems.js' @@ -58,7 +58,7 @@ export const DefaultNavClient: React.FC = () => { ) return ( -
+ {groups.map(({ entities, label }, key) => { return ( @@ -102,6 +102,6 @@ export const DefaultNavClient: React.FC = () => { ) })} -
+ ) } From 5f7fcfd3df64bcb944174bd3c146d4dfd610152d Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 29 Mar 2024 11:29:40 -0400 Subject: [PATCH 2/5] chore(next): uses visibileEntities in getViewsFromConfig --- .../src/views/Document/getViewsFromConfig.tsx | 21 +------------------ packages/next/src/views/Document/index.tsx | 11 ++++++++-- packages/next/src/views/List/index.tsx | 7 ++++--- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/packages/next/src/views/Document/getViewsFromConfig.tsx b/packages/next/src/views/Document/getViewsFromConfig.tsx index aa4e92701..92b8473c4 100644 --- a/packages/next/src/views/Document/getViewsFromConfig.tsx +++ b/packages/next/src/views/Document/getViewsFromConfig.tsx @@ -1,4 +1,4 @@ -import type { CollectionPermission, GlobalPermission, User } from 'payload/auth' +import type { CollectionPermission, GlobalPermission } from 'payload/auth' import type { EditViewComponent } from 'payload/config' import type { AdminViewComponent, @@ -7,7 +7,6 @@ import type { SanitizedGlobalConfig, } from 'payload/types' -import { isEntityHidden } from 'payload/utilities' import React from 'react' import { APIView as DefaultAPIView } from '../API/index.js' @@ -26,7 +25,6 @@ export const getViewsFromConfig = ({ docPermissions, globalConfig, routeSegments, - user, }: { collectionConfig?: SanitizedCollectionConfig config: SanitizedConfig @@ -34,7 +32,6 @@ export const getViewsFromConfig = ({ docPermissions: CollectionPermission | GlobalPermission globalConfig?: SanitizedGlobalConfig routeSegments: string[] - user: User }): { CustomView: EditViewComponent DefaultView: EditViewComponent @@ -74,14 +71,6 @@ export const getViewsFromConfig = ({ const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] = routeSegments - const { - admin: { hidden }, - } = collectionConfig - - if (isEntityHidden({ hidden, user })) { - return null - } - // `../:id`, or `../create` switch (routeSegments.length) { case 3: { @@ -204,14 +193,6 @@ export const getViewsFromConfig = ({ // eslint-disable-next-line @typescript-eslint/no-unused-vars const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments - const { - admin: { hidden }, - } = globalConfig - - if (isEntityHidden({ hidden, user })) { - return null - } - switch (routeSegments.length) { case 2: { if (docPermissions?.read?.permission) { diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index c049175f9..e96ee65b5 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -45,6 +45,7 @@ export const Document: React.FC = async ({ }, user, }, + visibleEntities, } = initPageResult const segments = Array.isArray(params?.segments) ? params.segments : [] @@ -64,6 +65,10 @@ export const Document: React.FC = async ({ let action: string if (collectionConfig) { + if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) { + return + } + try { docPermissions = await docAccessOperation({ id, @@ -95,7 +100,6 @@ export const Document: React.FC = async ({ config, docPermissions, routeSegments: segments, - user, }) CustomView = collectionViews?.CustomView @@ -109,6 +113,10 @@ export const Document: React.FC = async ({ } if (globalConfig) { + if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) { + return + } + docPermissions = permissions?.globals?.[globalSlug] hasSavePermission = isEditing && docPermissions?.update?.permission action = `${serverURL}${apiRoute}/globals/${globalSlug}` @@ -126,7 +134,6 @@ export const Document: React.FC = async ({ docPermissions, globalConfig, routeSegments: segments, - user, }) CustomView = globalViews?.CustomView diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index fc1b016e8..b808f6b2c 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -8,7 +8,7 @@ import { ListQueryProvider } from '@payloadcms/ui/providers/ListQuery' import { notFound } from 'next/navigation.js' import { createClientCollectionConfig } from 'payload/config' import { type AdminViewProps } from 'payload/types' -import { isEntityHidden, isNumber, mergeListSearchAndWhere } from 'payload/utilities' +import { isNumber, mergeListSearchAndWhere } from 'payload/utilities' import React, { Fragment } from 'react' import type { DefaultListViewProps, ListPreferences } from './Default/types.js' @@ -29,6 +29,7 @@ export const ListView: React.FC = async ({ initPageResult, searc query, user, }, + visibleEntities, } = initPageResult const collectionSlug = collectionConfig?.slug @@ -62,10 +63,10 @@ export const ListView: React.FC = async ({ initPageResult, searc if (collectionConfig) { const { - admin: { components: { views: { List: CustomList } = {} } = {}, hidden }, + admin: { components: { views: { List: CustomList } = {} } = {} }, } = collectionConfig - if (isEntityHidden({ hidden, user })) { + if (!visibleEntities.collections.includes(collectionSlug)) { return notFound() } From a0cddbe9b38186cf59d7de450700d2a3df0a8680 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 29 Mar 2024 11:36:14 -0400 Subject: [PATCH 3/5] fix(richtext-slate): uses entity visibility hook when enabling relationships --- .../EnabledRelationshipsCondition.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/richtext-slate/src/field/elements/EnabledRelationshipsCondition.tsx b/packages/richtext-slate/src/field/elements/EnabledRelationshipsCondition.tsx index d57bb2201..9a0642a92 100644 --- a/packages/richtext-slate/src/field/elements/EnabledRelationshipsCondition.tsx +++ b/packages/richtext-slate/src/field/elements/EnabledRelationshipsCondition.tsx @@ -1,26 +1,30 @@ 'use client' import type { ClientUser } from 'payload/auth' -import type { ClientCollectionConfig } from 'payload/types' +import type { ClientCollectionConfig, VisibleEntities } from 'payload/types' import { useAuth } from '@payloadcms/ui/providers/Auth' import { useConfig } from '@payloadcms/ui/providers/Config' +import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility' import * as React from 'react' -type options = { +type Options = { uploads: boolean user: ClientUser + visibleEntities: VisibleEntities } type FilteredCollectionsT = ( collections: ClientCollectionConfig[], - options?: options, + options?: Options, ) => ClientCollectionConfig[] + const filterRichTextCollections: FilteredCollectionsT = (collections, options) => { - return collections.filter(({ admin: { enableRichTextRelationship, hidden }, upload }) => { - if (hidden === true || (typeof hidden === 'function' && hidden({ user: options.user }))) { + return collections.filter(({ slug, admin: { enableRichTextRelationship }, upload }) => { + if (!options.visibleEntities.collections.includes(slug)) { return false } + if (options?.uploads) { return enableRichTextRelationship && Boolean(upload) === true } @@ -33,8 +37,12 @@ export const EnabledRelationshipsCondition: React.FC = (props) => { const { children, uploads = false, ...rest } = props const { collections } = useConfig() const { user } = useAuth() + const { visibleEntities } = useEntityVisibility() + const [enabledCollectionSlugs] = React.useState(() => - filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug), + filterRichTextCollections(collections, { uploads, user, visibleEntities }).map( + ({ slug }) => slug, + ), ) if (!enabledCollectionSlugs.length) { From f5d9b4717710484389b63f2c9801f17958730d42 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 29 Mar 2024 11:54:02 -0400 Subject: [PATCH 4/5] fix(richtext-lexical): uses entity visibility hook when enabling relationships --- .../payload/src/collections/config/client.ts | 2 +- .../utils/EnabledRelationshipsCondition.tsx | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/payload/src/collections/config/client.ts b/packages/payload/src/collections/config/client.ts index ba6c9986d..a390232e7 100644 --- a/packages/payload/src/collections/config/client.ts +++ b/packages/payload/src/collections/config/client.ts @@ -16,7 +16,7 @@ export type ClientCollectionConfig = Omit< > & { admin: Omit< SanitizedCollectionConfig['admin'], - ServerOnlyCollectionAdminProperties & 'fields' & 'livePreview' + 'fields' | 'livePreview' | ServerOnlyCollectionAdminProperties > & { livePreview?: Omit } diff --git a/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx b/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx index 7fe30f852..f3d1c7253 100644 --- a/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx +++ b/packages/richtext-lexical/src/field/features/relationship/utils/EnabledRelationshipsCondition.tsx @@ -1,24 +1,28 @@ import type { ClientUser } from 'payload/auth' -import type { ClientCollectionConfig } from 'payload/types' +import type { ClientCollectionConfig, VisibleEntities } from 'payload/types' import { useAuth } from '@payloadcms/ui/providers/Auth' import { useConfig } from '@payloadcms/ui/providers/Config' +import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility' import * as React from 'react' -type options = { +type Options = { uploads: boolean user: ClientUser + visibleEntities: VisibleEntities } type FilteredCollectionsT = ( collections: ClientCollectionConfig[], - options?: options, + options?: Options, ) => ClientCollectionConfig[] + const filterRichTextCollections: FilteredCollectionsT = (collections, options) => { - return collections.filter(({ admin: { enableRichTextRelationship, hidden }, upload }) => { - if (hidden === true || (typeof hidden === 'function' && hidden({ user: options.user }))) { + return collections.filter(({ slug, admin: { enableRichTextRelationship }, upload }) => { + if (options.visibleEntities.collections.includes(slug)) { return false } + if (options?.uploads) { return enableRichTextRelationship && Boolean(upload) === true } @@ -31,8 +35,12 @@ export const EnabledRelationshipsCondition: React.FC = (props) => { const { children, uploads = false, ...rest } = props const { collections } = useConfig() const { user } = useAuth() + const { visibleEntities } = useEntityVisibility() + const [enabledCollectionSlugs] = React.useState(() => - filterRichTextCollections(collections, { uploads, user }).map(({ slug }) => slug), + filterRichTextCollections(collections, { uploads, user, visibleEntities }).map( + ({ slug }) => slug, + ), ) if (!enabledCollectionSlugs.length) { From e6b166da7db3c4c5d8ae6247f58af9318a11d395 Mon Sep 17 00:00:00 2001 From: Jacob Fletcher Date: Fri, 29 Mar 2024 13:30:00 -0400 Subject: [PATCH 5/5] fix(next): proper 404 handling --- .../admin/[[...segments]]/not-found.tsx | 9 ++- packages/next/src/exports/views.ts | 2 +- packages/next/src/utilities/initPage.ts | 8 +-- .../src/views/Document/getViewsFromConfig.tsx | 10 ---- packages/next/src/views/Document/index.tsx | 23 +++++--- packages/next/src/views/NotFound/index.tsx | 59 +++++++++++++++++-- packages/next/src/views/Root/index.tsx | 11 ++-- packages/ui/src/templates/Default/index.tsx | 41 +++++++------ 8 files changed, 109 insertions(+), 54 deletions(-) diff --git a/app/(payload)/admin/[[...segments]]/not-found.tsx b/app/(payload)/admin/[[...segments]]/not-found.tsx index 01e12371f..f3a774329 100644 --- a/app/(payload)/admin/[[...segments]]/not-found.tsx +++ b/app/(payload)/admin/[[...segments]]/not-found.tsx @@ -1,7 +1,9 @@ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ +import type { Metadata } from 'next' + import config from '@payload-config' /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ -import { NotFoundView } from '@payloadcms/next/views/NotFound/index.js' +import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views/NotFound/index.js' type Args = { params: { @@ -12,6 +14,9 @@ type Args = { } } -const NotFound = ({ params, searchParams }: Args) => NotFoundView({ config, params, searchParams }) +export const generateMetadata = ({ params, searchParams }: Args): Promise => + generatePageMetadata({ config, params, searchParams }) + +const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams }) export default NotFound diff --git a/packages/next/src/exports/views.ts b/packages/next/src/exports/views.ts index 05439ff42..4055e4d62 100644 --- a/packages/next/src/exports/views.ts +++ b/packages/next/src/exports/views.ts @@ -1,3 +1,3 @@ export { EditView } from '../views/Edit/index.js' -export { NotFoundView } from '../views/NotFound/index.js' +export { NotFoundPage } from '../views/NotFound/index.js' export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js' diff --git a/packages/next/src/utilities/initPage.ts b/packages/next/src/utilities/initPage.ts index c81d65ecd..fb2663128 100644 --- a/packages/next/src/utilities/initPage.ts +++ b/packages/next/src/utilities/initPage.ts @@ -22,8 +22,8 @@ import { getRequestLanguage } from './getRequestLanguage.js' type Args = { config: Promise | SanitizedConfig redirectUnauthenticatedUser?: boolean - route?: string - searchParams?: { [key: string]: string | string[] | undefined } + route: string + searchParams: { [key: string]: string | string[] | undefined } } export const initPage = async ({ @@ -59,7 +59,7 @@ export const initPage = async ({ const { collections, globals, localization, routes } = payload.config if (redirectUnauthenticatedUser && !user && route !== '/login') { - if ('redirect' in searchParams) delete searchParams.redirect + if (searchParams && 'redirect' in searchParams) delete searchParams.redirect const stringifiedSearchParams = Object.keys(searchParams ?? {}).length ? `?${qs.stringify(searchParams)}` @@ -81,7 +81,7 @@ export const initPage = async ({ translations, }) - const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}` + const queryString = `${qs.stringify(searchParams ?? {}, { addQueryPrefix: true })}` const req = createLocalReq( { diff --git a/packages/next/src/views/Document/getViewsFromConfig.tsx b/packages/next/src/views/Document/getViewsFromConfig.tsx index 92b8473c4..d56cbf55b 100644 --- a/packages/next/src/views/Document/getViewsFromConfig.tsx +++ b/packages/next/src/views/Document/getViewsFromConfig.tsx @@ -7,12 +7,9 @@ import type { SanitizedGlobalConfig, } from 'payload/types' -import React from 'react' - import { APIView as DefaultAPIView } from '../API/index.js' import { EditView as DefaultEditView } from '../Edit/index.js' import { LivePreviewView as DefaultLivePreviewView } from '../LivePreview/index.js' -import { NotFoundClient } from '../NotFound/index.client.js' import { Unauthorized } from '../Unauthorized/index.js' import { VersionView as DefaultVersionView } from '../Version/index.js' import { VersionsView as DefaultVersionsView } from '../Versions/index.js' @@ -140,9 +137,6 @@ export const getViewsFromConfig = ({ currentRoute, views, }) - - if (!CustomView) ErrorView = () => - break } } @@ -172,8 +166,6 @@ export const getViewsFromConfig = ({ currentRoute, views, }) - - if (!CustomView) ErrorView = () => } break } @@ -266,8 +258,6 @@ export const getViewsFromConfig = ({ currentRoute, views, }) - - if (!CustomView) ErrorView = () => } break } diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index e96ee65b5..6bf907f90 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -9,13 +9,12 @@ import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomCompo import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo' import { EditDepthProvider } from '@payloadcms/ui/providers/EditDepth' import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams' +import { notFound } from 'next/navigation.js' import { docAccessOperation } from 'payload/operations' import React from 'react' import type { GenerateEditViewMetadata } from './getMetaBySegment.js' -import { NotFoundClient } from '../NotFound/index.client.js' -import { NotFoundView } from '../NotFound/index.js' import { getMetaBySegment } from './getMetaBySegment.js' import { getViewsFromConfig } from './getViewsFromConfig.js' @@ -57,7 +56,7 @@ export const Document: React.FC = async ({ let ViewOverride: EditViewComponent let CustomView: EditViewComponent let DefaultView: EditViewComponent - let ErrorView: AdminViewComponent = NotFoundView + let ErrorView: AdminViewComponent let docPermissions: DocumentPermissions let hasSavePermission: boolean @@ -66,7 +65,7 @@ export const Document: React.FC = async ({ if (collectionConfig) { if (!visibleEntities?.collections?.find((visibleSlug) => visibleSlug === collectionSlug)) { - return + notFound() } try { @@ -78,7 +77,7 @@ export const Document: React.FC = async ({ req, }) } catch (error) { - return + notFound() } action = `${serverURL}${apiRoute}/${collectionSlug}${isEditing ? `/${id}` : ''}` @@ -108,13 +107,17 @@ export const Document: React.FC = async ({ } if (!CustomView && !DefaultView && !ViewOverride) { - return + if (ErrorView) { + return + } + + notFound() } } if (globalConfig) { if (!visibleEntities?.globals?.find((visibleSlug) => visibleSlug === globalSlug)) { - return + notFound() } docPermissions = permissions?.globals?.[globalSlug] @@ -141,7 +144,11 @@ export const Document: React.FC = async ({ ErrorView = globalViews?.ErrorView if (!CustomView && !DefaultView && !ViewOverride) { - return + if (ErrorView) { + return + } + + notFound() } } } diff --git a/packages/next/src/views/NotFound/index.tsx b/packages/next/src/views/NotFound/index.tsx index f5eba8795..3b8718a9d 100644 --- a/packages/next/src/views/NotFound/index.tsx +++ b/packages/next/src/views/NotFound/index.tsx @@ -1,14 +1,61 @@ -import type { AdminViewComponent } from 'payload/types' +import type { I18n } from '@payloadcms/translations' +import type { Metadata } from 'next' +import type { SanitizedConfig } from 'payload/types' +import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser' import { DefaultTemplate } from '@payloadcms/ui/templates/Default' -import React from 'react' +import React, { Fragment } from 'react' +import { initPage } from '../../utilities/initPage.js' import { NotFoundClient } from './index.client.js' -export const NotFoundView: AdminViewComponent = ({ initPageResult }) => { +export const generatePageMetadata = async ({ + i18n, +}: { + config: SanitizedConfig + i18n: I18n + params?: { [key: string]: string | string[] } + //eslint-disable-next-line @typescript-eslint/require-await +}): Promise => { + return { + title: i18n.t('general:notFound'), + } +} + +export type GenerateViewMetadata = (args: { + config: SanitizedConfig + i18n: I18n + params?: { [key: string]: string | string[] } +}) => Promise + +export const NotFoundPage = async ({ + config: configPromise, + searchParams, +}: { + config: Promise + params: { + segments: string[] + } + searchParams: { + [key: string]: string | string[] + } +}) => { + const initPageResult = await initPage({ + config: configPromise, + redirectUnauthenticatedUser: true, + route: '/not-found', + searchParams, + }) + return ( - - - + + + + + + ) } diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx index f76ea0eba..85096e608 100644 --- a/packages/next/src/views/Root/index.tsx +++ b/packages/next/src/views/Root/index.tsx @@ -2,11 +2,10 @@ import type { I18n } from '@payloadcms/translations' import type { Metadata } from 'next' import type { SanitizedConfig } from 'payload/types' -import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility' import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal' import { notFound, redirect } from 'next/navigation.js' -import React from 'react' +import React, { Fragment } from 'react' import { initPage } from '../../utilities/initPage.js' import { getViewFromConfig } from './getViewFromConfig.js' @@ -83,13 +82,15 @@ export const RootPage = async ({ ) return ( - + {templateType === 'minimal' && ( {RenderedView} )} {templateType === 'default' && ( - {RenderedView} + + {RenderedView} + )} - + ) } diff --git a/packages/ui/src/templates/Default/index.tsx b/packages/ui/src/templates/Default/index.tsx index 9c014605e..7bba8afd6 100644 --- a/packages/ui/src/templates/Default/index.tsx +++ b/packages/ui/src/templates/Default/index.tsx @@ -1,5 +1,6 @@ -import type { SanitizedConfig } from 'payload/types' +import type { SanitizedConfig, VisibleEntities } from 'payload/types' +import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility' import React from 'react' import type { NavProps } from '../../elements/Nav/index.js' @@ -18,12 +19,14 @@ export type DefaultTemplateProps = { children?: React.ReactNode className?: string config: Promise | SanitizedConfig + visibleEntities?: VisibleEntities } export const DefaultTemplate: React.FC = async ({ children, className, config: configPromise, + visibleEntities, }) => { const config = await configPromise @@ -40,23 +43,25 @@ export const DefaultTemplate: React.FC = async ({ } return ( -
- - - -
- - {children} + +
+ - -
+ + +
+ + {children} +
+
+
+ ) }