From 56dec138206d922476d59d018ac1ee9e8a85fdaa Mon Sep 17 00:00:00 2001 From: Jarrod Flesch <30633324+JarrodMFlesch@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:55:36 -0500 Subject: [PATCH] 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. --- .../DocumentHeader/Tabs/Tab/TabLink.tsx | 2 +- .../next/src/elements/Nav/index.client.tsx | 3 +- .../utilities/initPage/handleAuthRedirect.ts | 2 +- .../src/views/Dashboard/Default/index.tsx | 3 +- packages/next/src/views/Document/index.tsx | 3 +- .../next/src/views/ForgotPassword/index.tsx | 3 +- packages/next/src/views/List/index.tsx | 4 +- .../src/views/LivePreview/index.client.tsx | 2 +- .../next/src/views/Login/LoginForm/index.tsx | 3 +- .../next/src/views/Logout/LogoutClient.tsx | 2 +- packages/next/src/views/NotFound/index.tsx | 2 +- .../ResetPassword/ResetPasswordForm/index.tsx | 2 +- .../next/src/views/ResetPassword/index.tsx | 3 +- .../next/src/views/Root/getViewFromConfig.ts | 2 +- packages/next/src/views/Root/index.tsx | 4 +- .../next/src/views/Unauthorized/index.tsx | 2 +- packages/next/src/views/Verify/index.tsx | 2 +- .../src/views/Version/Default/SetStepNav.tsx | 4 +- .../next/src/views/Version/Restore/index.tsx | 3 +- .../views/Versions/cells/CreatedAt/index.tsx | 3 +- .../src/auth/operations/forgotPassword.ts | 9 ++- packages/payload/src/config/types.ts | 56 +++++++++++++------ packages/payload/src/exports/shared.ts | 4 +- .../payload/src/utilities/formatAdminURL.ts | 24 ++++++++ .../src/Search/ui/LinkToDoc/index.client.tsx | 2 +- .../richtext-lexical/src/cell/rscEntry.tsx | 2 +- packages/richtext-slate/src/cell/rscEntry.tsx | 2 +- packages/ui/src/elements/AppHeader/index.tsx | 2 +- .../elements/BulkUpload/EditForm/index.tsx | 2 +- .../ui/src/elements/DeleteDocument/index.tsx | 2 +- .../src/elements/DocumentControls/index.tsx | 2 +- .../src/elements/DuplicateDocument/index.tsx | 2 +- packages/ui/src/elements/Logout/index.tsx | 2 +- .../ui/src/elements/StayLoggedIn/index.tsx | 2 +- .../src/elements/Table/DefaultCell/index.tsx | 5 +- packages/ui/src/graphics/Account/index.tsx | 2 +- packages/ui/src/providers/Auth/index.tsx | 2 +- packages/ui/src/utilities/formatAdminURL.ts | 23 +------- .../src/utilities/handleBackToDashboard.tsx | 2 +- packages/ui/src/utilities/handleGoBack.tsx | 2 +- .../views/Edit/SetDocumentStepNav/index.tsx | 2 +- packages/ui/src/views/Edit/index.tsx | 2 +- test/helpers.ts | 26 ++++----- test/helpers/adminUrlUtil.ts | 4 +- 44 files changed, 135 insertions(+), 102 deletions(-) create mode 100644 packages/payload/src/utilities/formatAdminURL.ts diff --git a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx index 526551dfb..55832945b 100644 --- a/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx +++ b/packages/next/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx @@ -2,8 +2,8 @@ import type { SanitizedConfig } from 'payload' import { Link } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' import { useParams, usePathname, useSearchParams } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React from 'react' export const DocumentTabLink: React.FC<{ diff --git a/packages/next/src/elements/Nav/index.client.tsx b/packages/next/src/elements/Nav/index.client.tsx index dd1274f55..95379a148 100644 --- a/packages/next/src/elements/Nav/index.client.tsx +++ b/packages/next/src/elements/Nav/index.client.tsx @@ -5,8 +5,9 @@ import type { NavPreferences } from 'payload' import { getTranslation } from '@payloadcms/translations' 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 { formatAdminURL } from 'payload/shared' import React, { Fragment } from 'react' const baseClass = 'nav' diff --git a/packages/next/src/utilities/initPage/handleAuthRedirect.ts b/packages/next/src/utilities/initPage/handleAuthRedirect.ts index 937592fdf..ea5463ff0 100644 --- a/packages/next/src/utilities/initPage/handleAuthRedirect.ts +++ b/packages/next/src/utilities/initPage/handleAuthRedirect.ts @@ -1,6 +1,6 @@ import type { User } from 'payload' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' type Args = { diff --git a/packages/next/src/views/Dashboard/Default/index.tsx b/packages/next/src/views/Dashboard/Default/index.tsx index 524761f8c..e8111bdb1 100644 --- a/packages/next/src/views/Dashboard/Default/index.tsx +++ b/packages/next/src/views/Dashboard/Default/index.tsx @@ -4,7 +4,8 @@ import type { ClientUser, Locale, ServerProps } from 'payload' import { getTranslation } from '@payloadcms/translations' import { Button, Card, Gutter, Locked } from '@payloadcms/ui' 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 './index.scss' diff --git a/packages/next/src/views/Document/index.tsx b/packages/next/src/views/Document/index.tsx index 5b3723098..74d96af92 100644 --- a/packages/next/src/views/Document/index.tsx +++ b/packages/next/src/views/Document/index.tsx @@ -9,10 +9,11 @@ import type { import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui' 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 { notFound, redirect } from 'next/navigation.js' import { logError } from 'payload' +import { formatAdminURL } from 'payload/shared' import React from 'react' import type { GenerateEditViewMetadata } from './getMetaBySegment.js' diff --git a/packages/next/src/views/ForgotPassword/index.tsx b/packages/next/src/views/ForgotPassword/index.tsx index 38d8e42a5..4dc4f2501 100644 --- a/packages/next/src/views/ForgotPassword/index.tsx +++ b/packages/next/src/views/ForgotPassword/index.tsx @@ -1,7 +1,8 @@ import type { AdminViewServerProps } from 'payload' 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 { FormHeader } from '../../elements/FormHeader/index.js' diff --git a/packages/next/src/views/List/index.tsx b/packages/next/src/views/List/index.tsx index 7671018f3..3ad3e8f8e 100644 --- a/packages/next/src/views/List/index.tsx +++ b/packages/next/src/views/List/index.tsx @@ -1,7 +1,7 @@ import { DefaultListView, HydrateAuthProvider, ListQueryProvider } from '@payloadcms/ui' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' 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 { type AdminViewServerProps, @@ -12,7 +12,7 @@ import { type ListViewServerPropsOnly, type Where, } from 'payload' -import { isNumber, transformColumnsToPreferences } from 'payload/shared' +import { formatAdminURL, isNumber, transformColumnsToPreferences } from 'payload/shared' import React, { Fragment } from 'react' import { renderListViewSlots } from './renderListViewSlots.js' diff --git a/packages/next/src/views/LivePreview/index.client.tsx b/packages/next/src/views/LivePreview/index.client.tsx index a1cb8eb93..30181d28f 100644 --- a/packages/next/src/views/LivePreview/index.client.tsx +++ b/packages/next/src/views/LivePreview/index.client.tsx @@ -35,13 +35,13 @@ import { } from '@payloadcms/ui' import { abortAndIgnore, - formatAdminURL, handleAbortRef, handleBackToDashboard, handleGoBack, handleTakeOver, } from '@payloadcms/ui/shared' import { useRouter, useSearchParams } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { useLivePreviewContext } from './Context/context.js' diff --git a/packages/next/src/views/Login/LoginForm/index.tsx b/packages/next/src/views/Login/LoginForm/index.tsx index 1816c8b8c..a258da9bc 100644 --- a/packages/next/src/views/Login/LoginForm/index.tsx +++ b/packages/next/src/views/Login/LoginForm/index.tsx @@ -16,8 +16,7 @@ import { useConfig, useTranslation, } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' -import { getLoginOptions } from 'payload/shared' +import { formatAdminURL, getLoginOptions } from 'payload/shared' import type { LoginFieldProps } from '../LoginField/index.js' diff --git a/packages/next/src/views/Logout/LogoutClient.tsx b/packages/next/src/views/Logout/LogoutClient.tsx index 88fba7f02..066d32c37 100644 --- a/packages/next/src/views/Logout/LogoutClient.tsx +++ b/packages/next/src/views/Logout/LogoutClient.tsx @@ -7,8 +7,8 @@ import { useRouteTransition, useTranslation, } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' import { useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { useEffect } from 'react' import './index.scss' diff --git a/packages/next/src/views/NotFound/index.tsx b/packages/next/src/views/NotFound/index.tsx index 45f24d76c..49e0e3b8d 100644 --- a/packages/next/src/views/NotFound/index.tsx +++ b/packages/next/src/views/NotFound/index.tsx @@ -2,7 +2,7 @@ import type { I18n } from '@payloadcms/translations' import type { Metadata } from 'next' import type { AdminViewServerProps, ImportMap, SanitizedConfig } from 'payload' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' import { DefaultTemplate } from '../../templates/Default/index.js' diff --git a/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx b/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx index 8e1aa600f..1bb70bffc 100644 --- a/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx +++ b/packages/next/src/views/ResetPassword/ResetPasswordForm/index.tsx @@ -9,9 +9,9 @@ import { useConfig, useTranslation, } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' import { useRouter } from 'next/navigation.js' import { type FormState } from 'payload' +import { formatAdminURL } from 'payload/shared' import React from 'react' type Args = { diff --git a/packages/next/src/views/ResetPassword/index.tsx b/packages/next/src/views/ResetPassword/index.tsx index 46a60f3f9..138e92473 100644 --- a/packages/next/src/views/ResetPassword/index.tsx +++ b/packages/next/src/views/ResetPassword/index.tsx @@ -1,7 +1,8 @@ import type { AdminViewServerProps } from 'payload' 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 { FormHeader } from '../../elements/FormHeader/index.js' diff --git a/packages/next/src/views/Root/getViewFromConfig.ts b/packages/next/src/views/Root/getViewFromConfig.ts index 3ae02d6c8..491179188 100644 --- a/packages/next/src/views/Root/getViewFromConfig.ts +++ b/packages/next/src/views/Root/getViewFromConfig.ts @@ -9,7 +9,7 @@ import type { } from 'payload' import type React from 'react' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import type { initPage } from '../../utilities/initPage/index.js' diff --git a/packages/next/src/views/Root/index.tsx b/packages/next/src/views/Root/index.tsx index 9877ed539..625510b25 100644 --- a/packages/next/src/views/Root/index.tsx +++ b/packages/next/src/views/Root/index.tsx @@ -8,9 +8,9 @@ import type { } from 'payload' import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent' -import { formatAdminURL } from '@payloadcms/ui/shared' import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig' import { notFound, redirect } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { Fragment } from 'react' import { DefaultTemplate } from '../../templates/Default/index.js' @@ -56,7 +56,7 @@ export const RootPage = async ({ const currentRoute = formatAdminURL({ 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 : [] diff --git a/packages/next/src/views/Unauthorized/index.tsx b/packages/next/src/views/Unauthorized/index.tsx index cb297fc8b..eee1e8e63 100644 --- a/packages/next/src/views/Unauthorized/index.tsx +++ b/packages/next/src/views/Unauthorized/index.tsx @@ -1,7 +1,7 @@ import type { AdminViewServerProps } from 'payload' import { Button } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' import { FormHeader } from '../../elements/FormHeader/index.js' diff --git a/packages/next/src/views/Verify/index.tsx b/packages/next/src/views/Verify/index.tsx index 5fcc3d7b5..18601ec58 100644 --- a/packages/next/src/views/Verify/index.tsx +++ b/packages/next/src/views/Verify/index.tsx @@ -1,6 +1,6 @@ import type { AdminViewServerProps } from 'payload' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' import { Logo } from '../../elements/Logo/index.js' diff --git a/packages/next/src/views/Version/Default/SetStepNav.tsx b/packages/next/src/views/Version/Default/SetStepNav.tsx index 8af753fe9..53717e103 100644 --- a/packages/next/src/views/Version/Default/SetStepNav.tsx +++ b/packages/next/src/views/Version/Default/SetStepNav.tsx @@ -5,8 +5,8 @@ import type React from 'react' import { getTranslation } from '@payloadcms/translations' import { useConfig, useLocale, useStepNav, useTranslation } from '@payloadcms/ui' -import { formatAdminURL, formatDate } from '@payloadcms/ui/shared' -import { fieldAffectsData } from 'payload/shared' +import { formatDate } from '@payloadcms/ui/shared' +import { fieldAffectsData, formatAdminURL } from 'payload/shared' import { useEffect } from 'react' export const SetStepNav: React.FC<{ diff --git a/packages/next/src/views/Version/Restore/index.tsx b/packages/next/src/views/Version/Restore/index.tsx index bd0c0fa47..d10de9d12 100644 --- a/packages/next/src/views/Version/Restore/index.tsx +++ b/packages/next/src/views/Version/Restore/index.tsx @@ -11,8 +11,9 @@ import { useRouteTransition, useTranslation, } from '@payloadcms/ui' -import { formatAdminURL, requests } from '@payloadcms/ui/shared' +import { requests } from '@payloadcms/ui/shared' import { useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { Fragment, useCallback, useState } from 'react' import type { Props } from './types.js' diff --git a/packages/next/src/views/Versions/cells/CreatedAt/index.tsx b/packages/next/src/views/Versions/cells/CreatedAt/index.tsx index 9bee7ca9c..ce245fb2b 100644 --- a/packages/next/src/views/Versions/cells/CreatedAt/index.tsx +++ b/packages/next/src/views/Versions/cells/CreatedAt/index.tsx @@ -1,6 +1,7 @@ 'use client' 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' type CreatedAtCellProps = { diff --git a/packages/payload/src/auth/operations/forgotPassword.ts b/packages/payload/src/auth/operations/forgotPassword.ts index f0ce36af7..a51f947b3 100644 --- a/packages/payload/src/auth/operations/forgotPassword.ts +++ b/packages/payload/src/auth/operations/forgotPassword.ts @@ -14,6 +14,7 @@ import { buildAfterOperation } from '../../collections/operations/utils.js' import { APIError } from '../../errors/index.js' import { Forbidden } from '../../index.js' import { commitTransaction } from '../../utilities/commitTransaction.js' +import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { getLoginOptions } from '../getLoginOptions.js' @@ -155,9 +156,13 @@ export const forgotPasswordOperation = async ( config.serverURL !== null && config.serverURL !== '' ? config.serverURL : `${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')} - ${serverURL}${config.routes.admin}${config.admin.routes.reset}/${token} + ${forgotURL} ${req.t('authentication:youDidNotRequestPassword')}` if (typeof collectionConfig.auth.forgotPassword?.generateEmailHTML === 'function') { diff --git a/packages/payload/src/config/types.ts b/packages/payload/src/config/types.ts index 41aa07e0a..1d82a4366 100644 --- a/packages/payload/src/config/types.ts +++ b/packages/payload/src/config/types.ts @@ -881,22 +881,46 @@ export type Config = { /** Base meta data to use for the Admin Panel. Included properties are titleSuffix, ogImage, and favicon. */ meta?: MetaConfig routes?: { - /** The route for the account page. */ - account?: string - /** The route for the create first user page. */ - createFirstUser?: string - /** The route for the forgot password page. */ - forgot?: string - /** The route the user will be redirected to after being inactive for too long. */ - inactivity?: string - /** The route for the login page. */ - login?: string - /** The route for the logout page. */ - logout?: string - /** The route for the reset password page. */ - reset?: string - /** The route for the unauthorized page. */ - unauthorized?: string + /** The route for the account page. + * + * @default '/account' + */ + account?: `/${string}` + /** The route for the create first user page. + * + * @default '/create-first-user' + */ + createFirstUser?: `/${string}` + /** The route for the forgot password page. + * + * @default '/forgot' + */ + forgot?: `/${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 tag. diff --git a/packages/payload/src/exports/shared.ts b/packages/payload/src/exports/shared.ts index f55aa13a6..520f369ba 100644 --- a/packages/payload/src/exports/shared.ts +++ b/packages/payload/src/exports/shared.ts @@ -64,10 +64,12 @@ export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js' export { flattenAllFields } from '../utilities/flattenAllFields.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 { getSiblingData } from '../utilities/getSiblingData.js' + export { getUniqueListBy } from '../utilities/getUniqueListBy.js' export { isNextBuild } from '../utilities/isNextBuild.js' diff --git a/packages/payload/src/utilities/formatAdminURL.ts b/packages/payload/src/utilities/formatAdminURL.ts new file mode 100644 index 000000000..2ba612838 --- /dev/null +++ b/packages/payload/src/utilities/formatAdminURL.ts @@ -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['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}` +} diff --git a/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx b/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx index b3960fd24..1f31a074a 100644 --- a/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx +++ b/packages/plugin-search/src/Search/ui/LinkToDoc/index.client.tsx @@ -1,7 +1,7 @@ 'use client' import { CopyToClipboard, Link, useConfig, useField } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' export const LinkToDocClient: React.FC = () => { diff --git a/packages/richtext-lexical/src/cell/rscEntry.tsx b/packages/richtext-lexical/src/cell/rscEntry.tsx index 7ecec35be..c8f1649b1 100644 --- a/packages/richtext-lexical/src/cell/rscEntry.tsx +++ b/packages/richtext-lexical/src/cell/rscEntry.tsx @@ -3,7 +3,7 @@ import type { Payload } from 'payload' import { getTranslation, type I18nClient } from '@payloadcms/translations' import { Link } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' import type { SanitizedServerEditorConfig } from '../lexical/config/types.js' diff --git a/packages/richtext-slate/src/cell/rscEntry.tsx b/packages/richtext-slate/src/cell/rscEntry.tsx index 3bc44aff7..563b7a80d 100644 --- a/packages/richtext-slate/src/cell/rscEntry.tsx +++ b/packages/richtext-slate/src/cell/rscEntry.tsx @@ -2,7 +2,7 @@ import type { DefaultServerCellComponentProps, Payload } from 'payload' import { getTranslation, type I18nClient } from '@payloadcms/translations' import { Link } from '@payloadcms/ui' -import { formatAdminURL } from '@payloadcms/ui/shared' +import { formatAdminURL } from 'payload/shared' import React from 'react' export const RscEntrySlateCell: React.FC< diff --git a/packages/ui/src/elements/AppHeader/index.tsx b/packages/ui/src/elements/AppHeader/index.tsx index da539789b..e68c2768c 100644 --- a/packages/ui/src/elements/AppHeader/index.tsx +++ b/packages/ui/src/elements/AppHeader/index.tsx @@ -1,11 +1,11 @@ 'use client' +import { formatAdminURL } from 'payload/shared' import React, { useEffect, useRef, useState } from 'react' 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 { Link } from '../Link/index.js' import { Localizer } from '../Localizer/index.js' diff --git a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx index 3cc517d09..7c186d498 100644 --- a/packages/ui/src/elements/BulkUpload/EditForm/index.tsx +++ b/packages/ui/src/elements/BulkUpload/EditForm/index.tsx @@ -1,6 +1,7 @@ 'use client' import { useRouter, useSearchParams } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { useCallback, useEffect } from 'react' 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 { useUploadEdits } from '../../../providers/UploadEdits/index.js' import { abortAndIgnore, handleAbortRef } from '../../../utilities/abortAndIgnore.js' -import { formatAdminURL } from '../../../utilities/formatAdminURL.js' import { useDocumentDrawerContext } from '../../DocumentDrawer/Provider.js' import { DocumentFields } from '../../DocumentFields/index.js' import { Upload } from '../../Upload/index.js' diff --git a/packages/ui/src/elements/DeleteDocument/index.tsx b/packages/ui/src/elements/DeleteDocument/index.tsx index cb934a5bd..59e5d24fd 100644 --- a/packages/ui/src/elements/DeleteDocument/index.tsx +++ b/packages/ui/src/elements/DeleteDocument/index.tsx @@ -4,6 +4,7 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { useCallback } from 'react' import { toast } from 'sonner' @@ -15,7 +16,6 @@ import { useDocumentInfo } from '../../providers/DocumentInfo/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' -import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { ConfirmationModal } from '../ConfirmationModal/index.js' import { PopupList } from '../Popup/index.js' import { Translation } from '../Translation/index.js' diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index be8d3495d..58712616f 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -7,6 +7,7 @@ import type { } from 'payload' import { getTranslation } from '@payloadcms/translations' +import { formatAdminURL } from 'payload/shared' import React, { Fragment, useEffect } from 'react' 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 { useEditDepth } from '../../providers/EditDepth/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 { Button } from '../Button/index.js' diff --git a/packages/ui/src/elements/DuplicateDocument/index.tsx b/packages/ui/src/elements/DuplicateDocument/index.tsx index bac226678..65e9710c3 100644 --- a/packages/ui/src/elements/DuplicateDocument/index.tsx +++ b/packages/ui/src/elements/DuplicateDocument/index.tsx @@ -5,6 +5,7 @@ import type { SanitizedCollectionConfig } from 'payload' import { useModal } from '@faceless-ui/modal' import { getTranslation } from '@payloadcms/translations' import { useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { useCallback } from 'react' import { toast } from 'sonner' @@ -16,7 +17,6 @@ import { useLocale } from '../../providers/Locale/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' -import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { ConfirmationModal } from '../ConfirmationModal/index.js' import { PopupList } from '../Popup/index.js' diff --git a/packages/ui/src/elements/Logout/index.tsx b/packages/ui/src/elements/Logout/index.tsx index 2da58c732..182fd7a1c 100644 --- a/packages/ui/src/elements/Logout/index.tsx +++ b/packages/ui/src/elements/Logout/index.tsx @@ -1,10 +1,10 @@ 'use client' +import { formatAdminURL } from 'payload/shared' import React from 'react' import { LogOutIcon } from '../../icons/LogOut/index.js' import { useConfig } from '../../providers/Config/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { Link } from '../Link/index.js' const baseClass = 'nav' diff --git a/packages/ui/src/elements/StayLoggedIn/index.tsx b/packages/ui/src/elements/StayLoggedIn/index.tsx index 10655240e..bd4b8c1b1 100644 --- a/packages/ui/src/elements/StayLoggedIn/index.tsx +++ b/packages/ui/src/elements/StayLoggedIn/index.tsx @@ -1,5 +1,6 @@ 'use client' import { useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { useCallback } from 'react' 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 { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' -import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { ConfirmationModal } from '../ConfirmationModal/index.js' export const stayLoggedInModalSlug = 'stay-logged-in' diff --git a/packages/ui/src/elements/Table/DefaultCell/index.tsx b/packages/ui/src/elements/Table/DefaultCell/index.tsx index e9ba11ae0..b1969f4b5 100644 --- a/packages/ui/src/elements/Table/DefaultCell/index.tsx +++ b/packages/ui/src/elements/Table/DefaultCell/index.tsx @@ -1,13 +1,12 @@ 'use client' -import type { ClientCollectionConfig, DefaultCellComponentProps, UploadFieldClient } from 'payload' +import type { DefaultCellComponentProps, UploadFieldClient } from 'payload' 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 { useConfig } from '../../../providers/Config/index.js' import { useTranslation } from '../../../providers/Translation/index.js' -import { formatAdminURL } from '../../../utilities/formatAdminURL.js' import { Link } from '../../Link/index.js' import { CodeCell } from './fields/Code/index.js' import { cellComponents } from './fields/index.js' diff --git a/packages/ui/src/graphics/Account/index.tsx b/packages/ui/src/graphics/Account/index.tsx index d64c51e29..8332cf0b8 100644 --- a/packages/ui/src/graphics/Account/index.tsx +++ b/packages/ui/src/graphics/Account/index.tsx @@ -1,11 +1,11 @@ 'use client' import { usePathname } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React from 'react' // import { RenderComponent } from '../../elements/RenderComponent/client.js' 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' diff --git a/packages/ui/src/providers/Auth/index.tsx b/packages/ui/src/providers/Auth/index.tsx index afd62b1e7..ba35bc388 100644 --- a/packages/ui/src/providers/Auth/index.tsx +++ b/packages/ui/src/providers/Auth/index.tsx @@ -3,6 +3,7 @@ import type { ClientUser, SanitizedPermissions, User } from 'payload' import { useModal } from '@faceless-ui/modal' import { usePathname, useRouter } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import * as qs from 'qs-esm' import React, { createContext, useCallback, useContext, useEffect, useState } from 'react' import { toast } from 'sonner' @@ -11,7 +12,6 @@ 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' import { useRouteTransition } from '../RouteTransition/index.js' diff --git a/packages/ui/src/utilities/formatAdminURL.ts b/packages/ui/src/utilities/formatAdminURL.ts index 20a053a4b..e2df8abf0 100644 --- a/packages/ui/src/utilities/formatAdminURL.ts +++ b/packages/ui/src/utilities/formatAdminURL.ts @@ -1,23 +1,2 @@ -import type { Config } from 'payload' - /** Will read the `routes.admin` config and appropriately handle `"/"` admin paths */ -export const formatAdminURL = (args: { - 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}` -} +export { formatAdminURL } from 'payload/shared' diff --git a/packages/ui/src/utilities/handleBackToDashboard.tsx b/packages/ui/src/utilities/handleBackToDashboard.tsx index fbc40f128..ca8ecf28e 100644 --- a/packages/ui/src/utilities/handleBackToDashboard.tsx +++ b/packages/ui/src/utilities/handleBackToDashboard.tsx @@ -1,6 +1,6 @@ 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 = { adminRoute: string diff --git a/packages/ui/src/utilities/handleGoBack.tsx b/packages/ui/src/utilities/handleGoBack.tsx index da51b745a..ab86552f2 100644 --- a/packages/ui/src/utilities/handleGoBack.tsx +++ b/packages/ui/src/utilities/handleGoBack.tsx @@ -1,6 +1,6 @@ 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 = { adminRoute: string diff --git a/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx b/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx index 7cffb4b17..918563bf0 100644 --- a/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx +++ b/packages/ui/src/views/Edit/SetDocumentStepNav/index.tsx @@ -2,6 +2,7 @@ import type { SanitizedCollectionConfig, SanitizedGlobalConfig } from 'payload' import { getTranslation } from '@payloadcms/translations' +import { formatAdminURL } from 'payload/shared' import { useEffect } from 'react' 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 { useEntityVisibility } from '../../../providers/EntityVisibility/index.js' import { useTranslation } from '../../../providers/Translation/index.js' -import { formatAdminURL } from '../../../utilities/formatAdminURL.js' export const SetDocumentStepNav: React.FC<{ collectionSlug?: SanitizedCollectionConfig['slug'] diff --git a/packages/ui/src/views/Edit/index.tsx b/packages/ui/src/views/Edit/index.tsx index 0925d6ba7..4c5ce2238 100644 --- a/packages/ui/src/views/Edit/index.tsx +++ b/packages/ui/src/views/Edit/index.tsx @@ -3,6 +3,7 @@ import type { ClientUser, DocumentViewClientProps, FormState } from 'payload' import { useRouter, useSearchParams } from 'next/navigation.js' +import { formatAdminURL } from 'payload/shared' import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' 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 { useUploadEdits } from '../../providers/UploadEdits/index.js' import { abortAndIgnore, handleAbortRef } from '../../utilities/abortAndIgnore.js' -import { formatAdminURL } from '../../utilities/formatAdminURL.js' import { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js' import { handleGoBack } from '../../utilities/handleGoBack.js' import { handleTakeOver } from '../../utilities/handleTakeOver.js' diff --git a/test/helpers.ts b/test/helpers.ts index 4510cc382..2c10b86c8 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -17,15 +17,17 @@ import { setTimeout } from 'timers/promises' import { devUser } from './credentials.js' import { POLL_TOPASS_TIMEOUT } from './playwright.config.js' +type AdminRoutes = NonNullable['routes'] + type FirstRegisterArgs = { - customAdminRoutes?: Config['admin']['routes'] + customAdminRoutes?: AdminRoutes customRoutes?: Config['routes'] page: Page serverURL: string } type LoginArgs = { - customAdminRoutes?: Config['admin']['routes'] + customAdminRoutes?: AdminRoutes customRoutes?: Config['routes'] data?: { email: string @@ -78,16 +80,14 @@ export async function ensureCompilationIsDone({ noAutoLogin, readyURL, }: { - customAdminRoutes?: Config['admin']['routes'] + customAdminRoutes?: AdminRoutes customRoutes?: Config['routes'] noAutoLogin?: boolean page: Page readyURL?: string serverURL: string }): Promise { - const { - routes: { admin: adminRoute }, - } = getRoutes({ customAdminRoutes, customRoutes }) + const { routes: { admin: adminRoute } = {} } = getRoutes({ customAdminRoutes, customRoutes }) const adminURL = `${serverURL}${adminRoute}` @@ -170,9 +170,7 @@ export async function throttleTest({ export async function firstRegister(args: FirstRegisterArgs): Promise { const { customAdminRoutes, customRoutes, page, serverURL } = args - const { - routes: { admin: adminRoute }, - } = getRoutes({ customAdminRoutes, customRoutes }) + const { routes: { admin: adminRoute } = {} } = getRoutes({ customAdminRoutes, customRoutes }) await page.goto(`${serverURL}${adminRoute}`) await page.fill('#field-email', devUser.email) @@ -187,10 +185,8 @@ export async function login(args: LoginArgs): Promise { const { customAdminRoutes, customRoutes, data = devUser, page, serverURL } = args const { - admin: { - routes: { createFirstUser, login: incomingLoginRoute }, - }, - routes: { admin: incomingAdminRoute }, + admin: { routes: { createFirstUser, login: incomingLoginRoute } = {} }, + routes: { admin: incomingAdminRoute } = {}, } = getRoutes({ customAdminRoutes, customRoutes }) const adminRoute = formatAdminURL({ serverURL, adminRoute: incomingAdminRoute, path: '' }) @@ -462,8 +458,6 @@ export function describeIfInCIOrHasLocalstack(): jest.Describe { return describe } -type AdminRoutes = Config['admin']['routes'] - export function getRoutes({ customAdminRoutes, customRoutes, @@ -477,7 +471,7 @@ export function getRoutes({ routes: Config['routes'] } { let routes = defaults.routes - let adminRoutes = defaults.admin.routes + let adminRoutes = defaults.admin?.routes if (customAdminRoutes) { adminRoutes = { diff --git a/test/helpers/adminUrlUtil.ts b/test/helpers/adminUrlUtil.ts index 46bcdef11..326097d0f 100644 --- a/test/helpers/adminUrlUtil.ts +++ b/test/helpers/adminUrlUtil.ts @@ -71,7 +71,7 @@ export class AdminUrlUtil { collection(slug: string): string { return formatAdminURL({ - adminRoute: this.routes.admin, + adminRoute: this.routes?.admin, path: `/collections/${slug}`, serverURL: this.serverURL, }) @@ -83,7 +83,7 @@ export class AdminUrlUtil { global(slug: string): string { return formatAdminURL({ - adminRoute: this.routes.admin, + adminRoute: this.routes?.admin, path: `/globals/${slug}`, serverURL: this.serverURL, })