diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a1dba6cd3..4d5b2d8ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -217,7 +217,7 @@ jobs: # find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {} suite: - _community - # - access-control + - access-control # - admin - auth - field-error-states diff --git a/packages/next/src/utilities/initPage.ts b/packages/next/src/utilities/initPage.ts index 033097904..c81d65ecd 100644 --- a/packages/next/src/utilities/initPage.ts +++ b/packages/next/src/utilities/initPage.ts @@ -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, } } diff --git a/packages/next/src/views/Dashboard/Default/index.client.tsx b/packages/next/src/views/Dashboard/Default/index.client.tsx index 4338153a3..5bd773019 100644 --- a/packages/next/src/views/Dashboard/Default/index.client.tsx +++ b/packages/next/src/views/Dashboard/Default/index.client.tsx @@ -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 ( diff --git a/packages/next/src/views/Dashboard/Default/index.tsx b/packages/next/src/views/Dashboard/Default/index.tsx index a937a3240..6e52c7028 100644 --- a/packages/next/src/views/Dashboard/Default/index.tsx +++ b/packages/next/src/views/Dashboard/Default/index.tsx @@ -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 config: SanitizedConfig permissions: Permissions - visibleCollections: string[] - visibleGlobals: string[] + visibleEntities: VisibleEntities } export const DefaultDashboard: React.FC = (props) => { @@ -28,8 +27,7 @@ export const DefaultDashboard: React.FC = (props) => { }, }, permissions, - visibleCollections, - visibleGlobals, + visibleEntities, } = props return ( @@ -42,8 +40,7 @@ export const DefaultDashboard: React.FC = (props) => { {Array.isArray(afterDashboard) && afterDashboard.map((Component, i) => )} diff --git a/packages/next/src/views/Dashboard/index.tsx b/packages/next/src/views/Dashboard/index.tsx index 90c9c51e6..01c00c426 100644 --- a/packages/next/src/views/Dashboard/index.tsx +++ b/packages/next/src/views/Dashboard/index.tsx @@ -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 = ({ - initPageResult, - // searchParams, -}) => { +export const Dashboard: React.FC = ({ 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 ( diff --git a/packages/next/src/views/Edit/Default/index.tsx b/packages/next/src/views/Edit/Default/index.tsx index 1094e1fda..2add29452 100644 --- a/packages/next/src/views/Edit/Default/index.tsx +++ b/packages/next/src/views/Edit/Default/index.tsx @@ -124,7 +124,7 @@ export const DefaultEditView: React.FC = () => { }) } - if (!isEditing) { + if (!isEditing && depth < 2) { // Redirect to the same locale if it's been set const redirectRoute = `${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}` router.push(redirectRoute) @@ -144,6 +144,7 @@ export const DefaultEditView: React.FC = () => { id, entitySlug, user, + depth, collectionSlug, getVersions, getDocPermissions, diff --git a/packages/next/src/views/NotFound/index.tsx b/packages/next/src/views/NotFound/index.tsx index dc50b5a90..f5eba8795 100644 --- a/packages/next/src/views/NotFound/index.tsx +++ b/packages/next/src/views/NotFound/index.tsx @@ -7,12 +7,7 @@ import { NotFoundClient } from './index.client.js' export const NotFoundView: AdminViewComponent = ({ initPageResult }) => { return ( - + ) diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx index bb7416cee..f76ea0eba 100644 --- a/packages/next/src/views/Root/index.tsx +++ b/packages/next/src/views/Root/index.tsx @@ -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 ({ ) - if (templateType === 'minimal') { - return {RenderedView} - } - - if (templateType === 'default') { - return ( - - {RenderedView} - - ) - } - - return RenderedView + return ( + + {templateType === 'minimal' && ( + {RenderedView} + )} + {templateType === 'default' && ( + {RenderedView} + )} + + ) } diff --git a/packages/payload/src/admin/types.ts b/packages/payload/src/admin/types.ts index 5e3eec2da..42c66d675 100644 --- a/packages/payload/src/admin/types.ts +++ b/packages/payload/src/admin/types.ts @@ -30,4 +30,5 @@ export type { EditViewProps, InitPageResult, ServerSideEditViewProps, + VisibleEntities, } from './views/types.js' diff --git a/packages/payload/src/admin/views/types.ts b/packages/payload/src/admin/views/types.ts index 9500532d1..7e0f4a9b9 100644 --- a/packages/payload/src/admin/views/types.ts +++ b/packages/payload/src/admin/views/types.ts @@ -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 @@ -38,6 +43,7 @@ export type InitPageResult = { permissions: Permissions req: PayloadRequest translations: Translations + visibleEntities: VisibleEntities } export type ServerSideEditViewProps = { diff --git a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx index 4097348e2..d7e2e82c8 100644 --- a/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx +++ b/packages/ui/src/elements/DocumentDrawer/DrawerContent.tsx @@ -39,6 +39,7 @@ export const DocumentDrawerContent: React.FC = ({ const { closeModal, modalState, toggleModal } = useModal() const locale = useLocale() const { t } = useTranslation() + const [createdID, setCreatedID] = useState() const [isOpen, setIsOpen] = useState(false) const [collectionConfig] = useRelatedCollections(collectionSlug) const { formQueryParams } = useFormQueryParams() @@ -69,6 +70,7 @@ export const DocumentDrawerContent: React.FC = ({ const onSave = useCallback( (args) => { + setCreatedID(args.doc.id) if (typeof onSaveFromProps === 'function') { void onSaveFromProps({ ...args, @@ -113,7 +115,7 @@ export const DocumentDrawerContent: React.FC = ({ // Same reason as above. We need to fully-fetch the docPreferences from the server. This is done in DocumentInfoProvider if we set it to null here. hasSavePermission={null} // isLoading, - id={id} + id={id || createdID} isEditing={isEditing} onLoadError={onLoadError} onSave={onSave} diff --git a/packages/ui/src/elements/Nav/index.client.tsx b/packages/ui/src/elements/Nav/index.client.tsx index d5b54a595..9d0124dba 100644 --- a/packages/ui/src/elements/Nav/index.client.tsx +++ b/packages/ui/src/elements/Nav/index.client.tsx @@ -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 ( = (props) => { +} + +export const DefaultNav: React.FC = (props) => { const { config } = props if (!config) { diff --git a/packages/ui/src/forms/Form/index.tsx b/packages/ui/src/forms/Form/index.tsx index e24e79645..57fe040a9 100644 --- a/packages/ui/src/forms/Form/index.tsx +++ b/packages/ui/src/forms/Form/index.tsx @@ -264,6 +264,7 @@ export const Form: React.FC = (props) => { if (res.status < 400) { if (typeof onSuccess === 'function') await onSuccess(json) setSubmitted(false) + setProcessing(false) if (redirect) { router.push(redirect) @@ -271,6 +272,8 @@ export const Form: React.FC = (props) => { toast.success(json.message || t('general:submissionSuccessful'), { autoClose: 3000 }) } } else { + setProcessing(false) + contextRef.current = { ...contextRef.current } // triggers rerender of all components that subscribe to form if (json.message) { toast.error(json.message) @@ -327,8 +330,6 @@ export const Form: React.FC = (props) => { toast.error(message) } - - setProcessing(false) } catch (err) { setProcessing(false) diff --git a/packages/ui/src/providers/ClientFunction/index.tsx b/packages/ui/src/providers/ClientFunction/index.tsx index 330c225a9..aabdf6405 100644 --- a/packages/ui/src/providers/ClientFunction/index.tsx +++ b/packages/ui/src/providers/ClientFunction/index.tsx @@ -1,18 +1,24 @@ 'use client' import React from 'react' -type AddClientFunctionContextType = (func: any) => void +type ModifyClientFunctionContextType = { + addClientFunction: (args: ModifyFunctionArgs) => void + removeClientFunction: (args: ModifyFunctionArgs) => void +} type ClientFunctionsContextType = Record -const AddClientFunctionContext = React.createContext(() => null) +const ModifyClientFunctionContext = React.createContext({ + addClientFunction: () => null, + removeClientFunction: () => null, +}) const ClientFunctionsContext = React.createContext({}) -type AddFunctionArgs = { func: any; key: string } +type ModifyFunctionArgs = { func: any; key: string } export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [clientFunctions, setClientFunctions] = React.useState({}) - const addClientFunction = React.useCallback((args: AddFunctionArgs) => { + const addClientFunction = React.useCallback((args: ModifyFunctionArgs) => { setClientFunctions((state) => { const newState = { ...state } newState[args.key] = args.func @@ -20,24 +26,44 @@ export const ClientFunctionProvider: React.FC<{ children: React.ReactNode }> = ( }) }, []) + const removeClientFunction = React.useCallback((args: ModifyFunctionArgs) => { + setClientFunctions((state) => { + const newState = { ...state } + delete newState[args.key] + return newState + }) + }, []) + return ( - + {children} - + ) } export const useAddClientFunction = (key: string, func: any) => { - const addClientFunction = React.useContext(AddClientFunctionContext) + const { addClientFunction, removeClientFunction } = React.useContext(ModifyClientFunctionContext) React.useEffect(() => { addClientFunction({ func, key, }) - }, [func, key, addClientFunction]) + + return () => { + removeClientFunction({ + func, + key, + }) + } + }, [func, key, addClientFunction, removeClientFunction]) } export const useClientFunctions = () => { diff --git a/packages/ui/src/providers/EntityVisibility/index.tsx b/packages/ui/src/providers/EntityVisibility/index.tsx new file mode 100644 index 000000000..3dc5bfabb --- /dev/null +++ b/packages/ui/src/providers/EntityVisibility/index.tsx @@ -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 ( + + {children} + + ) +} + +export const useEntityVisibility = (): VisibleEntitiesContextType => + useContext(EntityVisibilityContext) diff --git a/packages/ui/src/templates/Default/index.tsx b/packages/ui/src/templates/Default/index.tsx index 9c037e443..9c014605e 100644 --- a/packages/ui/src/templates/Default/index.tsx +++ b/packages/ui/src/templates/Default/index.tsx @@ -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 - i18n: any - permissions: Permissions - user: User } export const DefaultTemplate: React.FC = async ({ children, className, config: configPromise, - i18n, - permissions, - user, }) => { const config = await configPromise @@ -42,7 +35,10 @@ export const DefaultTemplate: React.FC = 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 (