chore: slate upload ssr

This commit is contained in:
James
2024-02-22 14:20:13 -05:00
parent 732402159c
commit 56c325b526
58 changed files with 410 additions and 97 deletions

View File

@@ -14,7 +14,7 @@ import ol from './ol'
// import relationship from './relationship'
// import textAlign from './textAlign'
import ul from './ul'
// import upload from './upload'
import upload from './upload'
const elements: Record<string, RichTextCustomElement> = {
blockquote,
@@ -31,7 +31,7 @@ const elements: Record<string, RichTextCustomElement> = {
// relationship,
// textAlign,
ul,
// upload,
upload,
}
export default elements

View File

@@ -1,10 +1,11 @@
'use client'
import type { FormState } from '@payloadcms/ui'
import { useModal } from '@faceless-ui/modal'
import { getTranslation } from '@payloadcms/translations'
import {
Button,
FormState,
Popup,
Translation,
getFormState,
@@ -21,11 +22,13 @@ import React, { useCallback, useEffect, useState } from 'react'
import { Editor, Node, Transforms } from 'slate'
import { ReactEditor, useSlate } from 'slate-react'
import type { LinkElementType } from '../types'
import { useElement } from '../../../providers/ElementProvider'
import { LinkDrawer } from '../LinkDrawer'
import { linkFieldsSchemaPath } from '../shared'
import { unwrapLink } from '../utilities'
import './index.scss'
import { LinkElementType } from '../types'
const baseClass = 'rich-text-link'
@@ -58,10 +61,10 @@ export const LinkElement = () => {
const { attributes, children, editorRef, element, fieldProps, schemaPath } =
useElement<LinkElementType>()
const linkFieldsSchemaPath = `${schemaPath}.link.fields`
const fieldMapPath = `${schemaPath}.${linkFieldsSchemaPath}`
const { richTextComponentMap } = fieldProps
const fieldMap = richTextComponentMap.get(linkFieldsSchemaPath)
const fieldMap = richTextComponentMap.get(fieldMapPath)
const editor = useSlate()
const config = useConfig()
@@ -102,7 +105,7 @@ export const LinkElement = () => {
data,
docPreferences,
operation: 'update',
schemaPath: linkFieldsSchemaPath,
schemaPath: fieldMapPath,
},
serverURL: config.serverURL,
})
@@ -111,7 +114,7 @@ export const LinkElement = () => {
}
awaitInitialState()
}, [renderModal, element, user, locale, t, getDocPreferences, config])
}, [renderModal, element, user, locale, t, getDocPreferences, config, id, fieldMapPath])
return (
<span className={baseClass} {...attributes}>

View File

@@ -42,9 +42,9 @@ const UploadButton: React.FC<ButtonProps> = ({ enabledCollectionSlugs }) => {
})
const onSelect = useCallback(
({ collectionConfig, docID }) => {
({ collectionSlug, docID }) => {
insertUpload(editor, {
relationTo: collectionConfig.slug,
relationTo: collectionSlug,
value: {
id: docID,
},

View File

@@ -9,49 +9,50 @@ import {
Form,
FormSubmit,
RenderFields,
buildStateFromSchema,
fieldTypes,
getFormState,
useAuth,
useConfig,
useDocumentInfo,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import { sanitizeFields } from 'payload/config'
import { deepCopyObject } from 'payload/utilities'
import React, { useCallback, useEffect, useState } from 'react'
import { Transforms } from 'slate'
import { ReactEditor, useSlateStatic } from 'slate-react'
import type { ElementProps } from '..'
import type { FormFieldBase } from '../../../../../../../ui/src/forms/fields/shared'
import type { UploadElementType } from '../../types'
export const UploadDrawer: React.FC<
ElementProps & {
drawerSlug: string
relatedCollection: SanitizedCollectionConfig
import { FieldPathProvider } from '../../../../../../../ui/src/forms/FieldPathProvider'
import { uploadFieldsSchemaPath } from '../../shared'
export const UploadDrawer: React.FC<{
drawerSlug: string
element: UploadElementType
fieldProps: FormFieldBase & {
name: string
richTextComponentMap: Map<string, React.ReactNode>
}
> = (props) => {
relatedCollection: SanitizedCollectionConfig
schemaPath: string
}> = (props) => {
const editor = useSlateStatic()
const { drawerSlug, element, fieldProps, relatedCollection } = props
const { drawerSlug, element, fieldProps, relatedCollection, schemaPath } = props
const { i18n, t } = useTranslation()
const { code: locale } = useLocale()
const { user } = useAuth()
const { closeModal } = useModal()
const { getDocPreferences } = useDocumentInfo()
const { id, getDocPreferences } = useDocumentInfo()
const [initialState, setInitialState] = useState({})
const fieldSchemaUnsanitized =
fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields
const config = useConfig()
const { richTextComponentMap } = fieldProps
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const relatedFieldSchemaPath = `${uploadFieldsSchemaPath}.${relatedCollection.slug}`
const fieldMap = richTextComponentMap.get(relatedFieldSchemaPath)
const config = useConfig()
const handleUpdateEditData = useCallback(
(_, data) => {
@@ -68,31 +69,38 @@ export const UploadDrawer: React.FC<
)
useEffect(() => {
// Sanitize custom fields here
const validRelationships = config.collections.map((c) => c.slug) || []
const fieldSchema = sanitizeFields({
config: config,
fields: fieldSchemaUnsanitized,
validRelationships,
})
const data = deepCopyObject(element?.fields || {})
const awaitInitialState = async () => {
const preferences = await getDocPreferences()
const state = await buildStateFromSchema({
config,
data: deepCopyObject(element?.fields || {}),
fieldSchema,
locale,
operation: 'update',
preferences,
t,
user,
const docPreferences = await getDocPreferences()
const state = await getFormState({
apiRoute: config.routes.api,
body: {
id,
data,
docPreferences,
operation: 'update',
schemaPath: `${schemaPath}.${uploadFieldsSchemaPath}.${relatedCollection.slug}`,
},
serverURL: config.serverURL,
})
setInitialState(state)
}
awaitInitialState()
}, [fieldSchemaUnsanitized, config, element.fields, user, locale, t, getDocPreferences])
}, [
config,
element?.fields,
user,
locale,
t,
getDocPreferences,
id,
schemaPath,
relatedCollection.slug,
])
return (
<Drawer
@@ -101,10 +109,12 @@ export const UploadDrawer: React.FC<
label: getTranslation(relatedCollection.labels.singular, i18n),
})}
>
<Form initialState={initialState} onSubmit={handleUpdateEditData}>
<RenderFields fieldSchema={fieldSchema} fieldTypes={fieldTypes} readOnly={false} />
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
</Form>
<FieldPathProvider path="" schemaPath="">
<Form initialState={initialState} onSubmit={handleUpdateEditData}>
<RenderFields fieldMap={Array.isArray(fieldMap) ? fieldMap : []} />
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
</Form>
</FieldPathProvider>
</Drawer>
)
}

View File

@@ -1,7 +1,6 @@
'use client'
import type { SanitizedCollectionConfig } from 'payload/types'
import type { HTMLAttributes } from 'react'
import { getTranslation } from '@payloadcms/translations'
import {
@@ -20,9 +19,12 @@ import React, { useCallback, useReducer, useState } from 'react'
import { Transforms } from 'slate'
import { ReactEditor, useFocused, useSelected, useSlateStatic } from 'slate-react'
import type { FieldProps } from '../../../../types'
import type { FormFieldBase } from '../../../../../../ui/src/forms/fields/shared'
import type { UploadElementType } from '../types'
import { useElement } from '../../../providers/ElementProvider'
import { EnabledRelationshipsCondition } from '../../EnabledRelationshipsCondition'
import { uploadFieldsSchemaPath, uploadName } from '../shared'
import { UploadDrawer } from './UploadDrawer'
import './index.scss'
@@ -32,23 +34,22 @@ const initialParams = {
depth: 0,
}
export type ElementProps = {
attributes: HTMLAttributes<HTMLDivElement>
children: React.ReactNode
element: any
enabledCollectionSlugs: string[]
fieldProps: FieldProps
type Props = FormFieldBase & {
name: string
richTextComponentMap: Map<string, React.ReactNode>
}
const Element: React.FC<ElementProps> = (props) => {
const Element: React.FC<Props & { enabledCollectionSlugs?: string[] }> = ({
enabledCollectionSlugs,
}) => {
const {
attributes,
children,
element: { relationTo, value },
element,
enabledCollectionSlugs,
fieldProps,
} = props
schemaPath,
} = useElement<UploadElementType>()
const {
collections,
@@ -83,7 +84,7 @@ const Element: React.FC<ElementProps> = (props) => {
{ initialParams },
)
const thumbnailSRC = useThumbnail(relatedCollection, data)
const thumbnailSRC = useThumbnail(relatedCollection.upload, data)
const removeUpload = useCallback(() => {
const elementPath = ReactEditor.findPath(editor, element)
@@ -103,8 +104,6 @@ const Element: React.FC<ElementProps> = (props) => {
Transforms.setNodes(editor, newNode, { at: elementPath })
// setRelatedCollection(collections.find((coll) => coll.slug === collectionConfig.slug));
setParams({
...initialParams,
cacheBust, // do this to get the usePayloadAPI to re-fetch the data even though the URL string hasn't changed
@@ -117,17 +116,17 @@ const Element: React.FC<ElementProps> = (props) => {
)
const swapUpload = React.useCallback(
({ collectionConfig, docID }) => {
({ collectionSlug, docID }) => {
const newNode = {
children: [{ text: ' ' }],
relationTo: collectionConfig.slug,
type: 'upload',
relationTo: collectionSlug,
type: uploadName,
value: { id: docID },
}
const elementPath = ReactEditor.findPath(editor, element)
setRelatedCollection(collections.find((coll) => coll.slug === collectionConfig.slug))
setRelatedCollection(collections.find((coll) => coll.slug === collectionSlug))
Transforms.setNodes(editor, newNode, { at: elementPath })
@@ -137,7 +136,8 @@ const Element: React.FC<ElementProps> = (props) => {
[closeListDrawer, editor, element, collections],
)
const customFields = fieldProps?.admin?.upload?.collections?.[relatedCollection.slug]?.fields
const relatedFieldSchemaPath = `${uploadFieldsSchemaPath}.${relatedCollection.slug}`
const customFieldsMap = fieldProps.richTextComponentMap.get(relatedFieldSchemaPath)
return (
<div
@@ -157,10 +157,10 @@ const Element: React.FC<ElementProps> = (props) => {
{getTranslation(relatedCollection.labels.singular, i18n)}
</div>
<div className={`${baseClass}__actions`}>
{customFields?.length > 0 && (
{Boolean(customFieldsMap) && (
<DrawerToggler
className={`${baseClass}__upload-drawer-toggler`}
disabled={fieldProps?.admin?.readOnly}
disabled={fieldProps?.readOnly}
slug={drawerSlug}
>
<Button
@@ -177,11 +177,11 @@ const Element: React.FC<ElementProps> = (props) => {
)}
<ListDrawerToggler
className={`${baseClass}__list-drawer-toggler`}
disabled={fieldProps?.admin?.readOnly}
disabled={fieldProps?.readOnly}
>
<Button
buttonStyle="icon-label"
disabled={fieldProps?.admin?.readOnly}
disabled={fieldProps?.readOnly}
el="div"
icon="swap"
onClick={() => {
@@ -194,7 +194,7 @@ const Element: React.FC<ElementProps> = (props) => {
<Button
buttonStyle="icon-label"
className={`${baseClass}__removeButton`}
disabled={fieldProps?.admin?.readOnly}
disabled={fieldProps?.readOnly}
icon="x"
onClick={(e) => {
e.preventDefault()
@@ -215,12 +215,12 @@ const Element: React.FC<ElementProps> = (props) => {
{children}
{value?.id && <DocumentDrawer onSave={updateUpload} />}
<ListDrawer onSelect={swapUpload} />
<UploadDrawer drawerSlug={drawerSlug} relatedCollection={relatedCollection} {...props} />
<UploadDrawer {...{ drawerSlug, element, fieldProps, relatedCollection, schemaPath }} />
</div>
)
}
export default (props: ElementProps): React.ReactNode => {
export default (props: Props): React.ReactNode => {
return (
<EnabledRelationshipsCondition {...props} uploads>
<Element {...props} />

View File

@@ -1,9 +1,15 @@
import type { RichTextCustomElement } from '../../..'
import Button from './Button'
import Element from './Element'
import plugin from './plugin'
import { WithUpload } from './plugin'
import { uploadName } from './shared'
export default {
const upload: RichTextCustomElement = {
name: uploadName,
Button,
Element,
plugins: [plugin],
plugins: [WithUpload],
}
export default upload

View File

@@ -1,10 +1,18 @@
const withRelationship = (incomingEditor) => {
const editor = incomingEditor
const { isVoid } = editor
'use client'
editor.isVoid = (element) => (element.type === 'upload' ? true : isVoid(element))
import type React from 'react'
return editor
import { useSlatePlugin } from '../../../utilities/useSlatePlugin'
import { uploadName } from './shared'
export const WithUpload: React.FC = () => {
useSlatePlugin('withUpload', (incomingEditor) => {
const editor = incomingEditor
const { isVoid } = editor
editor.isVoid = (element) => (element.type === uploadName ? true : isVoid(element))
return editor
})
return null
}
export default withRelationship

View File

@@ -0,0 +1,2 @@
export const uploadName = 'upload'
export const uploadFieldsSchemaPath = 'upload.fields'

View File

@@ -0,0 +1,9 @@
import type { Element } from 'slate'
export type UploadElementType = Element & {
fields: Record<string, unknown>
relationTo: string
value: {
id: number | string
} | null
}

View File

@@ -9,7 +9,9 @@ import React from 'react'
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from '.'
import elementTypes from './field/elements'
import { linkFieldsSchemaPath } from './field/elements/link/shared'
import { transformExtraFields } from './field/elements/link/utilities'
import { uploadFieldsSchemaPath } from './field/elements/upload/shared'
import leafTypes from './field/leaves'
export const getGenerateComponentMap =
@@ -81,13 +83,44 @@ export const getGenerateComponentMap =
readOnly: false,
})
componentMap.set('link.fields', mappedFields)
componentMap.set(linkFieldsSchemaPath, mappedFields)
break
}
case 'upload':
case 'upload': {
const uploadEnabledCollections = config.collections.filter(
({ admin: { enableRichTextRelationship, hidden }, upload }) => {
if (hidden === true) {
return false
}
return enableRichTextRelationship && Boolean(upload) === true
},
)
uploadEnabledCollections.forEach((collection) => {
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
const uploadFields = sanitizeFields({
config,
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
validRelationships,
})
const mappedFields = mapFields({
config,
fieldSchema: uploadFields,
operation: 'update',
permissions: {},
readOnly: false,
})
componentMap.set(`${uploadFieldsSchemaPath}.${collection.slug}`, mappedFields)
}
})
break
}
case 'relationship':
break

View File

@@ -9,6 +9,7 @@ import type { AdapterArguments, RichTextCustomElement } from '.'
import elementTypes from './field/elements'
import { linkFieldsSchemaPath } from './field/elements/link/shared'
import { transformExtraFields } from './field/elements/link/utilities'
import { uploadFieldsSchemaPath } from './field/elements/upload/shared'
export const getGenerateSchemaMap =
(args: AdapterArguments): RichTextAdapter['generateSchemaMap'] =>
@@ -29,18 +30,44 @@ export const getGenerateSchemaMap =
switch (element.name) {
case 'link': {
const linkFields = sanitizeFields({
config: config,
config,
fields: transformExtraFields(args.admin?.link?.fields, config, i18n),
validRelationships,
})
schemaMap.set(`${schemaPath}.${linkFieldsSchemaPath}`, linkFields)
return
break
}
case 'upload':
case 'upload': {
const uploadEnabledCollections = config.collections.filter(
({ admin: { enableRichTextRelationship, hidden }, upload }) => {
if (hidden === true) {
return false
}
return enableRichTextRelationship && Boolean(upload) === true
},
)
uploadEnabledCollections.forEach((collection) => {
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
const uploadFields = sanitizeFields({
config,
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
validRelationships,
})
schemaMap.set(
`${schemaPath}.${uploadFieldsSchemaPath}.${collection.slug}`,
uploadFields,
)
}
})
break
}
case 'relationship':
break