feat(richtext-lexical): uploads
This commit is contained in:
@@ -124,7 +124,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
|
||||
const formContent = useMemo(() => {
|
||||
return (
|
||||
reducedBlock &&
|
||||
initialState && (
|
||||
initialState !== false && (
|
||||
<FieldPathProvider path="" schemaPath="">
|
||||
<Form
|
||||
fields={fieldMap}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@payloadcms/ui'
|
||||
import { useFieldPath } from '@payloadcms/ui'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||
import './index.scss'
|
||||
@@ -74,21 +75,22 @@ export const LinkDrawer: React.FC<Props> = ({ drawerSlug, handleModalSubmit, sta
|
||||
)
|
||||
|
||||
return (
|
||||
initialState !== false && (
|
||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink') ?? ''}>
|
||||
<Drawer className={baseClass} slug={drawerSlug} title={t('fields:editLink') ?? ''}>
|
||||
{initialState !== false && (
|
||||
<FieldPathProvider path="" schemaPath="">
|
||||
<Form
|
||||
fields={Array.isArray(fieldMap) ? fieldMap : []}
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleModalSubmit}
|
||||
uuid={uuid()}
|
||||
>
|
||||
<RenderFields fieldMap={Array.isArray(fieldMap) ? fieldMap : []} forceRender />
|
||||
|
||||
<FormSubmit>{t('general:submit')}</FormSubmit>
|
||||
</Form>
|
||||
</FieldPathProvider>
|
||||
</Drawer>
|
||||
)
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const LinkFeature: FeatureProviderProviderServer<LinkFeatureServerProps,
|
||||
disabledCollections: props.disabledCollections,
|
||||
enabledCollections: props.enabledCollections,
|
||||
} as ExclusiveLinkCollectionsProps,
|
||||
generateSchemaMap: ({ config, props, schemaMap, schemaPath }) => {
|
||||
generateSchemaMap: ({ config, props }) => {
|
||||
const i18n = initI18n({ config: config.i18n, context: 'client', translations })
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,24 +6,24 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import { getTranslation } from '@payloadcms/translations'
|
||||
import {
|
||||
Drawer,
|
||||
FieldPathProvider,
|
||||
Form,
|
||||
type FormProps,
|
||||
type FormState,
|
||||
FormSubmit,
|
||||
RenderFields,
|
||||
buildStateFromSchema,
|
||||
fieldTypes,
|
||||
useAuth,
|
||||
getFormState,
|
||||
useConfig,
|
||||
useDocumentInfo,
|
||||
useLocale,
|
||||
useFieldPath,
|
||||
useTranslation,
|
||||
} from '@payloadcms/ui'
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import { sanitizeFields } from 'payload/config'
|
||||
import { deepCopyObject } from 'payload/utilities'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import type { ElementProps } from '..'
|
||||
import type { UploadFeatureProps } from '../..'
|
||||
import type { UploadData, UploadNode } from '../../nodes/UploadNode'
|
||||
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider'
|
||||
@@ -46,28 +46,58 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
||||
} = props
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const { editorConfig, field } = useEditorConfigContext()
|
||||
|
||||
const { closeModal } = useModal()
|
||||
|
||||
const { i18n, t } = useTranslation()
|
||||
const { code: locale } = useLocale()
|
||||
const { user } = useAuth()
|
||||
const { closeModal } = useModal()
|
||||
const { getDocPreferences } = useDocumentInfo()
|
||||
const [initialState, setInitialState] = useState({})
|
||||
const fieldSchemaUnsanitized = (
|
||||
editorConfig?.resolvedFeatureMap.get('upload')?.props as UploadFeatureProps
|
||||
)?.collections?.[relatedCollection.slug]?.fields
|
||||
const { id } = useDocumentInfo()
|
||||
const { schemaPath } = useFieldPath()
|
||||
const config = useConfig()
|
||||
const [initialState, setInitialState] = useState<FormState | false>(false)
|
||||
const {
|
||||
field: { richTextComponentMap },
|
||||
} = useEditorConfigContext()
|
||||
|
||||
// Sanitize custom fields here
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
const fieldSchema = sanitizeFields({
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
fields: fieldSchemaUnsanitized,
|
||||
validRelationships,
|
||||
})
|
||||
const componentMapRenderedFieldsPath = `feature.upload.fields.${relatedCollection.slug}`
|
||||
const schemaFieldsPath = `${schemaPath}.feature.upload.${relatedCollection.slug}`
|
||||
|
||||
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema
|
||||
|
||||
useEffect(() => {
|
||||
const awaitInitialState = async () => {
|
||||
const state = await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
data: deepCopyObject(fields || {}),
|
||||
operation: 'update',
|
||||
schemaPath: schemaFieldsPath,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
}) // Form State
|
||||
|
||||
setInitialState(state)
|
||||
}
|
||||
|
||||
void awaitInitialState()
|
||||
}, [config.routes.api, config.serverURL, schemaFieldsPath, id, fields])
|
||||
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
return await getFormState({
|
||||
apiRoute: config.routes.api,
|
||||
body: {
|
||||
id,
|
||||
formState: prevFormState,
|
||||
operation: 'update',
|
||||
schemaPath: schemaFieldsPath,
|
||||
},
|
||||
serverURL: config.serverURL,
|
||||
})
|
||||
},
|
||||
|
||||
[config.routes.api, config.serverURL, schemaFieldsPath, id],
|
||||
)
|
||||
|
||||
const handleUpdateEditData = useCallback(
|
||||
(_, data) => {
|
||||
@@ -88,33 +118,6 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
||||
[closeModal, editor, drawerSlug, nodeKey],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// Sanitize custom fields here
|
||||
const validRelationships = config.collections.map((c) => c.slug) || []
|
||||
const fieldSchema = sanitizeFields({
|
||||
// TODO: fix this
|
||||
// @ts-expect-error-next-line
|
||||
config,
|
||||
fields: fieldSchemaUnsanitized,
|
||||
validRelationships,
|
||||
})
|
||||
|
||||
const awaitInitialState = async () => {
|
||||
const preferences = await getDocPreferences()
|
||||
const state = await buildStateFromSchema({
|
||||
data: deepCopyObject(fields || {}),
|
||||
fieldSchema,
|
||||
locale,
|
||||
operation: 'update',
|
||||
preferences,
|
||||
req,
|
||||
})
|
||||
setInitialState(state)
|
||||
}
|
||||
|
||||
void awaitInitialState()
|
||||
}, [user, locale, t, getDocPreferences, fields, fieldSchemaUnsanitized, config])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
slug={drawerSlug}
|
||||
@@ -122,11 +125,20 @@ export const ExtraFieldsUploadDrawer: React.FC<
|
||||
label: getTranslation(relatedCollection.labels.singular, i18n),
|
||||
})}
|
||||
>
|
||||
<Form initialState={initialState} onSubmit={handleUpdateEditData}>
|
||||
[RenderFields]
|
||||
{/* <RenderFields fieldSchema={fieldSchema} fieldTypes={fieldTypes} readOnly={false} /> */}
|
||||
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
|
||||
</Form>
|
||||
{initialState !== false && (
|
||||
<FieldPathProvider path="" schemaPath="">
|
||||
<Form
|
||||
fields={fieldMap}
|
||||
initialState={initialState}
|
||||
onChange={[onChange]}
|
||||
onSubmit={handleUpdateEditData}
|
||||
uuid={uuid()}
|
||||
>
|
||||
<RenderFields fieldMap={fieldMap} forceRender readOnly={false} />
|
||||
<FormSubmit>{t('fields:saveChanges')}</FormSubmit>
|
||||
</Form>
|
||||
</FieldPathProvider>
|
||||
)}
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
import { $getNodeByKey } from 'lexical'
|
||||
import React, { useCallback, useReducer, useState } from 'react'
|
||||
|
||||
import type { UploadFeatureProps } from '..'
|
||||
import type { ClientComponentProps } from '../../types'
|
||||
import type { UploadFeaturePropsClient } from '../feature.client'
|
||||
import type { UploadData } from '../nodes/UploadNode'
|
||||
|
||||
import { useEditorConfigContext } from '../../../lexical/config/client/EditorConfigProvider'
|
||||
@@ -95,9 +96,10 @@ const Component: React.FC<ElementProps> = (props) => {
|
||||
[setParams, cacheBust, closeDrawer],
|
||||
)
|
||||
|
||||
const customFields = (
|
||||
editorConfig?.resolvedFeatureMap?.get('upload')?.props as UploadFeatureProps
|
||||
)?.collections?.[relatedCollection.slug]?.fields
|
||||
const hasExtraFields = (
|
||||
editorConfig?.resolvedFeatureMap?.get('upload')
|
||||
?.clientFeatureProps as ClientComponentProps<UploadFeaturePropsClient>
|
||||
).collections?.[relatedCollection.slug]?.hasExtraFields
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,16 +113,14 @@ const Component: React.FC<ElementProps> = (props) => {
|
||||
</div>
|
||||
<div className={`${baseClass}__topRowRightPanel`}>
|
||||
<div className={`${baseClass}__collectionLabel`}>
|
||||
{/* TODO: fix this */}
|
||||
{/* @ts-expect-error-next-line */}
|
||||
{getTranslation(relatedCollection.labels.singular, i18n)}
|
||||
</div>
|
||||
{editor.isEditable() && (
|
||||
<div className={`${baseClass}__actions`}>
|
||||
{customFields?.length > 0 && (
|
||||
{hasExtraFields ? (
|
||||
<DrawerToggler
|
||||
className={`${baseClass}__upload-drawer-toggler`}
|
||||
disabled={field?.admin?.readOnly}
|
||||
disabled={field?.readOnly}
|
||||
slug={drawerSlug}
|
||||
>
|
||||
<Button
|
||||
@@ -134,11 +134,11 @@ const Component: React.FC<ElementProps> = (props) => {
|
||||
tooltip={t('fields:editRelationship')}
|
||||
/>
|
||||
</DrawerToggler>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
disabled={field?.admin?.readOnly}
|
||||
disabled={field?.readOnly}
|
||||
el="div"
|
||||
icon="swap"
|
||||
onClick={() => {
|
||||
@@ -152,7 +152,7 @@ const Component: React.FC<ElementProps> = (props) => {
|
||||
<Button
|
||||
buttonStyle="icon-label"
|
||||
className={`${baseClass}__removeButton`}
|
||||
disabled={field?.admin?.readOnly}
|
||||
disabled={field?.readOnly}
|
||||
icon="x"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -77,11 +77,11 @@ const UploadDrawerComponent: React.FC<Props> = ({ enabledCollectionSlugs }) => {
|
||||
}, [editor, openDrawer])
|
||||
|
||||
const onSelect = useCallback(
|
||||
({ collectionConfig, docID }) => {
|
||||
({ collectionSlug, docID }) => {
|
||||
insertUpload({
|
||||
id: docID,
|
||||
editor,
|
||||
relationTo: collectionConfig.slug,
|
||||
relationTo: collectionSlug,
|
||||
replaceNodeKey,
|
||||
})
|
||||
closeDrawer()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import type { FeatureProviderProviderClient } from '../types'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { UploadIcon } from '../../lexical/ui/icons/Upload'
|
||||
import { createClientComponent } from '../createClientComponent'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
|
||||
import { UploadNode } from './nodes/UploadNode'
|
||||
import { UploadPlugin } from './plugin'
|
||||
|
||||
export type UploadFeaturePropsClient = {
|
||||
collections: {
|
||||
[collection: string]: {
|
||||
hasExtraFields: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const UploadFeatureClient: FeatureProviderProviderClient<UploadFeaturePropsClient> = (props) => {
|
||||
return {
|
||||
clientFeatureProps: props,
|
||||
feature: () => ({
|
||||
clientFeatureProps: props,
|
||||
nodes: [UploadNode],
|
||||
plugins: [
|
||||
{
|
||||
Component: UploadPlugin,
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
slashMenu: {
|
||||
options: [
|
||||
{
|
||||
displayName: 'Basic',
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('upload', {
|
||||
Icon: UploadIcon,
|
||||
displayName: 'Upload',
|
||||
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_UPLOAD_WITH_DRAWER_COMMAND, {
|
||||
replace: false,
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const UploadFeatureClientComponent = createClientComponent(UploadFeatureClient)
|
||||
@@ -3,12 +3,11 @@ import type { Field } from 'payload/types'
|
||||
import payload from 'payload'
|
||||
|
||||
import type { HTMLConverter } from '../converters/html/converter/types'
|
||||
import type { FeatureProvider } from '../types'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
import type { FeatureProviderProviderServer } from '../types'
|
||||
import type { UploadFeaturePropsClient } from './feature.client'
|
||||
|
||||
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
|
||||
import { INSERT_UPLOAD_WITH_DRAWER_COMMAND } from './drawer/commands'
|
||||
import { UploadNode } from './nodes/UploadNode'
|
||||
import { UploadFeatureClientComponent } from './feature.client'
|
||||
import { type SerializedUploadNode, UploadNode } from './nodes/UploadNode'
|
||||
import { uploadPopulationPromiseHOC } from './populationPromise'
|
||||
import { uploadValidation } from './validate'
|
||||
|
||||
@@ -27,13 +26,43 @@ function getAbsoluteURL(url: string): string {
|
||||
return url?.startsWith('http') ? url : (payload?.config?.serverURL || '') + url
|
||||
}
|
||||
|
||||
export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
export const UploadFeature: FeatureProviderProviderServer<
|
||||
UploadFeatureProps,
|
||||
UploadFeaturePropsClient
|
||||
> = (props) => {
|
||||
if (!props) {
|
||||
props = { collections: {} }
|
||||
}
|
||||
|
||||
const clientProps: UploadFeaturePropsClient = { collections: {} }
|
||||
if (props.collections) {
|
||||
for (const collection in props.collections) {
|
||||
clientProps.collections[collection] = {
|
||||
hasExtraFields: props.collections[collection].fields.length >= 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
feature: () => {
|
||||
return {
|
||||
ClientComponent: UploadFeatureClientComponent,
|
||||
clientFeatureProps: clientProps,
|
||||
generateSchemaMap: ({ props }) => {
|
||||
if (!props?.collections) return {}
|
||||
|
||||
const map: {
|
||||
[key: string]: Field[]
|
||||
} = {}
|
||||
|
||||
for (const collection in props.collections) {
|
||||
map[collection] = props.collections[collection].fields
|
||||
}
|
||||
|
||||
return map
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
type: UploadNode.getType(),
|
||||
converters: {
|
||||
html: {
|
||||
converter: async ({ node }) => {
|
||||
@@ -96,39 +125,10 @@ export const UploadFeature = (props?: UploadFeatureProps): FeatureProvider => {
|
||||
validations: [uploadValidation()],
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
{
|
||||
Component: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('./plugin').then((module) => module.UploadPlugin),
|
||||
position: 'normal',
|
||||
},
|
||||
],
|
||||
props,
|
||||
slashMenu: {
|
||||
options: [
|
||||
{
|
||||
displayName: 'Basic',
|
||||
key: 'basic',
|
||||
options: [
|
||||
new SlashMenuOption('upload', {
|
||||
Icon: () =>
|
||||
// @ts-expect-error-next-line
|
||||
import('../../lexical/ui/icons/Upload').then((module) => module.UploadIcon),
|
||||
displayName: 'Upload',
|
||||
keywords: ['upload', 'image', 'file', 'img', 'picture', 'photo', 'media'],
|
||||
onSelect: ({ editor }) => {
|
||||
editor.dispatchCommand(INSERT_UPLOAD_WITH_DRAWER_COMMAND, {
|
||||
replace: false,
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
},
|
||||
key: 'upload',
|
||||
serverFeatureProps: props,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UploadFeatureProps } from '.'
|
||||
import type { UploadFeatureProps } from '@payloadcms/richtext-lexical'
|
||||
|
||||
import type { PopulationPromise } from '../types'
|
||||
import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
|
||||
|
||||
@@ -7,20 +7,26 @@ import type { SerializedUploadNode } from './nodes/UploadNode'
|
||||
import { CAN_USE_DOM } from '../../lexical/utils/canUseDOM'
|
||||
|
||||
export const uploadValidation = (): NodeValidation<SerializedUploadNode> => {
|
||||
const uploadValidation: NodeValidation<SerializedUploadNode> = async ({
|
||||
const uploadValidation: NodeValidation<SerializedUploadNode> = ({
|
||||
node,
|
||||
payloadConfig,
|
||||
validation,
|
||||
validation: {
|
||||
options: {
|
||||
req: {
|
||||
payload: { config, db },
|
||||
t,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) => {
|
||||
if (!CAN_USE_DOM) {
|
||||
const idField = payloadConfig.collections
|
||||
const idField = config.collections
|
||||
.find(({ slug }) => slug === node.relationTo)
|
||||
.fields.find((field) => fieldAffectsData(field) && field.name === 'id')
|
||||
|
||||
const type = getIDType(idField, validation?.options?.payload?.db?.defaultIDType)
|
||||
const type = getIDType(idField, db?.defaultIDType)
|
||||
|
||||
if (!isValidID(node.value?.id, type)) {
|
||||
return validation.options.t('validation:validUploadID')
|
||||
return t('validation:validUploadID')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { OrderedListFeature } from '../../../features/lists/orderedlist/feature.
|
||||
import { UnorderedListFeature } from '../../../features/lists/unorderedlist/feature.server'
|
||||
import { ParagraphFeature } from '../../../features/paragraph/feature.server'
|
||||
import { RelationshipFeature } from '../../../features/relationship/feature.server'
|
||||
import { UploadFeature } from '../../../features/upload'
|
||||
import { UploadFeature } from '../../../features/upload/feature.server'
|
||||
import { LexicalEditorTheme } from '../../theme/EditorTheme'
|
||||
import { sanitizeServerEditorConfig } from './sanitize'
|
||||
|
||||
|
||||
@@ -346,9 +346,9 @@ export type {
|
||||
ServerFeature,
|
||||
ServerFeatureProviderMap,
|
||||
} from './field/features/types'
|
||||
export { UploadFeature } from './field/features/upload'
|
||||
export { UploadFeature } from './field/features/upload/feature.server'
|
||||
|
||||
export type { UploadFeatureProps } from './field/features/upload'
|
||||
export type { UploadFeatureProps } from './field/features/upload/feature.server'
|
||||
|
||||
export type { RawUploadPayload } from './field/features/upload/nodes/UploadNode'
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
TreeViewFeature,
|
||||
UnderlineFeature,
|
||||
UnorderedListFeature,
|
||||
UploadFeature,
|
||||
lexicalEditor,
|
||||
} from '@payloadcms/richtext-lexical'
|
||||
import path from 'path'
|
||||
@@ -97,7 +98,14 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
features: [
|
||||
ParagraphFeature(),
|
||||
RelationshipFeature(),
|
||||
LinkFeature(),
|
||||
LinkFeature({
|
||||
fields: [
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}),
|
||||
CheckListFeature(),
|
||||
UnorderedListFeature(),
|
||||
OrderedListFeature(),
|
||||
@@ -105,6 +113,18 @@ export function buildConfigWithDefaults(testConfig?: Partial<Config>): Promise<S
|
||||
BlockQuoteFeature(),
|
||||
BoldFeature(),
|
||||
ItalicFeature(),
|
||||
UploadFeature({
|
||||
collections: {
|
||||
media: {
|
||||
fields: [
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
UnderlineFeature(),
|
||||
StrikethroughFeature(),
|
||||
SubscriptFeature(),
|
||||
|
||||
Reference in New Issue
Block a user