feat: separate Input from Upload, Text & TextArea fields, get plugin-seo to load without errors

This commit is contained in:
Alessio Gravili
2024-03-04 16:56:14 -05:00
parent 56ecd2ac14
commit de99aabf7f
16 changed files with 569 additions and 333 deletions

View File

@@ -3,9 +3,11 @@
import type { FieldType, Options } from '@payloadcms/ui'
import type { TextareaField } from 'payload/types'
import { Textarea, useAllFormFields, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'
import { useFieldPath } from '@payloadcms/ui'
import { useTranslation } from '@payloadcms/ui'
import { TextareaInput } from '@payloadcms/ui'
import { useAllFormFields, useDocumentInfo, useField, useLocale } from '@payloadcms/ui'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginConfig } from '../types'
@@ -22,8 +24,9 @@ type MetaDescriptionProps = TextareaField & {
export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const { name, label, path, pluginConfig, required } = props
const { path: pathFromContext, schemaPath } = useFieldPath()
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
const locale = useLocale()
const [fields] = useAllFormFields()
@@ -38,7 +41,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
const { errorMessage, setValue, showError, value } = field
const regenerateDescription = useCallback(async () => {
const { generateDescription } = pluginConfig
/*const { generateDescription } = pluginConfig
let generatedDescription
if (typeof generateDescription === 'function') {
@@ -49,7 +52,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
})
}
setValue(generatedDescription)
setValue(generatedDescription)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
return (
@@ -78,7 +81,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
</span>
)}
{typeof pluginConfig.generateDescription === 'function' && (
{typeof pluginConfig?.generateDescription === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -94,7 +97,7 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
}}
type="button"
>
{t('autoGenerate')}
{t('plugin-seo:autoGenerate')}
</button>
</React.Fragment>
)}
@@ -104,13 +107,13 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
color: '#9A9A9A',
}}
>
{t('lengthTipDescription', { maxLength, minLength })}
{t('plugin-seo:lengthTipDescription', { maxLength, minLength })}
<a
href="https://developers.google.com/search/docs/advanced/appearance/snippet#meta-descriptions"
rel="noopener noreferrer"
target="_blank"
>
{t('bestPractices')}
{t('plugin-seo:bestPractices')}
</a>
</div>
</div>
@@ -120,11 +123,10 @@ export const MetaDescription: React.FC<MetaDescriptionProps> = (props) => {
position: 'relative',
}}
>
<Textarea
errorMessage={errorMessage}
name={name}
<TextareaInput
Error={errorMessage} // TODO: Fix
onChange={setValue}
path={name}
path={name || pathFromContext}
required={required}
showError={showError}
style={{

View File

@@ -1,12 +1,17 @@
'use client'
import type { Props as UploadInputProps } from 'payload/components/fields/Upload'
import type { FieldType, Options } from 'payload/dist/admin/components/forms/useField/types'
import type { FieldType, Options, UploadInputProps } from '@payloadcms/ui'
import { UploadInput, useAllFormFields, useField } from 'payload/components/forms'
import { useConfig, useDocumentInfo, useLocale } from 'payload/components/utilities'
import {
UploadInput,
useAllFormFields,
useConfig,
useDocumentInfo,
useField,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginConfig } from '../types'
@@ -19,11 +24,11 @@ type MetaImageProps = UploadInputProps & {
}
export const MetaImage: React.FC<MetaImageProps> = (props) => {
const { name, fieldTypes, label, pluginConfig, relationTo, required } = props || {}
const { label, pluginConfig, relationTo, required } = props || {}
const field: FieldType<string> = useField(props as Options)
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
const locale = useLocale()
const [fields] = useAllFormFields()
@@ -32,7 +37,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
const { errorMessage, setValue, showError, value } = field
const regenerateImage = useCallback(async () => {
const { generateImage } = pluginConfig
/*const { generateImage } = pluginConfig
let generatedImage
if (typeof generateImage === 'function') {
@@ -43,7 +48,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
})
}
setValue(generatedImage)
setValue(generatedImage)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
const hasImage = Boolean(value)
@@ -80,7 +85,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
</span>
)}
{typeof pluginConfig.generateImage === 'function' && (
{typeof pluginConfig?.generateImage === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -96,18 +101,18 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
}}
type="button"
>
{t('autoGenerate')}
{t('plugin-seo:autoGenerate')}
</button>
</React.Fragment>
)}
</div>
{typeof pluginConfig.generateImage === 'function' && (
{typeof pluginConfig?.generateImage === 'function' && (
<div
style={{
color: '#9A9A9A',
}}
>
{t('imageAutoGenerationTip')}
{t('plugin-seo:imageAutoGenerationTip')}
</div>
)}
</div>
@@ -118,13 +123,11 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
}}
>
<UploadInput
Error={errorMessage} // TODO: Fix
api={api}
collection={collection}
errorMessage={errorMessage}
fieldTypes={fieldTypes}
filterOptions={{}}
label={undefined}
name={name}
onChange={(incomingImage) => {
if (incomingImage !== null) {
const { id: incomingID } = incomingImage
@@ -133,7 +136,6 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
setValue(null)
}
}}
path={name}
relationTo={relationTo}
required={required}
serverURL={serverURL}
@@ -154,7 +156,7 @@ export const MetaImage: React.FC<MetaImageProps> = (props) => {
<Pill
backgroundColor={hasImage ? 'green' : 'red'}
color="white"
label={hasImage ? t('good') : t('noImage')}
label={hasImage ? t('plugin-seo:good') : t('plugin-seo:noImage')}
/>
</div>
</div>

View File

@@ -1,15 +1,18 @@
'use client'
import type {
FieldType as FieldType,
Options,
} from 'payload/dist/admin/components/forms/useField/types'
import type { FieldType, Options } from '@payloadcms/ui'
import type { TextField as TextFieldType } from 'payload/types'
import { TextInput, useAllFormFields, useField } from 'payload/components/forms'
import { useDocumentInfo, useLocale } from 'payload/components/utilities'
import { useFieldPath } from '@payloadcms/ui'
import {
TextInput,
useAllFormFields,
useDocumentInfo,
useField,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginConfig } from '../types'
@@ -26,8 +29,10 @@ type MetaTitleProps = TextFieldType & {
export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { name, label, path, pluginConfig, required } = props || {}
console.log('props tit', props)
const { path: pathFromContext, schemaPath } = useFieldPath()
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
const field: FieldType<string> = useField({
name,
@@ -42,7 +47,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
const { errorMessage, setValue, showError, value } = field
const regenerateTitle = useCallback(async () => {
const { generateTitle } = pluginConfig
/* const { generateTitle } = pluginConfig
let generatedTitle
if (typeof generateTitle === 'function') {
@@ -53,7 +58,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
})
}
setValue(generatedTitle)
setValue(generatedTitle)*/
}, [fields, setValue, pluginConfig, locale, docInfo])
return (
@@ -82,7 +87,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
</span>
)}
{typeof pluginConfig.generateTitle === 'function' && (
{typeof pluginConfig?.generateTitle === 'function' && (
<React.Fragment>
&nbsp; &mdash; &nbsp;
<button
@@ -98,7 +103,7 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
}}
type="button"
>
{t('autoGenerate')}
{t('plugin-seo:autoGenerate')}
</button>
</React.Fragment>
)}
@@ -108,13 +113,13 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
color: '#9A9A9A',
}}
>
{t('lengthTipTitle', { maxLength, minLength })}
{t('plugin-seo:lengthTipTitle', { maxLength, minLength })}
<a
href="https://developers.google.com/search/docs/advanced/appearance/title-link#page-titles"
rel="noopener noreferrer"
target="_blank"
>
{t('bestPractices')}
{t('plugin-seo:bestPractices')}
</a>
.
</div>
@@ -126,10 +131,9 @@ export const MetaTitle: React.FC<MetaTitleProps> = (props) => {
}}
>
<TextInput
errorMessage={errorMessage}
name={name}
Error={errorMessage} // TODO: fix errormessage
onChange={setValue}
path={name}
path={name || pathFromContext}
required={required}
showError={showError}
style={{

View File

@@ -1,16 +1,17 @@
import type { Config } from 'payload/config'
import type { Field, GroupField, TabsField } from 'payload/types'
import type { Field, GroupField, TabsField, TextField } from 'payload/types'
import { deepMerge } from 'payload/utilities'
import React from 'react'
import type { PluginConfig } from './types'
import { getMetaDescriptionField } from './fields/MetaDescription'
import { getMetaImageField } from './fields/MetaImage'
import { getMetaTitleField } from './fields/MetaTitle'
import { MetaDescription, getMetaDescriptionField } from './fields/MetaDescription'
import { MetaImage, getMetaImageField } from './fields/MetaImage'
import { MetaTitle, getMetaTitleField } from './fields/MetaTitle'
import translations from './translations'
import { Overview } from './ui/Overview'
import { getPreviewField } from './ui/Preview'
import { Preview, getPreviewField } from './ui/Preview'
const seo =
(pluginConfig: PluginConfig) =>
@@ -35,18 +36,22 @@ const seo =
type: 'text',
admin: {
components: {
Field: (props) => getMetaTitleField({ ...props, pluginConfig }),
Field: (props) => {
return <MetaTitle {...props} />
},
},
},
localized: true,
...(pluginConfig?.fieldOverrides?.title ?? {}),
...((pluginConfig?.fieldOverrides?.title as TextField) ?? {}),
},
{
name: 'description',
type: 'textarea',
admin: {
components: {
Field: (props) => getMetaDescriptionField({ ...props, pluginConfig }),
Field: (props) => {
return <MetaDescription {...props} />
},
},
},
localized: true,
@@ -60,7 +65,9 @@ const seo =
type: 'upload',
admin: {
components: {
Field: (props) => getMetaImageField({ ...props, pluginConfig }),
Field: (props) => {
return <MetaImage {...props} />
},
},
description:
'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
@@ -78,7 +85,9 @@ const seo =
type: 'ui',
admin: {
components: {
Field: (props) => getPreviewField({ ...props, pluginConfig }),
Field: (props) => {
return <Preview {...props} />
},
},
},
label: 'Preview',
@@ -198,8 +207,8 @@ const seo =
}) || [],
i18n: {
...config.i18n,
resources: {
...deepMerge(translations, config.i18n?.resources),
translations: {
...deepMerge(translations, config.i18n?.translations),
},
},
}

View File

@@ -1,7 +1,7 @@
'use client'
import { useTranslation } from '@payloadcms/ui'
import React, { Fragment, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pill } from './Pill'
@@ -19,7 +19,7 @@ export const LengthIndicator: React.FC<{
const [label, setLabel] = useState('')
const [barWidth, setBarWidth] = useState<number>(0)
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
useEffect(() => {
const textLength = text?.length || 0
@@ -38,13 +38,13 @@ export const LengthIndicator: React.FC<{
const ratioUntilMin = textLength / minLength
if (ratioUntilMin > 0.9) {
setLabel(t('almostThere'))
setLabel(t('plugin-seo:almostThere'))
setLabelStyle({
backgroundColor: 'orange',
color: 'white',
})
} else {
setLabel(t('tooShort'))
setLabel(t('plugin-seo:tooShort'))
setLabelStyle({
backgroundColor: 'orangered',
color: 'white',
@@ -55,7 +55,7 @@ export const LengthIndicator: React.FC<{
}
if (progress >= 0 && progress <= 1) {
setLabel(t('good'))
setLabel(t('plugin-seo:good'))
setLabelStyle({
backgroundColor: 'green',
color: 'white',
@@ -64,7 +64,7 @@ export const LengthIndicator: React.FC<{
}
if (progress > 1) {
setLabel(t('tooLong'))
setLabel(t('plugin-seo:tooLong'))
setLabelStyle({
backgroundColor: 'red',
color: 'white',
@@ -97,15 +97,17 @@ export const LengthIndicator: React.FC<{
}}
>
<small>
{t('characterCount', { current: text?.length || 0, maxLength, minLength })}
{t('plugin-seo:characterCount', { current: text?.length || 0, maxLength, minLength })}
{(textLength === 0 || charsUntilMin > 0) && (
<Fragment>{t('charactersToGo', { characters: charsUntilMin })}</Fragment>
<Fragment>{t('plugin-seo:charactersToGo', { characters: charsUntilMin })}</Fragment>
)}
{charsUntilMin <= 0 && charsUntilMax >= 0 && (
<Fragment>{t('charactersLeftOver', { characters: charsUntilMax })}</Fragment>
<Fragment>{t('plugin-seo:charactersLeftOver', { characters: charsUntilMax })}</Fragment>
)}
{charsUntilMax < 0 && (
<Fragment>{t('charactersTooMany', { characters: charsUntilMax * -1 })}</Fragment>
<Fragment>
{t('plugin-seo:charactersTooMany', { characters: charsUntilMax * -1 })}
</Fragment>
)}
</small>
</div>

View File

@@ -2,9 +2,8 @@
import type { FormField } from 'payload/types'
import { useAllFormFields, useForm } from 'payload/components/forms'
import { useAllFormFields, useForm, useTranslation } from '@payloadcms/ui'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { defaults } from '../defaults'
@@ -26,7 +25,7 @@ export const Overview: React.FC = () => {
'meta.title': { value: metaTitle } = {} as FormField,
},
] = useAllFormFields()
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
const [titleIsValid, setTitleIsValid] = useState<boolean | undefined>()
const [descIsValid, setDescIsValid] = useState<boolean | undefined>()
@@ -60,7 +59,9 @@ export const Overview: React.FC = () => {
marginBottom: '20px',
}}
>
<div>{t('checksPassing', { current: numberOfPasses, max: testResults.length })}</div>
<div>
{t('plugin-seo:checksPassing', { current: numberOfPasses, max: testResults.length })}
</div>
</div>
)
}

View File

@@ -2,10 +2,8 @@
import type { FormField, UIField } from 'payload/types'
import { useAllFormFields } from 'payload/components/forms'
import { useDocumentInfo, useLocale } from 'payload/components/utilities'
import { useAllFormFields, useDocumentInfo, useLocale, useTranslation } from '@payloadcms/ui'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginConfig } from '../types'
@@ -15,11 +13,9 @@ type PreviewProps = UIField & {
}
export const Preview: React.FC<PreviewProps> = (props) => {
const {
pluginConfig: { generateURL },
} = props || {}
const { pluginConfig: { generateURL } = {} } = props || {}
const { t } = useTranslation('plugin-seo')
const { t } = useTranslation()
const locale = useLocale()
const [fields] = useAllFormFields()
@@ -33,7 +29,7 @@ export const Preview: React.FC<PreviewProps> = (props) => {
const [href, setHref] = useState<string>()
useEffect(() => {
const getHref = async () => {
/* const getHref = async () => {
if (typeof generateURL === 'function' && !href) {
const newHref = await generateURL({
...docInfo,
@@ -45,19 +41,19 @@ export const Preview: React.FC<PreviewProps> = (props) => {
}
}
getHref() // eslint-disable-line @typescript-eslint/no-floating-promises
getHref() // eslint-disable-line @typescript-eslint/no-floating-promises*/
}, [generateURL, fields, href, locale, docInfo])
return (
<div>
<div>{t('preview')}</div>
<div>{t('plugin-seo:preview')}</div>
<div
style={{
color: '#9A9A9A',
marginBottom: '5px',
}}
>
{t('previewDescription')}
{t('plugin-seo:previewDescription')}
</div>
<div
style={{

View File

@@ -33,8 +33,14 @@ export type { OnChange } from '../forms/fields/RadioGroup/types'
export { default as Select } from '../forms/fields/Select'
export { default as SelectInput } from '../forms/fields/Select'
export { default as Text } from '../forms/fields/Text'
export { TextInput, type TextInputProps } from '../forms/fields/Text/Input'
export type { Props as TextFieldProps } from '../forms/fields/Text/types'
export { default as Textarea } from '../forms/fields/Textarea'
export { type TextAreaInputProps, TextareaInput } from '../forms/fields/Textarea/Input'
export { default as Upload } from '../forms/fields/Upload'
export { UploadInput, type UploadInputProps } from '../forms/fields/Upload/Input'
export { fieldBaseClass } from '../forms/fields/shared'
export { default as useField } from '../forms/useField'
export type { FieldType, Options } from '../forms/useField/types'

View File

@@ -0,0 +1,117 @@
'use client'
import type { ChangeEvent } from 'react'
import { getTranslation } from '@payloadcms/translations'
import React from 'react'
import type { Option } from '../../../elements/ReactSelect/types'
import ReactSelect from '../../../elements/ReactSelect'
import { useTranslation } from '../../../providers/Translation'
import { type FormFieldBase, fieldBaseClass } from '../shared'
import './index.scss'
export type TextInputProps = Omit<FormFieldBase, 'type'> & {
hasMany?: boolean
inputRef?: React.MutableRefObject<HTMLInputElement>
maxRows?: number
minRows?: number
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
showError?: boolean
value?: string
valueToRender?: Option[]
}
export const TextInput: React.FC<TextInputProps> = (props) => {
const {
AfterInput,
BeforeInput,
Description,
Error,
Label,
className,
hasMany,
inputRef,
maxRows,
onChange,
onKeyDown,
path,
placeholder,
readOnly,
rtl,
showError,
style,
value,
valueToRender,
width,
} = props
const { i18n, t } = useTranslation()
return (
<div
className={[
fieldBaseClass,
'text',
className,
showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{Error}
{Label}
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
// prevent adding additional options if maxRows is reached
filterOption={() =>
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
onChange={onChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender}
/>
) : (
<div>
{BeforeInput}
<input
data-rtl={rtl}
disabled={readOnly}
id={`field-${path?.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={value || ''}
/>
{AfterInput}
</div>
)}
{Description}
</div>
)
}

View File

@@ -1,20 +1,19 @@
'use client'
import type { ClientValidate } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useEffect, useState } from 'react'
import type { Option } from '../../../elements/ReactSelect/types'
import type { Props } from './types'
import ReactSelect from '../../../elements/ReactSelect'
import { useConfig } from '../../../providers/Config'
import { useLocale } from '../../../providers/Locale'
import { useTranslation } from '../../../providers/Translation'
import LabelComp from '../../Label'
import useField from '../../useField'
import { withCondition } from '../../withCondition'
import { fieldBaseClass, isFieldRTL } from '../shared'
import { isFieldRTL } from '../shared'
import { TextInput } from './Input'
import './index.scss'
const Text: React.FC<Props> = (props) => {
@@ -66,6 +65,8 @@ const Text: React.FC<Props> = (props) => {
validate: memoizedValidate,
})
console.log('text props', props, pathFromProps || name, 'V', value)
const renderRTL = isFieldRTL({
fieldLocalized: localized,
fieldRTL: rtl,
@@ -115,71 +116,35 @@ const Text: React.FC<Props> = (props) => {
}, [value, hasMany])
return (
<div
className={[
fieldBaseClass,
'number',
className,
showError && 'error',
readOnly && 'read-only',
hasMany && 'has-many',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{Error}
{Label}
{hasMany ? (
<ReactSelect
className={`field-${path.replace(/\./g, '__')}`}
disabled={readOnly}
// prevent adding additional options if maxRows is reached
filterOption={() =>
!maxRows ? true : !(Array.isArray(value) && maxRows && value.length >= maxRows)
}
isClearable
isCreatable
isMulti
isSortable
noOptionsMessage={() => {
const isOverHasMany = Array.isArray(value) && value.length >= maxRows
if (isOverHasMany) {
return t('validation:limitReached', { max: maxRows, value: value.length + 1 })
}
return null
}}
onChange={handleHasManyChange}
options={[]}
placeholder={t('general:enterAValue')}
showError={showError}
value={valueToRender as Option[]}
/>
) : (
<div>
{BeforeInput}
<input
data-rtl={renderRTL}
disabled={readOnly}
id={`field-${path?.replace(/\./g, '__')}`}
name={path}
onChange={(e) => {
<TextInput
AfterInput={AfterInput}
BeforeInput={BeforeInput}
Description={Description}
Error={Error}
Label={Label}
className={className}
hasMany={hasMany}
inputRef={inputRef}
maxRows={maxRows}
minRows={minRows}
onChange={
hasMany
? handleHasManyChange
: (e) => {
setValue(e.target.value)
}}
onKeyDown={onKeyDown}
placeholder={getTranslation(placeholder, i18n)}
ref={inputRef}
type="text"
value={(value as string) || ''}
/>
{AfterInput}
</div>
)}
{Description}
</div>
}
}
path={path}
placeholder={placeholder}
readOnly={readOnly}
required={required}
rtl={renderRTL}
showError={showError}
style={style}
value={(value as string) || ''}
valueToRender={valueToRender as Option[]}
width={width}
/>
)
}

View File

@@ -1,11 +1,11 @@
import type { FormFieldBase } from '../shared'
export type Props = FormFieldBase & {
inputRef?: React.MutableRefObject<HTMLInputElement>
name?: string
hasMany?: boolean
inputRef?: React.MutableRefObject<HTMLInputElement>
maxRows?: number
minRows?: number
name?: string
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>
path?: string
}

View File

@@ -0,0 +1,77 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import React, { type ChangeEvent } from 'react'
import { useTranslation } from '../../../providers/Translation'
import { type FormFieldBase, fieldBaseClass } from '../shared'
import './index.scss'
export type TextAreaInputProps = FormFieldBase & {
onChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
rows?: number
showError?: boolean
value?: string
}
export const TextareaInput: React.FC<TextAreaInputProps> = (props) => {
const {
AfterInput,
BeforeInput,
Description,
Error,
Label,
className,
onChange,
path,
placeholder,
readOnly,
rows,
rtl,
showError,
style,
value,
width,
} = props
const { i18n } = useTranslation()
return (
<div
className={[
fieldBaseClass,
'textarea',
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{Error}
{Label}
{BeforeInput}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
<textarea
className="textarea-element"
data-rtl={rtl}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={onChange}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
value={value || ''}
/>
</div>
</label>
{AfterInput}
{Description}
</div>
)
}

View File

@@ -11,7 +11,8 @@ import { useTranslation } from '../../../providers/Translation'
import LabelComp from '../../Label'
import useField from '../../useField'
import { withCondition } from '../../withCondition'
import { fieldBaseClass, isFieldRTL } from '../shared'
import { isFieldRTL } from '../shared'
import { TextareaInput } from './Input'
import './index.scss'
const Textarea: React.FC<Props> = (props) => {
@@ -67,43 +68,27 @@ const Textarea: React.FC<Props> = (props) => {
})
return (
<div
className={[
fieldBaseClass,
'textarea',
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
<TextareaInput
AfterInput={AfterInput}
BeforeInput={BeforeInput}
Description={Description}
Error={Error}
Label={Label}
className={className}
onChange={(e) => {
setValue(e.target.value)
}}
>
{Error}
{Label}
{BeforeInput}
<label className="textarea-outer" htmlFor={`field-${path.replace(/\./g, '__')}`}>
<div className="textarea-inner">
<div className="textarea-clone" data-value={value || placeholder || ''} />
<textarea
className="textarea-element"
data-rtl={isRTL}
disabled={readOnly}
id={`field-${path.replace(/\./g, '__')}`}
name={path}
onChange={setValue}
placeholder={getTranslation(placeholder, i18n)}
rows={rows}
value={value || ''}
/>
</div>
</label>
{AfterInput}
{Description}
</div>
path={path}
placeholder={getTranslation(placeholder, i18n)}
readOnly={readOnly}
required={required}
rows={rows}
rtl={isRTL}
showError={showError}
style={style}
value={value}
width={width}
/>
)
}

View File

@@ -0,0 +1,189 @@
'use client'
import type { SanitizedCollectionConfig, UploadField } from 'payload/types'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useEffect, useState } from 'react'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types'
import type { ListDrawerProps } from '../../../elements/ListDrawer/types'
import type { FilterOptionsResult } from '../Relationship/types'
import { Button } from '../../../elements/Button'
import { useDocumentDrawer } from '../../../elements/DocumentDrawer'
import FileDetails from '../../../elements/FileDetails'
import { useListDrawer } from '../../../elements/ListDrawer'
import { useTranslation } from '../../../providers/Translation'
import LabelComp from '../../Label'
import { type FormFieldBase, fieldBaseClass } from '../shared'
import './index.scss'
const baseClass = 'upload'
export type UploadInputProps = FormFieldBase & {
api?: string
collection?: SanitizedCollectionConfig
filterOptions?: UploadField['filterOptions']
onChange?: (e) => void
relationTo?: UploadField['relationTo']
serverURL?: string
showError?: boolean
value?: string
}
export const UploadInput: React.FC<UploadInputProps> = (props) => {
const {
Description,
Error,
Label: LabelFromProps,
api = '/api',
className,
collection,
label,
onChange,
readOnly,
relationTo,
required,
serverURL,
showError,
style,
value,
width,
} = props
const Label = LabelFromProps || <LabelComp label={label} required={required} />
const { i18n, t } = useTranslation()
const [file, setFile] = useState(undefined)
const [missingFile, setMissingFile] = useState(false)
const [collectionSlugs] = useState([collection?.slug])
const [filterOptionsResult, setFilterOptionsResult] = useState<FilterOptionsResult>()
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
collectionSlug: collectionSlugs[0],
})
const [ListDrawer, ListDrawerToggler, { closeDrawer: closeListDrawer }] = useListDrawer({
collectionSlugs,
filterOptions: filterOptionsResult,
})
useEffect(() => {
if (value !== null && typeof value !== 'undefined' && value !== '') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
})
if (response.ok) {
const json = await response.json()
setFile(json)
} else {
setMissingFile(true)
setFile(undefined)
}
}
void fetchFile()
} else {
setFile(undefined)
}
}, [value, relationTo, api, serverURL, i18n])
const onSave = useCallback<DocumentDrawerProps['onSave']>(
(args) => {
setMissingFile(false)
onChange(args.doc)
closeDrawer()
},
[onChange, closeDrawer],
)
const onSelect = useCallback<ListDrawerProps['onSelect']>(
(args) => {
setMissingFile(false)
onChange({
id: args.docID,
})
closeListDrawer()
},
[onChange, closeListDrawer],
)
if (collection.upload) {
return (
<div
className={[
fieldBaseClass,
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{/* <GetFilterOptions
{...{
filterOptions,
filterOptionsResult,
path,
relationTo,
setFilterOptionsResult,
}}
/> */}
{Error}
{Label}
{collection?.upload && (
<React.Fragment>
{file && !missingFile && (
<FileDetails
collectionSlug={relationTo}
doc={file}
handleRemove={
readOnly
? undefined
: () => {
onChange(null)
}
}
uploadConfig={collection.upload}
/>
)}
{(!file || missingFile) && (
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__buttons`}>
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:uploadNewLabel', {
label: getTranslation(collection.labels.singular, i18n),
})}
</Button>
</DocumentDrawerToggler>
<ListDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:chooseFromExisting')}
</Button>
</ListDrawerToggler>
</div>
</div>
)}
{Description}
</React.Fragment>
)}
{!readOnly && <DocumentDrawer onSave={onSave} />}
{!readOnly && <ListDrawer onSelect={onSelect} />}
</div>
)
}
return null
}

View File

@@ -1,26 +1,14 @@
'use client'
import { getTranslation } from '@payloadcms/translations'
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback } from 'react'
import type { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types'
import type { ListDrawerProps } from '../../../elements/ListDrawer/types'
import type { FilterOptionsResult } from '../Relationship/types'
import type { Props } from './types'
import { Button } from '../../../elements/Button'
import { useDocumentDrawer } from '../../../elements/DocumentDrawer'
import FileDetails from '../../../elements/FileDetails'
import { GetFilterOptions } from '../../../elements/GetFilterOptions'
import { useListDrawer } from '../../../elements/ListDrawer'
import { useConfig } from '../../../providers/Config'
import { useTranslation } from '../../../providers/Translation'
import LabelComp from '../../Label'
import useField from '../../useField'
import { fieldBaseClass } from '../shared'
import { UploadInput } from './Input'
import './index.scss'
const baseClass = 'upload'
const Upload: React.FC<Props> = (props) => {
const {
Description,
@@ -57,7 +45,7 @@ const Upload: React.FC<Props> = (props) => {
[validate, required],
)
const { path, setValue, showError, value } = useField({
const { path, setValue, showError, value } = useField<string>({
path: pathFromProps,
validate: memoizedValidate,
})
@@ -70,134 +58,27 @@ const Upload: React.FC<Props> = (props) => {
[setValue],
)
const { i18n, t } = useTranslation()
const [file, setFile] = useState(undefined)
const [missingFile, setMissingFile] = useState(false)
const [collectionSlugs] = useState([collection?.slug])
const [filterOptionsResult, setFilterOptionsResult] = useState<FilterOptionsResult>()
const [DocumentDrawer, DocumentDrawerToggler, { closeDrawer }] = useDocumentDrawer({
collectionSlug: collectionSlugs[0],
})
const [ListDrawer, ListDrawerToggler, { closeDrawer: closeListDrawer }] = useListDrawer({
collectionSlugs,
filterOptions: filterOptionsResult,
})
useEffect(() => {
if (value !== null && typeof value !== 'undefined' && value !== '') {
const fetchFile = async () => {
const response = await fetch(`${serverURL}${api}/${relationTo}/${value}`, {
credentials: 'include',
headers: {
'Accept-Language': i18n.language,
},
})
if (response.ok) {
const json = await response.json()
setFile(json)
} else {
setMissingFile(true)
setFile(undefined)
}
}
fetchFile()
} else {
setFile(undefined)
}
}, [value, relationTo, api, serverURL, i18n])
const onSave = useCallback<DocumentDrawerProps['onSave']>(
(args) => {
setMissingFile(false)
onChange(args.doc)
closeDrawer()
},
[onChange, closeDrawer],
)
const onSelect = useCallback<ListDrawerProps['onSelect']>(
(args) => {
setMissingFile(false)
onChange({
id: args.docID,
})
closeListDrawer()
},
[onChange, closeListDrawer],
)
if (collection.upload) {
return (
<div
className={[
fieldBaseClass,
baseClass,
className,
showError && 'error',
readOnly && 'read-only',
]
.filter(Boolean)
.join(' ')}
style={{
...style,
width,
}}
>
{/* <GetFilterOptions
{...{
filterOptions,
filterOptionsResult,
path,
relationTo,
setFilterOptionsResult,
}}
/> */}
{Error}
{Label}
{collection?.upload && (
<React.Fragment>
{file && !missingFile && (
<FileDetails
collectionSlug={relationTo}
doc={file}
handleRemove={
readOnly
? undefined
: () => {
onChange(null)
}
}
uploadConfig={collection.upload}
/>
)}
{(!file || missingFile) && (
<div className={`${baseClass}__wrap`}>
<div className={`${baseClass}__buttons`}>
<DocumentDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:uploadNewLabel', {
label: getTranslation(collection.labels.singular, i18n),
})}
</Button>
</DocumentDrawerToggler>
<ListDrawerToggler className={`${baseClass}__toggler`} disabled={readOnly}>
<Button buttonStyle="secondary" disabled={readOnly} el="div">
{t('fields:chooseFromExisting')}
</Button>
</ListDrawerToggler>
</div>
</div>
)}
{Description}
</React.Fragment>
)}
{!readOnly && <DocumentDrawer onSave={onSave} />}
{!readOnly && <ListDrawer onSelect={onSelect} />}
</div>
<UploadInput
Description={Description}
Error={Error}
Label={Label}
api={api}
className={className}
collection={collection}
filterOptions={filterOptions}
onChange={onChange}
path={path}
readOnly={readOnly}
relationTo={relationTo}
required={required}
serverURL={serverURL}
showError={showError}
style={style}
value={value}
width={width}
/>
)
}