fix: format admin url inside forgot pw email (#11509)

### What?
Supersedes https://github.com/payloadcms/payload/pull/11490.

Refactors imports of `formatAdminURL` to import from `payload/shared`
instead of `@payloadcms/ui/shared`. The ui package now imports and
re-exports the function to prevent this from being a breaking change.

### Why?
This makes it easier for other packages/plugins to consume the
`formatAdminURL` function instead of needing to implement their own or
rely on the ui package for the utility.
This commit is contained in:
Jarrod Flesch
2025-03-04 11:55:36 -05:00
committed by GitHub
parent 1d168318d0
commit 56dec13820
44 changed files with 135 additions and 102 deletions

View File

@@ -2,8 +2,8 @@
import type { SanitizedConfig } from 'payload' import type { SanitizedConfig } from 'payload'
import { Link } from '@payloadcms/ui' import { Link } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { useParams, usePathname, useSearchParams } from 'next/navigation.js' import { useParams, usePathname, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
export const DocumentTabLink: React.FC<{ export const DocumentTabLink: React.FC<{

View File

@@ -5,8 +5,9 @@ import type { NavPreferences } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui' import { Link, NavGroup, useConfig, useTranslation } from '@payloadcms/ui'
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared' import { EntityType } from '@payloadcms/ui/shared'
import { usePathname } from 'next/navigation.js' import { usePathname } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
const baseClass = 'nav' const baseClass = 'nav'

View File

@@ -1,6 +1,6 @@
import type { User } from 'payload' import type { User } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
type Args = { type Args = {

View File

@@ -4,7 +4,8 @@ import type { ClientUser, Locale, ServerProps } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { Button, Card, Gutter, Locked } from '@payloadcms/ui' import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { EntityType, formatAdminURL } from '@payloadcms/ui/shared' import { EntityType } from '@payloadcms/ui/shared'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import './index.scss' import './index.scss'

View File

@@ -9,10 +9,11 @@ import type {
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui' import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL, isEditing as getIsEditing } from '@payloadcms/ui/shared' import { isEditing as getIsEditing } from '@payloadcms/ui/shared'
import { buildFormState } from '@payloadcms/ui/utilities/buildFormState' import { buildFormState } from '@payloadcms/ui/utilities/buildFormState'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import { logError } from 'payload' import { logError } from 'payload'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import type { GenerateEditViewMetadata } from './getMetaBySegment.js' import type { GenerateEditViewMetadata } from './getMetaBySegment.js'

View File

@@ -1,7 +1,8 @@
import type { AdminViewServerProps } from 'payload' import type { AdminViewServerProps } from 'payload'
import { Button, Link } from '@payloadcms/ui' import { Button, Link } from '@payloadcms/ui'
import { formatAdminURL, Translation } from '@payloadcms/ui/shared' import { Translation } from '@payloadcms/ui/shared'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { FormHeader } from '../../elements/FormHeader/index.js' import { FormHeader } from '../../elements/FormHeader/index.js'

View File

@@ -1,7 +1,7 @@
import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui' import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc' import { renderFilters, renderTable, upsertPreferences } from '@payloadcms/ui/rsc'
import { formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared' import { mergeListSearchAndWhere } from '@payloadcms/ui/shared'
import { notFound } from 'next/navigation.js' import { notFound } from 'next/navigation.js'
import { import {
type AdminViewServerProps, type AdminViewServerProps,
@@ -12,7 +12,7 @@ import {
type ListViewServerPropsOnly, type ListViewServerPropsOnly,
type Where, type Where,
} from 'payload' } from 'payload'
import { isNumber, transformColumnsToPreferences } from 'payload/shared' import { formatAdminURL, isNumber, transformColumnsToPreferences } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { renderListViewSlots } from './renderListViewSlots.js' import { renderListViewSlots } from './renderListViewSlots.js'

View File

@@ -35,13 +35,13 @@ import {
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { import {
abortAndIgnore, abortAndIgnore,
formatAdminURL,
handleAbortRef, handleAbortRef,
handleBackToDashboard, handleBackToDashboard,
handleGoBack, handleGoBack,
handleTakeOver, handleTakeOver,
} from '@payloadcms/ui/shared' } from '@payloadcms/ui/shared'
import { useRouter, useSearchParams } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useLivePreviewContext } from './Context/context.js' import { useLivePreviewContext } from './Context/context.js'

View File

@@ -16,8 +16,7 @@ import {
useConfig, useConfig,
useTranslation, useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL, getLoginOptions } from 'payload/shared'
import { getLoginOptions } from 'payload/shared'
import type { LoginFieldProps } from '../LoginField/index.js' import type { LoginFieldProps } from '../LoginField/index.js'

View File

@@ -7,8 +7,8 @@ import {
useRouteTransition, useRouteTransition,
useTranslation, useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import './index.scss' import './index.scss'

View File

@@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next' import type { Metadata } from 'next'
import type { AdminViewServerProps, ImportMap, SanitizedConfig } from 'payload' import type { AdminViewServerProps, ImportMap, SanitizedConfig } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js' import { DefaultTemplate } from '../../templates/Default/index.js'

View File

@@ -9,9 +9,9 @@ import {
useConfig, useConfig,
useTranslation, useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { type FormState } from 'payload' import { type FormState } from 'payload'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
type Args = { type Args = {

View File

@@ -1,7 +1,8 @@
import type { AdminViewServerProps } from 'payload' import type { AdminViewServerProps } from 'payload'
import { Button, Link } from '@payloadcms/ui' import { Button, Link } from '@payloadcms/ui'
import { formatAdminURL, Translation } from '@payloadcms/ui/shared' import { Translation } from '@payloadcms/ui/shared'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import { FormHeader } from '../../elements/FormHeader/index.js' import { FormHeader } from '../../elements/FormHeader/index.js'

View File

@@ -9,7 +9,7 @@ import type {
} from 'payload' } from 'payload'
import type React from 'react' import type React from 'react'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import type { initPage } from '../../utilities/initPage/index.js' import type { initPage } from '../../utilities/initPage/index.js'

View File

@@ -8,9 +8,9 @@ import type {
} from 'payload' } from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig' import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { notFound, redirect } from 'next/navigation.js' import { notFound, redirect } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js' import { DefaultTemplate } from '../../templates/Default/index.js'
@@ -56,7 +56,7 @@ export const RootPage = async ({
const currentRoute = formatAdminURL({ const currentRoute = formatAdminURL({
adminRoute, adminRoute,
path: `${Array.isArray(params.segments) ? `/${params.segments.join('/')}` : ''}`, path: Array.isArray(params.segments) ? `/${params.segments.join('/')}` : null,
}) })
const segments = Array.isArray(params.segments) ? params.segments : [] const segments = Array.isArray(params.segments) ? params.segments : []

View File

@@ -1,7 +1,7 @@
import type { AdminViewServerProps } from 'payload' import type { AdminViewServerProps } from 'payload'
import { Button } from '@payloadcms/ui' import { Button } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import { FormHeader } from '../../elements/FormHeader/index.js' import { FormHeader } from '../../elements/FormHeader/index.js'

View File

@@ -1,6 +1,6 @@
import type { AdminViewServerProps } from 'payload' import type { AdminViewServerProps } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import { Logo } from '../../elements/Logo/index.js' import { Logo } from '../../elements/Logo/index.js'

View File

@@ -5,8 +5,8 @@ import type React from 'react'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui' import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui'
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared' import { formatDate } from '@payloadcms/ui/shared'
import { fieldAffectsData } from 'payload/shared' import { fieldAffectsData, formatAdminURL } from 'payload/shared'
import { useEffect } from 'react' import { useEffect } from 'react'
export const SetStepNav: React.FC<{ export const SetStepNav: React.FC<{

View File

@@ -11,8 +11,9 @@ import {
useRouteTransition, useRouteTransition,
useTranslation, useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import { formatAdminURL, requests } from '@payloadcms/ui/shared' import { requests } from '@payloadcms/ui/shared'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment, useCallback, useState } from 'react' import React, { Fragment, useCallback, useState } from 'react'
import type { Props } from './types.js' import type { Props } from './types.js'

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { Link, useConfig, useTranslation } from '@payloadcms/ui' import { Link, useConfig, useTranslation } from '@payloadcms/ui'
import { formatAdminURL, formatDate } from '@payloadcms/ui/shared' import { formatDate } from '@payloadcms/ui/shared'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
type CreatedAtCellProps = { type CreatedAtCellProps = {

View File

@@ -14,6 +14,7 @@ import { buildAfterOperation } from '../../collections/operations/utils.js'
import { APIError } from '../../errors/index.js' import { APIError } from '../../errors/index.js'
import { Forbidden } from '../../index.js' import { Forbidden } from '../../index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js' import { commitTransaction } from '../../utilities/commitTransaction.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { initTransaction } from '../../utilities/initTransaction.js' import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js'
import { getLoginOptions } from '../getLoginOptions.js' import { getLoginOptions } from '../getLoginOptions.js'
@@ -155,9 +156,13 @@ export const forgotPasswordOperation = async <TSlug extends CollectionSlug>(
config.serverURL !== null && config.serverURL !== '' config.serverURL !== null && config.serverURL !== ''
? config.serverURL ? config.serverURL
: `${protocol}//${req.headers.get('host')}` : `${protocol}//${req.headers.get('host')}`
const forgotURL = formatAdminURL({
adminRoute: config.routes.admin,
path: `${config.admin.routes.reset}/${token}`,
serverURL,
})
let html = `${req.t('authentication:youAreReceivingResetPassword')} let html = `${req.t('authentication:youAreReceivingResetPassword')}
<a href="${serverURL}${config.routes.admin}${config.admin.routes.reset}/${token}">${serverURL}${config.routes.admin}${config.admin.routes.reset}/${token}</a> <a href="${forgotURL}">${forgotURL}</a>
${req.t('authentication:youDidNotRequestPassword')}` ${req.t('authentication:youDidNotRequestPassword')}`
if (typeof collectionConfig.auth.forgotPassword?.generateEmailHTML === 'function') { if (typeof collectionConfig.auth.forgotPassword?.generateEmailHTML === 'function') {

View File

@@ -881,22 +881,46 @@ export type Config = {
/** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */ /** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */
meta?: MetaConfig meta?: MetaConfig
routes?: { routes?: {
/** The route for the account page. */ /** The route for the account page.
account?: string *
/** The route for the create first user page. */ * @default '/account'
createFirstUser?: string */
/** The route for the forgot password page. */ account?: `/${string}`
forgot?: string /** The route for the create first user page.
/** The route the user will be redirected to after being inactive for too long. */ *
inactivity?: string * @default '/create-first-user'
/** The route for the login page. */ */
login?: string createFirstUser?: `/${string}`
/** The route for the logout page. */ /** The route for the forgot password page.
logout?: string *
/** The route for the reset password page. */ * @default '/forgot'
reset?: string */
/** The route for the unauthorized page. */ forgot?: `/${string}`
unauthorized?: string /** The route the user will be redirected to after being inactive for too long.
*
* @default '/logout-inactivity'
*/
inactivity?: `/${string}`
/** The route for the login page.
*
* @default '/login'
*/
login?: `/${string}`
/** The route for the logout page.
*
* @default '/logout'
*/
logout?: `/${string}`
/** The route for the reset password page.
*
* @default '/reset'
*/
reset?: `/${string}`
/** The route for the unauthorized page.
*
* @default '/unauthorized'
*/
unauthorized?: `/${string}`
} }
/** /**
* Suppresses React hydration mismatch warnings during the hydration of the root <html> tag. * Suppresses React hydration mismatch warnings during the hydration of the root <html> tag.

View File

@@ -64,10 +64,12 @@ export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { flattenAllFields } from '../utilities/flattenAllFields.js' export { flattenAllFields } from '../utilities/flattenAllFields.js'
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js' export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
export { getDataByPath } from '../utilities/getDataByPath.js' export { formatAdminURL } from '../utilities/formatAdminURL.js'
export { getDataByPath } from '../utilities/getDataByPath.js'
export { getSelectMode } from '../utilities/getSelectMode.js' export { getSelectMode } from '../utilities/getSelectMode.js'
export { getSiblingData } from '../utilities/getSiblingData.js' export { getSiblingData } from '../utilities/getSiblingData.js'
export { getUniqueListBy } from '../utilities/getUniqueListBy.js' export { getUniqueListBy } from '../utilities/getUniqueListBy.js'
export { isNextBuild } from '../utilities/isNextBuild.js' export { isNextBuild } from '../utilities/isNextBuild.js'

View File

@@ -0,0 +1,24 @@
import type { Config } from '../config/types.js'
/** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */
export const formatAdminURL = (args: {
adminRoute: NonNullable<Config['routes']>['admin']
basePath?: string
path: '' | `/${string}` | null | undefined
serverURL?: Config['serverURL']
}): string => {
const { adminRoute, basePath = '', path: pathFromArgs, serverURL } = args
const path = pathFromArgs || ''
if (adminRoute) {
if (adminRoute === '/') {
if (!path) {
return `${serverURL || ''}${basePath}${adminRoute}`
}
} else {
return `${serverURL || ''}${basePath}${adminRoute}${path}`
}
}
return `${serverURL || ''}${basePath}${path}`
}

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import { CopyToClipboard, Link, useConfig, useField } from '@payloadcms/ui' import { CopyToClipboard, Link, useConfig, useField } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
export const LinkToDocClient: React.FC = () => { export const LinkToDocClient: React.FC = () => {

View File

@@ -3,7 +3,7 @@ import type { Payload } from 'payload'
import { getTranslation, type I18nClient } from '@payloadcms/translations' import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { Link } from '@payloadcms/ui' import { Link } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js' import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'

View File

@@ -2,7 +2,7 @@ import type { DefaultServerCellComponentProps, Payload } from 'payload'
import { getTranslation, type I18nClient } from '@payloadcms/translations' import { getTranslation, type I18nClient } from '@payloadcms/translations'
import { Link } from '@payloadcms/ui' import { Link } from '@payloadcms/ui'
import { formatAdminURL } from '@payloadcms/ui/shared' import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
export const RscEntrySlateCell: React.FC< export const RscEntrySlateCell: React.FC<

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import { formatAdminURL } from 'payload/shared'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { Account } from '../../graphics/Account/index.js' import { Account } from '../../graphics/Account/index.js'
import { useActions } from '../../providers/Actions/index.js' import { useActions } from '../../providers/Actions/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { Hamburger } from '../Hamburger/index.js' import { Hamburger } from '../Hamburger/index.js'
import { Link } from '../Link/index.js' import { Link } from '../Link/index.js'
import { Localizer } from '../Localizer/index.js' import { Localizer } from '../Localizer/index.js'

View File

@@ -1,6 +1,7 @@
'use client' 'use client'
import { useRouter, useSearchParams } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback, useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import type { EditFormProps } from './types.js' import type { EditFormProps } from './types.js'
@@ -17,7 +18,6 @@ import { useRouteTransition } from '../../../providers/RouteTransition/index.js'
import { useServerFunctions } from '../../../providers/ServerFunctions/index.js' import { useServerFunctions } from '../../../providers/ServerFunctions/index.js'
import { useUploadEdits } from '../../../providers/UploadEdits/index.js' import { useUploadEdits } from '../../../providers/UploadEdits/index.js'
import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js' import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js'
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js' import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js'
import { DocumentFields } from '../../DocumentFields/index.js' import { DocumentFields } from '../../DocumentFields/index.js'
import { Upload } from '../../Upload/index.js' import { Upload } from '../../Upload/index.js'

View File

@@ -4,6 +4,7 @@ import type { SanitizedCollectionConfig } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -15,7 +16,6 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { PopupList } from '../Popup/index.js' import { PopupList } from '../Popup/index.js'
import { Translation } from '../Translation/index.js' import { Translation } from '../Translation/index.js'

View File

@@ -7,6 +7,7 @@ import type {
} from 'payload' } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment, useEffect } from 'react' import React, { Fragment, useEffect } from 'react'
import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js'
@@ -15,7 +16,6 @@ import { useFormInitializing, useFormProcessing } from '../../forms/Form/context
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useEditDepth } from '../../providers/EditDepth/index.js' import { useEditDepth } from '../../providers/EditDepth/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { formatDate } from '../../utilities/formatDate.js' import { formatDate } from '../../utilities/formatDate.js'
import { Autosave } from '../Autosave/index.js' import { Autosave } from '../Autosave/index.js'
import { Button } from '../Button/index.js' import { Button } from '../Button/index.js'

View File

@@ -5,6 +5,7 @@ import type { SanitizedCollectionConfig } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -16,7 +17,6 @@ import { useLocale } from '../../providers/Locale/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js'
import { PopupList } from '../Popup/index.js' import { PopupList } from '../Popup/index.js'

View File

@@ -1,10 +1,10 @@
'use client' 'use client'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
import { LogOutIcon } from '../../icons/LogOut/index.js' import { LogOutIcon } from '../../icons/LogOut/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { Link } from '../Link/index.js' import { Link } from '../Link/index.js'
const baseClass = 'nav' const baseClass = 'nav'

View File

@@ -1,5 +1,6 @@
'use client' 'use client'
import { useRouter } from 'next/navigation.js' import { useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import type { OnCancel } from '../ConfirmationModal/index.js' import type { OnCancel } from '../ConfirmationModal/index.js'
@@ -8,7 +9,6 @@ import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { ConfirmationModal } from '../ConfirmationModal/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js'
export const stayLoggedInModalSlug = 'stay-logged-in' export const stayLoggedInModalSlug = 'stay-logged-in'

View File

@@ -1,13 +1,12 @@
'use client' 'use client'
import type { ClientCollectionConfig, DefaultCellComponentProps, UploadFieldClient } from 'payload' import type { DefaultCellComponentProps, UploadFieldClient } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { fieldAffectsData, fieldIsID } from 'payload/shared' import { fieldAffectsData, fieldIsID, formatAdminURL } from 'payload/shared'
import React from 'react' // TODO: abstract this out to support all routers import React from 'react' // TODO: abstract this out to support all routers
import { useConfig } from '../../../providers/Config/index.js' import { useConfig } from '../../../providers/Config/index.js'
import { useTranslation } from '../../../providers/Translation/index.js' import { useTranslation } from '../../../providers/Translation/index.js'
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
import { Link } from '../../Link/index.js' import { Link } from '../../Link/index.js'
import { CodeCell } from './fields/Code/index.js' import { CodeCell } from './fields/Code/index.js'
import { cellComponents } from './fields/index.js' import { cellComponents } from './fields/index.js'

View File

@@ -1,11 +1,11 @@
'use client' 'use client'
import { usePathname } from 'next/navigation.js' import { usePathname } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React from 'react' import React from 'react'
// import { RenderComponent } from '../../elements/RenderComponent/client.js' // import { RenderComponent } from '../../elements/RenderComponent/client.js'
import { useAuth } from '../../providers/Auth/index.js' import { useAuth } from '../../providers/Auth/index.js'
import { useConfig } from '../../providers/Config/index.js' import { useConfig } from '../../providers/Config/index.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { DefaultAccountIcon } from './Default/index.js' import { DefaultAccountIcon } from './Default/index.js'
import { GravatarAccountIcon } from './Gravatar/index.js' import { GravatarAccountIcon } from './Gravatar/index.js'

View File

@@ -3,6 +3,7 @@ import type { ClientUser, SanitizedPermissions, User } from 'payload'
import { useModal } from '@faceless-ui/modal' import { useModal } from '@faceless-ui/modal'
import { usePathname, useRouter } from 'next/navigation.js' import { usePathname, useRouter } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import * as qs from 'qs-esm' import * as qs from 'qs-esm'
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
@@ -11,7 +12,6 @@ import { stayLoggedInModalSlug } from '../../elements/StayLoggedIn/index.js'
import { useDebounce } from '../../hooks/useDebounce.js' import { useDebounce } from '../../hooks/useDebounce.js'
import { useTranslation } from '../../providers/Translation/index.js' import { useTranslation } from '../../providers/Translation/index.js'
import { requests } from '../../utilities/api.js' import { requests } from '../../utilities/api.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { useConfig } from '../Config/index.js' import { useConfig } from '../Config/index.js'
import { useRouteTransition } from '../RouteTransition/index.js' import { useRouteTransition } from '../RouteTransition/index.js'

View File

@@ -1,23 +1,2 @@
import type { Config } from 'payload'
/** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */ /** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */
export const formatAdminURL = (args: { export { formatAdminURL } from 'payload/shared'
adminRoute: Config['routes']['admin']
basePath?: string
path: string
serverURL?: Config['serverURL']
}): string => {
const { adminRoute, basePath = '', path, serverURL } = args
if (adminRoute) {
if (adminRoute === '/') {
if (!path) {
return `${serverURL || ''}${basePath}${adminRoute}`
}
} else {
return `${serverURL || ''}${basePath}${adminRoute}${path}`
}
}
return `${serverURL || ''}${basePath}${path}`
}

View File

@@ -1,6 +1,6 @@
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js' import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
import { formatAdminURL } from './formatAdminURL.js' import { formatAdminURL } from 'payload/shared'
type BackToDashboardProps = { type BackToDashboardProps = {
adminRoute: string adminRoute: string

View File

@@ -1,6 +1,6 @@
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js' import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
import { formatAdminURL } from './formatAdminURL.js' import { formatAdminURL } from 'payload/shared'
type GoBackProps = { type GoBackProps = {
adminRoute: string adminRoute: string

View File

@@ -2,6 +2,7 @@
import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload' import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload'
import { getTranslation } from '@payloadcms/translations' import { getTranslation } from '@payloadcms/translations'
import { formatAdminURL } from 'payload/shared'
import { useEffect } from 'react' import { useEffect } from 'react'
import type { StepNavItem } from '../../../elements/StepNav/index.js' import type { StepNavItem } from '../../../elements/StepNav/index.js'
@@ -12,7 +13,6 @@ import { useDocumentInfo } from '../../../providers/DocumentInfo/index.js'
import { useEditDepth } from '../../../providers/EditDepth/index.js' import { useEditDepth } from '../../../providers/EditDepth/index.js'
import { useEntityVisibility } from '../../../providers/EntityVisibility/index.js' import { useEntityVisibility } from '../../../providers/EntityVisibility/index.js'
import { useTranslation } from '../../../providers/Translation/index.js' import { useTranslation } from '../../../providers/Translation/index.js'
import { formatAdminURL } from '../../../utilities/formatAdminURL.js'
export const SetDocumentStepNav: React.FC<{ export const SetDocumentStepNav: React.FC<{
collectionSlug?: SanitizedCollectionConfig['slug'] collectionSlug?: SanitizedCollectionConfig['slug']

View File

@@ -3,6 +3,7 @@
import type { ClientUser, DocumentViewClientProps, FormState } from 'payload' import type { ClientUser, DocumentViewClientProps, FormState } from 'payload'
import { useRouter, useSearchParams } from 'next/navigation.js' import { useRouter, useSearchParams } from 'next/navigation.js'
import { formatAdminURL } from 'payload/shared'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { FormProps } from '../../forms/Form/index.js' import type { FormProps } from '../../forms/Form/index.js'
@@ -27,7 +28,6 @@ import { useRouteTransition } from '../../providers/RouteTransition/index.js'
import { useServerFunctions } from '../../providers/ServerFunctions/index.js' import { useServerFunctions } from '../../providers/ServerFunctions/index.js'
import { useUploadEdits } from '../../providers/UploadEdits/index.js' import { useUploadEdits } from '../../providers/UploadEdits/index.js'
import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js'
import { formatAdminURL } from '../../utilities/formatAdminURL.js'
import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js' import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
import { handleGoBack } from '../../utilities/handleGoBack.js' import { handleGoBack } from '../../utilities/handleGoBack.js'
import { handleTakeOver } from '../../utilities/handleTakeOver.js' import { handleTakeOver } from '../../utilities/handleTakeOver.js'

View File

@@ -17,15 +17,17 @@ import { setTimeout } from 'timers/promises'
import { devUser } from './credentials.js' import { devUser } from './credentials.js'
import { POLL_TOPASS_TIMEOUT } from './playwright.config.js' import { POLL_TOPASS_TIMEOUT } from './playwright.config.js'
type AdminRoutes = NonNullable<Config['admin']>['routes']
type FirstRegisterArgs = { type FirstRegisterArgs = {
customAdminRoutes?: Config['admin']['routes'] customAdminRoutes?: AdminRoutes
customRoutes?: Config['routes'] customRoutes?: Config['routes']
page: Page page: Page
serverURL: string serverURL: string
} }
type LoginArgs = { type LoginArgs = {
customAdminRoutes?: Config['admin']['routes'] customAdminRoutes?: AdminRoutes
customRoutes?: Config['routes'] customRoutes?: Config['routes']
data?: { data?: {
email: string email: string
@@ -78,16 +80,14 @@ export async function ensureCompilationIsDone({
noAutoLogin, noAutoLogin,
readyURL, readyURL,
}: { }: {
customAdminRoutes?: Config['admin']['routes'] customAdminRoutes?: AdminRoutes
customRoutes?: Config['routes'] customRoutes?: Config['routes']
noAutoLogin?: boolean noAutoLogin?: boolean
page: Page page: Page
readyURL?: string readyURL?: string
serverURL: string serverURL: string
}): Promise<void> { }): Promise<void> {
const { const { routes: { admin: adminRoute } = {} } = getRoutes({ customAdminRoutes, customRoutes })
routes: { admin: adminRoute },
} = getRoutes({ customAdminRoutes, customRoutes })
const adminURL = `${serverURL}${adminRoute}` const adminURL = `${serverURL}${adminRoute}`
@@ -170,9 +170,7 @@ export async function throttleTest({
export async function firstRegister(args: FirstRegisterArgs): Promise<void> { export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
const { customAdminRoutes, customRoutes, page, serverURL } = args const { customAdminRoutes, customRoutes, page, serverURL } = args
const { const { routes: { admin: adminRoute } = {} } = getRoutes({ customAdminRoutes, customRoutes })
routes: { admin: adminRoute },
} = getRoutes({ customAdminRoutes, customRoutes })
await page.goto(`${serverURL}${adminRoute}`) await page.goto(`${serverURL}${adminRoute}`)
await page.fill('#field-email', devUser.email) await page.fill('#field-email', devUser.email)
@@ -187,10 +185,8 @@ export async function login(args: LoginArgs): Promise<void> {
const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args
const { const {
admin: { admin: { routes: { createFirstUser, login: incomingLoginRoute } = {} },
routes: { createFirstUser, login: incomingLoginRoute }, routes: { admin: incomingAdminRoute } = {},
},
routes: { admin: incomingAdminRoute },
} = getRoutes({ customAdminRoutes, customRoutes }) } = getRoutes({ customAdminRoutes, customRoutes })
const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' }) const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' })
@@ -462,8 +458,6 @@ export function describeIfInCIOrHasLocalstack(): jest.Describe {
return describe return describe
} }
type AdminRoutes = Config['admin']['routes']
export function getRoutes({ export function getRoutes({
customAdminRoutes, customAdminRoutes,
customRoutes, customRoutes,
@@ -477,7 +471,7 @@ export function getRoutes({
routes: Config['routes'] routes: Config['routes']
} { } {
let routes = defaults.routes let routes = defaults.routes
let adminRoutes = defaults.admin.routes let adminRoutes = defaults.admin?.routes
if (customAdminRoutes) { if (customAdminRoutes) {
adminRoutes = { adminRoutes = {

View File

@@ -71,7 +71,7 @@ export class AdminUrlUtil {
collection(slug: string): string { collection(slug: string): string {
return formatAdminURL({ return formatAdminURL({
adminRoute: this.routes.admin, adminRoute: this.routes?.admin,
path: `/collections/${slug}`, path: `/collections/${slug}`,
serverURL: this.serverURL, serverURL: this.serverURL,
}) })
@@ -83,7 +83,7 @@ export class AdminUrlUtil {
global(slug: string): string { global(slug: string): string {
return formatAdminURL({ return formatAdminURL({
adminRoute: this.routes.admin, adminRoute: this.routes?.admin,
path: `/globals/${slug}`, path: `/globals/${slug}`,
serverURL: this.serverURL, serverURL: this.serverURL,
}) })