Skip to content

Commit

Permalink
fix(ui): disables form during locale change (#8705)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jacobsfletch authored Jan 10, 2025
1 parent 4fc6956 commit f4596fc
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 180 deletions.
7 changes: 4 additions & 3 deletions packages/next/src/layouts/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -92,9 +92,10 @@ export const RootLayout = async ({
importMap,
})

req.user = user

const locale = await getRequestLocale({
payload,
user,
req,
})

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
31 changes: 16 additions & 15 deletions packages/next/src/utilities/getRequestLocale.ts
Original file line number Diff line number Diff line change
@@ -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',
)
)
}
Expand Down
4 changes: 1 addition & 3 deletions packages/next/src/utilities/initPage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions packages/ui/src/elements/Localizer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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()
Expand All @@ -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),
Expand All @@ -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>
)
})}
Expand Down
71 changes: 37 additions & 34 deletions packages/ui/src/elements/Popup/PopupButtonList/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -42,46 +42,49 @@ export const Button: React.FC<MenuButtonProps> = ({
active,
children,
className,
disabled,
href,
onClick,
}) => {
const classes = [`${baseClass}__button`, active && `${baseClass}__button--selected`, className]
.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 (
Expand Down
20 changes: 16 additions & 4 deletions packages/ui/src/providers/DocumentInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@ 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'

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'
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit f4596fc

Please sign in to comment.