fix: email and username fields rendering in drawers (#7520)

Fixes https://github.com/payloadcms/payload/issues/7428

Now email and username fields are rendered with the RenderFields
component, making them behave similarly to other fields. They now appear
and can respect doc permissions, readOnly settings, etc.
This commit is contained in:
Jarrod Flesch
2024-08-05 20:18:32 -04:00
committed by GitHub
parent 5d1cc760c9
commit 442189ec48
23 changed files with 131 additions and 50 deletions

7
.vscode/launch.json vendored
View File

@@ -56,6 +56,13 @@
"request": "launch", "request": "launch",
"type": "node-terminal" "type": "node-terminal"
}, },
{
"command": "node --no-deprecation test/dev.js login-with-username",
"cwd": "${workspaceFolder}",
"name": "Run Dev Login-With-Username",
"request": "launch",
"type": "node-terminal"
},
{ {
"command": "pnpm run dev plugin-cloud-storage", "command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",

View File

@@ -1,45 +1,103 @@
'use client' 'use client'
import type { LoginWithUsernameOptions } from 'payload' import type { FieldPermissions, LoginWithUsernameOptions } from 'payload'
import { EmailField, TextField, useTranslation } from '@payloadcms/ui' import { EmailField, RenderFields, TextField, useTranslation } from '@payloadcms/ui'
import { email, username } from 'payload/shared' import { email, username } from 'payload/shared'
import React from 'react' import React from 'react'
type Props = { type Props = {
loginWithUsername?: LoginWithUsernameOptions | false loginWithUsername?: LoginWithUsernameOptions | false
} }
export const EmailAndUsernameFields: React.FC<Props> = ({ loginWithUsername }) => { function EmailFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation() const { t } = useTranslation()
const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail) const requireEmail = !loginWithUsername || (loginWithUsername && loginWithUsername.requireEmail)
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showEmailField = const showEmailField =
!loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin !loginWithUsername || loginWithUsername?.requireEmail || loginWithUsername?.allowEmailLogin
if (showEmailField) {
return (
<EmailField
autoComplete="off"
label={t('general:email')}
name="email"
path="email"
required={requireEmail}
validate={email}
/>
)
}
return null
}
function UsernameFieldComponent(props: Props) {
const { loginWithUsername } = props
const { t } = useTranslation()
const requireUsername = loginWithUsername && loginWithUsername.requireUsername
const showUsernameField = Boolean(loginWithUsername) const showUsernameField = Boolean(loginWithUsername)
return ( if (showUsernameField) {
<React.Fragment> return (
{showEmailField && ( <TextField
<EmailField label={t('authentication:username')}
autoComplete="email" name="username"
label={t('general:email')} path="username"
name="email" required={requireUsername}
path="email" validate={username}
required={requireEmail} />
validate={email} )
/> }
)}
{showUsernameField && ( return null
<TextField }
label={t('authentication:username')}
name="username" type RenderEmailAndUsernameFieldsProps = {
path="username" className?: string
required={requireUsername} loginWithUsername?: LoginWithUsernameOptions | false
validate={username} operation?: 'create' | 'update'
/> permissions?: {
)} [fieldName: string]: FieldPermissions
</React.Fragment> }
readOnly: boolean
}
export function RenderEmailAndUsernameFields(props: RenderEmailAndUsernameFieldsProps) {
const { className, loginWithUsername, operation, permissions, readOnly } = props
return (
<RenderFields
className={className}
fieldMap={[
{
name: 'email',
type: 'text',
CustomField: <EmailFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'email', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
{
name: 'username',
type: 'text',
CustomField: <UsernameFieldComponent loginWithUsername={loginWithUsername} />,
cellComponentProps: null,
fieldComponentProps: { type: 'text', readOnly },
fieldIsPresentational: false,
isFieldAffectingData: true,
localized: false,
},
]}
forceRender
operation={operation}
path=""
permissions={permissions}
readOnly={readOnly}
schemaPath=""
/>
) )
} }

View File

@@ -15,7 +15,7 @@ import {
import { getFormState } from '@payloadcms/ui/shared' import { getFormState } from '@payloadcms/ui/shared'
import React from 'react' import React from 'react'
import { EmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js' import { RenderEmailAndUsernameFields } from '../../elements/EmailAndUsername/index.js'
export const CreateFirstUserClient: React.FC<{ export const CreateFirstUserClient: React.FC<{
initialState: FormState initialState: FormState
@@ -57,7 +57,12 @@ export const CreateFirstUserClient: React.FC<{
redirect={admin} redirect={admin}
validationOperation="create" validationOperation="create"
> >
<EmailAndUsernameFields loginWithUsername={loginWithUsername} /> <RenderEmailAndUsernameFields
className="emailAndUsername"
loginWithUsername={loginWithUsername}
operation="create"
readOnly={false}
/>
<PasswordField <PasswordField
label={t('authentication:newPassword')} label={t('authentication:newPassword')}
name="password" name="password"

View File

@@ -3,3 +3,7 @@
margin-bottom: var(--base); margin-bottom: var(--base);
} }
} }
.emailAndUsername {
margin-bottom: var(--base);
}

View File

@@ -17,7 +17,7 @@ import { toast } from 'sonner'
import type { Props } from './types.js' import type { Props } from './types.js'
import { EmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js' import { RenderEmailAndUsernameFields } from '../../../../elements/EmailAndUsername/index.js'
import { APIKey } from './APIKey.js' import { APIKey } from './APIKey.js'
import './index.scss' import './index.scss'
@@ -47,7 +47,7 @@ export const Auth: React.FC<Props> = (props) => {
const dispatchFields = useFormFields((reducer) => reducer[1]) const dispatchFields = useFormFields((reducer) => reducer[1])
const modified = useFormModified() const modified = useFormModified()
const { i18n, t } = useTranslation() const { i18n, t } = useTranslation()
const { isInitializing } = useDocumentInfo() const { docPermissions, isInitializing } = useDocumentInfo()
const { const {
routes: { api }, routes: { api },
@@ -138,7 +138,12 @@ export const Auth: React.FC<Props> = (props) => {
<div className={[baseClass, className].filter(Boolean).join(' ')}> <div className={[baseClass, className].filter(Boolean).join(' ')}>
{!disableLocalStrategy && ( {!disableLocalStrategy && (
<React.Fragment> <React.Fragment>
<EmailAndUsernameFields loginWithUsername={loginWithUsername} /> <RenderEmailAndUsernameFields
loginWithUsername={loginWithUsername}
operation={operation}
permissions={docPermissions?.fields}
readOnly={readOnly}
/>
{(showPasswordFields || requirePassword) && ( {(showPasswordFields || requirePassword) && (
<div className={`${baseClass}__changing-password`}> <div className={`${baseClass}__changing-password`}>
<PasswordField <PasswordField

View File

@@ -15,7 +15,7 @@ export type ArrayFieldProps = {
name?: string name?: string
validate?: ArrayFieldValidation validate?: ArrayFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type ArrayFieldLabelComponent = LabelComponent<'array'> export type ArrayFieldLabelComponent = LabelComponent<'array'>

View File

@@ -15,7 +15,7 @@ export type BlocksFieldProps = {
slug?: string slug?: string
validate?: BlockFieldValidation validate?: BlockFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type ReducedBlock = { export type ReducedBlock = {
LabelComponent: Block['admin']['components']['Label'] LabelComponent: Block['admin']['components']['Label']

View File

@@ -12,7 +12,7 @@ export type CheckboxFieldProps = {
path?: string path?: string
validate?: CheckboxFieldValidation validate?: CheckboxFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type CheckboxFieldLabelComponent = LabelComponent<'checkbox'> export type CheckboxFieldLabelComponent = LabelComponent<'checkbox'>

View File

@@ -10,7 +10,7 @@ export type CodeFieldProps = {
path?: string path?: string
validate?: CodeFieldValidation validate?: CodeFieldValidation
width: string width: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type CodeFieldLabelComponent = LabelComponent<'code'> export type CodeFieldLabelComponent = LabelComponent<'code'>

View File

@@ -10,7 +10,7 @@ export type DateFieldProps = {
placeholder?: DateField['admin']['placeholder'] | string placeholder?: DateField['admin']['placeholder'] | string
validate?: DateFieldValidation validate?: DateFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type DateFieldLabelComponent = LabelComponent<'date'> export type DateFieldLabelComponent = LabelComponent<'date'>

View File

@@ -10,7 +10,7 @@ export type EmailFieldProps = {
placeholder?: EmailField['admin']['placeholder'] placeholder?: EmailField['admin']['placeholder']
validate?: EmailFieldValidation validate?: EmailFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type EmailFieldLabelComponent = LabelComponent<'email'> export type EmailFieldLabelComponent = LabelComponent<'email'>

View File

@@ -10,7 +10,7 @@ export type JSONFieldProps = {
path?: string path?: string
validate?: JSONFieldValidation validate?: JSONFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type JSONFieldLabelComponent = LabelComponent<'json'> export type JSONFieldLabelComponent = LabelComponent<'json'>

View File

@@ -15,7 +15,7 @@ export type NumberFieldProps = {
step?: number step?: number
validate?: NumberFieldValidation validate?: NumberFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type NumberFieldLabelComponent = LabelComponent<'number'> export type NumberFieldLabelComponent = LabelComponent<'number'>

View File

@@ -9,7 +9,7 @@ export type PointFieldProps = {
step?: number step?: number
validate?: PointFieldValidation validate?: PointFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type PointFieldLabelComponent = LabelComponent<'point'> export type PointFieldLabelComponent = LabelComponent<'point'>

View File

@@ -12,7 +12,7 @@ export type RadioFieldProps = {
validate?: RadioFieldValidation validate?: RadioFieldValidation
value?: string value?: string
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type OnChange<T = string> = (value: T) => void export type OnChange<T = string> = (value: T) => void

View File

@@ -12,7 +12,7 @@ export type RelationshipFieldProps = {
sortOptions?: RelationshipField['admin']['sortOptions'] sortOptions?: RelationshipField['admin']['sortOptions']
validate?: RelationshipFieldValidation validate?: RelationshipFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type RelationshipFieldLabelComponent = LabelComponent<'relationship'> export type RelationshipFieldLabelComponent = LabelComponent<'relationship'>

View File

@@ -8,7 +8,7 @@ export type RichTextComponentProps = {
richTextComponentMap?: Map<string, MappedField[] | React.ReactNode> richTextComponentMap?: Map<string, MappedField[] | React.ReactNode>
validate?: RichTextFieldValidation validate?: RichTextFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type RichTextFieldLabelComponent = LabelComponent<'richText'> export type RichTextFieldLabelComponent = LabelComponent<'richText'>

View File

@@ -14,7 +14,7 @@ export type SelectFieldProps = {
validate?: SelectFieldValidation validate?: SelectFieldValidation
value?: string value?: string
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type SelectFieldLabelComponent = LabelComponent<'select'> export type SelectFieldLabelComponent = LabelComponent<'select'>

View File

@@ -16,7 +16,7 @@ export type TextFieldProps = {
placeholder?: TextField['admin']['placeholder'] placeholder?: TextField['admin']['placeholder']
validate?: TextFieldValidation validate?: TextFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type TextFieldLabelComponent = LabelComponent<'text'> export type TextFieldLabelComponent = LabelComponent<'text'>

View File

@@ -12,7 +12,7 @@ export type TextareaFieldProps = {
rows?: number rows?: number
validate?: TextareaFieldValidation validate?: TextareaFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type TextareaFieldLabelComponent = LabelComponent<'textarea'> export type TextareaFieldLabelComponent = LabelComponent<'textarea'>

View File

@@ -15,7 +15,7 @@ export type UploadFieldProps = {
relationTo?: UploadField['relationTo'] relationTo?: UploadField['relationTo']
validate?: UploadFieldValidation validate?: UploadFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
export type UploadFieldLabelComponent = LabelComponent<'upload'> export type UploadFieldLabelComponent = LabelComponent<'upload'>

View File

@@ -1,13 +1,15 @@
'use client' 'use client'
import type { TextFieldProps } from 'payload' import type { SelectFieldValidation, TextFieldProps } from 'payload'
import { SelectField, useForm } from '@payloadcms/ui' import { SelectField, useForm } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import type { SelectFieldOption } from '../../types.js' import type { SelectFieldOption } from '../../types.js'
export const DynamicFieldSelector: React.FC<TextFieldProps> = (props) => { export const DynamicFieldSelector: React.FC<
{ validate: SelectFieldValidation } & TextFieldProps
> = (props) => {
const { fields, getDataByPath } = useForm() const { fields, getDataByPath } = useForm()
const [options, setOptions] = useState<SelectFieldOption[]>([]) const [options, setOptions] = useState<SelectFieldOption[]>([])

View File

@@ -58,7 +58,7 @@ const RichTextField: React.FC<
richTextComponentMap: Map<string, React.ReactNode> richTextComponentMap: Map<string, React.ReactNode>
validate?: RichTextFieldValidation validate?: RichTextFieldValidation
width?: string width?: string
} & FormFieldBase } & Omit<FormFieldBase, 'validate'>
> = (props) => { > = (props) => {
const { const {
name, name,