chore: merge

This commit is contained in:
James
2024-03-01 09:30:37 -05:00
157 changed files with 1099 additions and 654 deletions

View File

@@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.0", "@apollo/client": "^3.7.0",
"@faceless-ui/css-grid": "^1.2.0", "@faceless-ui/css-grid": "^1.2.0",
"@faceless-ui/modal": "^2.0.1", "@faceless-ui/modal": "^2.0.2",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"next": "^13.5.6", "next": "^13.5.6",

View File

@@ -18,7 +18,7 @@
"@payloadcms/bundler-webpack": "latest", "@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "latest", "@payloadcms/db-mongodb": "latest",
"@payloadcms/richtext-slate": "latest", "@payloadcms/richtext-slate": "latest",
"@faceless-ui/modal": "^2.0.1", "@faceless-ui/modal": "^2.0.2",
"@payloadcms/plugin-form-builder": "^1.0.12", "@payloadcms/plugin-form-builder": "^1.0.12",
"@payloadcms/plugin-seo": "^1.0.8", "@payloadcms/plugin-seo": "^1.0.8",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@@ -1,4 +1,4 @@
@import '../../scss/styles.scss'; @import '../../../../ui/src/scss/styles.scss';
.leave-without-saving { .leave-without-saving {
@include blur-bg; @include blur-bg;

View File

@@ -1,12 +1,13 @@
'use client' 'use client'
import { Modal, useModal } from '@faceless-ui/modal' import { Modal, useModal } from '@payloadcms/ui'
import React, { useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { Button } from '../../elements/Button' import { Button } from '../../../../ui/src/elements/Button'
import { useFormModified } from '../../forms/Form/context' import { useFormModified } from '../../../../ui/src/forms/Form/context'
import { useAuth } from '../../providers/Auth' import { useAuth } from '../../../../ui/src/providers/Auth'
import { useTranslation } from '../../providers/Translation' import { useTranslation } from '../../../../ui/src/providers/Translation'
import './index.scss' import './index.scss'
import { usePreventLeave } from './usePreventLeave'
const modalSlug = 'leave-without-saving' const modalSlug = 'leave-without-saving'
@@ -17,15 +18,15 @@ const Component: React.FC<{
onCancel: () => void onCancel: () => void
onConfirm: () => void onConfirm: () => void
}> = ({ isActive, onCancel, onConfirm }) => { }> = ({ isActive, onCancel, onConfirm }) => {
const { closeModal, openModal, modalState } = useModal() const { closeModal, modalState, openModal } = useModal()
const { t } = useTranslation() const { t } = useTranslation()
// Manually check for modal state as 'esc' key will not trigger the nav inactivity // Manually check for modal state as 'esc' key will not trigger the nav inactivity
useEffect(() => { // useEffect(() => {
if (!modalState?.[modalSlug]?.isOpen && isActive) { // if (!modalState?.[modalSlug]?.isOpen && isActive) {
onCancel() // onCancel()
} // }
}, [modalState]) // }, [modalState, isActive, onCancel])
useEffect(() => { useEffect(() => {
if (isActive) openModal(modalSlug) if (isActive) openModal(modalSlug)
@@ -53,11 +54,26 @@ const Component: React.FC<{
export const LeaveWithoutSaving: React.FC = () => { export const LeaveWithoutSaving: React.FC = () => {
const modified = useFormModified() const modified = useFormModified()
const { user } = useAuth() const { user } = useAuth()
const [show, setShow] = React.useState(false)
const [hasAccepted, setHasAccepted] = React.useState(false)
return null const prevent = Boolean(modified && user)
// <NavigationPrompt renderIfNotActive when={Boolean(modified && user)}>
// {({ isActive, onCancel, onConfirm }) => ( const onPrevent = useCallback(() => {
// <Component isActive={isActive} onCancel={onCancel} onConfirm={onConfirm} /> setShow(true)
// )} }, [])
// </NavigationPrompt>
usePreventLeave({ hasAccepted, onPrevent, prevent })
return (
<Component
isActive={show}
onCancel={() => {
setShow(false)
}}
onConfirm={() => {
setHasAccepted(true)
}}
/>
)
} }

View File

@@ -0,0 +1,148 @@
// Credit: @Taiki92777
// - Source: https://github.com/vercel/next.js/discussions/32231#discussioncomment-7284386
// Credit: `react-use` maintainers
// - Source: https://github.com/streamich/react-use/blob/ade8d3905f544305515d010737b4ae604cc51024/src/useBeforeUnload.ts#L2
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useRef } from 'react'
function on<T extends Document | EventTarget | HTMLElement | Window>(
obj: T | null,
...args: [string, Function | null, ...any] | Parameters<T['addEventListener']>
): void {
if (obj && obj.addEventListener) {
obj.addEventListener(...(args as Parameters<HTMLElement['addEventListener']>))
}
}
function off<T extends Document | EventTarget | HTMLElement | Window>(
obj: T | null,
...args: [string, Function | null, ...any] | Parameters<T['removeEventListener']>
): void {
if (obj && obj.removeEventListener) {
obj.removeEventListener(...(args as Parameters<HTMLElement['removeEventListener']>))
}
}
export const useBeforeUnload = (enabled: (() => boolean) | boolean = true, message?: string) => {
const handler = useCallback(
(event: BeforeUnloadEvent) => {
const finalEnabled = typeof enabled === 'function' ? enabled() : true
if (!finalEnabled) {
return
}
event.preventDefault()
if (message) {
event.returnValue = message
}
return message
},
[enabled, message],
)
useEffect(() => {
if (!enabled) {
return
}
on(window, 'beforeunload', handler)
return () => off(window, 'beforeunload', handler)
}, [enabled, handler])
}
export const usePreventLeave = ({
hasAccepted = false,
message = 'Are you sure want to leave this page?',
onPrevent,
prevent = true,
}: {
hasAccepted: boolean
// if no `onPrevent` is provided, the message will be displayed in a confirm dialog
message?: string
// to use a custom confirmation dialog, provide a function that returns a boolean
onPrevent?: () => void
prevent: boolean
}) => {
// check when page is about to be reloaded
useBeforeUnload(prevent, message)
const router = useRouter()
const cancelledURL = useRef<string>('')
// check when page is about to be changed
useEffect(() => {
function isAnchorOfCurrentUrl(currentUrl: string, newUrl: string) {
const currentUrlObj = new URL(currentUrl)
const newUrlObj = new URL(newUrl)
// Compare hostname, pathname, and search parameters
if (
currentUrlObj.hostname === newUrlObj.hostname &&
currentUrlObj.pathname === newUrlObj.pathname &&
currentUrlObj.search === newUrlObj.search
) {
// Check if the new URL is just an anchor of the current URL page
const currentHash = currentUrlObj.hash
const newHash = newUrlObj.hash
return (
currentHash !== newHash &&
currentUrlObj.href.replace(currentHash, '') === newUrlObj.href.replace(newHash, '')
)
}
return false
}
function findClosestAnchor(element: HTMLElement | null): HTMLAnchorElement | null {
while (element && element.tagName.toLowerCase() !== 'a') {
element = element.parentElement
}
return element as HTMLAnchorElement
}
function handleClick(event: MouseEvent) {
try {
const target = event.target as HTMLElement
const anchor = findClosestAnchor(target)
if (anchor) {
const currentUrl = window.location.href
const newUrl = anchor.href
const isAnchor = isAnchorOfCurrentUrl(currentUrl, newUrl)
const isDownloadLink = anchor.download !== ''
const isPageLeaving = !(newUrl === currentUrl || isAnchor || isDownloadLink)
if (isPageLeaving && prevent && (!onPrevent ? !window.confirm(message) : true)) {
// Keep a reference of the href
cancelledURL.current = newUrl
// Cancel the route change
event.preventDefault()
event.stopPropagation()
if (typeof onPrevent === 'function') {
onPrevent()
}
}
}
} catch (err) {
alert(err)
}
}
// Add the global click event listener
document.addEventListener('click', handleClick, true)
// Clean up the global click event listener when the component is unmounted
return () => {
document.removeEventListener('click', handleClick, true)
}
}, [onPrevent, prevent, message])
useEffect(() => {
if (hasAccepted && cancelledURL.current) {
router.push(cancelledURL.current)
}
}, [hasAccepted, router])
}

View File

@@ -7,7 +7,6 @@ import {
FieldPathProvider, FieldPathProvider,
Form, Form,
FormLoadingOverlayToggle, FormLoadingOverlayToggle,
LeaveWithoutSaving,
OperationProvider, OperationProvider,
getFormState, getFormState,
useComponentMap, useComponentMap,
@@ -17,6 +16,7 @@ import {
import React, { Fragment, useCallback } from 'react' import React, { Fragment, useCallback } from 'react'
import { Upload } from '../../../../../ui/src/elements/Upload' import { Upload } from '../../../../../ui/src/elements/Upload'
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving'
// import { getTranslation } from '@payloadcms/translations' // import { getTranslation } from '@payloadcms/translations'
import Auth from './Auth' import Auth from './Auth'
import { SetDocumentTitle } from './SetDocumentTitle' import { SetDocumentTitle } from './SetDocumentTitle'
@@ -45,7 +45,7 @@ export const DefaultEditView: React.FC = () => {
hasSavePermission, hasSavePermission,
initialData: data, initialData: data,
initialState, initialState,
onSave: onSaveFromProps, onSave: onSaveFromContext,
} = useDocumentInfo() } = useDocumentInfo()
const config = useConfig() const config = useConfig()
@@ -57,7 +57,7 @@ export const DefaultEditView: React.FC = () => {
serverURL, serverURL,
} = config } = config
const { getFieldMap } = useComponentMap() const { componentMap, getFieldMap } = useComponentMap()
const collectionConfig = const collectionConfig =
collectionSlug && collections.find((collection) => collection.slug === collectionSlug) collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
@@ -95,8 +95,8 @@ export const DefaultEditView: React.FC = () => {
// await refreshCookieAsync() // await refreshCookieAsync()
// } // }
if (typeof onSaveFromProps === 'function') { if (typeof onSaveFromContext === 'function') {
onSaveFromProps({ onSaveFromContext({
...json, ...json,
operation: id ? 'update' : 'create', operation: id ? 'update' : 'create',
}) })
@@ -104,7 +104,7 @@ export const DefaultEditView: React.FC = () => {
}, },
[ [
id, id,
onSaveFromProps, onSaveFromContext,
// refreshCookieAsync, // refreshCookieAsync,
// reportUpdate // reportUpdate
], ],
@@ -142,6 +142,8 @@ export const DefaultEditView: React.FC = () => {
[serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug], [serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug],
) )
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
return ( return (
<main className={classes}> <main className={classes}>
<FieldPathProvider path="" schemaPath={schemaPath}> <FieldPathProvider path="" schemaPath={schemaPath}>
@@ -220,11 +222,14 @@ export const DefaultEditView: React.FC = () => {
/> />
)} )}
{upload && ( {upload && (
<React.Fragment>
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
<Upload <Upload
collectionSlug={collectionConfig.slug} collectionSlug={collectionConfig.slug}
initialState={initialState} initialState={initialState}
uploadConfig={upload} uploadConfig={upload}
/> />
</React.Fragment>
)} )}
</Fragment> </Fragment>
) )

View File

@@ -1,12 +1,17 @@
'use client' 'use client'
import type { EditViewProps } from 'payload/config' import type { EditViewProps } from 'payload/config'
import { LoadingOverlay, useComponentMap, useDocumentInfo } from '@payloadcms/ui' import { LoadingOverlay, useComponentMap, useConfig, useDocumentInfo } from '@payloadcms/ui'
import React, { Fragment } from 'react' import { redirect } from 'next/navigation'
import React, { Fragment, useEffect } from 'react'
import { useCallback } from 'react' import { useCallback } from 'react'
export const EditViewClient: React.FC<EditViewProps> = () => { export const EditViewClient: React.FC<EditViewProps> = () => {
const { id, collectionSlug, getDocPermissions, getVersions, globalSlug } = useDocumentInfo() const { id, collectionSlug, getDocPermissions, getVersions, globalSlug, setDocumentInfo } =
useDocumentInfo()
const {
routes: { api: adminRoute },
} = useConfig()
const { componentMap } = useComponentMap() const { componentMap } = useComponentMap()
@@ -22,7 +27,7 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
getDocPermissions() getDocPermissions()
if (!isEditing) { if (!isEditing) {
// setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`) redirect(`${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}`)
} else { } else {
// buildState(json.doc, { // buildState(json.doc, {
// fieldSchema: collection.fields, // fieldSchema: collection.fields,
@@ -33,9 +38,16 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
// })) // }))
} }
}, },
[getVersions, isEditing, getDocPermissions, collectionSlug], [getVersions, isEditing, getDocPermissions, collectionSlug, adminRoute],
) )
useEffect(() => {
setDocumentInfo((current) => ({
...current,
onSave,
}))
}, [setDocumentInfo, onSave])
// Allow the `DocumentInfoProvider` to hydrate // Allow the `DocumentInfoProvider` to hydrate
if (!Edit || (!collectionSlug && !globalSlug)) { if (!Edit || (!collectionSlug && !globalSlug)) {
return <LoadingOverlay /> return <LoadingOverlay />

View File

@@ -11,12 +11,13 @@ const baseClass = 'file'
export interface FileCellProps extends CellComponentProps<any> {} export interface FileCellProps extends CellComponentProps<any> {}
export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext, rowData }) => { export const FileCell: React.FC<FileCellProps> = ({ cellData, customCellContext, rowData }) => {
const { uploadConfig } = customCellContext const { collectionSlug, uploadConfig } = customCellContext
return ( return (
<div className={baseClass}> <div className={baseClass}>
<Thumbnail <Thumbnail
className={`${baseClass}__thumbnail`} className={`${baseClass}__thumbnail`}
collectionSlug={collectionSlug}
doc={{ doc={{
...rowData, ...rowData,
filename: cellData, filename: cellData,

View File

@@ -8,7 +8,6 @@ import {
DocumentFields, DocumentFields,
FieldPathProvider, FieldPathProvider,
Form, Form,
LeaveWithoutSaving,
LoadingOverlay, LoadingOverlay,
OperationProvider, OperationProvider,
getFormState, getFormState,
@@ -19,6 +18,7 @@ import {
} from '@payloadcms/ui' } from '@payloadcms/ui'
import React, { Fragment, useCallback } from 'react' import React, { Fragment, useCallback } from 'react'
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving'
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle' import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle'
import { SetStepNav } from '../Edit/Default/SetStepNav' import { SetStepNav } from '../Edit/Default/SetStepNav'
import { LivePreviewProvider } from './Context' import { LivePreviewProvider } from './Context'

View File

@@ -41,7 +41,7 @@
"translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts" "translateNewKeys": "ts-node -T ./scripts/translateNewKeys.ts"
}, },
"dependencies": { "dependencies": {
"@payloadcms/translations": "workspace:^", "@payloadcms/translations": "workspace:*",
"bson-objectid": "2.0.4", "bson-objectid": "2.0.4",
"conf": "10.2.0", "conf": "10.2.0",
"console-table-printer": "2.11.2", "console-table-printer": "2.11.2",

View File

@@ -72,6 +72,10 @@ const sanitizeCollections = (
if ('hidden' in sanitized.admin) { if ('hidden' in sanitized.admin) {
delete sanitized.admin.hidden delete sanitized.admin.hidden
} }
if ('preview' in sanitized.admin) {
delete sanitized.admin.preview
}
} }
return sanitized return sanitized
@@ -95,6 +99,10 @@ const sanitizeGlobals = (globals: SanitizedConfig['globals']): ClientConfig['glo
if ('hidden' in sanitized.admin) { if ('hidden' in sanitized.admin) {
delete sanitized.admin.hidden delete sanitized.admin.hidden
} }
if ('preview' in sanitized.admin) {
delete sanitized.admin.preview
}
} }
return sanitized return sanitized

View File

@@ -25,56 +25,57 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
const mimeType: Field = { const mimeType: Field = {
name: 'mimeType', name: 'mimeType',
type: 'text',
admin: { admin: {
hidden: true, hidden: true,
readOnly: true, readOnly: true,
}, },
label: 'MIME Type', label: 'MIME Type',
type: 'text',
} }
const url: Field = { const url: Field = {
name: 'url', name: 'url',
type: 'text',
admin: { admin: {
hidden: true, hidden: true,
readOnly: true, readOnly: true,
}, },
label: 'URL', label: 'URL',
type: 'text',
} }
const width: Field = { const width: Field = {
name: 'width', name: 'width',
type: 'number',
admin: { admin: {
hidden: true, hidden: true,
readOnly: true, readOnly: true,
}, },
label: labels['upload:width'], label: labels['upload:width'],
type: 'number',
} }
const height: Field = { const height: Field = {
name: 'height', name: 'height',
type: 'number',
admin: { admin: {
hidden: true, hidden: true,
readOnly: true, readOnly: true,
}, },
label: labels['upload:height'], label: labels['upload:height'],
type: 'number',
} }
const filesize: Field = { const filesize: Field = {
name: 'filesize', name: 'filesize',
type: 'number',
admin: { admin: {
hidden: true, hidden: true,
readOnly: true, readOnly: true,
}, },
label: labels['upload:fileSize'], label: labels['upload:fileSize'],
type: 'number',
} }
const filename: Field = { const filename: Field = {
name: 'filename', name: 'filename',
type: 'text',
admin: { admin: {
disableBulkEdit: true, disableBulkEdit: true,
hidden: true, hidden: true,
@@ -82,7 +83,6 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
}, },
index: true, index: true,
label: labels['upload:fileName'], label: labels['upload:fileName'],
type: 'text',
unique: true, unique: true,
} }
@@ -93,10 +93,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
afterRead: [ afterRead: [
({ data }) => { ({ data }) => {
if (data?.filename) { if (data?.filename) {
if (uploadOptions.staticURL.startsWith('/')) { return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${data.filename}`
return `${config.serverURL}${uploadOptions.staticURL}/${data.filename}`
}
return `${uploadOptions.staticURL}/${data.filename}`
} }
return undefined return undefined
@@ -119,11 +116,13 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
uploadFields = uploadFields.concat([ uploadFields = uploadFields.concat([
{ {
name: 'sizes', name: 'sizes',
type: 'group',
admin: { admin: {
hidden: true, hidden: true,
}, },
fields: uploadOptions.imageSizes.map((size) => ({ fields: uploadOptions.imageSizes.map((size) => ({
name: size.name, name: size.name,
type: 'group',
admin: { admin: {
hidden: true, hidden: true,
}, },
@@ -136,10 +135,7 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
const sizeFilename = data?.sizes?.[size.name]?.filename const sizeFilename = data?.sizes?.[size.name]?.filename
if (sizeFilename) { if (sizeFilename) {
if (uploadOptions.staticURL.startsWith('/')) { return `${config.serverURL}${config.routes.api}/${collection.slug}/file/${sizeFilename}`
return `${config.serverURL}${uploadOptions.staticURL}/${sizeFilename}`
}
return `${uploadOptions.staticURL}/${sizeFilename}`
} }
return null return null
@@ -157,10 +153,8 @@ const getBaseUploadFields = ({ collection, config }: Options): Field[] => {
}, },
], ],
label: size.name, label: size.name,
type: 'group',
})), })),
label: labels['upload:Sizes'], label: labels['upload:Sizes'],
type: 'group',
}, },
]) ])
} }

View File

@@ -70,7 +70,7 @@ export type ImageSize = Omit<ResizeOptions, 'withoutEnlargement'> & {
export type GetAdminThumbnail = (args: { doc: Record<string, unknown> }) => false | null | string export type GetAdminThumbnail = (args: { doc: Record<string, unknown> }) => false | null | string
export type IncomingUploadType = { export type IncomingUploadType = {
adminThumbnail?: GetAdminThumbnail | string adminThumbnail?: React.ComponentType | string
crop?: boolean crop?: boolean
disableLocalStorage?: boolean disableLocalStorage?: boolean
filesRequiredOnCreate?: boolean filesRequiredOnCreate?: boolean
@@ -88,7 +88,12 @@ export type IncomingUploadType = {
} }
export type Upload = { export type Upload = {
adminThumbnail?: GetAdminThumbnail | string /**
* Represents an admin thumbnail, which can be either a React component or a string.
* - If a string, it should be one of the image size names.
* - If a React component, register a function that generates the thumbnail URL using the `useAdminThumbnail` hook.
**/
adminThumbnail?: React.ComponentType | string
crop?: boolean crop?: boolean
disableLocalStorage?: boolean disableLocalStorage?: boolean
filesRequiredOnCreate?: boolean filesRequiredOnCreate?: boolean

View File

@@ -11,7 +11,7 @@
"scripts": { "scripts": {
"build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "build:types": "tsc --emitDeclarationOnly --outDir dist",
"build": "pnpm build:swc && pnpm build:types", "build": "echo \"Build temporarily disabled.\" && exit 0",
"clean": "rimraf {dist,*.tsbuildinfo}", "clean": "rimraf {dist,*.tsbuildinfo}",
"prepublishOnly": "pnpm clean && pnpm turbo build", "prepublishOnly": "pnpm clean && pnpm turbo build",
"test": "echo \"No tests available.\"" "test": "echo \"No tests available.\""
@@ -21,7 +21,7 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}, },
"dependencies": { "dependencies": {
"@payloadcms/ui": "workspace:^", "@payloadcms/ui": "workspace:*",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"escape-html": "^1.0.3" "escape-html": "^1.0.3"
}, },

View File

@@ -30,7 +30,7 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}, },
"dependencies": { "dependencies": {
"@payloadcms/ui": "workspace:^", "@payloadcms/ui": "workspace:*",
"ts-deepmerge": "^2.0.1" "ts-deepmerge": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -7,7 +7,7 @@
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "pnpm build:swc && pnpm build:types", "build": "echo \"Build temporarily disabled.\" && exit 0",
"build:swc": "swc ./src -d ./dist --config-file .swcrc", "build:swc": "swc ./src -d ./dist --config-file .swcrc",
"build:types": "tsc --emitDeclarationOnly --outDir dist", "build:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}", "clean": "rimraf {dist,*.tsbuildinfo}",
@@ -31,7 +31,7 @@
"payload": "^1.1.8 || ^2.0.0" "payload": "^1.1.8 || ^2.0.0"
}, },
"dependencies": { "dependencies": {
"@payloadcms/ui": "workspace:^", "@payloadcms/ui": "workspace:*",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"stripe": "^10.2.0", "stripe": "^10.2.0",
"uuid": "^9.0.0" "uuid": "^9.0.0"

View File

@@ -18,7 +18,7 @@
"prepublishOnly": "pnpm clean && pnpm turbo build" "prepublishOnly": "pnpm clean && pnpm turbo build"
}, },
"dependencies": { "dependencies": {
"@faceless-ui/modal": "2.0.1", "@faceless-ui/modal": "2.0.2",
"@lexical/headless": "0.13.1", "@lexical/headless": "0.13.1",
"@lexical/link": "0.13.1", "@lexical/link": "0.13.1",
"@lexical/list": "0.13.1", "@lexical/list": "0.13.1",
@@ -29,7 +29,6 @@
"@lexical/selection": "0.13.1", "@lexical/selection": "0.13.1",
"@lexical/utils": "0.13.1", "@lexical/utils": "0.13.1",
"@payloadcms/translations": "workspace:*", "@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"bson-objectid": "2.0.4", "bson-objectid": "2.0.4",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"deep-equal": "2.2.3", "deep-equal": "2.2.3",
@@ -43,6 +42,7 @@
}, },
"devDependencies": { "devDependencies": {
"@payloadcms/eslint-config": "workspace:*", "@payloadcms/eslint-config": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/json-schema": "7.0.15", "@types/json-schema": "7.0.15",
"@types/node": "20.6.2", "@types/node": "20.6.2",
"@types/react": "18.2.15", "@types/react": "18.2.15",
@@ -50,8 +50,8 @@
"payload": "workspace:*" "payload": "workspace:*"
}, },
"peerDependencies": { "peerDependencies": {
"@payloadcms/translations": "workspace:^", "@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:^", "@payloadcms/ui": "workspace:*",
"payload": "^2.4.0" "payload": "^2.4.0"
}, },
"exports": { "exports": {

View File

@@ -1,25 +0,0 @@
/**
* Wraps the input formData in a blockFieldWrapperName, so that it can be read by the RenderFields component
* which requires it to be wrapped in a group field
*/
export function transformInputFormData(data: any, blockFieldWrapperName: string) {
const dataCopy = JSON.parse(JSON.stringify(data))
const fieldDataWithoutBlockFields = { ...dataCopy }
delete fieldDataWithoutBlockFields['id']
delete fieldDataWithoutBlockFields['blockName']
delete fieldDataWithoutBlockFields['blockType']
// Wrap all fields inside blockFieldWrapperName.
// This is necessary, because blockFieldWrapperName is set as the 'base' path for all fields in the block (in the RenderFields component).
// Thus, in order for the data to be read, it has to be wrapped in this blockFieldWrapperName, as it's expected to be there.
// Why are we doing this? Because that way, all rendered fields of the blocks have different paths and names, and thus don't conflict with each other.
// They have different paths and names, because they are wrapped in the blockFieldWrapperName, which has a name that is unique for each block.
return {
id: dataCopy.id,
[blockFieldWrapperName]: fieldDataWithoutBlockFields,
blockName: dataCopy.blockName,
blockType: dataCopy.blockType,
}
}

View File

@@ -1,31 +0,0 @@
import type { Field } from 'payload/types'
export function transformInputFormSchema(formSchema: any, blockFieldWrapperName: string): Field[] {
const formSchemaCopy = [...formSchema]
// First, check if it needs wrapping
const hasBlockFieldWrapper = formSchemaCopy.some(
(field) => 'name' in field && field.name === blockFieldWrapperName,
)
if (hasBlockFieldWrapper) {
return formSchemaCopy
}
// Add a group in the field schema, which represents all values saved in the blockFieldWrapperName
return [
...formSchemaCopy.filter(
(field) => 'name' in field && ['blockName', 'blockType', 'id'].includes(field.name),
),
{
name: blockFieldWrapperName,
type: 'group',
admin: {
hideGutter: true,
},
fields: formSchemaCopy.filter(
(field) => !('name' in field) || !['blockName', 'blockType', 'id'].includes(field.name),
),
label: '',
},
]
}

View File

@@ -11,7 +11,6 @@ import {
ErrorPill, ErrorPill,
Pill, Pill,
SectionTitle, SectionTitle,
createNestedFieldPath,
useDocumentInfo, useDocumentInfo,
useFormSubmitted, useFormSubmitted,
useTranslation, useTranslation,
@@ -21,7 +20,6 @@ import { $getNodeByKey } from 'lexical'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types' import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
import type { FieldProps } from '../../../../types'
import type { BlockFields, BlockNode } from '../nodes/BlocksNode' import type { BlockFields, BlockNode } from '../nodes/BlocksNode'
import { FormSavePlugin } from './FormSavePlugin' import { FormSavePlugin } from './FormSavePlugin'
@@ -89,8 +87,6 @@ export const BlockContent: React.FC<Props> = (props) => {
.filter(Boolean) .filter(Boolean)
.join(' ') .join(' ')
const path = '' as const
const onFormChange = useCallback( const onFormChange = useCallback(
({ ({
fullFieldsWithValues, fullFieldsWithValues,
@@ -101,9 +97,9 @@ export const BlockContent: React.FC<Props> = (props) => {
}) => { }) => {
newFormData = { newFormData = {
...newFormData, ...newFormData,
id: formData.id, // TODO: Why does form updatee not include theeeeem id: formData.id,
blockName: formData.blockName, // TODO: Why does form updatee not include theeeeem blockName: newFormData.blockName2, // TODO: Find a better solution for this. We have to wrap it in blockName2 when using it here, as blockName does not accept or provide any updated values for some reason.
blockType: formData.blockType, // TODO: Why does form updatee not include theeeeem blockType: formData.blockType,
} }
// Recursively remove all undefined values from even being present in formData, as they will // Recursively remove all undefined values from even being present in formData, as they will
@@ -123,8 +119,6 @@ export const BlockContent: React.FC<Props> = (props) => {
removeUndefinedAndNullRecursively(newFormData) removeUndefinedAndNullRecursively(newFormData)
removeUndefinedAndNullRecursively(formData) removeUndefinedAndNullRecursively(formData)
console.log('before saving node data...', newFormData, 'old', formData)
// Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change, // Only update if the data has actually changed. Otherwise, we may be triggering an unnecessary value change,
// which would trigger the "Leave without saving" dialog unnecessarily // which would trigger the "Leave without saving" dialog unnecessarily
if (!isDeepEqual(formData, newFormData)) { if (!isDeepEqual(formData, newFormData)) {
@@ -136,7 +130,6 @@ export const BlockContent: React.FC<Props> = (props) => {
editor.update(() => { editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey) const node: BlockNode = $getNodeByKey(nodeKey)
if (node) { if (node) {
console.log('saving node data...', newFormData)
node.setFields(newFormData as BlockFields) node.setFields(newFormData as BlockFields)
} }
}) })
@@ -197,14 +190,14 @@ export const BlockContent: React.FC<Props> = (props) => {
? getTranslation(labels.singular, i18n) ? getTranslation(labels.singular, i18n)
: '[Singular Label]'} : '[Singular Label]'}
</Pill> </Pill>
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} /> <SectionTitle path="blockName2" readOnly={field?.readOnly} />
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />} {fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div> </div>
{editor.isEditable() && ( {editor.isEditable() && (
<Button <Button
buttonStyle="icon-label" buttonStyle="icon-label"
className={`${baseClass}__removeButton`} className={`${baseClass}__removeButton`}
disabled={field?.admin?.readOnly} disabled={field?.readOnly}
icon="x" icon="x"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()

View File

@@ -25,7 +25,6 @@ export const FormSavePlugin: React.FC<Props> = (props) => {
const newFormData = reduceFieldsToValues(fields, true) const newFormData = reduceFieldsToValues(fields, true)
useEffect(() => { useEffect(() => {
console.log('FormSavePlugin', newFormData)
if (onChange) { if (onChange) {
onChange({ fullFieldsWithValues: fields, newFormData }) onChange({ fullFieldsWithValues: fields, newFormData })
} }

View File

@@ -4,15 +4,11 @@ import {
Form, Form,
type FormProps, type FormProps,
type FormState, type FormState,
buildInitialState,
buildStateFromSchema,
getFormState, getFormState,
useConfig, useConfig,
useDocumentInfo, useDocumentInfo,
useFieldPath, useFieldPath,
useFormSubmitted, useFormSubmitted,
useLocale,
useTranslation,
} from '@payloadcms/ui' } from '@payloadcms/ui'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
@@ -21,6 +17,8 @@ const baseClass = 'lexical-block'
import type { Data } from 'payload/types' import type { Data } from 'payload/types'
import { v4 as uuid } from 'uuid'
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types' import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
import type { ClientComponentProps } from '../../types' import type { ClientComponentProps } from '../../types'
import type { BlocksFeatureClientProps } from '../feature.client' import type { BlocksFeatureClientProps } from '../feature.client'
@@ -54,8 +52,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
field: { richTextComponentMap }, field: { richTextComponentMap },
} = useEditorConfigContext() } = useEditorConfigContext()
console.log('1. Loading node data', formData)
const componentMapRenderedFieldsPath = `feature.blocks.fields.${formData?.blockType}` const componentMapRenderedFieldsPath = `feature.blocks.fields.${formData?.blockType}`
const schemaFieldsPath = `${schemaPath}.feature.blocks.${formData?.blockType}` const schemaFieldsPath = `${schemaPath}.feature.blocks.${formData?.blockType}`
@@ -64,7 +60,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps> ?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType) )?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath)
// Field Schema
useEffect(() => { useEffect(() => {
const awaitInitialState = async () => { const awaitInitialState = async () => {
const state = await getFormState({ const state = await getFormState({
@@ -79,7 +76,15 @@ export const BlockComponent: React.FC<Props> = (props) => {
}) // Form State }) // Form State
if (state) { if (state) {
setInitialState(state) setInitialState({
...state,
blockName2: {
initialValue: '',
passesCondition: true,
valid: true,
value: formData.blockName,
},
})
} }
} }
@@ -90,7 +95,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
const onChange: FormProps['onChange'][0] = useCallback( const onChange: FormProps['onChange'][0] = useCallback(
async ({ formState: prevFormState }) => { async ({ formState: prevFormState }) => {
return await getFormState({ const formState = await getFormState({
apiRoute: config.routes.api, apiRoute: config.routes.api,
body: { body: {
id, id,
@@ -100,18 +105,21 @@ export const BlockComponent: React.FC<Props> = (props) => {
}, },
serverURL: config.serverURL, serverURL: config.serverURL,
}) })
return {
...formState,
blockName2: {
initialValue: '',
passesCondition: true,
valid: true,
value: formData.blockName,
},
}
}, },
[config.routes.api, config.serverURL, schemaFieldsPath, id], [config.routes.api, config.serverURL, schemaFieldsPath, id, formData.blockName],
) )
// Sanitize block's fields here. This is done here and not in the feature, because the payload config is available here
//const formSchema = transformInputFormSchema(fieldMap, blockFieldWrapperName)
const initialStateRef = React.useRef<Data>(null) // Store initial value in a ref, so it doesn't change on re-render and only gets initialized once
console.log('Bloocks initialState', initialState)
// Memoized Form JSX // Memoized Form JSX
const formContent = useMemo(() => { const formContent = useMemo(() => {
return ( return (
@@ -123,6 +131,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
initialState={initialState} initialState={initialState}
onChange={[onChange]} onChange={[onChange]}
submitted={submitted} submitted={submitted}
uuid={uuid()}
> >
<BlockContent <BlockContent
baseClass={baseClass} baseClass={baseClass}
@@ -136,7 +145,16 @@ export const BlockComponent: React.FC<Props> = (props) => {
</FieldPathProvider> </FieldPathProvider>
) )
) )
}, [fieldMap, parentLexicalRichTextField, nodeKey, submitted, initialState, reducedBlock]) }, [
fieldMap,
parentLexicalRichTextField,
nodeKey,
submitted,
initialState,
reducedBlock,
blockFieldWrapperName,
onChange,
])
return <div className={baseClass}>{formContent}</div> return <div className={baseClass}>{formContent}</div>
} }

View File

@@ -14,8 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import ObjectID from 'bson-objectid' import ObjectID from 'bson-objectid'
import React from 'react' import React from 'react'
import { transformInputFormData } from '../utils/transformInputFormData'
export type BlockFields = { export type BlockFields = {
/** Block form data */ /** Block form data */
[key: string]: any [key: string]: any
@@ -90,17 +88,7 @@ export class BlockNode extends DecoratorBlockNode {
return false return false
} }
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element { decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
const blockFieldWrapperName = this.getFields().blockType + '-' + this.getFields().id return <BlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
const transformedFormData = transformInputFormData(this.getFields(), blockFieldWrapperName)
return (
<BlockComponent
blockFieldWrapperName={blockFieldWrapperName}
formData={this.getFields()}
nodeKey={this.getKey()}
transformedFormData={transformedFormData}
/>
)
} }
exportDOM(): DOMExportOutput { exportDOM(): DOMExportOutput {
@@ -129,18 +117,7 @@ export class BlockNode extends DecoratorBlockNode {
} }
setFields(fields: BlockFields): void { setFields(fields: BlockFields): void {
let fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields const fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
// Possibly transform fields
const blockFieldWrapperName = fieldsCopy.blockType + '-' + fieldsCopy.id
if (fieldsCopy[blockFieldWrapperName]) {
fieldsCopy = {
id: fieldsCopy.id,
blockName: fieldsCopy.blockName,
blockType: fieldsCopy.blockType,
...fieldsCopy[blockFieldWrapperName],
}
delete fieldsCopy[blockFieldWrapperName]
}
const writable = this.getWritable() const writable = this.getWritable()
writable.__fields = fieldsCopy writable.__fields = fieldsCopy

View File

@@ -0,0 +1,25 @@
import type { HTMLConverter } from '@payloadcms/richtext-lexical'
import type { FeatureProviderProviderServer } from '../../types'
export type HTMLConverterFeatureProps = {
converters?:
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
| HTMLConverter[]
}
export const HTMLConverterFeature: FeatureProviderProviderServer<
HTMLConverterFeatureProps,
undefined
> = (props) => {
return {
feature: () => {
return {
clientFeatureProps: null,
serverFeatureProps: props,
}
},
key: 'htmlConverter',
serverFeatureProps: props,
}
}

View File

@@ -1,10 +1,10 @@
import type { SerializedEditorState } from 'lexical' import type { SerializedEditorState } from 'lexical'
import type { Field, RichTextField, TextField } from 'payload/types' import type { Field, RichTextField, TextField } from 'payload/types'
import type { LexicalRichTextAdapter, SanitizedEditorConfig } from '../../../../../index' import type { LexicalRichTextAdapter, SanitizedServerEditorConfig } from '../../../../../index'
import type { AdapterProps } from '../../../../../types' import type { AdapterProps } from '../../../../../types'
import type { HTMLConverter } from '../converter/types' import type { HTMLConverter } from '../converter/types'
import type { HTMLConverterFeatureProps } from '../index' import type { HTMLConverterFeatureProps } from '../feature.server'
import { convertLexicalToHTML } from '../converter' import { convertLexicalToHTML } from '../converter'
import { defaultHTMLConverters } from '../converter/defaultConverters' import { defaultHTMLConverters } from '../converter/defaultConverters'
@@ -21,10 +21,11 @@ type Props = {
export const consolidateHTMLConverters = ({ export const consolidateHTMLConverters = ({
editorConfig, editorConfig,
}: { }: {
editorConfig: SanitizedEditorConfig editorConfig: SanitizedServerEditorConfig
}) => { }) => {
const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter') const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter')
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature?.props const htmlConverterFeatureProps: HTMLConverterFeatureProps =
htmlConverterFeature?.serverFeatureProps
const defaultConvertersWithConvertersFromFeatures = defaultHTMLConverters const defaultConvertersWithConvertersFromFeatures = defaultHTMLConverters
@@ -55,7 +56,7 @@ export const lexicalHTML: (
) => TextField = (lexicalFieldName, props) => { ) => TextField = (lexicalFieldName, props) => {
const { name = 'lexicalHTML' } = props const { name = 'lexicalHTML' } = props
return { return {
name: name, name,
type: 'text', type: 'text',
admin: { admin: {
hidden: true, hidden: true,

View File

@@ -1,27 +0,0 @@
import type { FeatureProvider } from '../../types'
import type { HTMLConverter } from './converter/types'
export type HTMLConverterFeatureProps = {
converters?:
| (({ defaultConverters }: { defaultConverters: HTMLConverter[] }) => HTMLConverter[])
| HTMLConverter[]
}
/**
* This feature only manages the converters. They are read and actually run / executed by the
* Lexical field.
*/
export const HTMLConverterFeature = (props?: HTMLConverterFeatureProps): FeatureProvider => {
if (!props) {
props = {}
}
return {
feature: () => {
return {
props,
}
},
key: 'htmlConverter',
}
}

View File

@@ -1,20 +0,0 @@
import type { FeatureProvider } from '../../types'
export const TestRecorderFeature = (): FeatureProvider => {
return {
feature: () => {
return {
plugins: [
{
Component: () =>
// @ts-expect-error-next-line
import('./plugin').then((module) => module.TestRecorderPlugin),
position: 'bottom',
},
],
props: null,
}
},
key: 'debug-testrecorder',
}
}

View File

@@ -1,20 +0,0 @@
import type { FeatureProvider } from '../../types'
export const TreeViewFeature = (): FeatureProvider => {
return {
feature: () => {
return {
plugins: [
{
Component: () =>
// @ts-expect-error-next-line
import('./plugin').then((module) => module.TreeViewPlugin),
position: 'bottom',
},
],
props: null,
}
},
key: 'debug-treeview',
}
}

View File

@@ -0,0 +1,22 @@
'use client'
import type { FeatureProviderProviderClient } from '../../types'
import { createClientComponent } from '../../createClientComponent'
import { TestRecorderPlugin } from './plugin'
const TestRecorderFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => ({
clientFeatureProps: props,
plugins: [
{
Component: TestRecorderPlugin,
position: 'bottom',
},
],
}),
}
}
export const TestRecorderFeatureClientComponent = createClientComponent(TestRecorderFeatureClient)

View File

@@ -0,0 +1,16 @@
import type { FeatureProviderProviderServer } from '../../types'
import { TestRecorderFeatureClientComponent } from './feature.client'
export const TestRecorderFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: TestRecorderFeatureClientComponent,
serverFeatureProps: props,
}
},
key: 'testrecorder',
serverFeatureProps: props,
}
}

View File

@@ -389,6 +389,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault() e.preventDefault()
}} }}
title={isRecording ? 'Disable test recorder' : 'Enable test recorder'} title={isRecording ? 'Disable test recorder' : 'Enable test recorder'}
type="button"
> >
{isRecording ? 'Disable test recorder' : 'Enable test recorder'} {isRecording ? 'Disable test recorder' : 'Enable test recorder'}
</button> </button>
@@ -404,6 +405,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault() e.preventDefault()
}} }}
title="Insert snapshot" title="Insert snapshot"
type="button"
> >
Insert Snapshot Insert Snapshot
</button> </button>
@@ -415,6 +417,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault() e.preventDefault()
}} }}
title="Copy to clipboard" title="Copy to clipboard"
type="button"
> >
Copy Copy
</button> </button>
@@ -426,6 +429,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault() e.preventDefault()
}} }}
title="Download as a file" title="Download as a file"
type="button"
> >
Download Download
</button> </button>

View File

@@ -0,0 +1,22 @@
'use client'
import type { FeatureProviderProviderClient } from '../../types'
import { createClientComponent } from '../../createClientComponent'
import { TreeViewPlugin } from './plugin'
const TreeViewFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => ({
clientFeatureProps: props,
plugins: [
{
Component: TreeViewPlugin,
position: 'bottom',
},
],
}),
}
}
export const TreeViewFeatureClientComponent = createClientComponent(TreeViewFeatureClient)

View File

@@ -0,0 +1,16 @@
import type { FeatureProviderProviderServer } from '../../types'
import { TreeViewFeatureClientComponent } from './feature.client'
export const TreeViewFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: TreeViewFeatureClientComponent,
serverFeatureProps: props,
}
},
key: 'treeview',
serverFeatureProps: props,
}
}

View File

@@ -5,7 +5,7 @@ import * as React from 'react'
import './index.scss' import './index.scss'
export function TreeViewPlugin(): JSX.Element { export function TreeViewPlugin(): React.ReactNode {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
return ( return (
<TreeView <TreeView

View File

@@ -1,7 +1,10 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { BoldIcon } from '../../../lexical/ui/icons/Bold'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { import {
BOLD_ITALIC_STAR, BOLD_ITALIC_STAR,
@@ -10,9 +13,9 @@ import {
BOLD_UNDERSCORE, BOLD_UNDERSCORE,
} from './markdownTransformers' } from './markdownTransformers'
export const BoldTextFeature = (): FeatureProvider => { const BoldFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
dependenciesSoft: ['italic'], clientFeatureProps: props,
feature: ({ featureProviderMap }) => { feature: ({ featureProviderMap }) => {
const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE] const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE]
if (featureProviderMap.get('italic')) { if (featureProviderMap.get('italic')) {
@@ -20,13 +23,12 @@ export const BoldTextFeature = (): FeatureProvider => {
} }
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: BoldIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Bold').then((module) => module.BoldIcon),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('bold') return selection.hasFormat('bold')
@@ -42,10 +44,10 @@ export const BoldTextFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
markdownTransformers: markdownTransformers, markdownTransformers,
props: null,
} }
}, },
key: 'bold',
} }
} }
export const BoldFeatureClientComponent = createClientComponent(BoldFeatureClient)

View File

@@ -0,0 +1,29 @@
import type { FeatureProviderProviderServer } from '../../types'
import { BoldFeatureClientComponent } from './feature.client'
import {
BOLD_ITALIC_STAR,
BOLD_ITALIC_UNDERSCORE,
BOLD_STAR,
BOLD_UNDERSCORE,
} from './markdownTransformers'
export const BoldFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
dependenciesSoft: ['italic'],
feature: ({ featureProviderMap }) => {
const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE]
if (featureProviderMap.get('italic')) {
markdownTransformers.push(BOLD_ITALIC_UNDERSCORE, BOLD_ITALIC_STAR)
}
return {
ClientComponent: BoldFeatureClientComponent,
markdownTransformers,
serverFeatureProps: props,
}
},
key: 'bold',
serverFeatureProps: props,
}
}

View File

@@ -1,21 +1,25 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { CodeIcon } from '../../../lexical/ui/icons/Code'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { INLINE_CODE } from './markdownTransformers' import { INLINE_CODE } from './markdownTransformers'
export const InlineCodeTextFeature = (): FeatureProvider => { const InlineCodeFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: CodeIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Code').then((module) => module.CodeIcon),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('code') return selection.hasFormat('code')
@@ -31,10 +35,11 @@ export const InlineCodeTextFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
markdownTransformers: [INLINE_CODE], markdownTransformers: [INLINE_CODE],
props: null,
} }
}, },
key: 'inlineCode',
} }
} }
export const InlineCodeFeatureClientComponent = createClientComponent(InlineCodeFeatureClient)

View File

@@ -0,0 +1,18 @@
import type { FeatureProviderProviderServer } from '../../types'
import { InlineCodeFeatureClientComponent } from './feature.client'
import { INLINE_CODE } from './markdownTransformers'
export const InlineCodeFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: InlineCodeFeatureClientComponent,
markdownTransformers: [INLINE_CODE],
serverFeatureProps: props,
}
},
key: 'inlinecode',
serverFeatureProps: props,
}
}

View File

@@ -1,21 +1,26 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { ItalicIcon } from '../../../lexical/ui/icons/Italic'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers' import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
export const ItalicTextFeature = (): FeatureProvider => { const ItalicFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: ItalicIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Italic').then((module) => module.ItalicIcon),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('italic') return selection.hasFormat('italic')
@@ -32,9 +37,9 @@ export const ItalicTextFeature = (): FeatureProvider => {
], ],
}, },
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE], markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
props: null,
} }
}, },
key: 'italic',
} }
} }
export const ItalicFeatureClientComponent = createClientComponent(ItalicFeatureClient)

View File

@@ -0,0 +1,18 @@
import type { FeatureProviderProviderServer } from '../../types'
import { ItalicFeatureClientComponent } from './feature.client'
import { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
export const ItalicFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: ItalicFeatureClientComponent,
markdownTransformers: [ITALIC_STAR, ITALIC_UNDERSCORE],
serverFeatureProps: props,
}
},
key: 'italic',
serverFeatureProps: props,
}
}

View File

@@ -1,23 +1,26 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { StrikethroughIcon } from '../../../lexical/ui/icons/Strikethrough'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
import { STRIKETHROUGH } from './markdownTransformers' import { STRIKETHROUGH } from './markdownTransformers'
export const StrikethroughTextFeature = (): FeatureProvider => { const StrikethroughFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: StrikethroughIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Strikethrough').then(
(module) => module.StrikethroughIcon,
),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('strikethrough') return selection.hasFormat('strikethrough')
@@ -34,9 +37,9 @@ export const StrikethroughTextFeature = (): FeatureProvider => {
], ],
}, },
markdownTransformers: [STRIKETHROUGH], markdownTransformers: [STRIKETHROUGH],
props: null,
} }
}, },
key: 'strikethrough',
} }
} }
export const StrikethroughFeatureClientComponent = createClientComponent(StrikethroughFeatureClient)

View File

@@ -0,0 +1,21 @@
import type { FeatureProviderProviderServer } from '../../types'
import { StrikethroughFeatureClientComponent } from './feature.client'
import { STRIKETHROUGH } from './markdownTransformers'
export const StrikethroughFeature: FeatureProviderProviderServer<undefined, undefined> = (
props,
) => {
return {
feature: () => {
return {
ClientComponent: StrikethroughFeatureClientComponent,
markdownTransformers: [STRIKETHROUGH],
serverFeatureProps: props,
}
},
key: 'strikethrough',
serverFeatureProps: props,
}
}

View File

@@ -1,22 +1,24 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { SubscriptIcon } from '../../../lexical/ui/icons/Subscript'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const SubscriptTextFeature = (): FeatureProvider => { const SubscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: SubscriptIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Subscript').then(
(module) => module.SubscriptIcon,
),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('subscript') return selection.hasFormat('subscript')
@@ -32,9 +34,9 @@ export const SubscriptTextFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
props: null,
} }
}, },
key: 'subscript',
} }
} }
export const SubscriptFeatureClientComponent = createClientComponent(SubscriptFeatureClient)

View File

@@ -0,0 +1,16 @@
import type { FeatureProviderProviderServer } from '../../types'
import { SubscriptFeatureClientComponent } from './feature.client'
export const SubscriptFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: SubscriptFeatureClientComponent,
serverFeatureProps: props,
}
},
key: 'subscript',
serverFeatureProps: props,
}
}

View File

@@ -1,22 +1,24 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { SuperscriptIcon } from '../../../lexical/ui/icons/Superscript'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const SuperscriptTextFeature = (): FeatureProvider => { const SuperscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: SuperscriptIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Superscript').then(
(module) => module.SuperscriptIcon,
),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('superscript') return selection.hasFormat('superscript')
@@ -32,9 +34,9 @@ export const SuperscriptTextFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
props: null,
} }
}, },
key: 'superscript',
} }
} }
export const SuperscriptFeatureClientComponent = createClientComponent(SuperscriptFeatureClient)

View File

@@ -0,0 +1,16 @@
import type { FeatureProviderProviderServer } from '../../types'
import { SuperscriptFeatureClientComponent } from './feature.client'
export const SuperscriptFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: SuperscriptFeatureClientComponent,
serverFeatureProps: props,
}
},
key: 'superscript',
serverFeatureProps: props,
}
}

View File

@@ -1,22 +1,24 @@
'use client'
import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical' import { $isRangeSelection, FORMAT_TEXT_COMMAND } from 'lexical'
import type { FeatureProvider } from '../../types' import type { FeatureProviderProviderClient } from '../../types'
import { UnderlineIcon } from '../../../lexical/ui/icons/Underline'
import { createClientComponent } from '../../createClientComponent'
import { SectionWithEntries } from '../common/floatingSelectToolbarSection' import { SectionWithEntries } from '../common/floatingSelectToolbarSection'
export const UnderlineTextFeature = (): FeatureProvider => { const UnderlineFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
SectionWithEntries([ SectionWithEntries([
{ {
ChildComponent: () => ChildComponent: UnderlineIcon,
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Underline').then(
(module) => module.UnderlineIcon,
),
isActive: ({ selection }) => { isActive: ({ selection }) => {
if ($isRangeSelection(selection)) { if ($isRangeSelection(selection)) {
return selection.hasFormat('underline') return selection.hasFormat('underline')
@@ -32,9 +34,9 @@ export const UnderlineTextFeature = (): FeatureProvider => {
]), ]),
], ],
}, },
props: null,
} }
}, },
key: 'underline',
} }
} }
export const UnderlineFeatureClientComponent = createClientComponent(UnderlineFeatureClient)

View File

@@ -0,0 +1,16 @@
import type { FeatureProviderProviderServer } from '../../types'
import { UnderlineFeatureClientComponent } from './feature.client'
export const UnderlineFeature: FeatureProviderProviderServer<undefined, undefined> = (props) => {
return {
feature: () => {
return {
ClientComponent: UnderlineFeatureClientComponent,
serverFeatureProps: props,
}
},
key: 'underline',
serverFeatureProps: props,
}
}

View File

@@ -1,15 +1,24 @@
import type { HeadingTagType, SerializedHeadingNode } from '@lexical/rich-text' 'use client'
import { $createHeadingNode, HeadingNode } from '@lexical/rich-text' import type { HeadingTagType } from '@lexical/rich-text'
import { HeadingNode } from '@lexical/rich-text'
import { $createHeadingNode } from '@lexical/rich-text'
import { $setBlocksType } from '@lexical/selection' import { $setBlocksType } from '@lexical/selection'
import { $getSelection } from 'lexical' import { $getSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types' import type { FeatureProviderProviderClient } from '../types'
import type { FeatureProvider } from '../types' import type { HeadingFeatureProps } from './feature.server'
import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types' import { SlashMenuOption } from '../../lexical/plugins/SlashMenu/LexicalTypeaheadMenuPlugin/types'
import { H1Icon } from '../../lexical/ui/icons/H1'
import { H2Icon } from '../../lexical/ui/icons/H2'
import { H3Icon } from '../../lexical/ui/icons/H3'
import { H4Icon } from '../../lexical/ui/icons/H4'
import { H5Icon } from '../../lexical/ui/icons/H5'
import { H6Icon } from '../../lexical/ui/icons/H6'
import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection' import { TextDropdownSectionWithEntries } from '../common/floatingSelectToolbarTextDropdownSection'
import { convertLexicalNodesToHTML } from '../converters/html/converter' import { createClientComponent } from '../createClientComponent'
import { MarkdownTransformer } from './markdownTransformer' import { MarkdownTransformer } from './markdownTransformer'
const setHeading = (headingSize: HeadingTagType) => { const setHeading = (headingSize: HeadingTagType) => {
@@ -17,31 +26,23 @@ const setHeading = (headingSize: HeadingTagType) => {
$setBlocksType(selection, () => $createHeadingNode(headingSize)) $setBlocksType(selection, () => $createHeadingNode(headingSize))
} }
type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const iconImports = { const iconImports = {
// @ts-expect-error-next-line h1: H1Icon,
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon), h2: H2Icon,
// @ts-expect-error-next-line h3: H3Icon,
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon), h4: H4Icon,
// @ts-expect-error-next-line h5: H5Icon,
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon), h6: H6Icon,
// @ts-expect-error-next-line
h4: () => import('../../lexical/ui/icons/H4').then((module) => module.H4Icon),
// @ts-expect-error-next-line
h5: () => import('../../lexical/ui/icons/H5').then((module) => module.H5Icon),
// @ts-expect-error-next-line
h6: () => import('../../lexical/ui/icons/H6').then((module) => module.H6Icon),
} }
export const HeadingFeature = (props: Props): FeatureProvider => { const HeadingFeatureClient: FeatureProviderProviderClient<HeadingFeatureProps> = (props) => {
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return { return {
clientFeatureProps: props,
feature: () => { feature: () => {
return { return {
clientFeatureProps: props,
floatingSelectToolbar: { floatingSelectToolbar: {
sections: [ sections: [
...enabledHeadingSizes.map((headingSize, i) => ...enabledHeadingSizes.map((headingSize, i) =>
@@ -63,30 +64,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
], ],
}, },
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)], markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [ nodes: [HeadingNode],
{
type: HeadingNode.getType(),
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
node: HeadingNode,
},
],
props,
slashMenu: { slashMenu: {
options: [ options: [
...enabledHeadingSizes.map((headingSize) => { ...enabledHeadingSizes.map((headingSize) => {
@@ -109,6 +87,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
}, },
} }
}, },
key: 'heading',
} }
} }
export const HeadingFeatureClientComponent = createClientComponent(HeadingFeatureClient)

View File

@@ -0,0 +1,60 @@
import type { HeadingTagType } from '@lexical/rich-text'
import { HeadingNode, type SerializedHeadingNode } from '@lexical/rich-text'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProviderProviderServer } from '../types'
import { convertLexicalNodesToHTML } from '../converters/html/converter'
import { HeadingFeatureClientComponent } from './feature.client'
import { MarkdownTransformer } from './markdownTransformer'
export type HeadingFeatureProps = {
enabledHeadingSizes?: HeadingTagType[]
}
export const HeadingFeature: FeatureProviderProviderServer<
HeadingFeatureProps,
HeadingFeatureProps
> = (props) => {
if (!props) {
props = {}
}
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return {
feature: () => {
return {
ClientComponent: HeadingFeatureClientComponent,
markdownTransformers: [MarkdownTransformer(enabledHeadingSizes)],
nodes: [
{
type: HeadingNode.getType(),
converters: {
html: {
converter: async ({ converters, node, parent }) => {
const childrenText = await convertLexicalNodesToHTML({
converters,
lexicalNodes: node.children,
parent: {
...node,
parent,
},
})
return '<' + node?.tag + '>' + childrenText + '</' + node?.tag + '>'
},
nodeTypes: [HeadingNode.getType()],
} as HTMLConverter<SerializedHeadingNode>,
},
node: HeadingNode,
},
],
serverFeatureProps: props,
}
},
key: 'heading',
serverFeatureProps: props,
}
}

View File

@@ -86,6 +86,6 @@ export const CheckListFeature = (): FeatureProvider => {
}, },
} }
}, },
key: 'checkList', key: 'checklist',
} }
} }

View File

@@ -85,6 +85,6 @@ export const OrderedListFeature = (): FeatureProvider => {
}, },
} }
}, },
key: 'orderedList', key: 'orderedlist',
} }
} }

View File

@@ -81,6 +81,6 @@ export const UnorderedListFeature = (): FeatureProvider => {
}, },
} }
}, },
key: 'unorderedList', key: 'unorderedlist',
} }
} }

Some files were not shown because too many files have changed in this diff Show More