fix(ui): disables form during locale change (#8705)

Editing fields during a locale change on slow networks can lead to
changes being reset when the new form state is returned. This is because
the fields receive new values from context when the new locale loads in,
which may have occurred _after_ changes were made to the fields. The fix
is to subscribe to a new `localeIsLoading` context which is set
immediately after changing locales, and then reset once the new locale
loads in. This also removes the misleading `@deprecated` flag from the
`useLocale` hook itself.
This commit is contained in:
Jacob Fletcher
2025-01-10 14:03:36 -05:00
committed by GitHub
parent 4fc6956617
commit f4596fc82b
13 changed files with 362 additions and 180 deletions

View File

@@ -54,7 +54,7 @@ export const RootLayout = async ({
const payload = await getPayload({ config, importMap })
const { i18n, permissions, user } = await initReq(config)
const { i18n, permissions, req, user } = await initReq(config)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
@@ -92,9 +92,10 @@ export const RootLayout = async ({
importMap,
})
req.user = user
const locale = await getRequestLocale({
payload,
user,
req,
})
return (

View File

@@ -2,7 +2,7 @@ import type { Payload, User } from 'payload'
import { cache } from 'react'
export const getPreference = cache(
export const getPreferences = cache(
async <T>(key: string, payload: Payload, user: User): Promise<T> => {
let result: T = null

View File

@@ -1,29 +1,30 @@
import type { Locale, Payload, User } from 'payload'
import type { Locale, PayloadRequest } from 'payload'
import { upsertPreferences } from '@payloadcms/ui/rsc'
import { findLocaleFromCode } from '@payloadcms/ui/shared'
import { getPreference } from './getPreference.js'
import { getPreferences } from './getPreferences.js'
type GetRequestLocalesArgs = {
localeFromParams?: string
payload: Payload
user: User
req: PayloadRequest
}
export async function getRequestLocale({
localeFromParams,
payload,
user,
}: GetRequestLocalesArgs): Promise<Locale> {
if (payload.config.localization) {
export async function getRequestLocale({ req }: GetRequestLocalesArgs): Promise<Locale> {
if (req.payload.config.localization) {
const localeFromParams = req.query.locale as string | undefined
if (localeFromParams) {
await upsertPreferences<Locale['code']>({ key: 'locale', req, value: localeFromParams })
}
return (
findLocaleFromCode(
payload.config.localization,
localeFromParams || (await getPreference<Locale['code']>('locale', payload, user)),
req.payload.config.localization,
localeFromParams || (await getPreferences<Locale['code']>('locale', req.payload, req.user)),
) ||
findLocaleFromCode(
payload.config.localization,
payload.config.localization.defaultLocale || 'en',
req.payload.config.localization,
req.payload.config.localization.defaultLocale || 'en',
)
)
}

View File

@@ -74,9 +74,7 @@ export const initPage = async ({
req.user = user
const locale = await getRequestLocale({
localeFromParams: req.query.locale as string,
payload,
user,
req,
})
req.locale = locale?.code

View File

@@ -2,10 +2,10 @@
import { getTranslation } from '@payloadcms/translations'
import { useSearchParams } from 'next/navigation.js'
import * as qs from 'qs-esm'
import React from 'react'
import React, { Fragment } from 'react'
import { useConfig } from '../../providers/Config/index.js'
import { useLocale } from '../../providers/Locale/index.js'
import { useLocale, useLocaleLoading } from '../../providers/Locale/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
import { Popup, PopupList } from '../Popup/index.js'
@@ -21,6 +21,7 @@ export const Localizer: React.FC<{
const { config } = useConfig()
const { localization } = config
const searchParams = useSearchParams()
const { setLocaleIsLoading } = useLocaleLoading()
const { i18n } = useTranslation()
const locale = useLocale()
@@ -41,6 +42,7 @@ export const Localizer: React.FC<{
return (
<PopupList.Button
active={locale.code === localeOption.code}
disabled={locale.code === localeOption.code}
href={qs.stringify(
{
...parseSearchParams(searchParams),
@@ -49,10 +51,22 @@ export const Localizer: React.FC<{
{ addQueryPrefix: true },
)}
key={localeOption.code}
onClick={close}
onClick={() => {
setLocaleIsLoading(true)
close()
}}
>
{localeOptionLabel}
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
{localeOptionLabel !== localeOption.code ? (
<Fragment>
{localeOptionLabel}
&nbsp;
<span className={`${baseClass}__locale-code`}>
{`(${localeOption.code})`}
</span>
</Fragment>
) : (
<span className={`${baseClass}__locale-code`}>{localeOptionLabel}</span>
)}
</PopupList.Button>
)
})}

View File

@@ -1,9 +1,8 @@
'use client'
// TODO: abstract the `next/link` dependency out from this component
import type { LinkProps } from 'next/link.js'
import LinkImport from 'next/link.js'
import * as React from 'react' // TODO: abstract this out to support all routers
import * as React from 'react'
import './index.scss'
@@ -32,6 +31,7 @@ type MenuButtonProps = {
active?: boolean
children: React.ReactNode
className?: string
disabled?: boolean
href?: LinkProps['href']
id?: string
onClick?: (e?: React.MouseEvent) => void
@@ -42,6 +42,7 @@ export const Button: React.FC<MenuButtonProps> = ({
active,
children,
className,
disabled,
href,
onClick,
}) => {
@@ -49,39 +50,41 @@ export const Button: React.FC<MenuButtonProps> = ({
.filter(Boolean)
.join(' ')
if (href) {
return (
<Link
className={classes}
href={href}
id={id}
onClick={(e) => {
if (onClick) {
onClick(e)
}
}}
prefetch={false}
>
{children}
</Link>
)
}
if (!disabled) {
if (href) {
return (
<Link
className={classes}
href={href}
id={id}
onClick={(e) => {
if (onClick) {
onClick(e)
}
}}
prefetch={false}
>
{children}
</Link>
)
}
if (onClick) {
return (
<button
className={classes}
id={id}
onClick={(e) => {
if (onClick) {
onClick(e)
}
}}
type="button"
>
{children}
</button>
)
if (onClick) {
return (
<button
className={classes}
id={id}
onClick={(e) => {
if (onClick) {
onClick(e)
}
}}
type="button"
>
{children}
</button>
)
}
}
return (

View File

@@ -8,7 +8,15 @@ import type {
} from 'payload'
import * as qs from 'qs-esm'
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import type { DocumentInfoContext, DocumentInfoProps } from './types.js'
@@ -16,7 +24,7 @@ import { useAuth } from '../../providers/Auth/index.js'
import { requests } from '../../utilities/api.js'
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
import { useConfig } from '../Config/index.js'
import { useLocale } from '../Locale/index.js'
import { useLocale, useLocaleLoading } from '../Locale/index.js'
import { usePreferences } from '../Preferences/index.js'
import { useTranslation } from '../Translation/index.js'
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
@@ -113,10 +121,14 @@ const DocumentInfo: React.FC<
setUploadStatus(status)
}, [])
const isInitializing = initialState === undefined || initialData === undefined
const { getPreference, setPreference } = usePreferences()
const { code: locale } = useLocale()
const { localeIsLoading } = useLocaleLoading()
const isInitializing = useMemo(
() => initialState === undefined || initialData === undefined || localeIsLoading,
[initialData, initialState, localeIsLoading],
)
const baseURL = `${serverURL}${api}`
let slug: string

View File

@@ -3,7 +3,7 @@
import type { Locale } from 'payload'
import { useSearchParams } from 'next/navigation.js'
import React, { createContext, useContext, useEffect, useState } from 'react'
import React, { createContext, useContext, useEffect, useRef, useState } from 'react'
import { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
import { useAuth } from '../Auth/index.js'
@@ -12,9 +12,30 @@ import { usePreferences } from '../Preferences/index.js'
const LocaleContext = createContext({} as Locale)
export const LocaleLoadingContext = createContext({
localeIsLoading: false,
setLocaleIsLoading: (_: boolean) => undefined,
})
const fetchPreferences = async <T extends Record<string, unknown> | string>(
key: string,
): Promise<{ id: string; value: T }> =>
await fetch(`/api/payload-preferences/${key}`, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'GET',
})?.then((res) => res.json() as Promise<{ id: string; value: T }>)
export const LocaleProvider: React.FC<{ children?: React.ReactNode; locale?: Locale['code'] }> = ({
children,
locale: localeFromProps,
/**
The `locale` prop originates from the root layout, which does not have access to search params
This component uses the `useSearchParams` hook to get the locale from the URL as precedence over this prop
This prop does not update as the user navigates the site, because the root layout does not re-render
*/
locale: initialLocaleFromPrefs,
}) => {
const {
config: { localization = false },
@@ -28,61 +49,74 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode; locale?: Loc
const { getPreference, setPreference } = usePreferences()
const localeFromParams = useSearchParams().get('locale')
// `localeFromProps` originates from the root layout, which does not have access to search params
const [localeCode, setLocaleCode] = useState<string>(localeFromProps || localeFromParams)
const locale: Locale = React.useMemo(() => {
const [locale, setLocale] = React.useState<Locale>(() => {
if (!localization) {
// TODO: return null V4
return {} as Locale
}
return (
findLocaleFromCode(localization, localeFromParams || localeCode) ||
findLocaleFromCode(localization, localeFromParams) ||
findLocaleFromCode(localization, initialLocaleFromPrefs) ||
findLocaleFromCode(localization, defaultLocale)
)
}, [localeCode, localeFromParams, localization, defaultLocale])
})
const [isLoading, setLocaleIsLoading] = useState(false)
const prevLocale = useRef<Locale>(locale)
useEffect(() => {
async function setInitialLocale() {
if (localization && user) {
if (typeof localeFromParams !== 'string') {
try {
const localeToSet = await getPreference<string>('locale')
setLocaleCode(localeToSet)
} catch (_) {
setLocaleCode(defaultLocale)
}
} else {
void setPreference(
'locale',
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
)
}
/**
* We need to set `isLoading` back to false once the locale is detected to be different
* This happens when the user clicks an anchor link which appends the `?locale=` query param
* This triggers a client-side navigation, which re-renders the page with the new locale
* In Next.js, local state is persisted during this type of navigation because components are not unmounted
*/
if (locale.code !== prevLocale.current.code) {
setLocaleIsLoading(false)
}
prevLocale.current = locale
}, [locale])
useEffect(() => {
/**
* This effect should only run when `localeFromParams` changes, i.e. when the user clicks an anchor link
* The root layout, which sends the initial locale from prefs, will not re-render as the user navigates the site
* For this reason, we need to fetch the locale from prefs if the search params clears the `locale` query param
*/
async function resetLocale() {
if (localization && user?.id) {
const localeToUse =
localeFromParams ||
(await fetchPreferences<Locale['code']>('locale')?.then((res) => res.value)) ||
defaultLocale
const newLocale =
findLocaleFromCode(localization, localeToUse) ||
findLocaleFromCode(localization, defaultLocale)
setLocale(newLocale)
}
}
void setInitialLocale()
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])
void resetLocale()
}, [defaultLocale, getPreference, localization, localeFromParams, user?.id])
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>
return (
<LocaleContext.Provider value={locale}>
<LocaleLoadingContext.Provider value={{ localeIsLoading: isLoading, setLocaleIsLoading }}>
{children}
</LocaleLoadingContext.Provider>
</LocaleContext.Provider>
)
}
export const useLocaleLoading = () => useContext(LocaleLoadingContext)
/**
* @deprecated A hook that returns the current locale object.
*
* ---
*
* #### 🚨 V4 Breaking Change
* The `useLocale` return type now reflects `null | Locale` instead of `false | Locale`.
*
* **Old (V3):**
* ```ts
* const { code } = useLocale();
* ```
* **New (V4):**
* ```ts
* const locale = useLocale();
* ```
* TODO: V4
* The return type of the `useLocale` hook will change in v4. It will return `null | Locale` instead of `false | {} | Locale`.
*/
export const useLocale = (): Locale => useContext(LocaleContext)

View File

@@ -142,7 +142,7 @@ export const buildTableState = async (
value: {
columns,
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
sort: query?.sort,
sort: query?.sort as string,
},
})

View File

@@ -1,27 +1,17 @@
import type { PayloadRequest } from 'payload'
import type { DefaultDocumentIDType, PayloadRequest } from 'payload'
import { dequal } from 'dequal/lite'
import { removeUndefined } from './removeUndefined.js'
/**
* Will update the given preferences by key, creating a new record if it doesn't already exist, or merging existing preferences with the new value.
* This is not possible to do with the existing `db.upsert` operation because it stores on the `value` key and does not perform a deep merge beyond the first level.
* I.e. if you have a preferences record with a `value` key, `db.upsert` will overwrite the existing value. In the future if this supported we should use that instead.
* @param req - The PayloadRequest object
* @param key - The key of the preferences to update
* @param value - The new value to merge with the existing preferences
*/
export const upsertPreferences = async <T extends Record<string, any>>({
const getPreferences = async <T>({
key,
req,
value: incomingValue,
}: {
key: string
req: PayloadRequest
value: Record<string, any>
}): Promise<T> => {
const preferencesResult = await req.payload
}): Promise<{ id: DefaultDocumentIDType; value: T }> => {
const result = (await req.payload
.find({
collection: 'payload-preferences',
depth: 0,
@@ -47,11 +37,36 @@ export const upsertPreferences = async <T extends Record<string, any>>({
],
},
})
.then((res) => res.docs[0] ?? { id: null, value: {} })
.then((res) => res.docs?.[0])) as { id: DefaultDocumentIDType; value: T }
let newPrefs = preferencesResult.value
return result
}
if (!preferencesResult.id) {
/**
* Will update the given preferences by key, creating a new record if it doesn't already exist, or merging existing preferences with the new value.
* This is not possible to do with the existing `db.upsert` operation because it stores on the `value` key and does not perform a deep merge beyond the first level.
* I.e. if you have a preferences record with a `value` key, `db.upsert` will overwrite the existing value. In the future if this supported we should use that instead.
* @param req - The PayloadRequest object
* @param key - The key of the preferences to update
* @param value - The new value to merge with the existing preferences
*/
export const upsertPreferences = async <T extends Record<string, unknown> | string>({
key,
req,
value: incomingValue,
}: {
key: string
req: PayloadRequest
value: T
}): Promise<T> => {
const existingPrefs = await getPreferences<T>({
key,
req,
})
let newPrefs = existingPrefs?.value
if (!existingPrefs?.id) {
await req.payload.create({
collection: 'payload-preferences',
data: {
@@ -66,15 +81,19 @@ export const upsertPreferences = async <T extends Record<string, any>>({
req,
})
} else {
const mergedPrefs = {
...(preferencesResult?.value || {}), // Shallow merge existing prefs to acquire any missing keys from incoming value
...removeUndefined(incomingValue || {}),
}
// Strings are valid JSON, i.e. `locale` saved as a string to the locale preferences
const mergedPrefs =
typeof incomingValue === 'object'
? {
...(typeof existingPrefs.value === 'object' ? existingPrefs?.value : {}), // Shallow merge existing prefs to acquire any missing keys from incoming value
...removeUndefined(incomingValue || {}),
}
: incomingValue
if (!dequal(mergedPrefs, preferencesResult.value)) {
if (!dequal(mergedPrefs, existingPrefs.value)) {
newPrefs = await req.payload
.update({
id: preferencesResult.id,
id: existingPrefs.id,
collection: 'payload-preferences',
data: {
key,

View File

@@ -1,4 +1,10 @@
import type { BrowserContext, ChromiumBrowserContext, Locator, Page } from '@playwright/test'
import type {
BrowserContext,
CDPSession,
ChromiumBrowserContext,
Locator,
Page,
} from '@playwright/test'
import type { Config } from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared'
@@ -52,6 +58,11 @@ const networkConditions = {
latency: 1000,
upload: ((10 * 1000 * 1000) / 8) * 0.8,
},
None: {
download: 0,
latency: -1,
upload: -1,
},
}
/**
@@ -134,7 +145,7 @@ export async function throttleTest({
context: BrowserContext
delay: keyof typeof networkConditions
page: Page
}) {
}): Promise<CDPSession> {
const cdpSession = await context.newCDPSession(page)
await cdpSession.send('Network.emulateNetworkConditions', {
@@ -151,6 +162,8 @@ export async function throttleTest({
const client = await (page.context() as ChromiumBrowserContext).newCDPSession(page)
await client.send('Emulation.setCPUThrottlingRate', { rate: 8 }) // 8x slowdown
return client
}
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
@@ -278,18 +291,50 @@ export async function closeNav(page: Page): Promise<void> {
await expect(page.locator('.template-default.template-default--nav-open')).toBeHidden()
}
export async function openLocaleSelector(page: Page): Promise<void> {
const button = page.locator('.localizer button.popup-button')
const popup = page.locator('.localizer .popup.popup--active')
if (!(await popup.isVisible())) {
await button.click()
await expect(popup).toBeVisible()
}
}
export async function closeLocaleSelector(page: Page): Promise<void> {
const popup = page.locator('.localizer .popup.popup--active')
if (await popup.isVisible()) {
await page.click('body', { position: { x: 0, y: 0 } })
await expect(popup).toBeHidden()
}
}
export async function changeLocale(page: Page, newLocale: string) {
await page.locator('.localizer >> button').first().click()
await page
.locator(`.localizer .popup.popup--active .popup-button-list__button`, {
hasText: newLocale,
})
.first()
.click()
await openLocaleSelector(page)
const regexPattern = new RegExp(`locale=${newLocale}`)
const currentlySelectedLocale = await page
.locator(
`.localizer .popup.popup--active .popup-button-list__button--selected .localizer__locale-code`,
)
.textContent()
await expect(page).toHaveURL(regexPattern)
if (currentlySelectedLocale !== `(${newLocale})`) {
const localeToSelect = page
.locator('.localizer .popup.popup--active .popup-button-list__button')
.locator('.localizer__locale-code', {
hasText: `(${newLocale})`,
})
await expect(localeToSelect).toBeEnabled()
await localeToSelect.click()
const regexPattern = new RegExp(`locale=${newLocale}`)
await expect(page).toHaveURL(regexPattern)
}
await closeLocaleSelector(page)
}
export function exactText(text: string) {

View File

@@ -10,9 +10,11 @@ import type { Config, LocalizedPost } from './payload-types.js'
import {
changeLocale,
closeLocaleSelector,
ensureCompilationIsDone,
initPageConsoleErrorCatch,
openDocDrawer,
openLocaleSelector,
saveDocAndAssert,
throttleTest,
} from '../helpers.js'
@@ -30,6 +32,7 @@ import {
withRequiredLocalizedFields,
} from './shared.js'
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
import { RESTClient } from 'helpers/rest.js'
import { GeneratedTypes } from 'helpers/sdk/types.js'
@@ -45,7 +48,7 @@ const dirname = path.dirname(filename)
* Repeat above for Globals
*/
const { beforeAll, beforeEach, describe } = test
const { beforeAll, beforeEach, describe, afterEach } = test
let url: AdminUrlUtil
let urlWithRequiredLocalizedFields: AdminUrlUtil
let urlRelationshipLocalized: AdminUrlUtil
@@ -92,35 +95,40 @@ describe('Localization', () => {
})
describe('localizer', async () => {
test('should not render default locale in locale selector when prefs are not default', async () => {
// change prefs to spanish, then load page and check that the localizer label does not say English
await upsertPrefs<Config, GeneratedTypes<any>>({
payload,
user: client.user,
key: 'locale',
value: 'es',
})
test('should show localizer controls', async () => {
await page.goto(url.create)
await expect(page.locator('.localizer.app-header__localizer')).toBeVisible()
await page.locator('.localizer >> button').first().click()
await expect(page.locator('.localizer .popup.popup--active')).toBeVisible()
})
await page.goto(url.list)
test('should disable control for active locale', async () => {
await page.goto(url.create)
const localeLabel = await page
.locator('.localizer.app-header__localizer .localizer-button__current-label')
.innerText()
await page.locator('.localizer button.popup-button').first().click()
expect(localeLabel).not.toEqual('English')
await expect(page.locator('.localizer .popup')).toHaveClass(/popup--active/)
const activeOption = await page.locator(
`.localizer .popup.popup--active .popup-button-list__button--selected`,
)
await expect(activeOption).toBeVisible()
const tagName = await activeOption.evaluate((node) => node.tagName)
await expect(tagName).not.toBe('A')
await expect(activeOption).not.toHaveAttribute('href')
await expect(tagName).not.toBe('BUTTON')
await expect(tagName).toBe('DIV')
})
})
describe('localized text', () => {
test('create english post, switch to spanish', async () => {
await changeLocale(page, defaultLocale)
await page.goto(url.create)
await fillValues({ description, title })
await saveDocAndAssert(page)
// Change back to English
await changeLocale(page, 'es')
// Localized field should not be populated
@@ -131,12 +139,10 @@ describe('Localization', () => {
.not.toBe(title)
await expect(page.locator('#field-description')).toHaveValue(description)
await fillValues({ description, title: spanishTitle })
await saveDocAndAssert(page)
await changeLocale(page, defaultLocale)
// Expect english title
await changeLocale(page, defaultLocale)
await expect(page.locator('#field-title')).toHaveValue(title)
await expect(page.locator('#field-description')).toHaveValue(description)
})
@@ -227,7 +233,6 @@ describe('Localization', () => {
await page.waitForURL(url.create)
await changeLocale(page, defaultLocale)
await fillValues({ description, title: englishTitle })
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
await page.locator('#field-localizedCheckbox').click()
await page.locator('#action-save').click()
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
@@ -260,34 +265,51 @@ describe('Localization', () => {
const originalID = await page.locator('.id-label').innerText()
await openDocControls(page)
await page.locator('#action-duplicate').click()
await expect(page.locator('.id-label')).not.toContainText(originalID)
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
await expect(page.locator('.payload-toast-container')).toContainText(
'successfully duplicated',
)
await expect(page.locator('.id-label')).not.toContainText(originalID)
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
await expect(page.locator('.id-label')).not.toContainText(originalID)
})
})
describe('locale preference', () => {
test('ensure preference is used when query param is not', async () => {
test('should set preference on locale change and use preference no locale param is present', async () => {
await page.goto(url.create)
await changeLocale(page, spanishLocale)
await expect(page.locator('#field-title')).toBeEmpty()
await fillValues({ title: spanishTitle })
await saveDocAndAssert(page)
await page.goto(url.admin)
await page.goto(url.list)
await expect(page.locator('.row-1 .cell-title')).toContainText(spanishTitle)
})
test('should not render default locale in locale selector when prefs are not default', async () => {
await upsertPrefs<Config, GeneratedTypes<any>>({
payload,
user: client.user,
key: 'locale',
value: 'es',
})
await page.goto(url.list)
const localeLabel = await page
.locator('.localizer.app-header__localizer .localizer-button__current-label')
.innerText()
expect(localeLabel).not.toEqual('English')
})
})
describe('localized relationships', () => {
test('ensure relationship field fetches are localized as well', async () => {
await changeLocale(page, spanishLocale)
await navigateToDoc(page, url)
const selectField = page.locator('#field-children .rs__control')
await selectField.click()
await page.locator('#field-children .rs__control').click()
await expect(page.locator('#field-children .rs__menu')).toContainText('spanish-relation2')
})
@@ -318,6 +340,7 @@ describe('Localization', () => {
await setToLocale(page, 'Spanish')
await runCopy(page)
await expect(page.locator('#field-title')).toHaveValue(title)
await changeLocale(page, defaultLocale)
})
test('should copy rich text data to correct locale', async () => {
@@ -359,7 +382,6 @@ describe('Localization', () => {
await changeLocale(page, spanishLocale)
await createAndSaveDoc(page, url, { title })
await openCopyToLocaleDrawer(page)
const fromLocaleField = page.locator('#field-fromLocale')
await expect(fromLocaleField).toContainText('Spanish')
await page.locator('.drawer-close-button').click()
@@ -391,14 +413,13 @@ describe('Localization', () => {
await fillValues({ title: spanishTitle })
await saveDocAndAssert(page)
await changeLocale(page, defaultLocale)
await openCopyToLocaleDrawer(page)
await setToLocale(page, 'Spanish')
const overwriteCheckbox = page.locator('#field-overwriteExisting')
await overwriteCheckbox.click()
await runCopy(page)
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
await changeLocale(page, defaultLocale)
})
test('should not include current locale in toLocale options', async () => {
@@ -447,6 +468,46 @@ describe('Localization', () => {
await expect(page.locator('.payload-toast-container')).toContainText('unsaved')
})
})
describe('locale change', () => {
test('should disable fields during locale change', async () => {
await page.goto(url.create)
await changeLocale(page, defaultLocale)
await expect(page.locator('#field-title')).toBeEnabled()
await openLocaleSelector(page)
const localeToSelect = page
.locator('.localizer .popup.popup--active .popup-button-list__button')
.locator('.localizer__locale-code', {
hasText: `${spanishLocale}`,
})
// only throttle test after initial load to avoid timeouts
const cdpSession = await throttleTest({
page: page,
context,
delay: 'Fast 4G',
})
await localeToSelect.click()
await expect(page.locator('#field-title')).toBeDisabled()
const regexPattern = new RegExp(`locale=${spanishLocale}`)
await expect(page).toHaveURL(regexPattern)
await expect(page.locator('#field-title')).toBeEnabled()
await closeLocaleSelector(page)
await cdpSession.send('Network.emulateNetworkConditions', {
offline: false,
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
})
await cdpSession.detach()
})
})
})
async function fillValues(data: Partial<LocalizedPost>) {
@@ -461,9 +522,7 @@ async function fillValues(data: Partial<LocalizedPost>) {
}
async function runCopy(page) {
const copyDrawerClose = page.locator('.copy-locale-data__sub-header button')
await expect(copyDrawerClose).toBeVisible()
await copyDrawerClose.click()
await page.locator('.copy-locale-data__sub-header button').click()
}
async function createAndSaveDoc(page, url, values) {
@@ -473,12 +532,8 @@ async function createAndSaveDoc(page, url, values) {
}
async function openCopyToLocaleDrawer(page) {
const docControls = page.locator('.doc-controls__popup button.popup-button')
expect(docControls).toBeEnabled()
await docControls.click()
const copyButton = page.locator('#copy-locale-data__button')
await expect(copyButton).toBeVisible()
await copyButton.click()
await page.locator('.doc-controls__popup button.popup-button').click()
await page.locator('#copy-locale-data__button').click()
await expect(page.locator('#copy-locale')).toBeVisible()
await expect(page.locator('.copy-locale-data__content')).toBeVisible()
}