fix(ui): establishes pattern for hidden entities (#5546)
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ export type {
|
||||
EditViewProps,
|
||||
InitPageResult,
|
||||
ServerSideEditViewProps,
|
||||
VisibleEntities,
|
||||
} from './views/types.js'
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
56
packages/ui/src/providers/EntityVisibility/index.tsx
Normal file
56
packages/ui/src/providers/EntityVisibility/index.tsx
Normal 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)
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user