feat(next): root admin (#7276)
This commit is contained in:
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -292,6 +292,7 @@ jobs:
|
||||
- access-control
|
||||
- admin__e2e__1
|
||||
- admin__e2e__2
|
||||
- admin-root
|
||||
- auth
|
||||
- field-error-states
|
||||
- fields-relationship
|
||||
|
||||
@@ -169,19 +169,32 @@ The following options are available:
|
||||
|
||||
| Option | Default route | Description |
|
||||
| ------------------ | ----------------------- | ------------------------------------- |
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `admin` | `/admin` | The Admin Panel itself. |
|
||||
| `api` | `/api` | The [REST API](../rest-api/overview) base path. |
|
||||
| `graphQL` | `/graphql` | The [GraphQL API](../graphql/overview) base path. |
|
||||
| `graphQLPlayground`| `/graphql-playground` | The GraphQL Playground. |
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Warning:</strong>
|
||||
Changing Root-level Routes _after_ your project was generated will also require you to manually update the corresponding directories in your project. For example, changing `routes.admin` will require you to rename the `(payload)/admin` directory in your project to match the new route. [More details](#project-structure).
|
||||
</Banner>
|
||||
|
||||
<Banner type="success">
|
||||
<strong>Tip:</strong>
|
||||
You can easily add _new_ routes to the Admin Panel through the `endpoints` property of the Payload Config. See [Custom Endpoints](../rest-api/overview#custom-endpoints) for more information.
|
||||
You can easily add _new_ routes to the Admin Panel through [Custom Endpoints](../rest-api/overview#custom-endpoints) and [Custom Views](./views).
|
||||
</Banner>
|
||||
|
||||
#### Customizing Root-level Routes
|
||||
|
||||
You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application.
|
||||
|
||||
Changing Root-level Routes also requires a change to [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`, you would need to completely remove the `admin` directory from the project structure:
|
||||
|
||||
```plaintext
|
||||
app/
|
||||
├─ (payload)/
|
||||
├── [[...segments]]/
|
||||
├──── ...
|
||||
```
|
||||
|
||||
<Banner type="warning">
|
||||
<strong>Note:</strong>
|
||||
If you set Root-level Routes _before_ auto-generating the Admin Panel, your [Project Structure](#project-structure) will already be set up correctly.
|
||||
</Banner>
|
||||
|
||||
### Admin-level Routes
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: Checkbox field types allow the developer to save a boolean value in the da
|
||||
keywords: checkbox, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Checkbox Field type saves a boolean in the database.
|
||||
The Checkbox Field saves a boolean in the database.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/checkbox.png"
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: With the Collapsible field, you can place fields within a collapsible layo
|
||||
keywords: row, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Collapsible field is presentational-only and only affects the Admin Panel. By using it, you can place fields within a nice layout component that can be collapsed / expanded.
|
||||
The Collapsible Field is presentational-only and only affects the Admin Panel. By using it, you can place fields within a nice layout component that can be collapsed / expanded.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/collapsible.png"
|
||||
|
||||
@@ -6,7 +6,7 @@ desc: The Relationship field provides the ability to relate documents together.
|
||||
keywords: relationship, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||
---
|
||||
|
||||
The Relationship field is one of the most powerful fields Payload features. It provides for the ability to easily relate documents together.
|
||||
The Relationship Field is one of the most powerful fields Payload features. It provides for the ability to easily relate documents together.
|
||||
|
||||
<LightDarkImage
|
||||
srcLight="https://payloadcms.com/images/docs/fields/relationship.png"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { useSearchParams } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import { useParams, usePathname } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
@@ -38,7 +39,12 @@ export const DocumentTabLink: React.FC<{
|
||||
|
||||
const [entityType, entitySlug, segmentThree, segmentFour, ...rest] = params.segments || []
|
||||
const isCollection = entityType === 'collections'
|
||||
let docPath = `${adminRoute}/${isCollection ? 'collections' : 'globals'}/${entitySlug}`
|
||||
|
||||
let docPath = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/${isCollection ? 'collections' : 'globals'}/${entitySlug}`,
|
||||
})
|
||||
|
||||
if (isCollection && segmentThree) {
|
||||
// doc ID
|
||||
docPath += `/${segmentThree}`
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useNav,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { EntityType, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import { EntityType, formatAdminURL, groupNavItems } from '@payloadcms/ui/shared'
|
||||
import LinkWithDefault from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -25,7 +25,7 @@ export const DefaultNavClient: React.FC = () => {
|
||||
const {
|
||||
collections,
|
||||
globals,
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -69,13 +69,13 @@ export const DefaultNavClient: React.FC = () => {
|
||||
let id: string
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
href = `${admin}/collections/${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.labels.plural, i18n)
|
||||
id = `nav-${entity.slug}`
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
href = `${admin}/globals/${entity.slug}`
|
||||
href = formatAdminURL({ adminRoute, path: `/globals/${entity.slug}` })
|
||||
entityLabel = getTranslation(entity.label, i18n)
|
||||
id = `nav-global-${entity.slug}`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ export const handleAdminPage = ({
|
||||
route: string
|
||||
}) => {
|
||||
if (isAdminRoute(route, adminRoute)) {
|
||||
const routeSegments = route.replace(adminRoute, '').split('/').filter(Boolean)
|
||||
const baseAdminRoute = adminRoute && adminRoute !== '/' ? route.replace(adminRoute, '') : route
|
||||
const routeSegments = baseAdminRoute.split('/').filter(Boolean)
|
||||
const [entityType, entitySlug, createOrID] = routeSegments
|
||||
const collectionSlug = entityType === 'collections' ? entitySlug : undefined
|
||||
const globalSlug = entityType === 'globals' ? entitySlug : undefined
|
||||
@@ -56,4 +57,6 @@ export const handleAdminPage = ({
|
||||
globalConfig,
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
@@ -30,7 +31,7 @@ export const handleAuthRedirect = ({
|
||||
: undefined,
|
||||
)
|
||||
|
||||
const adminLoginRoute = `${adminRoute}${loginRouteFromConfig}`
|
||||
const adminLoginRoute = formatAdminURL({ adminRoute, path: loginRouteFromConfig })
|
||||
|
||||
const customLoginRoute =
|
||||
typeof redirectUnauthenticatedUser === 'string' ? redirectUnauthenticatedUser : undefined
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Permissions, ServerProps, VisibleEntities } from 'payload'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, Card, Gutter, SetStepNav, SetViewActions } from '@payloadcms/ui'
|
||||
import { EntityType, WithServerSideProps } from '@payloadcms/ui/shared'
|
||||
import { EntityType, WithServerSideProps, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import './index.scss'
|
||||
@@ -100,19 +100,31 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
|
||||
|
||||
if (type === EntityType.collection) {
|
||||
title = getTranslation(entity.labels.plural, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:showAllLabel', { label: title })
|
||||
href = `${adminRoute}/collections/${entity.slug}`
|
||||
createHREF = `${adminRoute}/collections/${entity.slug}/create`
|
||||
|
||||
href = formatAdminURL({ adminRoute, path: `/collections/${entity.slug}` })
|
||||
|
||||
createHREF = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${entity.slug}/create`,
|
||||
})
|
||||
|
||||
hasCreatePermission =
|
||||
permissions?.collections?.[entity.slug]?.create?.permission
|
||||
}
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = getTranslation(entity.label, i18n)
|
||||
|
||||
buttonAriaLabel = t('general:editLabel', {
|
||||
label: getTranslation(entity.label, i18n),
|
||||
})
|
||||
href = `${adminRoute}/globals/${entity.slug}`
|
||||
|
||||
href = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${entity.slug}`,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -126,7 +126,12 @@ export const getViewsFromConfig = ({
|
||||
}
|
||||
|
||||
default: {
|
||||
const baseRoute = [adminRoute, 'collections', collectionSlug, segment3]
|
||||
const baseRoute = [
|
||||
adminRoute !== '/' && adminRoute,
|
||||
'collections',
|
||||
collectionSlug,
|
||||
segment3,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
@@ -155,7 +160,12 @@ export const getViewsFromConfig = ({
|
||||
ErrorView = UnauthorizedView
|
||||
}
|
||||
} else {
|
||||
const baseRoute = [adminRoute, collectionEntity, collectionSlug, segment3]
|
||||
const baseRoute = [
|
||||
adminRoute !== '/' && adminRoute,
|
||||
collectionEntity,
|
||||
collectionSlug,
|
||||
segment3,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
@@ -260,7 +270,9 @@ export const getViewsFromConfig = ({
|
||||
ErrorView = UnauthorizedView
|
||||
}
|
||||
} else {
|
||||
const baseRoute = [adminRoute, 'globals', globalSlug].filter(Boolean).join('/')
|
||||
const baseRoute = [adminRoute !== '/' && adminRoute, 'globals', globalSlug]
|
||||
.filter(Boolean)
|
||||
.join('/')
|
||||
|
||||
const currentRoute = [baseRoute, segment3, ...remainingSegments]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdminViewComponent, AdminViewProps, EditViewComponent } from 'payload'
|
||||
|
||||
import { DocumentInfoProvider, EditDepthProvider, HydrateClientUser } from '@payloadcms/ui'
|
||||
import { RenderCustomComponent, isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { RenderCustomComponent, formatAdminURL , isEditing as getIsEditing } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
@@ -174,7 +174,11 @@ export const Document: React.FC<AdminViewProps> = async ({
|
||||
})
|
||||
|
||||
if (doc?.id) {
|
||||
const redirectURL = `${serverURL}${adminRoute}/collections/${collectionSlug}/${doc.id}`
|
||||
const redirectURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${doc.id}`,
|
||||
serverURL,
|
||||
})
|
||||
redirect(redirectURL)
|
||||
} else {
|
||||
notFound()
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
useStepNav,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const SetDocumentStepNav: React.FC<{
|
||||
@@ -35,7 +36,7 @@ export const SetDocumentStepNav: React.FC<{
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
const {
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const drawerDepth = useEditDepth()
|
||||
@@ -47,13 +48,23 @@ export const SetDocumentStepNav: React.FC<{
|
||||
if (collectionSlug) {
|
||||
nav.push({
|
||||
label: getTranslation(pluralLabel, i18n),
|
||||
url: isVisible ? `${admin}/collections/${collectionSlug}` : undefined,
|
||||
url: isVisible
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}`,
|
||||
})
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (isEditing) {
|
||||
nav.push({
|
||||
label: (useAsTitle && useAsTitle !== 'id' && title) || `${id}`,
|
||||
url: isVisible ? `${admin}/collections/${collectionSlug}/${id}` : undefined,
|
||||
url: isVisible
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${id}`,
|
||||
})
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
nav.push({
|
||||
@@ -63,7 +74,12 @@ export const SetDocumentStepNav: React.FC<{
|
||||
} else if (globalSlug) {
|
||||
nav.push({
|
||||
label: title,
|
||||
url: isVisible ? `${admin}/globals/${globalSlug}` : undefined,
|
||||
url: isVisible
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}`,
|
||||
})
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +98,7 @@ export const SetDocumentStepNav: React.FC<{
|
||||
pluralLabel,
|
||||
id,
|
||||
useAsTitle,
|
||||
admin,
|
||||
adminRoute,
|
||||
t,
|
||||
i18n,
|
||||
title,
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useEditDepth,
|
||||
useUploadEdits,
|
||||
} from '@payloadcms/ui'
|
||||
import { getFormState } from '@payloadcms/ui/shared'
|
||||
import { formatAdminURL, getFormState } from '@payloadcms/ui/shared'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback } from 'react'
|
||||
|
||||
@@ -127,7 +127,10 @@ export const DefaultEditView: React.FC = () => {
|
||||
|
||||
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}` : ''}`
|
||||
const redirectRoute = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${json?.doc?.id}${locale ? `?locale=${locale}` : ''}`,
|
||||
})
|
||||
router.push(redirectRoute)
|
||||
} else {
|
||||
resetUploadEdits()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { Button, Translation } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -22,9 +23,9 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
||||
|
||||
const {
|
||||
admin: {
|
||||
routes: { account: accountRoute },
|
||||
routes: { account: accountRoute, login: loginRoute },
|
||||
},
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (user) {
|
||||
@@ -34,14 +35,23 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
||||
<p>
|
||||
<Translation
|
||||
elements={{
|
||||
'0': ({ children }) => <Link href={`${admin}${accountRoute}`}>{children}</Link>,
|
||||
'0': ({ children }) => (
|
||||
<Link
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: accountRoute,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
i18nKey="authentication:loggedInChangePassword"
|
||||
t={i18n.t}
|
||||
/>
|
||||
</p>
|
||||
<br />
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={admin}>
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={adminRoute}>
|
||||
{i18n.t('general:backToDashboard')}
|
||||
</Button>
|
||||
</Fragment>
|
||||
@@ -51,7 +61,14 @@ export const ForgotPasswordView: React.FC<AdminViewProps> = ({ initPageResult })
|
||||
return (
|
||||
<Fragment>
|
||||
<ForgotPasswordForm />
|
||||
<Link href={`${admin}/login`}>{i18n.t('authentication:backToLogin')}</Link>
|
||||
<Link
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: loginRoute,
|
||||
})}
|
||||
>
|
||||
{i18n.t('authentication:backToLogin')}
|
||||
</Link>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ListQueryProvider,
|
||||
TableColumnsProvider,
|
||||
} from '@payloadcms/ui'
|
||||
import { RenderCustomComponent } from '@payloadcms/ui/shared'
|
||||
import { RenderCustomComponent, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { notFound } from 'next/navigation.js'
|
||||
import { createClientCollectionConfig, mergeListSearchAndWhere } from 'payload'
|
||||
import { isNumber, isReactComponentOrFunction } from 'payload/shared'
|
||||
@@ -66,7 +66,7 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
} catch (error) {} // eslint-disable-line no-empty
|
||||
|
||||
const {
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (collectionConfig) {
|
||||
@@ -132,7 +132,10 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
})}
|
||||
collectionSlug={collectionSlug}
|
||||
hasCreatePermission={permissions?.collections?.[collectionSlug]?.create?.permission}
|
||||
newDocumentURL={`${admin}/collections/${collectionSlug}/create`}
|
||||
newDocumentURL={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
})}
|
||||
>
|
||||
<ListQueryProvider
|
||||
data={data}
|
||||
@@ -160,7 +163,10 @@ export const ListView: React.FC<AdminViewProps> = async ({
|
||||
limit,
|
||||
listPreferences,
|
||||
locale: fullLocale,
|
||||
newDocumentURL: `${admin}/collections/${collectionSlug}/create`,
|
||||
newDocumentURL: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/create`,
|
||||
}),
|
||||
params,
|
||||
payload,
|
||||
permissions,
|
||||
|
||||
@@ -9,6 +9,7 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.
|
||||
import type { FormState, PayloadRequest } from 'payload'
|
||||
|
||||
import { Form, FormSubmit, PasswordField, useConfig, useTranslation } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { password } from 'payload/shared'
|
||||
|
||||
import type { LoginFieldProps } from '../LoginField/index.js'
|
||||
@@ -29,7 +30,7 @@ export const LoginForm: React.FC<{
|
||||
routes: { forgot: forgotRoute },
|
||||
user: userSlug,
|
||||
},
|
||||
routes: { admin, api },
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
} = config
|
||||
|
||||
const collectionConfig = config.collections?.find((collection) => collection?.slug === userSlug)
|
||||
@@ -71,12 +72,12 @@ export const LoginForm: React.FC<{
|
||||
|
||||
return (
|
||||
<Form
|
||||
action={`${api}/${userSlug}/login`}
|
||||
action={`${apiRoute}/${userSlug}/login`}
|
||||
className={baseClass}
|
||||
disableSuccessStatus
|
||||
initialState={initialState}
|
||||
method="POST"
|
||||
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : admin}
|
||||
redirect={typeof searchParams?.redirect === 'string' ? searchParams.redirect : adminRoute}
|
||||
waitForAutocomplete
|
||||
>
|
||||
<div className={`${baseClass}__inputWrap`}>
|
||||
@@ -104,7 +105,14 @@ export const LoginForm: React.FC<{
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Link href={`${admin}${forgotRoute}`}>{t('authentication:forgotPasswordQuestion')}</Link>
|
||||
<Link
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: forgotRoute,
|
||||
})}
|
||||
>
|
||||
{t('authentication:forgotPasswordQuestion')}
|
||||
</Link>
|
||||
<FormSubmit>{t('authentication:login')}</FormSubmit>
|
||||
</Form>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import { Button, useAuth, useTranslation } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React, { Fragment, useEffect } from 'react'
|
||||
|
||||
@@ -32,9 +33,12 @@ export const LogoutClient: React.FC<{
|
||||
Link={Link}
|
||||
buttonStyle="secondary"
|
||||
el="link"
|
||||
url={`${adminRoute}/login${
|
||||
redirect && redirect.length > 0 ? `?redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`}
|
||||
url={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/login${
|
||||
redirect && redirect.length > 0 ? `?redirect=${encodeURIComponent(redirect)}` : ''
|
||||
}`,
|
||||
})}
|
||||
>
|
||||
{t('authentication:logBackIn')}
|
||||
</Button>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const LogoutView: React.FC<
|
||||
req: {
|
||||
payload: {
|
||||
config: {
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -27,7 +27,7 @@ export const LogoutView: React.FC<
|
||||
return (
|
||||
<div className={`${baseClass}__wrap`}>
|
||||
<LogoutClient
|
||||
adminRoute={admin}
|
||||
adminRoute={adminRoute}
|
||||
inactivity={inactivity}
|
||||
redirect={searchParams.redirect as string}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const NotFoundClient: React.FC<{
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -38,7 +38,7 @@ export const NotFoundClient: React.FC<{
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
<h1>{t('general:nothingFound')}</h1>
|
||||
<p>{t('general:sorryNotFound')}</p>
|
||||
<Button Link={Link} className={`${baseClass}__button`} el="link" to={`${admin}`}>
|
||||
<Button Link={Link} className={`${baseClass}__button`} el="link" to={adminRoute}>
|
||||
{t('general:backToDashboard')}
|
||||
</Button>
|
||||
</Gutter>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Metadata } from 'next'
|
||||
import type { AdminViewComponent, SanitizedConfig } from 'payload'
|
||||
|
||||
import { HydrateClientUser } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import { DefaultTemplate } from '../../templates/Default/index.js'
|
||||
@@ -52,7 +53,7 @@ export const NotFoundPage = async ({
|
||||
const initPageResult = await initPage({
|
||||
config,
|
||||
redirectUnauthenticatedUser: true,
|
||||
route: `${adminRoute}/not-found`,
|
||||
route: formatAdminURL({ adminRoute, path: '/not-found' }),
|
||||
searchParams,
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
useFormFields,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -36,8 +37,11 @@ const initialState: FormState = {
|
||||
export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
|
||||
const i18n = useTranslation()
|
||||
const {
|
||||
admin: { user: userSlug },
|
||||
routes: { admin, api },
|
||||
admin: {
|
||||
routes: { login: loginRoute },
|
||||
user: userSlug,
|
||||
},
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
|
||||
@@ -49,18 +53,23 @@ export const ResetPasswordClient: React.FC<Args> = ({ token }) => {
|
||||
async (data) => {
|
||||
if (data.token) {
|
||||
await fetchFullUser()
|
||||
history.push(`${admin}`)
|
||||
history.push(adminRoute)
|
||||
} else {
|
||||
history.push(`${admin}/login`)
|
||||
history.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: loginRoute,
|
||||
}),
|
||||
)
|
||||
toast.success(i18n.t('general:updatedSuccessfully'))
|
||||
}
|
||||
},
|
||||
[fetchFullUser, history, admin, i18n],
|
||||
[fetchFullUser, history, adminRoute, i18n],
|
||||
)
|
||||
|
||||
return (
|
||||
<Form
|
||||
action={`${serverURL}${api}/${userSlug}/reset-password`}
|
||||
action={`${serverURL}${apiRoute}/${userSlug}/reset-password`}
|
||||
initialState={initialState}
|
||||
method="POST"
|
||||
onSuccess={onSuccess}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { Button, Translation } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
@@ -31,7 +32,7 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
admin: {
|
||||
routes: { account: accountRoute },
|
||||
},
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
if (user) {
|
||||
@@ -42,14 +43,23 @@ export const ResetPassword: React.FC<AdminViewProps> = ({ initPageResult, params
|
||||
<p>
|
||||
<Translation
|
||||
elements={{
|
||||
'0': ({ children }) => <Link href={`${admin}${accountRoute}`}>{children}</Link>,
|
||||
'0': ({ children }) => (
|
||||
<Link
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: accountRoute,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
i18nKey="authentication:loggedInChangePassword"
|
||||
t={i18n.t}
|
||||
/>
|
||||
</p>
|
||||
<br />
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={admin}>
|
||||
<Button Link={Link} buttonStyle="secondary" el="link" to={adminRoute}>
|
||||
{i18n.t('general:backToDashboard')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AdminViewComponent, SanitizedConfig } from 'payload'
|
||||
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
|
||||
import type { initPage } from '../../utilities/initPage/index.js'
|
||||
|
||||
import { Account } from '../Account/index.js'
|
||||
@@ -93,7 +95,7 @@ export const getViewFromConfig = ({
|
||||
return isPathMatchingRoute({
|
||||
currentRoute,
|
||||
exact: true,
|
||||
path: `${adminRoute}${route}`,
|
||||
path: formatAdminURL({ adminRoute, path: route }),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { I18nClient } from '@payloadcms/translations'
|
||||
import type { Metadata } from 'next'
|
||||
import type { SanitizedConfig } from 'payload'
|
||||
|
||||
import { WithServerSideProps } from '@payloadcms/ui/shared'
|
||||
import { WithServerSideProps, formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { notFound, redirect } from 'next/navigation.js'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
@@ -37,13 +37,16 @@ export const RootPage = async ({
|
||||
|
||||
const {
|
||||
admin: {
|
||||
routes: { createFirstUser: createFirstUserRoute },
|
||||
routes: { createFirstUser: _createFirstUserRoute },
|
||||
user: userSlug,
|
||||
},
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const currentRoute = `${adminRoute}${Array.isArray(params.segments) ? `/${params.segments.join('/')}` : ''}`
|
||||
const currentRoute = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `${Array.isArray(params.segments) ? `/${params.segments.join('/')}` : ''}`,
|
||||
})
|
||||
|
||||
const segments = Array.isArray(params.segments) ? params.segments : []
|
||||
|
||||
@@ -71,20 +74,20 @@ export const RootPage = async ({
|
||||
})
|
||||
?.then((doc) => !!doc)
|
||||
|
||||
const routeWithAdmin = `${adminRoute}${createFirstUserRoute}`
|
||||
const createFirstUserRoute = formatAdminURL({ adminRoute, path: _createFirstUserRoute })
|
||||
|
||||
const collectionConfig = config.collections.find(({ slug }) => slug === userSlug)
|
||||
const disableLocalStrategy = collectionConfig?.auth?.disableLocalStrategy
|
||||
|
||||
if (disableLocalStrategy && currentRoute === routeWithAdmin) {
|
||||
if (disableLocalStrategy && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
|
||||
if (!dbHasUser && currentRoute !== routeWithAdmin && !disableLocalStrategy) {
|
||||
redirect(routeWithAdmin)
|
||||
if (!dbHasUser && currentRoute !== createFirstUserRoute && !disableLocalStrategy) {
|
||||
redirect(createFirstUserRoute)
|
||||
}
|
||||
|
||||
if (dbHasUser && currentRoute === routeWithAdmin) {
|
||||
if (dbHasUser && currentRoute === createFirstUserRoute) {
|
||||
redirect(adminRoute)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AdminViewProps } from 'payload'
|
||||
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import { redirect } from 'next/navigation.js'
|
||||
import React from 'react'
|
||||
|
||||
@@ -39,10 +40,10 @@ export const Verify: React.FC<AdminViewProps> = async ({
|
||||
token,
|
||||
})
|
||||
|
||||
return redirect(`${adminRoute}/login`)
|
||||
return redirect(formatAdminURL({ adminRoute, path: '/login' }))
|
||||
} catch (e) {
|
||||
// already verified
|
||||
if (e?.status === 202) redirect(`${adminRoute}/login`)
|
||||
if (e?.status === 202) redirect(formatAdminURL({ adminRoute, path: '/login' }))
|
||||
textToRender = req.t('authentication:unableToVerify')
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type React from 'react'
|
||||
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export const SetStepNav: React.FC<{
|
||||
@@ -62,15 +62,18 @@ export const SetStepNav: React.FC<{
|
||||
nav = [
|
||||
{
|
||||
label: getTranslation(pluralLabel, i18n),
|
||||
url: `${adminRoute}/collections/${collectionSlug}`,
|
||||
url: formatAdminURL({ adminRoute, path: `/collections/${collectionSlug}` }),
|
||||
},
|
||||
{
|
||||
label: docLabel,
|
||||
url: `${adminRoute}/collections/${collectionSlug}/${id}`,
|
||||
url: formatAdminURL({ adminRoute, path: `/collections/${collectionSlug}/${id}` }),
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
url: `${adminRoute}/collections/${collectionSlug}/${id}/versions`,
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${id}/versions`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt
|
||||
@@ -84,11 +87,17 @@ export const SetStepNav: React.FC<{
|
||||
nav = [
|
||||
{
|
||||
label: globalConfig.label,
|
||||
url: `${adminRoute}/globals/${globalConfig.slug}`,
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalConfig.slug}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
url: `${adminRoute}/globals/${globalConfig.slug}/versions`,
|
||||
url: formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalConfig.slug}/versions`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: doc?.createdAt
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import { Button, Modal, Pill, useConfig, useModal, useTranslation } from '@payloadcms/ui'
|
||||
import { requests } from '@payloadcms/ui/shared'
|
||||
import { formatAdminURL, requests } from '@payloadcms/ui/shared'
|
||||
import { useRouter } from 'next/navigation.js'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -24,9 +24,10 @@ const Restore: React.FC<Props> = ({
|
||||
versionID,
|
||||
}) => {
|
||||
const {
|
||||
routes: { admin, api },
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
|
||||
const { toggleModal } = useModal()
|
||||
const [processing, setProcessing] = useState(false)
|
||||
const router = useRouter()
|
||||
@@ -37,17 +38,23 @@ const Restore: React.FC<Props> = ({
|
||||
versionDate,
|
||||
})
|
||||
|
||||
let fetchURL = `${serverURL}${api}`
|
||||
let fetchURL = `${serverURL}${apiRoute}`
|
||||
let redirectURL: string
|
||||
|
||||
if (collectionSlug) {
|
||||
fetchURL = `${fetchURL}/${collectionSlug}/versions/${versionID}`
|
||||
redirectURL = `${admin}/collections/${collectionSlug}/${originalDocID}`
|
||||
redirectURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${originalDocID}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (globalSlug) {
|
||||
fetchURL = `${fetchURL}/globals/${globalSlug}/versions/${versionID}`
|
||||
redirectURL = `${admin}/globals/${globalSlug}`
|
||||
redirectURL = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}`,
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestore = useCallback(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import { useConfig, useTableCell, useTranslation } from '@payloadcms/ui'
|
||||
import { formatDate } from '@payloadcms/ui/shared'
|
||||
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared'
|
||||
import LinkImport from 'next/link.js'
|
||||
import React from 'react'
|
||||
|
||||
@@ -19,7 +19,7 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
}) => {
|
||||
const {
|
||||
admin: { dateFormat },
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const { i18n } = useTranslation()
|
||||
@@ -30,9 +30,17 @@ export const CreatedAtCell: React.FC<CreatedAtCellProps> = ({
|
||||
|
||||
let to: string
|
||||
|
||||
if (collectionSlug) to = `${admin}/collections/${collectionSlug}/${docID}/versions/${versionID}`
|
||||
if (collectionSlug)
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}/${docID}/versions/${versionID}`,
|
||||
})
|
||||
|
||||
if (globalSlug) to = `${admin}/globals/${globalSlug}/versions/${versionID}`
|
||||
if (globalSlug)
|
||||
to = formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/globals/${globalSlug}/versions/${versionID}`,
|
||||
})
|
||||
|
||||
return (
|
||||
<Link href={to}>
|
||||
|
||||
@@ -699,7 +699,10 @@ export type Config = {
|
||||
plugins?: Plugin[]
|
||||
/** Control the routing structure that Payload binds itself to. */
|
||||
routes?: {
|
||||
/** @default "/admin" */
|
||||
/** The route for the admin panel.
|
||||
* @example "/my-admin"
|
||||
* @default "/admin"
|
||||
*/
|
||||
admin?: string
|
||||
/** @default "/api" */
|
||||
api?: string
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type { FormState } from 'payload'
|
||||
|
||||
import { useConfig, useWatchForm } from '@payloadcms/ui'
|
||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||
import React from 'react'
|
||||
// TODO: fix this import to work in dev mode within the monorepo in a way that is backwards compatible with 1.x
|
||||
// import CopyToClipboard from 'payload/dist/admin/components/elements/CopyToClipboard'
|
||||
@@ -35,7 +36,10 @@ export const LinkToDocClient: React.FC = () => {
|
||||
serverURL,
|
||||
} = config
|
||||
|
||||
const href = `${serverURL}${adminRoute}/collections/${relationTo}/${docId}`
|
||||
const href = `${serverURL}${formatAdminURL({
|
||||
adminRoute,
|
||||
path: '/collections/${relationTo}/${docId}',
|
||||
})}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Account } from '../../graphics/Account/index.js'
|
||||
import { useActions } from '../../providers/Actions/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Hamburger } from '../Hamburger/index.js'
|
||||
import { LocalizerLabel } from '../Localizer/LocalizerLabel/index.js'
|
||||
import { Localizer } from '../Localizer/index.js'
|
||||
@@ -89,7 +90,7 @@ export const AppHeader: React.FC = () => {
|
||||
<LinkElement
|
||||
aria-label={t('authentication:account')}
|
||||
className={`${baseClass}__account`}
|
||||
href={`${adminRoute}${accountRoute}`}
|
||||
href={formatAdminURL({ adminRoute, path: accountRoute })}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Account />
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
import { Translation } from '../Translation/index.js'
|
||||
@@ -32,7 +33,7 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
const { id, buttonId, collectionSlug, singularLabel, title: titleFromProps } = props
|
||||
|
||||
const {
|
||||
routes: { admin, api },
|
||||
routes: { admin: adminRoute, api },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
|
||||
@@ -75,7 +76,12 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
json.message,
|
||||
)
|
||||
|
||||
return router.push(`${admin}/collections/${collectionSlug}`)
|
||||
return router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionSlug}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
toggleModal(modalSlug)
|
||||
if (json.errors) {
|
||||
@@ -104,7 +110,7 @@ export const DeleteDocument: React.FC<Props> = (props) => {
|
||||
i18n,
|
||||
title,
|
||||
router,
|
||||
admin,
|
||||
adminRoute,
|
||||
addDefaultError,
|
||||
])
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { Fragment, useEffect } from 'react'
|
||||
import { useComponentMap } from '../../providers/ComponentMap/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { formatDate } from '../../utilities/formatDate.js'
|
||||
import { Autosave } from '../Autosave/index.js'
|
||||
import { DeleteDocument } from '../DeleteDocument/index.js'
|
||||
@@ -199,7 +200,10 @@ export const DocumentControls: React.FC<{
|
||||
{hasCreatePermission && (
|
||||
<React.Fragment>
|
||||
<PopupList.Button
|
||||
href={`${adminRoute}/collections/${collectionConfig?.slug}/create`}
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${collectionConfig?.slug}/create`,
|
||||
})}
|
||||
id="action-create"
|
||||
>
|
||||
{i18n.t('general:createNew')}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useLocale } from '../../providers/Locale/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { Button } from '../Button/index.js'
|
||||
import { PopupList } from '../Popup/index.js'
|
||||
import './index.scss'
|
||||
@@ -31,13 +32,16 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
const { toggleModal } = useModal()
|
||||
const locale = useLocale()
|
||||
const { setModified } = useForm()
|
||||
|
||||
const {
|
||||
routes: { api },
|
||||
routes: { api: apiRoute },
|
||||
serverURL,
|
||||
} = useConfig()
|
||||
|
||||
const {
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = useConfig()
|
||||
|
||||
const [hasClicked, setHasClicked] = useState<boolean>(false)
|
||||
const { i18n, t } = useTranslation()
|
||||
|
||||
@@ -53,7 +57,7 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
}
|
||||
await requests
|
||||
.post(
|
||||
`${serverURL}${api}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
`${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
{
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
@@ -72,7 +76,10 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
)
|
||||
setModified(false)
|
||||
router.push(
|
||||
`${admin}/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
toast.error(
|
||||
@@ -87,7 +94,7 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
locale,
|
||||
modified,
|
||||
serverURL,
|
||||
api,
|
||||
apiRoute,
|
||||
slug,
|
||||
id,
|
||||
i18n,
|
||||
@@ -97,7 +104,7 @@ export const DuplicateDocument: React.FC<Props> = ({ id, slug, singularLabel })
|
||||
singularLabel,
|
||||
setModified,
|
||||
router,
|
||||
admin,
|
||||
adminRoute,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { LogOutIcon } from '../../icons/LogOut/index.js'
|
||||
import { useComponentMap } from '../../providers/ComponentMap/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
|
||||
const baseClass = 'nav'
|
||||
|
||||
@@ -19,18 +20,19 @@ const DefaultLogout: React.FC<{
|
||||
admin: {
|
||||
routes: { logout: logoutRoute },
|
||||
},
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const basePath = process.env.NEXT_BASE_PATH ?? ''
|
||||
|
||||
const LinkElement = Link || 'a'
|
||||
|
||||
return (
|
||||
<LinkElement
|
||||
aria-label={t('authentication:logOut')}
|
||||
className={`${baseClass}__log-out`}
|
||||
href={`${basePath}${admin}${logoutRoute}`}
|
||||
href={formatAdminURL({
|
||||
adminRoute,
|
||||
path: logoutRoute,
|
||||
})}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<LogOutIcon />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from '../../elements/Button/index.js'
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import './index.scss'
|
||||
|
||||
const baseClass = 'stay-logged-in'
|
||||
@@ -24,8 +25,9 @@ export const StayLoggedInModal: React.FC = () => {
|
||||
admin: {
|
||||
routes: { logout: logoutRoute },
|
||||
},
|
||||
routes: { admin },
|
||||
routes: { admin: adminRoute },
|
||||
} = config
|
||||
|
||||
const { toggleModal } = useModal()
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -41,7 +43,12 @@ export const StayLoggedInModal: React.FC = () => {
|
||||
buttonStyle="secondary"
|
||||
onClick={() => {
|
||||
toggleModal(stayLoggedInModalSlug)
|
||||
router.push(`${admin}${logoutRoute}`)
|
||||
router.push(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: logoutRoute,
|
||||
}),
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('authentication:logOut')}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getTranslation } from '@payloadcms/translations'
|
||||
|
||||
import { useConfig } from '../../../providers/Config/index.js'
|
||||
import { useTranslation } from '../../../providers/Translation/index.js'
|
||||
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
|
||||
import { useTableCell } from '../TableCellProvider/index.js'
|
||||
import { CodeCell } from './fields/Code/index.js'
|
||||
import { cellComponents } from './fields/index.js'
|
||||
@@ -56,7 +57,10 @@ export const DefaultCell: React.FC<CellComponentProps> = (props) => {
|
||||
if (isLink) {
|
||||
WrapElement = Link
|
||||
wrapElementProps.href = customCellContext?.collectionSlug
|
||||
? `${adminRoute}/collections/${customCellContext?.collectionSlug}/${rowData.id}`
|
||||
? formatAdminURL({
|
||||
adminRoute,
|
||||
path: `/collections/${customCellContext?.collectionSlug}/${rowData.id}`,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export { PayloadIcon } from '../../graphics/Icon/index.js'
|
||||
export { PayloadLogo } from '../../graphics/Logo/index.js'
|
||||
export { requests } from '../../utilities/api.js'
|
||||
export { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
|
||||
export { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
export { formatDate } from '../../utilities/formatDate.js'
|
||||
export { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
||||
export { getFormState } from '../../utilities/getFormState.js'
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react'
|
||||
|
||||
import { useAuth } from '../../providers/Auth/index.js'
|
||||
import { useConfig } from '../../providers/Config/index.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { DefaultAccountIcon } from './Default/index.js'
|
||||
import { GravatarAccountIcon } from './Gravatar/index.js'
|
||||
|
||||
@@ -18,9 +19,7 @@ export const Account = () => {
|
||||
|
||||
const { user } = useAuth()
|
||||
const pathname = usePathname()
|
||||
|
||||
const isOnAccountPage = pathname === `${adminRoute}${accountRoute}`
|
||||
|
||||
const isOnAccountPage = pathname === formatAdminURL({ adminRoute, path: accountRoute })
|
||||
if (!user?.email || Avatar === 'default') return <DefaultAccountIcon active={isOnAccountPage} />
|
||||
if (Avatar === 'gravatar') return <GravatarAccountIcon />
|
||||
if (Avatar) return <Avatar active={isOnAccountPage} />
|
||||
|
||||
@@ -11,6 +11,7 @@ import { stayLoggedInModalSlug } from '../../elements/StayLoggedIn/index.js'
|
||||
import { useDebounce } from '../../hooks/useDebounce.js'
|
||||
import { useTranslation } from '../../providers/Translation/index.js'
|
||||
import { requests } from '../../utilities/api.js'
|
||||
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
|
||||
import { useConfig } from '../Config/index.js'
|
||||
|
||||
export type AuthContext<T = ClientUser> = {
|
||||
@@ -48,7 +49,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
routes: { inactivity: logoutInactivityRoute },
|
||||
user: userSlug,
|
||||
},
|
||||
routes: { admin, api },
|
||||
routes: { admin: adminRoute, api: apiRoute },
|
||||
serverURL,
|
||||
} = config
|
||||
|
||||
@@ -62,14 +63,24 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const id = user?.id
|
||||
|
||||
const redirectToInactivityRoute = useCallback(() => {
|
||||
if (window.location.pathname.startsWith(admin)) {
|
||||
if (window.location.pathname.startsWith(adminRoute)) {
|
||||
const redirectParam = `?redirect=${encodeURIComponent(window.location.pathname)}`
|
||||
router.replace(`${admin}${logoutInactivityRoute}${redirectParam}`)
|
||||
router.replace(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: `${logoutInactivityRoute}${redirectParam}`,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
router.replace(`${admin}${logoutInactivityRoute}`)
|
||||
router.replace(
|
||||
formatAdminURL({
|
||||
adminRoute,
|
||||
path: logoutInactivityRoute,
|
||||
}),
|
||||
)
|
||||
}
|
||||
closeAllModals()
|
||||
}, [router, admin, logoutInactivityRoute, closeAllModals])
|
||||
}, [router, adminRoute, logoutInactivityRoute, closeAllModals])
|
||||
|
||||
const revokeTokenAndExpire = useCallback(() => {
|
||||
setTokenInMemory(undefined)
|
||||
@@ -97,11 +108,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
if (forceRefresh || (tokenExpiration && remainingTime < 120)) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
const request = await requests.post(
|
||||
`${serverURL}${apiRoute}/${userSlug}/refresh-token`,
|
||||
{
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (request.status === 200) {
|
||||
const json = await request.json()
|
||||
@@ -121,7 +135,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
[
|
||||
tokenExpiration,
|
||||
serverURL,
|
||||
api,
|
||||
apiRoute,
|
||||
userSlug,
|
||||
i18n,
|
||||
redirectToInactivityRoute,
|
||||
@@ -132,7 +146,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const refreshCookieAsync = useCallback(
|
||||
async (skipSetUser?: boolean): Promise<ClientUser> => {
|
||||
try {
|
||||
const request = await requests.post(`${serverURL}${api}/${userSlug}/refresh-token`, {
|
||||
const request = await requests.post(`${serverURL}${apiRoute}/${userSlug}/refresh-token`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
@@ -155,18 +169,18 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
return null
|
||||
}
|
||||
},
|
||||
[serverURL, api, userSlug, i18n, redirectToInactivityRoute, setTokenAndExpiration],
|
||||
[serverURL, apiRoute, userSlug, i18n, redirectToInactivityRoute, setTokenAndExpiration],
|
||||
)
|
||||
|
||||
const logOut = useCallback(async () => {
|
||||
setUser(null)
|
||||
revokeTokenAndExpire()
|
||||
try {
|
||||
await requests.post(`${serverURL}${api}/${userSlug}/logout`)
|
||||
await requests.post(`${serverURL}${apiRoute}/${userSlug}/logout`)
|
||||
} catch (e) {
|
||||
toast.error(`Logging out failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, userSlug, revokeTokenAndExpire])
|
||||
}, [serverURL, apiRoute, userSlug, revokeTokenAndExpire])
|
||||
|
||||
const refreshPermissions = useCallback(async () => {
|
||||
const params = {
|
||||
@@ -174,7 +188,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
}
|
||||
|
||||
try {
|
||||
const request = await requests.get(`${serverURL}${api}/access?${qs.stringify(params)}`, {
|
||||
const request = await requests.get(`${serverURL}${apiRoute}/access?${qs.stringify(params)}`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
@@ -189,11 +203,11 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} catch (e) {
|
||||
toast.error(`Refreshing permissions failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, i18n, code])
|
||||
}, [serverURL, apiRoute, i18n, code])
|
||||
|
||||
const fetchFullUser = React.useCallback(async () => {
|
||||
try {
|
||||
const request = await requests.get(`${serverURL}${api}/${userSlug}/me`, {
|
||||
const request = await requests.get(`${serverURL}${apiRoute}/${userSlug}/me`, {
|
||||
headers: {
|
||||
'Accept-Language': i18n.language,
|
||||
},
|
||||
@@ -216,7 +230,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
} catch (e) {
|
||||
toast.error(`Fetching user failed: ${e.message}`)
|
||||
}
|
||||
}, [serverURL, api, userSlug, i18n.language, setTokenAndExpiration, revokeTokenAndExpire])
|
||||
}, [serverURL, apiRoute, userSlug, i18n.language, setTokenAndExpiration, revokeTokenAndExpire])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
|
||||
22
packages/ui/src/utilities/formatAdminURL.ts
Normal file
22
packages/ui/src/utilities/formatAdminURL.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
/** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */
|
||||
export const formatAdminURL = (args: {
|
||||
adminRoute: Config['routes']['admin']
|
||||
path: string
|
||||
serverURL?: Config['serverURL']
|
||||
}): string => {
|
||||
const { adminRoute, path, serverURL } = args
|
||||
|
||||
if (adminRoute) {
|
||||
if (adminRoute === '/') {
|
||||
if (!path) {
|
||||
return `${serverURL || ''}${adminRoute}`
|
||||
}
|
||||
} else {
|
||||
return `${serverURL || ''}${adminRoute}${path}`
|
||||
}
|
||||
}
|
||||
|
||||
return `${serverURL || ''}${path}`
|
||||
}
|
||||
2
test/admin-root/.gitignore
vendored
Normal file
2
test/admin-root/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/media
|
||||
/media-gif
|
||||
22
test/admin-root/app/(payload)/[[...segments]]/not-found.tsx
Normal file
22
test/admin-root/app/(payload)/[[...segments]]/not-found.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/* 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 { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) => NotFoundPage({ config, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
22
test/admin-root/app/(payload)/[[...segments]]/page.tsx
Normal file
22
test/admin-root/app/(payload)/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/* 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 { RootPage, generatePageMetadata } from '@payloadcms/next/views'
|
||||
|
||||
type Args = {
|
||||
params: {
|
||||
segments: string[]
|
||||
}
|
||||
searchParams: {
|
||||
[key: string]: string | string[]
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) => RootPage({ config, params, searchParams })
|
||||
|
||||
export default Page
|
||||
10
test/admin-root/app/(payload)/api/[...slug]/route.ts
Normal file
10
test/admin-root/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
6
test/admin-root/app/(payload)/api/graphql/route.ts
Normal file
6
test/admin-root/app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY it because it could be re-written at any time. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
0
test/admin-root/app/(payload)/custom.scss
Normal file
0
test/admin-root/app/(payload)/custom.scss
Normal file
16
test/admin-root/app/(payload)/layout.tsx
Normal file
16
test/admin-root/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
import configPromise from '@payload-config'
|
||||
import { RootLayout } from '@payloadcms/next/layouts'
|
||||
// import '@payloadcms/ui/styles.css' // Uncomment this line if `@payloadcms/ui` in `tsconfig.json` points to `/ui/dist` instead of `/ui/src`
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import React from 'react'
|
||||
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => <RootLayout config={configPromise}>{children}</RootLayout>
|
||||
|
||||
export default Layout
|
||||
5
test/admin-root/app/my-route/route.ts
Normal file
5
test/admin-root/app/my-route/route.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const GET = () => {
|
||||
return Response.json({
|
||||
hello: 'elliot',
|
||||
})
|
||||
}
|
||||
19
test/admin-root/collections/Posts/index.ts
Normal file
19
test/admin-root/collections/Posts/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const postsSlug = 'posts'
|
||||
|
||||
export const PostsCollection: CollectionConfig = {
|
||||
slug: postsSlug,
|
||||
admin: {
|
||||
useAsTitle: 'text',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
}
|
||||
39
test/admin-root/config.ts
Normal file
39
test/admin-root/config.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
|
||||
import { MenuGlobal } from './globals/Menu/index.js'
|
||||
import { adminRoute } from './shared.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [PostsCollection],
|
||||
cors: ['http://localhost:3000', 'http://localhost:3001'],
|
||||
globals: [MenuGlobal],
|
||||
routes: {
|
||||
admin: adminRoute,
|
||||
},
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'example post',
|
||||
},
|
||||
})
|
||||
},
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
61
test/admin-root/e2e.spec.ts
Normal file
61
test/admin-root/e2e.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import * as path from 'path'
|
||||
import { adminRoute } from 'shared.js'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { ensureCompilationIsDone, initPageConsoleErrorCatch } from '../helpers.js'
|
||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
|
||||
import { TEST_TIMEOUT_LONG } from '../playwright.config.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
test.describe('Admin Panel (Root)', () => {
|
||||
let page: Page
|
||||
let url: AdminUrlUtil
|
||||
|
||||
test.beforeAll(async ({ browser }, testInfo) => {
|
||||
testInfo.setTimeout(TEST_TIMEOUT_LONG)
|
||||
|
||||
const { serverURL } = await initPayloadE2ENoConfig({ dirname })
|
||||
url = new AdminUrlUtil(serverURL, 'posts', {
|
||||
admin: adminRoute,
|
||||
})
|
||||
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
initPageConsoleErrorCatch(page)
|
||||
|
||||
await ensureCompilationIsDone({
|
||||
customRoutes: {
|
||||
admin: adminRoute,
|
||||
},
|
||||
page,
|
||||
serverURL,
|
||||
})
|
||||
})
|
||||
|
||||
test('renders admin panel at root', async () => {
|
||||
await page.goto(url.admin)
|
||||
const pageURL = page.url()
|
||||
expect(pageURL).toBe(url.admin)
|
||||
expect(pageURL).not.toContain('/admin')
|
||||
})
|
||||
|
||||
test('collection — navigates to list view', async () => {
|
||||
await page.goto(url.list)
|
||||
const pageURL = page.url()
|
||||
expect(pageURL).toContain(url.list)
|
||||
expect(pageURL).not.toContain('/admin')
|
||||
})
|
||||
|
||||
test('global — navigates to edit view', async () => {
|
||||
await page.goto(url.global('menu'))
|
||||
const pageURL = page.url()
|
||||
expect(pageURL).toBe(url.global('menu'))
|
||||
expect(pageURL).not.toContain('/admin')
|
||||
})
|
||||
})
|
||||
21
test/admin-root/eslint.config.js
Normal file
21
test/admin-root/eslint.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { rootParserOptions } from '../../eslint.config.js'
|
||||
import { testEslintConfig } from '../eslint.config.js'
|
||||
|
||||
/** @typedef {import('eslint').Linter.FlatConfig} */
|
||||
let FlatConfig
|
||||
|
||||
/** @type {FlatConfig[]} */
|
||||
export const index = [
|
||||
...testEslintConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json',
|
||||
tsconfigDirName: import.meta.dirname,
|
||||
...rootParserOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export default index
|
||||
13
test/admin-root/globals/Menu/index.ts
Normal file
13
test/admin-root/globals/Menu/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
export const menuSlug = 'menu'
|
||||
|
||||
export const MenuGlobal: GlobalConfig = {
|
||||
slug: menuSlug,
|
||||
fields: [
|
||||
{
|
||||
name: 'globalText',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
72
test/admin-root/int.spec.ts
Normal file
72
test/admin-root/int.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
import { postsSlug } from './collections/Posts/index.js'
|
||||
import configPromise from './config.js'
|
||||
|
||||
let payload: Payload
|
||||
let token: string
|
||||
let restClient: NextRESTClient
|
||||
|
||||
const { email, password } = devUser
|
||||
|
||||
describe('Admin (Root) Tests', () => {
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// Boilerplate test setup/teardown
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
beforeAll(async () => {
|
||||
const initialized = await initPayloadInt(configPromise)
|
||||
;({ payload, restClient } = initialized)
|
||||
|
||||
const data = await restClient
|
||||
.POST('/users/login', {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
token = data.token
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
// You can run tests against the local API or the REST API
|
||||
// use the tests below as a guide
|
||||
// --__--__--__--__--__--__--__--__--__
|
||||
|
||||
it('local API example', async () => {
|
||||
const newPost = await payload.create({
|
||||
collection: postsSlug,
|
||||
data: {
|
||||
text: 'LOCAL API EXAMPLE',
|
||||
},
|
||||
})
|
||||
|
||||
expect(newPost.text).toEqual('LOCAL API EXAMPLE')
|
||||
})
|
||||
|
||||
it('rest API example', async () => {
|
||||
const data = await restClient
|
||||
.POST(`/${postsSlug}`, {
|
||||
body: JSON.stringify({
|
||||
text: 'REST API EXAMPLE',
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `JWT ${token}`,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
expect(data.doc.text).toEqual('REST API EXAMPLE')
|
||||
})
|
||||
})
|
||||
5
test/admin-root/next-env.d.ts
vendored
Normal file
5
test/admin-root/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
30
test/admin-root/next.config.mjs
Normal file
30
test/admin-root/next.config.mjs
Normal file
@@ -0,0 +1,30 @@
|
||||
import bundleAnalyzer from '@next/bundle-analyzer'
|
||||
|
||||
import withPayload from '../../packages/next/src/withPayload.js'
|
||||
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
})
|
||||
|
||||
export default withBundleAnalyzer(
|
||||
withPayload({
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
},
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
}),
|
||||
)
|
||||
130
test/admin-root/payload-types.ts
Normal file
130
test/admin-root/payload-types.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
collections: {
|
||||
posts: Post;
|
||||
users: User;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {
|
||||
menu: Menu;
|
||||
};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
text?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "menu".
|
||||
*/
|
||||
export interface Menu {
|
||||
id: string;
|
||||
globalText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
1742
test/admin-root/schema.graphql
Normal file
1742
test/admin-root/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
1
test/admin-root/shared.js
Normal file
1
test/admin-root/shared.js
Normal file
@@ -0,0 +1 @@
|
||||
export const adminRoute = '/'
|
||||
13
test/admin-root/tsconfig.eslint.json
Normal file
13
test/admin-root/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
26
test/admin-root/tsconfig.json
Normal file
26
test/admin-root/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@payload-config": ["./config.ts"],
|
||||
"@payloadcms/ui/assets": ["../../packages/ui/src/assets/index.ts"],
|
||||
"@payloadcms/ui/elements/*": ["../../packages/ui/src/elements/*/index.tsx"],
|
||||
"@payloadcms/ui/fields/*": ["../../packages/ui/src/fields/*/index.tsx"],
|
||||
"@payloadcms/ui/forms/*": ["../../packages/ui/src/forms/*/index.tsx"],
|
||||
"@payloadcms/ui/graphics/*": ["../../packages/ui/src/graphics/*/index.tsx"],
|
||||
"@payloadcms/ui/hooks/*": ["../../packages/ui/src/hooks/*.ts"],
|
||||
"@payloadcms/ui/icons/*": ["../../packages/ui/src/icons/*/index.tsx"],
|
||||
"@payloadcms/ui/providers/*": ["../../packages/ui/src/providers/*/index.tsx"],
|
||||
"@payloadcms/ui/templates/*": ["../../packages/ui/src/templates/*/index.tsx"],
|
||||
"@payloadcms/ui/utilities/*": ["../../packages/ui/src/utilities/*.ts"],
|
||||
"@payloadcms/ui/scss": ["../../packages/ui/src/scss.scss"],
|
||||
"@payloadcms/ui/scss/app.scss": ["../../packages/ui/src/scss/app.scss"],
|
||||
"payload/types": ["../../packages/payload/src/exports/types.ts"],
|
||||
"@payloadcms/next/*": ["../../packages/next/src/*"],
|
||||
"@payloadcms/next": ["../../packages/next/src/exports/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Config } from 'payload'
|
||||
|
||||
export const slugSingularLabel = 'Post'
|
||||
|
||||
export const slugPluralLabel = 'Posts'
|
||||
@@ -35,7 +37,7 @@ export const customCollectionParamViewPathBase = '/custom-param'
|
||||
|
||||
export const customCollectionParamViewPath = `${customCollectionParamViewPathBase}/:slug`
|
||||
|
||||
export const customAdminRoutes = {
|
||||
export const customAdminRoutes: Config['admin']['routes'] = {
|
||||
inactivity: '/custom-inactivity',
|
||||
logout: '/custom-logout',
|
||||
}
|
||||
|
||||
@@ -160,9 +160,7 @@ export async function buildConfigWithDefaults(
|
||||
secret: 'TEST_SECRET',
|
||||
sharp,
|
||||
telemetry: false,
|
||||
|
||||
...testConfig,
|
||||
|
||||
i18n: {
|
||||
supportedLanguages: {
|
||||
de,
|
||||
@@ -182,6 +180,7 @@ export async function buildConfigWithDefaults(
|
||||
if (!config.admin) {
|
||||
config.admin = {}
|
||||
}
|
||||
|
||||
if (config.admin.autoLogin === undefined) {
|
||||
config.admin.autoLogin =
|
||||
process.env.PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN === 'true'
|
||||
|
||||
@@ -33,15 +33,15 @@ process.env.PAYLOAD_DROP_DATABASE = 'true'
|
||||
const { beforeTest } = await createTestHooks(testSuiteArg)
|
||||
await beforeTest()
|
||||
|
||||
const rootDir = getNextJSRootDir(testSuiteArg)
|
||||
const { rootDir, adminRoute } = getNextJSRootDir(testSuiteArg)
|
||||
|
||||
// Open the admin if the -o flag is passed
|
||||
if (args.o) {
|
||||
await open('http://localhost:3000/admin')
|
||||
await open(`http://localhost:3000${adminRoute}`)
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
await nextDev({ port: process.env.PORT || 3000, dirname: rootDir }, 'default', rootDir)
|
||||
|
||||
// fetch the admin url to force a render
|
||||
fetch(`http://localhost:${process.env.PORT || 3000}/admin`)
|
||||
fetch(`http://localhost:${process.env.PORT || 3000}${adminRoute}`)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// IMPORTANT: ensure that imports do not contain React components, etc. as this breaks Playwright tests
|
||||
// Instead of pointing to the bundled code, which will include React components, use direct import paths
|
||||
import { formatAdminURL } from '../../packages/ui/src/utilities/formatAdminURL.js' // eslint-disable-line payload/no-relative-monorepo-imports
|
||||
|
||||
import type { Config } from 'payload'
|
||||
|
||||
export class AdminUrlUtil {
|
||||
account: string
|
||||
|
||||
@@ -5,17 +11,53 @@ export class AdminUrlUtil {
|
||||
|
||||
create: string
|
||||
|
||||
entitySlug: string
|
||||
|
||||
list: string
|
||||
|
||||
constructor(serverURL: string, slug: string) {
|
||||
this.account = `${serverURL}/admin/account`
|
||||
this.admin = `${serverURL}/admin`
|
||||
this.list = `${this.admin}/collections/${slug}`
|
||||
this.create = `${this.list}/create`
|
||||
routes: Config['routes']
|
||||
|
||||
serverURL: string
|
||||
constructor(serverURL: string, slug: string, routes?: Config['routes']) {
|
||||
this.routes = {
|
||||
admin: routes?.admin || '/admin',
|
||||
}
|
||||
|
||||
this.serverURL = serverURL
|
||||
|
||||
this.entitySlug = slug
|
||||
|
||||
this.admin = formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: '',
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
|
||||
this.account = formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: '/account',
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
|
||||
this.list = formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: `/collections/${this.entitySlug}`,
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
|
||||
this.create = formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: `/collections/${this.entitySlug}/create`,
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
collection(slug: string): string {
|
||||
return `${this.admin}/collections/${slug}`
|
||||
return formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: `/collections/${slug}`,
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
}
|
||||
|
||||
edit(id: number | string): string {
|
||||
@@ -23,6 +65,10 @@ export class AdminUrlUtil {
|
||||
}
|
||||
|
||||
global(slug: string): string {
|
||||
return `${this.admin}/globals/${slug}`
|
||||
return formatAdminURL({
|
||||
adminRoute: this.routes.admin,
|
||||
path: `/globals/${slug}`,
|
||||
serverURL: this.serverURL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import fs from 'fs'
|
||||
import { dirname, resolve } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { adminRoute as rootAdminRoute } from '../admin-root/shared.js'
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url)
|
||||
const _dirname = dirname(_filename)
|
||||
|
||||
@@ -17,9 +19,23 @@ export const getNextJSRootDir = (testSuite) => {
|
||||
// Swallow err - no config found
|
||||
}
|
||||
|
||||
if (hasNextConfig) return testSuiteDir
|
||||
let adminRoute = '/admin'
|
||||
|
||||
if (testSuite === 'admin-root') {
|
||||
adminRoute = rootAdminRoute
|
||||
}
|
||||
|
||||
if (hasNextConfig) {
|
||||
return {
|
||||
rootDir: testSuiteDir,
|
||||
adminRoute,
|
||||
}
|
||||
}
|
||||
|
||||
// If no next config found in test suite,
|
||||
// return monorepo root dir
|
||||
return resolve(_dirname, '../../')
|
||||
return {
|
||||
rootDir: resolve(_dirname, '../../'),
|
||||
adminRoute,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function initPayloadE2ENoConfig<T extends GeneratedTypes<T>>({
|
||||
|
||||
await startMemoryDB()
|
||||
|
||||
const dir = getNextJSRootDir(testSuiteName)
|
||||
const { rootDir } = getNextJSRootDir(testSuiteName)
|
||||
|
||||
if (prebuild) {
|
||||
await new Promise<void>((res, rej) => {
|
||||
@@ -50,7 +50,7 @@ export async function initPayloadE2ENoConfig<T extends GeneratedTypes<T>>({
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
NODE_ENV: 'production',
|
||||
NEXTJS_DIR: dir,
|
||||
NEXTJS_DIR: rootDir,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function initPayloadE2ENoConfig<T extends GeneratedTypes<T>>({
|
||||
dev: !prebuild,
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
dir,
|
||||
dir: rootDir,
|
||||
})
|
||||
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#custom-css {
|
||||
font-family: monospace;
|
||||
background-image: url('/placeholder.png');
|
||||
}
|
||||
|
||||
#custom-css::after {
|
||||
|
||||
0
test/scripts/reset-tsconfig.js
Normal file
0
test/scripts/reset-tsconfig.js
Normal file
Reference in New Issue
Block a user