fix(ui): establishes pattern for hidden entities (#5546)

This commit is contained in:
Jacob Fletcher
2024-03-29 10:32:09 -04:00
committed by GitHub
parent c9c3a689d8
commit b6ad218126
12 changed files with 119 additions and 93 deletions

View File

@@ -4,6 +4,7 @@ import type {
SanitizedCollectionConfig, SanitizedCollectionConfig,
SanitizedConfig, SanitizedConfig,
SanitizedGlobalConfig, SanitizedGlobalConfig,
VisibleEntities,
} from 'payload/types' } from 'payload/types'
import { initI18n } from '@payloadcms/translations' import { initI18n } from '@payloadcms/translations'
@@ -11,7 +12,7 @@ import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode' import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js' import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import { createLocalReq } from 'payload/utilities' import { createLocalReq, isEntityHidden } from 'payload/utilities'
import qs from 'qs' import qs from 'qs'
import { getPayloadHMR } from '../utilities/getPayloadHMR.js' import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
@@ -40,6 +41,15 @@ export const initPage = async ({
payload, payload,
}) })
const visibleEntities: VisibleEntities = {
collections: payload.config.collections
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
globals: payload.config.globals
.map(({ slug, admin: { hidden } }) => (!isEntityHidden({ hidden, user }) ? slug : null))
.filter(Boolean),
}
const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean) const routeSegments = route.replace(payload.config.routes.admin, '').split('/').filter(Boolean)
const [entityType, entitySlug, createOrID] = routeSegments const [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined const collectionSlug = entityType === 'collections' ? entitySlug : undefined
@@ -73,7 +83,7 @@ export const initPage = async ({
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}` const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
const req = await createLocalReq( const req = createLocalReq(
{ {
fallbackLocale: null, fallbackLocale: null,
locale: locale.code, locale: locale.code,
@@ -118,5 +128,6 @@ export const initPage = async ({
permissions, permissions,
req, req,
translations: i18n.translations, translations: i18n.translations,
visibleEntities,
} }
} }

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems' import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
import type { Permissions } from 'payload/auth' import type { Permissions } from 'payload/auth'
import type { VisibleEntities } from 'payload/types'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { Button } from '@payloadcms/ui/elements/Button' import { Button } from '@payloadcms/ui/elements/Button'
@@ -19,9 +20,8 @@ const baseClass = 'dashboard'
export const DefaultDashboardClient: React.FC<{ export const DefaultDashboardClient: React.FC<{
Link: React.ComponentType Link: React.ComponentType
permissions: Permissions permissions: Permissions
visibleCollections: string[] visibleEntities: VisibleEntities
visibleGlobals: string[] }> = ({ Link, permissions, visibleEntities }) => {
}> = ({ Link, permissions, visibleCollections, visibleGlobals }) => {
const config = useConfig() const config = useConfig()
const { const {
@@ -40,13 +40,13 @@ export const DefaultDashboardClient: React.FC<{
const collections = collectionsConfig.filter( const collections = collectionsConfig.filter(
(collection) => (collection) =>
permissions?.collections?.[collection.slug]?.read?.permission && permissions?.collections?.[collection.slug]?.read?.permission &&
visibleCollections.includes(collection.slug), visibleEntities.collections.includes(collection.slug),
) )
const globals = globalsConfig.filter( const globals = globalsConfig.filter(
(global) => (global) =>
permissions?.globals?.[global.slug]?.read?.permission && permissions?.globals?.[global.slug]?.read?.permission &&
visibleGlobals.includes(global.slug), visibleEntities.globals.includes(global.slug),
) )
setGroups( setGroups(
@@ -73,15 +73,7 @@ export const DefaultDashboardClient: React.FC<{
i18n, i18n,
), ),
) )
}, [ }, [permissions, user, i18n, visibleEntities, collectionsConfig, globalsConfig])
permissions,
user,
i18n,
visibleCollections,
visibleGlobals,
collectionsConfig,
globalsConfig,
])
return ( return (
<Fragment> <Fragment>

View File

@@ -1,5 +1,5 @@
import type { Permissions } from 'payload/auth' import type { Permissions } from 'payload/auth'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig, VisibleEntities } from 'payload/types'
import { Gutter } from '@payloadcms/ui/elements/Gutter' import { Gutter } from '@payloadcms/ui/elements/Gutter'
import { SetStepNav } from '@payloadcms/ui/elements/StepNav' import { SetStepNav } from '@payloadcms/ui/elements/StepNav'
@@ -15,8 +15,7 @@ export type DashboardProps = {
Link: React.ComponentType<any> Link: React.ComponentType<any>
config: SanitizedConfig config: SanitizedConfig
permissions: Permissions permissions: Permissions
visibleCollections: string[] visibleEntities: VisibleEntities
visibleGlobals: string[]
} }
export const DefaultDashboard: React.FC<DashboardProps> = (props) => { export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
@@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
}, },
}, },
permissions, permissions,
visibleCollections, visibleEntities,
visibleGlobals,
} = props } = props
return ( return (
@@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
<DefaultDashboardClient <DefaultDashboardClient
Link={Link} Link={Link}
permissions={permissions} permissions={permissions}
visibleCollections={visibleCollections} visibleEntities={visibleEntities}
visibleGlobals={visibleGlobals}
/> />
{Array.isArray(afterDashboard) && {Array.isArray(afterDashboard) &&
afterDashboard.map((Component, i) => <Component key={i} />)} afterDashboard.map((Component, i) => <Component key={i} />)}

View File

@@ -3,7 +3,6 @@ import type { AdminViewProps } from 'payload/types'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser' import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent' import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import LinkImport from 'next/link.js' import LinkImport from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import type { DashboardProps } from './Default/index.js' import type { DashboardProps } from './Default/index.js'
@@ -14,40 +13,23 @@ export { generateDashboardMetadata } from './meta.js'
const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default
export const Dashboard: React.FC<AdminViewProps> = ({ export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
initPageResult,
// searchParams,
}) => {
const { const {
permissions, permissions,
req: { req: {
payload: { config }, payload: { config },
user, user,
}, },
visibleEntities,
} = initPageResult } = initPageResult
const CustomDashboardComponent = config.admin.components?.views?.Dashboard const CustomDashboardComponent = config.admin.components?.views?.Dashboard
const visibleCollections: string[] = config.collections.reduce((acc, collection) => {
if (!isEntityHidden({ hidden: collection.admin.hidden, user })) {
acc.push(collection.slug)
}
return acc
}, [])
const visibleGlobals: string[] = config.globals.reduce((acc, global) => {
if (!isEntityHidden({ hidden: global.admin.hidden, user })) {
acc.push(global.slug)
}
return acc
}, [])
const viewComponentProps: DashboardProps = { const viewComponentProps: DashboardProps = {
Link, Link,
config, config,
permissions, permissions,
visibleCollections, visibleEntities,
visibleGlobals,
} }
return ( return (

View File

@@ -7,12 +7,7 @@ import { NotFoundClient } from './index.client.js'
export const NotFoundView: AdminViewComponent = ({ initPageResult }) => { export const NotFoundView: AdminViewComponent = ({ initPageResult }) => {
return ( return (
<DefaultTemplate <DefaultTemplate config={initPageResult?.req?.payload.config}>
config={initPageResult?.req?.payload.config}
i18n={initPageResult?.req?.i18n}
permissions={initPageResult?.permissions}
user={initPageResult?.req?.user}
>
<NotFoundClient /> <NotFoundClient />
</DefaultTemplate> </DefaultTemplate>
) )

View File

@@ -2,6 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import { EntityVisibilityProvider } from '@payloadcms/ui/providers/EntityVisibility'
import { DefaultTemplate } from '@payloadcms/ui/templates/Default' import { DefaultTemplate } from '@payloadcms/ui/templates/Default'
import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal' import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
@@ -81,22 +82,14 @@ export const RootPage = async ({
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} /> <DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
) )
if (templateType === 'minimal') { return (
return <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate> <EntityVisibilityProvider visibleEntities={initPageResult.visibleEntities}>
} {templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
if (templateType === 'default') { )}
return ( {templateType === 'default' && (
<DefaultTemplate <DefaultTemplate config={config}>{RenderedView}</DefaultTemplate>
config={config} )}
i18n={initPageResult.req.i18n} </EntityVisibilityProvider>
permissions={initPageResult.permissions} )
user={initPageResult.req.user}
>
{RenderedView}
</DefaultTemplate>
)
}
return RenderedView
} }

View File

@@ -30,4 +30,5 @@ export type {
EditViewProps, EditViewProps,
InitPageResult, InitPageResult,
ServerSideEditViewProps, ServerSideEditViewProps,
VisibleEntities,
} from './views/types.js' } from './views/types.js'

View File

@@ -29,6 +29,11 @@ export type EditViewProps = {
globalSlug?: string globalSlug?: string
} }
export type VisibleEntities = {
collections: SanitizedCollectionConfig['slug'][]
globals: SanitizedGlobalConfig['slug'][]
}
export type InitPageResult = { export type InitPageResult = {
collectionConfig?: SanitizedCollectionConfig collectionConfig?: SanitizedCollectionConfig
cookies: Map<string, string> cookies: Map<string, string>
@@ -38,6 +43,7 @@ export type InitPageResult = {
permissions: Permissions permissions: Permissions
req: PayloadRequest req: PayloadRequest
translations: Translations translations: Translations
visibleEntities: VisibleEntities
} }
export type ServerSideEditViewProps = { export type ServerSideEditViewProps = {

View File

@@ -1,7 +1,8 @@
'use client' 'use client'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useEntityVisibility } from '@payloadcms/ui/providers/EntityVisibility'
import LinkWithDefault from 'next/link.js' import LinkWithDefault from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React from 'react' import React from 'react'
import type { EntityToGroup } from '../../utilities/groupNavItems.js' import type { EntityToGroup } from '../../utilities/groupNavItems.js'
@@ -17,7 +18,9 @@ import { useNav } from './context.js'
const baseClass = 'nav' const baseClass = 'nav'
export const DefaultNavClient: React.FC = () => { export const DefaultNavClient: React.FC = () => {
const { permissions, user } = useAuth() const { permissions } = useAuth()
const { isEntityVisible } = useEntityVisibility()
const { const {
collections, collections,
globals, globals,
@@ -30,8 +33,7 @@ export const DefaultNavClient: React.FC = () => {
const groups = groupNavItems( const groups = groupNavItems(
[ [
...collections ...collections
// @ts-expect-error todo: fix type error here .filter(({ slug }) => isEntityVisible({ collectionSlug: slug }))
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
.map((collection) => { .map((collection) => {
const entityToGroup: EntityToGroup = { const entityToGroup: EntityToGroup = {
type: EntityType.collection, type: EntityType.collection,
@@ -41,8 +43,7 @@ export const DefaultNavClient: React.FC = () => {
return entityToGroup return entityToGroup
}), }),
...globals ...globals
// @ts-expect-error todo: fix type error here .filter(({ slug }) => isEntityVisible({ globalSlug: slug }))
.filter(({ admin: { hidden } }) => !isEntityHidden({ hidden, user }))
.map((global) => { .map((global) => {
const entityToGroup: EntityToGroup = { const entityToGroup: EntityToGroup = {
type: EntityType.global, type: EntityType.global,
@@ -85,7 +86,6 @@ export const DefaultNavClient: React.FC = () => {
return ( return (
<LinkElement <LinkElement
// activeClassName="active"
className={`${baseClass}__link`} className={`${baseClass}__link`}
href={href} href={href}
id={id} id={id}

View File

@@ -11,9 +11,11 @@ const baseClass = 'nav'
import { DefaultNavClient } from './index.client.js' import { DefaultNavClient } from './index.client.js'
export const DefaultNav: React.FC<{ export type NavProps = {
config: SanitizedConfig config: SanitizedConfig
}> = (props) => { }
export const DefaultNav: React.FC<NavProps> = (props) => {
const { config } = props const { config } = props
if (!config) { if (!config) {

View File

@@ -0,0 +1,56 @@
'use client'
import type {
SanitizedCollectionConfig,
SanitizedGlobalConfig,
VisibleEntities,
} from 'packages/payload/src/exports/types.js'
import React, { createContext, useCallback, useContext } from 'react'
export type VisibleEntitiesContextType = {
isEntityVisible: ({
collectionSlug,
globalSlug,
}: {
collectionSlug?: SanitizedCollectionConfig['slug']
globalSlug?: SanitizedGlobalConfig['slug']
}) => boolean
visibleEntities: VisibleEntities
}
export const EntityVisibilityContext = createContext({} as VisibleEntitiesContextType)
export const EntityVisibilityProvider: React.FC<{
children: React.ReactNode
visibleEntities?: VisibleEntities
}> = ({ children, visibleEntities }) => {
const isEntityVisible = useCallback(
({
collectionSlug,
globalSlug,
}: {
collectionSlug: SanitizedCollectionConfig['slug']
globalSlug: SanitizedGlobalConfig['slug']
}) => {
if (collectionSlug) {
return visibleEntities.collections.includes(collectionSlug)
}
if (globalSlug) {
return visibleEntities.globals.includes(globalSlug)
}
return false
},
[visibleEntities],
)
return (
<EntityVisibilityContext.Provider value={{ isEntityVisible, visibleEntities }}>
{children}
</EntityVisibilityContext.Provider>
)
}
export const useEntityVisibility = (): VisibleEntitiesContextType =>
useContext(EntityVisibilityContext)

View File

@@ -1,16 +1,15 @@
import type { Permissions, User } from 'payload/auth'
import type { SanitizedConfig } from 'payload/types' import type { SanitizedConfig } from 'payload/types'
import React from 'react' import React from 'react'
import type { NavProps } from '../../elements/Nav/index.js'
import { AppHeader } from '../../elements/AppHeader/index.js' import { AppHeader } from '../../elements/AppHeader/index.js'
import { NavToggler } from '../../elements/Nav/NavToggler/index.js' import { NavToggler } from '../../elements/Nav/NavToggler/index.js'
import { DefaultNav } from '../../elements/Nav/index.js' import { DefaultNav } from '../../elements/Nav/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js' import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { NavHamburger } from './NavHamburger/index.js' import { NavHamburger } from './NavHamburger/index.js'
export { NavHamburger } from './NavHamburger/index.js'
import { Wrapper } from './Wrapper/index.js' import { Wrapper } from './Wrapper/index.js'
export { Wrapper } from './Wrapper/index.js'
import './index.scss' import './index.scss'
const baseClass = 'template-default' const baseClass = 'template-default'
@@ -19,18 +18,12 @@ export type DefaultTemplateProps = {
children?: React.ReactNode children?: React.ReactNode
className?: string className?: string
config: Promise<SanitizedConfig> | SanitizedConfig config: Promise<SanitizedConfig> | SanitizedConfig
i18n: any
permissions: Permissions
user: User
} }
export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
children, children,
className, className,
config: configPromise, config: configPromise,
i18n,
permissions,
user,
}) => { }) => {
const config = await configPromise const config = await configPromise
@@ -42,7 +35,10 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
} = {}, } = {},
} = config || {} } = config || {}
// #nav-toggler needs to be wrapped in a div, not Fragment. This fixes https://github.com/shadcn-ui/ui/issues/1355#issuecomment-1909192594 const navProps: NavProps = {
config,
}
return ( return (
<div> <div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler"> <div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
@@ -54,12 +50,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
<RenderCustomComponent <RenderCustomComponent
CustomComponent={CustomNav} CustomComponent={CustomNav}
DefaultComponent={DefaultNav} DefaultComponent={DefaultNav}
componentProps={{ componentProps={navProps}
config,
i18n,
permissions,
user,
}}
/> />
<div className={`${baseClass}__wrap`}> <div className={`${baseClass}__wrap`}>
<AppHeader /> <AppHeader />