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:
@@ -54,7 +54,7 @@ export const RootLayout = async ({
|
|||||||
|
|
||||||
const payload = await getPayload({ config, importMap })
|
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)
|
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
|
||||||
? 'RTL'
|
? 'RTL'
|
||||||
@@ -92,9 +92,10 @@ export const RootLayout = async ({
|
|||||||
importMap,
|
importMap,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
req.user = user
|
||||||
|
|
||||||
const locale = await getRequestLocale({
|
const locale = await getRequestLocale({
|
||||||
payload,
|
req,
|
||||||
user,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Payload, User } from 'payload'
|
|||||||
|
|
||||||
import { cache } from 'react'
|
import { cache } from 'react'
|
||||||
|
|
||||||
export const getPreference = cache(
|
export const getPreferences = cache(
|
||||||
async <T>(key: string, payload: Payload, user: User): Promise<T> => {
|
async <T>(key: string, payload: Payload, user: User): Promise<T> => {
|
||||||
let result: T = null
|
let result: T = null
|
||||||
|
|
||||||
@@ -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 { findLocaleFromCode } from '@payloadcms/ui/shared'
|
||||||
|
|
||||||
import { getPreference } from './getPreference.js'
|
import { getPreferences } from './getPreferences.js'
|
||||||
|
|
||||||
type GetRequestLocalesArgs = {
|
type GetRequestLocalesArgs = {
|
||||||
localeFromParams?: string
|
req: PayloadRequest
|
||||||
payload: Payload
|
|
||||||
user: User
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRequestLocale({
|
export async function getRequestLocale({ req }: GetRequestLocalesArgs): Promise<Locale> {
|
||||||
localeFromParams,
|
if (req.payload.config.localization) {
|
||||||
payload,
|
const localeFromParams = req.query.locale as string | undefined
|
||||||
user,
|
|
||||||
}: GetRequestLocalesArgs): Promise<Locale> {
|
if (localeFromParams) {
|
||||||
if (payload.config.localization) {
|
await upsertPreferences<Locale['code']>({ key: 'locale', req, value: localeFromParams })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
findLocaleFromCode(
|
findLocaleFromCode(
|
||||||
payload.config.localization,
|
req.payload.config.localization,
|
||||||
localeFromParams || (await getPreference<Locale['code']>('locale', payload, user)),
|
localeFromParams || (await getPreferences<Locale['code']>('locale', req.payload, req.user)),
|
||||||
) ||
|
) ||
|
||||||
findLocaleFromCode(
|
findLocaleFromCode(
|
||||||
payload.config.localization,
|
req.payload.config.localization,
|
||||||
payload.config.localization.defaultLocale || 'en',
|
req.payload.config.localization.defaultLocale || 'en',
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,9 +74,7 @@ export const initPage = async ({
|
|||||||
req.user = user
|
req.user = user
|
||||||
|
|
||||||
const locale = await getRequestLocale({
|
const locale = await getRequestLocale({
|
||||||
localeFromParams: req.query.locale as string,
|
req,
|
||||||
payload,
|
|
||||||
user,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
req.locale = locale?.code
|
req.locale = locale?.code
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { getTranslation } from '@payloadcms/translations'
|
import { getTranslation } from '@payloadcms/translations'
|
||||||
import { useSearchParams } from 'next/navigation.js'
|
import { useSearchParams } from 'next/navigation.js'
|
||||||
import * as qs from 'qs-esm'
|
import * as qs from 'qs-esm'
|
||||||
import React from 'react'
|
import React, { Fragment } from 'react'
|
||||||
|
|
||||||
import { useConfig } from '../../providers/Config/index.js'
|
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 { useTranslation } from '../../providers/Translation/index.js'
|
||||||
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
|
import { parseSearchParams } from '../../utilities/parseSearchParams.js'
|
||||||
import { Popup, PopupList } from '../Popup/index.js'
|
import { Popup, PopupList } from '../Popup/index.js'
|
||||||
@@ -21,6 +21,7 @@ export const Localizer: React.FC<{
|
|||||||
const { config } = useConfig()
|
const { config } = useConfig()
|
||||||
const { localization } = config
|
const { localization } = config
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
const { setLocaleIsLoading } = useLocaleLoading()
|
||||||
|
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const locale = useLocale()
|
const locale = useLocale()
|
||||||
@@ -41,6 +42,7 @@ export const Localizer: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<PopupList.Button
|
<PopupList.Button
|
||||||
active={locale.code === localeOption.code}
|
active={locale.code === localeOption.code}
|
||||||
|
disabled={locale.code === localeOption.code}
|
||||||
href={qs.stringify(
|
href={qs.stringify(
|
||||||
{
|
{
|
||||||
...parseSearchParams(searchParams),
|
...parseSearchParams(searchParams),
|
||||||
@@ -49,10 +51,22 @@ export const Localizer: React.FC<{
|
|||||||
{ addQueryPrefix: true },
|
{ addQueryPrefix: true },
|
||||||
)}
|
)}
|
||||||
key={localeOption.code}
|
key={localeOption.code}
|
||||||
onClick={close}
|
onClick={() => {
|
||||||
|
setLocaleIsLoading(true)
|
||||||
|
close()
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{localeOptionLabel}
|
{localeOptionLabel !== localeOption.code ? (
|
||||||
{localeOptionLabel !== localeOption.code && ` (${localeOption.code})`}
|
<Fragment>
|
||||||
|
{localeOptionLabel}
|
||||||
|
|
||||||
|
<span className={`${baseClass}__locale-code`}>
|
||||||
|
{`(${localeOption.code})`}
|
||||||
|
</span>
|
||||||
|
</Fragment>
|
||||||
|
) : (
|
||||||
|
<span className={`${baseClass}__locale-code`}>{localeOptionLabel}</span>
|
||||||
|
)}
|
||||||
</PopupList.Button>
|
</PopupList.Button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
// TODO: abstract the `next/link` dependency out from this component
|
|
||||||
import type { LinkProps } from 'next/link.js'
|
import type { LinkProps } from 'next/link.js'
|
||||||
|
|
||||||
import LinkImport 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'
|
import './index.scss'
|
||||||
|
|
||||||
@@ -32,6 +31,7 @@ type MenuButtonProps = {
|
|||||||
active?: boolean
|
active?: boolean
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
href?: LinkProps['href']
|
href?: LinkProps['href']
|
||||||
id?: string
|
id?: string
|
||||||
onClick?: (e?: React.MouseEvent) => void
|
onClick?: (e?: React.MouseEvent) => void
|
||||||
@@ -42,6 +42,7 @@ export const Button: React.FC<MenuButtonProps> = ({
|
|||||||
active,
|
active,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
disabled,
|
||||||
href,
|
href,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -49,39 +50,41 @@ export const Button: React.FC<MenuButtonProps> = ({
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
|
|
||||||
if (href) {
|
if (!disabled) {
|
||||||
return (
|
if (href) {
|
||||||
<Link
|
return (
|
||||||
className={classes}
|
<Link
|
||||||
href={href}
|
className={classes}
|
||||||
id={id}
|
href={href}
|
||||||
onClick={(e) => {
|
id={id}
|
||||||
if (onClick) {
|
onClick={(e) => {
|
||||||
onClick(e)
|
if (onClick) {
|
||||||
}
|
onClick(e)
|
||||||
}}
|
}
|
||||||
prefetch={false}
|
}}
|
||||||
>
|
prefetch={false}
|
||||||
{children}
|
>
|
||||||
</Link>
|
{children}
|
||||||
)
|
</Link>
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={classes}
|
className={classes}
|
||||||
id={id}
|
id={id}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e)
|
onClick(e)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ import type {
|
|||||||
} from 'payload'
|
} from 'payload'
|
||||||
|
|
||||||
import * as qs from 'qs-esm'
|
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'
|
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 { requests } from '../../utilities/api.js'
|
||||||
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
import { formatDocTitle } from '../../utilities/formatDocTitle.js'
|
||||||
import { useConfig } from '../Config/index.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 { usePreferences } from '../Preferences/index.js'
|
||||||
import { useTranslation } from '../Translation/index.js'
|
import { useTranslation } from '../Translation/index.js'
|
||||||
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
|
import { UploadEditsProvider, useUploadEdits } from '../UploadEdits/index.js'
|
||||||
@@ -113,10 +121,14 @@ const DocumentInfo: React.FC<
|
|||||||
setUploadStatus(status)
|
setUploadStatus(status)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const isInitializing = initialState === undefined || initialData === undefined
|
|
||||||
|
|
||||||
const { getPreference, setPreference } = usePreferences()
|
const { getPreference, setPreference } = usePreferences()
|
||||||
const { code: locale } = useLocale()
|
const { code: locale } = useLocale()
|
||||||
|
const { localeIsLoading } = useLocaleLoading()
|
||||||
|
|
||||||
|
const isInitializing = useMemo(
|
||||||
|
() => initialState === undefined || initialData === undefined || localeIsLoading,
|
||||||
|
[initialData, initialState, localeIsLoading],
|
||||||
|
)
|
||||||
|
|
||||||
const baseURL = `${serverURL}${api}`
|
const baseURL = `${serverURL}${api}`
|
||||||
let slug: string
|
let slug: string
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { Locale } from 'payload'
|
import type { Locale } from 'payload'
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation.js'
|
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 { findLocaleFromCode } from '../../utilities/findLocaleFromCode.js'
|
||||||
import { useAuth } from '../Auth/index.js'
|
import { useAuth } from '../Auth/index.js'
|
||||||
@@ -12,9 +12,30 @@ import { usePreferences } from '../Preferences/index.js'
|
|||||||
|
|
||||||
const LocaleContext = createContext({} as Locale)
|
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'] }> = ({
|
export const LocaleProvider: React.FC<{ children?: React.ReactNode; locale?: Locale['code'] }> = ({
|
||||||
children,
|
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 {
|
const {
|
||||||
config: { localization = false },
|
config: { localization = false },
|
||||||
@@ -28,61 +49,74 @@ export const LocaleProvider: React.FC<{ children?: React.ReactNode; locale?: Loc
|
|||||||
const { getPreference, setPreference } = usePreferences()
|
const { getPreference, setPreference } = usePreferences()
|
||||||
const localeFromParams = useSearchParams().get('locale')
|
const localeFromParams = useSearchParams().get('locale')
|
||||||
|
|
||||||
// `localeFromProps` originates from the root layout, which does not have access to search params
|
const [locale, setLocale] = React.useState<Locale>(() => {
|
||||||
const [localeCode, setLocaleCode] = useState<string>(localeFromProps || localeFromParams)
|
|
||||||
|
|
||||||
const locale: Locale = React.useMemo(() => {
|
|
||||||
if (!localization) {
|
if (!localization) {
|
||||||
// TODO: return null V4
|
// TODO: return null V4
|
||||||
return {} as Locale
|
return {} as Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
findLocaleFromCode(localization, localeFromParams || localeCode) ||
|
findLocaleFromCode(localization, localeFromParams) ||
|
||||||
|
findLocaleFromCode(localization, initialLocaleFromPrefs) ||
|
||||||
findLocaleFromCode(localization, defaultLocale)
|
findLocaleFromCode(localization, defaultLocale)
|
||||||
)
|
)
|
||||||
}, [localeCode, localeFromParams, localization, defaultLocale])
|
})
|
||||||
|
|
||||||
|
const [isLoading, setLocaleIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const prevLocale = useRef<Locale>(locale)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setInitialLocale() {
|
/**
|
||||||
if (localization && user) {
|
* We need to set `isLoading` back to false once the locale is detected to be different
|
||||||
if (typeof localeFromParams !== 'string') {
|
* This happens when the user clicks an anchor link which appends the `?locale=` query param
|
||||||
try {
|
* This triggers a client-side navigation, which re-renders the page with the new locale
|
||||||
const localeToSet = await getPreference<string>('locale')
|
* In Next.js, local state is persisted during this type of navigation because components are not unmounted
|
||||||
setLocaleCode(localeToSet)
|
*/
|
||||||
} catch (_) {
|
if (locale.code !== prevLocale.current.code) {
|
||||||
setLocaleCode(defaultLocale)
|
setLocaleIsLoading(false)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
void setPreference(
|
prevLocale.current = locale
|
||||||
'locale',
|
}, [locale])
|
||||||
findLocaleFromCode(localization, localeFromParams)?.code || defaultLocale,
|
|
||||||
)
|
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()
|
void resetLocale()
|
||||||
}, [defaultLocale, getPreference, localization, localeFromParams, setPreference, user])
|
}, [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.
|
* TODO: V4
|
||||||
*
|
* The return type of the `useLocale` hook will change in v4. It will return `null | Locale` instead of `false | {} | Locale`.
|
||||||
* ---
|
|
||||||
*
|
|
||||||
* #### 🚨 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();
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const useLocale = (): Locale => useContext(LocaleContext)
|
export const useLocale = (): Locale => useContext(LocaleContext)
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const buildTableState = async (
|
|||||||
value: {
|
value: {
|
||||||
columns,
|
columns,
|
||||||
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
|
limit: isNumber(query?.limit) ? Number(query.limit) : undefined,
|
||||||
sort: query?.sort,
|
sort: query?.sort as string,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
0
packages/ui/src/utilities/fetchPreferences.ts
Normal file
0
packages/ui/src/utilities/fetchPreferences.ts
Normal file
@@ -1,27 +1,17 @@
|
|||||||
import type { PayloadRequest } from 'payload'
|
import type { DefaultDocumentIDType, PayloadRequest } from 'payload'
|
||||||
|
|
||||||
import { dequal } from 'dequal/lite'
|
import { dequal } from 'dequal/lite'
|
||||||
|
|
||||||
import { removeUndefined } from './removeUndefined.js'
|
import { removeUndefined } from './removeUndefined.js'
|
||||||
|
|
||||||
/**
|
const getPreferences = async <T>({
|
||||||
* 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>>({
|
|
||||||
key,
|
key,
|
||||||
req,
|
req,
|
||||||
value: incomingValue,
|
|
||||||
}: {
|
}: {
|
||||||
key: string
|
key: string
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
value: Record<string, any>
|
}): Promise<{ id: DefaultDocumentIDType; value: T }> => {
|
||||||
}): Promise<T> => {
|
const result = (await req.payload
|
||||||
const preferencesResult = await req.payload
|
|
||||||
.find({
|
.find({
|
||||||
collection: 'payload-preferences',
|
collection: 'payload-preferences',
|
||||||
depth: 0,
|
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({
|
await req.payload.create({
|
||||||
collection: 'payload-preferences',
|
collection: 'payload-preferences',
|
||||||
data: {
|
data: {
|
||||||
@@ -66,15 +81,19 @@ export const upsertPreferences = async <T extends Record<string, any>>({
|
|||||||
req,
|
req,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const mergedPrefs = {
|
// Strings are valid JSON, i.e. `locale` saved as a string to the locale preferences
|
||||||
...(preferencesResult?.value || {}), // Shallow merge existing prefs to acquire any missing keys from incoming value
|
const mergedPrefs =
|
||||||
...removeUndefined(incomingValue || {}),
|
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
|
newPrefs = await req.payload
|
||||||
.update({
|
.update({
|
||||||
id: preferencesResult.id,
|
id: existingPrefs.id,
|
||||||
collection: 'payload-preferences',
|
collection: 'payload-preferences',
|
||||||
data: {
|
data: {
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -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 type { Config } from 'payload'
|
||||||
|
|
||||||
import { formatAdminURL } from '@payloadcms/ui/shared'
|
import { formatAdminURL } from '@payloadcms/ui/shared'
|
||||||
@@ -52,6 +58,11 @@ const networkConditions = {
|
|||||||
latency: 1000,
|
latency: 1000,
|
||||||
upload: ((10 * 1000 * 1000) / 8) * 0.8,
|
upload: ((10 * 1000 * 1000) / 8) * 0.8,
|
||||||
},
|
},
|
||||||
|
None: {
|
||||||
|
download: 0,
|
||||||
|
latency: -1,
|
||||||
|
upload: -1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +145,7 @@ export async function throttleTest({
|
|||||||
context: BrowserContext
|
context: BrowserContext
|
||||||
delay: keyof typeof networkConditions
|
delay: keyof typeof networkConditions
|
||||||
page: Page
|
page: Page
|
||||||
}) {
|
}): Promise<CDPSession> {
|
||||||
const cdpSession = await context.newCDPSession(page)
|
const cdpSession = await context.newCDPSession(page)
|
||||||
|
|
||||||
await cdpSession.send('Network.emulateNetworkConditions', {
|
await cdpSession.send('Network.emulateNetworkConditions', {
|
||||||
@@ -151,6 +162,8 @@ export async function throttleTest({
|
|||||||
|
|
||||||
const client = await (page.context() as ChromiumBrowserContext).newCDPSession(page)
|
const client = await (page.context() as ChromiumBrowserContext).newCDPSession(page)
|
||||||
await client.send('Emulation.setCPUThrottlingRate', { rate: 8 }) // 8x slowdown
|
await client.send('Emulation.setCPUThrottlingRate', { rate: 8 }) // 8x slowdown
|
||||||
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function firstRegister(args: FirstRegisterArgs): Promise<void> {
|
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()
|
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) {
|
export async function changeLocale(page: Page, newLocale: string) {
|
||||||
await page.locator('.localizer >> button').first().click()
|
await openLocaleSelector(page)
|
||||||
await page
|
|
||||||
.locator(`.localizer .popup.popup--active .popup-button-list__button`, {
|
|
||||||
hasText: newLocale,
|
|
||||||
})
|
|
||||||
.first()
|
|
||||||
.click()
|
|
||||||
|
|
||||||
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) {
|
export function exactText(text: string) {
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import type { Config, LocalizedPost } from './payload-types.js'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
changeLocale,
|
changeLocale,
|
||||||
|
closeLocaleSelector,
|
||||||
ensureCompilationIsDone,
|
ensureCompilationIsDone,
|
||||||
initPageConsoleErrorCatch,
|
initPageConsoleErrorCatch,
|
||||||
openDocDrawer,
|
openDocDrawer,
|
||||||
|
openLocaleSelector,
|
||||||
saveDocAndAssert,
|
saveDocAndAssert,
|
||||||
throttleTest,
|
throttleTest,
|
||||||
} from '../helpers.js'
|
} from '../helpers.js'
|
||||||
@@ -30,6 +32,7 @@ import {
|
|||||||
withRequiredLocalizedFields,
|
withRequiredLocalizedFields,
|
||||||
} from './shared.js'
|
} from './shared.js'
|
||||||
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
import { navigateToDoc } from 'helpers/e2e/navigateToDoc.js'
|
||||||
|
|
||||||
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
import { upsertPrefs } from 'helpers/e2e/upsertPrefs.js'
|
||||||
import { RESTClient } from 'helpers/rest.js'
|
import { RESTClient } from 'helpers/rest.js'
|
||||||
import { GeneratedTypes } from 'helpers/sdk/types.js'
|
import { GeneratedTypes } from 'helpers/sdk/types.js'
|
||||||
@@ -45,7 +48,7 @@ const dirname = path.dirname(filename)
|
|||||||
* Repeat above for Globals
|
* Repeat above for Globals
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { beforeAll, beforeEach, describe } = test
|
const { beforeAll, beforeEach, describe, afterEach } = test
|
||||||
let url: AdminUrlUtil
|
let url: AdminUrlUtil
|
||||||
let urlWithRequiredLocalizedFields: AdminUrlUtil
|
let urlWithRequiredLocalizedFields: AdminUrlUtil
|
||||||
let urlRelationshipLocalized: AdminUrlUtil
|
let urlRelationshipLocalized: AdminUrlUtil
|
||||||
@@ -92,35 +95,40 @@ describe('Localization', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('localizer', async () => {
|
describe('localizer', async () => {
|
||||||
test('should not render default locale in locale selector when prefs are not default', async () => {
|
test('should show localizer controls', async () => {
|
||||||
// change prefs to spanish, then load page and check that the localizer label does not say English
|
await page.goto(url.create)
|
||||||
await upsertPrefs<Config, GeneratedTypes<any>>({
|
await expect(page.locator('.localizer.app-header__localizer')).toBeVisible()
|
||||||
payload,
|
await page.locator('.localizer >> button').first().click()
|
||||||
user: client.user,
|
await expect(page.locator('.localizer .popup.popup--active')).toBeVisible()
|
||||||
key: 'locale',
|
})
|
||||||
value: 'es',
|
|
||||||
})
|
|
||||||
|
|
||||||
await page.goto(url.list)
|
test('should disable control for active locale', async () => {
|
||||||
|
await page.goto(url.create)
|
||||||
|
|
||||||
const localeLabel = await page
|
await page.locator('.localizer button.popup-button').first().click()
|
||||||
.locator('.localizer.app-header__localizer .localizer-button__current-label')
|
|
||||||
.innerText()
|
|
||||||
|
|
||||||
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', () => {
|
describe('localized text', () => {
|
||||||
test('create english post, switch to spanish', async () => {
|
test('create english post, switch to spanish', async () => {
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
|
|
||||||
await page.goto(url.create)
|
await page.goto(url.create)
|
||||||
|
|
||||||
await fillValues({ description, title })
|
await fillValues({ description, title })
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
|
|
||||||
// Change back to English
|
|
||||||
await changeLocale(page, 'es')
|
await changeLocale(page, 'es')
|
||||||
|
|
||||||
// Localized field should not be populated
|
// Localized field should not be populated
|
||||||
@@ -131,12 +139,10 @@ describe('Localization', () => {
|
|||||||
.not.toBe(title)
|
.not.toBe(title)
|
||||||
|
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
|
|
||||||
await fillValues({ description, title: spanishTitle })
|
await fillValues({ description, title: spanishTitle })
|
||||||
await saveDocAndAssert(page)
|
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-title')).toHaveValue(title)
|
||||||
await expect(page.locator('#field-description')).toHaveValue(description)
|
await expect(page.locator('#field-description')).toHaveValue(description)
|
||||||
})
|
})
|
||||||
@@ -227,7 +233,6 @@ describe('Localization', () => {
|
|||||||
await page.waitForURL(url.create)
|
await page.waitForURL(url.create)
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
await fillValues({ description, title: englishTitle })
|
await fillValues({ description, title: englishTitle })
|
||||||
await expect(page.locator('#field-localizedCheckbox')).toBeEnabled()
|
|
||||||
await page.locator('#field-localizedCheckbox').click()
|
await page.locator('#field-localizedCheckbox').click()
|
||||||
await page.locator('#action-save').click()
|
await page.locator('#action-save').click()
|
||||||
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain('create')
|
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()
|
const originalID = await page.locator('.id-label').innerText()
|
||||||
await openDocControls(page)
|
await openDocControls(page)
|
||||||
await page.locator('#action-duplicate').click()
|
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(
|
await expect(page.locator('.payload-toast-container')).toContainText(
|
||||||
'successfully duplicated',
|
'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)
|
await expect(page.locator('.id-label')).not.toContainText(originalID)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('locale preference', () => {
|
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 page.goto(url.create)
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
await expect(page.locator('#field-title')).toBeEmpty()
|
await expect(page.locator('#field-title')).toBeEmpty()
|
||||||
await fillValues({ title: spanishTitle })
|
await fillValues({ title: spanishTitle })
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
await page.goto(url.admin)
|
|
||||||
await page.goto(url.list)
|
await page.goto(url.list)
|
||||||
await expect(page.locator('.row-1 .cell-title')).toContainText(spanishTitle)
|
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', () => {
|
describe('localized relationships', () => {
|
||||||
test('ensure relationship field fetches are localized as well', async () => {
|
test('ensure relationship field fetches are localized as well', async () => {
|
||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
await navigateToDoc(page, url)
|
await navigateToDoc(page, url)
|
||||||
const selectField = page.locator('#field-children .rs__control')
|
await page.locator('#field-children .rs__control').click()
|
||||||
await selectField.click()
|
|
||||||
await expect(page.locator('#field-children .rs__menu')).toContainText('spanish-relation2')
|
await expect(page.locator('#field-children .rs__menu')).toContainText('spanish-relation2')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -318,6 +340,7 @@ describe('Localization', () => {
|
|||||||
await setToLocale(page, 'Spanish')
|
await setToLocale(page, 'Spanish')
|
||||||
await runCopy(page)
|
await runCopy(page)
|
||||||
await expect(page.locator('#field-title')).toHaveValue(title)
|
await expect(page.locator('#field-title')).toHaveValue(title)
|
||||||
|
await changeLocale(page, defaultLocale)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should copy rich text data to correct locale', async () => {
|
test('should copy rich text data to correct locale', async () => {
|
||||||
@@ -359,7 +382,6 @@ describe('Localization', () => {
|
|||||||
await changeLocale(page, spanishLocale)
|
await changeLocale(page, spanishLocale)
|
||||||
await createAndSaveDoc(page, url, { title })
|
await createAndSaveDoc(page, url, { title })
|
||||||
await openCopyToLocaleDrawer(page)
|
await openCopyToLocaleDrawer(page)
|
||||||
|
|
||||||
const fromLocaleField = page.locator('#field-fromLocale')
|
const fromLocaleField = page.locator('#field-fromLocale')
|
||||||
await expect(fromLocaleField).toContainText('Spanish')
|
await expect(fromLocaleField).toContainText('Spanish')
|
||||||
await page.locator('.drawer-close-button').click()
|
await page.locator('.drawer-close-button').click()
|
||||||
@@ -391,14 +413,13 @@ describe('Localization', () => {
|
|||||||
await fillValues({ title: spanishTitle })
|
await fillValues({ title: spanishTitle })
|
||||||
await saveDocAndAssert(page)
|
await saveDocAndAssert(page)
|
||||||
await changeLocale(page, defaultLocale)
|
await changeLocale(page, defaultLocale)
|
||||||
|
|
||||||
await openCopyToLocaleDrawer(page)
|
await openCopyToLocaleDrawer(page)
|
||||||
await setToLocale(page, 'Spanish')
|
await setToLocale(page, 'Spanish')
|
||||||
const overwriteCheckbox = page.locator('#field-overwriteExisting')
|
const overwriteCheckbox = page.locator('#field-overwriteExisting')
|
||||||
await overwriteCheckbox.click()
|
await overwriteCheckbox.click()
|
||||||
await runCopy(page)
|
await runCopy(page)
|
||||||
|
|
||||||
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
await expect(page.locator('#field-title')).toHaveValue(englishTitle)
|
||||||
|
await changeLocale(page, defaultLocale)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should not include current locale in toLocale options', async () => {
|
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')
|
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>) {
|
async function fillValues(data: Partial<LocalizedPost>) {
|
||||||
@@ -461,9 +522,7 @@ async function fillValues(data: Partial<LocalizedPost>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runCopy(page) {
|
async function runCopy(page) {
|
||||||
const copyDrawerClose = page.locator('.copy-locale-data__sub-header button')
|
await page.locator('.copy-locale-data__sub-header button').click()
|
||||||
await expect(copyDrawerClose).toBeVisible()
|
|
||||||
await copyDrawerClose.click()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createAndSaveDoc(page, url, values) {
|
async function createAndSaveDoc(page, url, values) {
|
||||||
@@ -473,12 +532,8 @@ async function createAndSaveDoc(page, url, values) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openCopyToLocaleDrawer(page) {
|
async function openCopyToLocaleDrawer(page) {
|
||||||
const docControls = page.locator('.doc-controls__popup button.popup-button')
|
await page.locator('.doc-controls__popup button.popup-button').click()
|
||||||
expect(docControls).toBeEnabled()
|
await page.locator('#copy-locale-data__button').click()
|
||||||
await docControls.click()
|
|
||||||
const copyButton = page.locator('#copy-locale-data__button')
|
|
||||||
await expect(copyButton).toBeVisible()
|
|
||||||
await copyButton.click()
|
|
||||||
await expect(page.locator('#copy-locale')).toBeVisible()
|
await expect(page.locator('#copy-locale')).toBeVisible()
|
||||||
await expect(page.locator('.copy-locale-data__content')).toBeVisible()
|
await expect(page.locator('.copy-locale-data__content')).toBeVisible()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user