diff --git a/packages/next/src/utilities/initPage.ts b/packages/next/src/utilities/initPage.ts index 1d0301ef00..e7e2637634 100644 --- a/packages/next/src/utilities/initPage.ts +++ b/packages/next/src/utilities/initPage.ts @@ -100,7 +100,7 @@ export const initPage = async ({ const docID = collectionSlug && createOrID !== 'create' ? createOrID : undefined const isAdminRoute = route.startsWith(adminRoute) - const isAuthRoute = authRoutes.some((r) => r === route.replace(adminRoute, '')) + const isAuthRoute = authRoutes.some((r) => route.replace(adminRoute, '').startsWith(r)) if (redirectUnauthenticatedUser && !user && !isAuthRoute) { if (searchParams && 'redirect' in searchParams) delete searchParams.redirect diff --git a/packages/next/src/views/ResetPassword/index.client.tsx b/packages/next/src/views/ResetPassword/index.client.tsx new file mode 100644 index 0000000000..936993d2b8 --- /dev/null +++ b/packages/next/src/views/ResetPassword/index.client.tsx @@ -0,0 +1,103 @@ +'use client' +import type { FormState } from 'payload/types' + +import { ConfirmPassword } from '@payloadcms/ui/fields/ConfirmPassword' +import { HiddenInput } from '@payloadcms/ui/fields/HiddenInput' +import { Password } from '@payloadcms/ui/fields/Password' +import { Form, useFormFields } from '@payloadcms/ui/forms/Form' +import { FormSubmit } from '@payloadcms/ui/forms/Submit' +import { useAuth } from '@payloadcms/ui/providers/Auth' +import { useConfig } from '@payloadcms/ui/providers/Config' +import { useTranslation } from '@payloadcms/ui/providers/Translation' +import { useRouter } from 'next/navigation.js' +import React from 'react' +import { toast } from 'react-toastify' + +type Args = { + token: string +} + +const initialState: FormState = { + 'confirm-password': { + initialValue: '', + valid: false, + value: '', + }, + password: { + initialValue: '', + valid: false, + value: '', + }, +} + +export const ResetPasswordClient: React.FC = ({ token }) => { + const i18n = useTranslation() + const { + admin: { user: userSlug }, + routes: { admin, api }, + serverURL, + } = useConfig() + + const history = useRouter() + + const { fetchFullUser } = useAuth() + + const onSuccess = React.useCallback( + async (data) => { + if (data.token) { + await fetchFullUser() + history.push(`${admin}`) + } else { + history.push(`${admin}/login`) + toast.success(i18n.t('general:updatedSuccessfully'), { autoClose: 3000 }) + } + }, + [fetchFullUser, history, admin, i18n], + ) + + return ( +
+ + + + {i18n.t('authentication:resetPassword')} + + ) +} + +const PasswordToConfirm = () => { + const { t } = useTranslation() + const { value: confirmValue } = useFormFields(([fields]) => { + return fields['confirm-password'] + }) + + const validate = React.useCallback( + (value: string) => { + if (!value) { + return t('validation:required') + } + + if (value === confirmValue) { + return true + } + + return t('fields:passwordsDoNotMatch') + }, + [confirmValue, t], + ) + + return ( + + ) +} diff --git a/packages/next/src/views/ResetPassword/index.scss b/packages/next/src/views/ResetPassword/index.scss index de3c6f1eef..d48d6edc37 100644 --- a/packages/next/src/views/ResetPassword/index.scss +++ b/packages/next/src/views/ResetPassword/index.scss @@ -1,15 +1,5 @@ .reset-password { - display: flex; - align-items: center; - flex-wrap: wrap; - min-height: 100vh; - - &__wrap { - margin: 0 auto var(--base); - width: 100%; - - svg { - width: 100%; - } + form > .field-type { + margin-bottom: var(--base); } } diff --git a/packages/next/src/views/ResetPassword/index.tsx b/packages/next/src/views/ResetPassword/index.tsx index ebe5593d1c..a3106f74ed 100644 --- a/packages/next/src/views/ResetPassword/index.tsx +++ b/packages/next/src/views/ResetPassword/index.tsx @@ -2,15 +2,11 @@ import type { AdminViewProps } from 'payload/types' import { Button } from '@payloadcms/ui/elements/Button' import { Translation } from '@payloadcms/ui/elements/Translation' -import { ConfirmPassword } from '@payloadcms/ui/fields/ConfirmPassword' -import { HiddenInput } from '@payloadcms/ui/fields/HiddenInput' -import { Password } from '@payloadcms/ui/fields/Password' -import { Form } from '@payloadcms/ui/forms/Form' -import { FormSubmit } from '@payloadcms/ui/forms/Submit' import { MinimalTemplate } from '@payloadcms/ui/templates/Minimal' import LinkImport from 'next/link.js' import React from 'react' +import { ResetPasswordClient } from './index.client.js' import './index.scss' export const resetPasswordBaseClass = 'reset-password' @@ -22,7 +18,9 @@ export { generateResetPasswordMetadata } from './meta.js' export const ResetPassword: React.FC = ({ initPageResult, params }) => { const { req } = initPageResult - const { token } = params + const { + segments: [_, token], + } = params const { i18n, @@ -31,21 +29,9 @@ export const ResetPassword: React.FC = ({ initPageResult, params } = req const { - admin: { user: userSlug }, - routes: { admin, api }, - serverURL, + routes: { admin }, } = config - // const onSuccess = async (data) => { - // if (data.token) { - // await fetchFullUser() - // history.push(`${admin}`) - // } else { - // history.push(`${admin}/login`) - // toast.success(i18n.t('general:updatedSuccessfully'), { autoClose: 3000 }) - // } - // } - if (user) { return ( @@ -73,22 +59,7 @@ export const ResetPassword: React.FC = ({ initPageResult, params

{i18n.t('authentication:resetPassword')}

-
- - - - {i18n.t('authentication:resetPassword')} - +
) diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts index 37d8e8e88b..02f75a520d 100644 --- a/packages/translations/src/clientKeys.ts +++ b/packages/translations/src/clientKeys.ts @@ -27,6 +27,8 @@ export const clientTranslationKeys = [ 'authentication:loggingOut', 'authentication:login', 'authentication:logOut', + 'authentication:loggedIn', + 'authentication:loggedInChangePassword', 'authentication:logout', 'authentication:logoutUser', 'authentication:logoutSuccessful', diff --git a/packages/ui/src/fields/ConfirmPassword/index.tsx b/packages/ui/src/fields/ConfirmPassword/index.tsx index 4eebd8069e..d6edcec71b 100644 --- a/packages/ui/src/fields/ConfirmPassword/index.tsx +++ b/packages/ui/src/fields/ConfirmPassword/index.tsx @@ -18,7 +18,7 @@ export type ConfirmPasswordFieldProps = { export const ConfirmPassword: React.FC = (props) => { const { disabled } = props - const password = useFormFields(([fields]) => fields.password) + const password = useFormFields(([fields]) => fields?.password) const { t } = useTranslation() const validate = useCallback( diff --git a/packages/ui/src/forms/useField/index.tsx b/packages/ui/src/forms/useField/index.tsx index 3b56c9bfd2..0183b62522 100644 --- a/packages/ui/src/forms/useField/index.tsx +++ b/packages/ui/src/forms/useField/index.tsx @@ -58,6 +58,7 @@ export const useField = (options: Options): FieldType => { const showError = valid === false && submitted const prevValid = useRef(valid) + const prevErrorMessage = useRef(field?.errorMessage) // Method to return from `useField`, used to // update field values from field component(s) @@ -175,8 +176,9 @@ export const useField = (options: Options): FieldType => { // Only dispatch if the validation result has changed // This will prevent unnecessary rerenders - if (valid !== prevValid.current) { + if (valid !== prevValid.current || errorMessage !== prevErrorMessage.current) { prevValid.current = valid + prevErrorMessage.current = errorMessage const update: UPDATE = { type: 'UPDATE', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 798cb66077..02a0eb6733 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,7 @@ importers: version: 9.1.8 next: specifier: ^14.3.0-canary.7 - version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1) node-mocks-http: specifier: ^1.14.1 version: 1.14.1 @@ -1485,7 +1485,7 @@ importers: version: 2.3.0 next: specifier: ^14.3.0-canary.7 - version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1) object-to-formdata: specifier: 4.5.1 version: 4.5.1 @@ -12354,48 +12354,6 @@ packages: /next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - /next@14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-loPrWTCvHvZgOy3rgL9+2WpxNDxlRNt462ihqm/DUuyK8LUZV1F4H920YTAu1wEiYC8RrpNUbpz8K7KRYAkQiA==} - engines: {node: '>=18.17.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.41.2 - react: ^18.0.0 - react-dom: ^18.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - sass: - optional: true - dependencies: - '@next/env': 14.3.0-canary.7 - '@playwright/test': 1.43.0 - '@swc/helpers': 0.5.5 - busboy: 1.6.0 - caniuse-lite: 1.0.30001607 - graceful-fs: 4.2.11 - postcss: 8.4.31 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.24.4)(react@18.2.0) - optionalDependencies: - '@next/swc-darwin-arm64': 14.3.0-canary.7 - '@next/swc-darwin-x64': 14.3.0-canary.7 - '@next/swc-linux-arm64-gnu': 14.3.0-canary.7 - '@next/swc-linux-arm64-musl': 14.3.0-canary.7 - '@next/swc-linux-x64-gnu': 14.3.0-canary.7 - '@next/swc-linux-x64-musl': 14.3.0-canary.7 - '@next/swc-win32-arm64-msvc': 14.3.0-canary.7 - '@next/swc-win32-ia32-msvc': 14.3.0-canary.7 - '@next/swc-win32-x64-msvc': 14.3.0-canary.7 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - /next@14.3.0-canary.7(@babel/core@7.24.4)(@playwright/test@1.43.0)(react-dom@18.2.0)(react@18.2.0)(sass@1.74.1): resolution: {integrity: sha512-loPrWTCvHvZgOy3rgL9+2WpxNDxlRNt462ihqm/DUuyK8LUZV1F4H920YTAu1wEiYC8RrpNUbpz8K7KRYAkQiA==} engines: {node: '>=18.17.0'} @@ -12438,7 +12396,6 @@ packages: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - dev: false /node-abi@3.57.0: resolution: {integrity: sha512-Dp+A9JWxRaKuHP35H77I4kCKesDy5HUDEmScia2FyncMTOXASMyg251F5PhFoDA5uqBrDDffiLpbqnrZmNXW+g==} diff --git a/test/admin/components/views/CustomView/index.client.tsx b/test/admin/components/views/CustomView/index.client.tsx new file mode 100644 index 0000000000..485b554829 --- /dev/null +++ b/test/admin/components/views/CustomView/index.client.tsx @@ -0,0 +1,54 @@ +'use client' +import { ConfirmPassword } from '@payloadcms/ui/fields/ConfirmPassword' +import { Password } from '@payloadcms/ui/fields/Password' +import { Form, useFormFields } from '@payloadcms/ui/forms/Form' +import { FormSubmit } from '@payloadcms/ui/forms/Submit' +import React from 'react' + +export const ClientForm: React.FC = () => { + return ( +
+ + + + Submit + + ) +} + +const CustomPassword: React.FC = () => { + const confirmPassword = useFormFields(([fields]) => { + return fields['confirm-password'] + }) + + const confirmValue = confirmPassword.value + + return ( + { + if (value && confirmValue) { + return confirmValue === value ? true : 'Passwords must match!!!!' + } + + return 'Field is required' + }} + /> + ) +} diff --git a/test/admin/components/views/CustomView/index.tsx b/test/admin/components/views/CustomView/index.tsx index 8bb8a3d200..169c0bbb28 100644 --- a/test/admin/components/views/CustomView/index.tsx +++ b/test/admin/components/views/CustomView/index.tsx @@ -1,13 +1,14 @@ +import type { AdminViewProps } from 'payload/types' + import LinkImport from 'next/link.js' import React from 'react' -import type { AdminViewProps } from '../../../../../packages/payload/types.js' - const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport.default import { Button } from '@payloadcms/ui/elements/Button' import { customNestedViewPath, customViewTitle } from '../../../shared.js' +import { ClientForm } from './index.client.js' export const CustomView: React.FC = ({ initPageResult }) => { const { @@ -48,6 +49,7 @@ export const CustomView: React.FC = ({ initPageResult }) => { > Go to Nested View + )