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,
SanitizedConfig,
SanitizedGlobalConfig,
VisibleEntities,
} from 'payload/types'
import { initI18n } from '@payloadcms/translations'
@@ -11,7 +12,7 @@ import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js'
import { createLocalReq } from 'payload/utilities'
import { createLocalReq, isEntityHidden } from 'payload/utilities'
import qs from 'qs'
import { getPayloadHMR } from '../utilities/getPayloadHMR.js'
@@ -40,6 +41,15 @@ export const initPage = async ({
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 [entityType, entitySlug, createOrID] = routeSegments
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
@@ -73,7 +83,7 @@ export const initPage = async ({
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
const req = await createLocalReq(
const req = createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
@@ -118,5 +128,6 @@ export const initPage = async ({
permissions,
req,
translations: i18n.translations,
visibleEntities,
}
}

View File

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

View File

@@ -1,5 +1,5 @@
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 { SetStepNav } from '@payloadcms/ui/elements/StepNav'
@@ -15,8 +15,7 @@ export type DashboardProps = {
Link: React.ComponentType<any>
config: SanitizedConfig
permissions: Permissions
visibleCollections: string[]
visibleGlobals: string[]
visibleEntities: VisibleEntities
}
export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
@@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
},
},
permissions,
visibleCollections,
visibleGlobals,
visibleEntities,
} = props
return (
@@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
<DefaultDashboardClient
Link={Link}
permissions={permissions}
visibleCollections={visibleCollections}
visibleGlobals={visibleGlobals}
visibleEntities={visibleEntities}
/>
{Array.isArray(afterDashboard) &&
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 { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import LinkImport from 'next/link.js'
import { isEntityHidden } from 'payload/utilities'
import React, { Fragment } from 'react'
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
export const Dashboard: React.FC<AdminViewProps> = ({
initPageResult,
// searchParams,
}) => {
export const Dashboard: React.FC<AdminViewProps> = ({ initPageResult }) => {
const {
permissions,
req: {
payload: { config },
user,
},
visibleEntities,
} = initPageResult
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 = {
Link,
config,
permissions,
visibleCollections,
visibleGlobals,
visibleEntities,
}
return (

View File

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

View File

@@ -2,6 +2,7 @@ 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'
@@ -81,22 +82,14 @@ export const RootPage = async ({
<DefaultView initPageResult={initPageResult} params={params} searchParams={searchParams} />
)
if (templateType === 'minimal') {
return <MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
}
if (templateType === 'default') {
return (
<DefaultTemplate
config={config}
i18n={initPageResult.req.i18n}
permissions={initPageResult.permissions}
user={initPageResult.req.user}
>
{RenderedView}
</DefaultTemplate>
)
}
return RenderedView
return (
<EntityVisibilityProvider visibleEntities={initPageResult.visibleEntities}>
{templateType === 'minimal' && (
<MinimalTemplate className={templateClassName}>{RenderedView}</MinimalTemplate>
)}
{templateType === 'default' && (
<DefaultTemplate config={config}>{RenderedView}</DefaultTemplate>
)}
</EntityVisibilityProvider>
)
}

View File

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

View File

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

View File

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

View File

@@ -11,9 +11,11 @@ const baseClass = 'nav'
import { DefaultNavClient } from './index.client.js'
export const DefaultNav: React.FC<{
export type NavProps = {
config: SanitizedConfig
}> = (props) => {
}
export const DefaultNav: React.FC<NavProps> = (props) => {
const { config } = props
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 React from 'react'
import type { NavProps } from '../../elements/Nav/index.js'
import { AppHeader } from '../../elements/AppHeader/index.js'
import { NavToggler } from '../../elements/Nav/NavToggler/index.js'
import { DefaultNav } from '../../elements/Nav/index.js'
import { RenderCustomComponent } from '../../elements/RenderCustomComponent/index.js'
import { NavHamburger } from './NavHamburger/index.js'
export { NavHamburger } from './NavHamburger/index.js'
import { Wrapper } from './Wrapper/index.js'
export { Wrapper } from './Wrapper/index.js'
import './index.scss'
const baseClass = 'template-default'
@@ -19,18 +18,12 @@ export type DefaultTemplateProps = {
children?: React.ReactNode
className?: string
config: Promise<SanitizedConfig> | SanitizedConfig
i18n: any
permissions: Permissions
user: User
}
export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
children,
className,
config: configPromise,
i18n,
permissions,
user,
}) => {
const config = await configPromise
@@ -42,7 +35,10 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
} = {},
} = 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 (
<div>
<div className={`${baseClass}__nav-toggler-wrapper`} id="nav-toggler">
@@ -54,12 +50,7 @@ export const DefaultTemplate: React.FC<DefaultTemplateProps> = async ({
<RenderCustomComponent
CustomComponent={CustomNav}
DefaultComponent={DefaultNav}
componentProps={{
config,
i18n,
permissions,
user,
}}
componentProps={navProps}
/>
<div className={`${baseClass}__wrap`}>
<AppHeader />