feat: separate Input from Upload, Text & TextArea fields, get plugin-seo to load without errors
This commit is contained in:
@@ -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>
|
||||
—
|
||||
<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={{
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
—
|
||||
<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={{
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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'
|
||||
|
||||
117
packages/ui/src/forms/fields/Text/Input.tsx
Normal file
117
packages/ui/src/forms/fields/Text/Input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
77
packages/ui/src/forms/fields/Textarea/Input.tsx
Normal file
77
packages/ui/src/forms/fields/Textarea/Input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
"app/**/*.tsx",
|
||||
"scripts/**/*.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user