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": {
"@apollo/client": "^3.7.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",
"graphql": "^16.8.1",
"next": "^13.5.6",

View File

@@ -18,7 +18,7 @@
"@payloadcms/bundler-webpack": "latest",
"@payloadcms/db-mongodb": "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-seo": "^1.0.8",
"dotenv": "^8.2.0",

View File

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

View File

@@ -1,12 +1,13 @@
'use client'
import { Modal, useModal } from '@faceless-ui/modal'
import React, { useEffect } from 'react'
import { Modal, useModal } from '@payloadcms/ui'
import React, { useCallback, useEffect } from 'react'
import { Button } from '../../elements/Button'
import { useFormModified } from '../../forms/Form/context'
import { useAuth } from '../../providers/Auth'
import { useTranslation } from '../../providers/Translation'
import { Button } from '../../../../ui/src/elements/Button'
import { useFormModified } from '../../../../ui/src/forms/Form/context'
import { useAuth } from '../../../../ui/src/providers/Auth'
import { useTranslation } from '../../../../ui/src/providers/Translation'
import './index.scss'
import { usePreventLeave } from './usePreventLeave'
const modalSlug = 'leave-without-saving'
@@ -17,15 +18,15 @@ const Component: React.FC<{
onCancel: () => void
onConfirm: () => void
}> = ({ isActive, onCancel, onConfirm }) => {
const { closeModal, openModal, modalState } = useModal()
const { closeModal, modalState, openModal } = useModal()
const { t } = useTranslation()
// Manually check for modal state as 'esc' key will not trigger the nav inactivity
useEffect(() => {
if (!modalState?.[modalSlug]?.isOpen && isActive) {
onCancel()
}
}, [modalState])
// useEffect(() => {
// if (!modalState?.[modalSlug]?.isOpen && isActive) {
// onCancel()
// }
// }, [modalState, isActive, onCancel])
useEffect(() => {
if (isActive) openModal(modalSlug)
@@ -53,11 +54,26 @@ const Component: React.FC<{
export const LeaveWithoutSaving: React.FC = () => {
const modified = useFormModified()
const { user } = useAuth()
const [show, setShow] = React.useState(false)
const [hasAccepted, setHasAccepted] = React.useState(false)
return null
// <NavigationPrompt renderIfNotActive when={Boolean(modified && user)}>
// {({ isActive, onCancel, onConfirm }) => (
// <Component isActive={isActive} onCancel={onCancel} onConfirm={onConfirm} />
// )}
// </NavigationPrompt>
const prevent = Boolean(modified && user)
const onPrevent = useCallback(() => {
setShow(true)
}, [])
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,
Form,
FormLoadingOverlayToggle,
LeaveWithoutSaving,
OperationProvider,
getFormState,
useComponentMap,
@@ -17,6 +16,7 @@ import {
import React, { Fragment, useCallback } from 'react'
import { Upload } from '../../../../../ui/src/elements/Upload'
import { LeaveWithoutSaving } from '../../../elements/LeaveWithoutSaving'
// import { getTranslation } from '@payloadcms/translations'
import Auth from './Auth'
import { SetDocumentTitle } from './SetDocumentTitle'
@@ -45,7 +45,7 @@ export const DefaultEditView: React.FC = () => {
hasSavePermission,
initialData: data,
initialState,
onSave: onSaveFromProps,
onSave: onSaveFromContext,
} = useDocumentInfo()
const config = useConfig()
@@ -57,7 +57,7 @@ export const DefaultEditView: React.FC = () => {
serverURL,
} = config
const { getFieldMap } = useComponentMap()
const { componentMap, getFieldMap } = useComponentMap()
const collectionConfig =
collectionSlug && collections.find((collection) => collection.slug === collectionSlug)
@@ -95,8 +95,8 @@ export const DefaultEditView: React.FC = () => {
// await refreshCookieAsync()
// }
if (typeof onSaveFromProps === 'function') {
onSaveFromProps({
if (typeof onSaveFromContext === 'function') {
onSaveFromContext({
...json,
operation: id ? 'update' : 'create',
})
@@ -104,7 +104,7 @@ export const DefaultEditView: React.FC = () => {
},
[
id,
onSaveFromProps,
onSaveFromContext,
// refreshCookieAsync,
// reportUpdate
],
@@ -142,6 +142,8 @@ export const DefaultEditView: React.FC = () => {
[serverURL, apiRoute, id, operation, schemaPath, collectionSlug, globalSlug],
)
const RegisterGetThumbnailFunction = componentMap?.[`${collectionSlug}.adminThumbnail`]
return (
<main className={classes}>
<FieldPathProvider path="" schemaPath={schemaPath}>
@@ -220,11 +222,14 @@ export const DefaultEditView: React.FC = () => {
/>
)}
{upload && (
<Upload
collectionSlug={collectionConfig.slug}
initialState={initialState}
uploadConfig={upload}
/>
<React.Fragment>
{RegisterGetThumbnailFunction && <RegisterGetThumbnailFunction />}
<Upload
collectionSlug={collectionConfig.slug}
initialState={initialState}
uploadConfig={upload}
/>
</React.Fragment>
)}
</Fragment>
)

View File

@@ -1,12 +1,17 @@
'use client'
import type { EditViewProps } from 'payload/config'
import { LoadingOverlay, useComponentMap, useDocumentInfo } from '@payloadcms/ui'
import React, { Fragment } from 'react'
import { LoadingOverlay, useComponentMap, useConfig, useDocumentInfo } from '@payloadcms/ui'
import { redirect } from 'next/navigation'
import React, { Fragment, useEffect } from 'react'
import { useCallback } from 'react'
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()
@@ -22,7 +27,7 @@ export const EditViewClient: React.FC<EditViewProps> = () => {
getDocPermissions()
if (!isEditing) {
// setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`)
redirect(`${adminRoute}/collections/${collectionSlug}/${json?.doc?.id}`)
} else {
// buildState(json.doc, {
// 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
if (!Edit || (!collectionSlug && !globalSlug)) {
return <LoadingOverlay />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"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:types": "tsc --emitDeclarationOnly --outDir dist",
"clean": "rimraf {dist,*.tsbuildinfo}",
@@ -31,7 +31,7 @@
"payload": "^1.1.8 || ^2.0.0"
},
"dependencies": {
"@payloadcms/ui": "workspace:^",
"@payloadcms/ui": "workspace:*",
"lodash.get": "^4.4.2",
"stripe": "^10.2.0",
"uuid": "^9.0.0"

View File

@@ -18,7 +18,7 @@
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"dependencies": {
"@faceless-ui/modal": "2.0.1",
"@faceless-ui/modal": "2.0.2",
"@lexical/headless": "0.13.1",
"@lexical/link": "0.13.1",
"@lexical/list": "0.13.1",
@@ -29,7 +29,6 @@
"@lexical/selection": "0.13.1",
"@lexical/utils": "0.13.1",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"bson-objectid": "2.0.4",
"classnames": "^2.3.2",
"deep-equal": "2.2.3",
@@ -43,6 +42,7 @@
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/ui": "workspace:*",
"@types/json-schema": "7.0.15",
"@types/node": "20.6.2",
"@types/react": "18.2.15",
@@ -50,8 +50,8 @@
"payload": "workspace:*"
},
"peerDependencies": {
"@payloadcms/translations": "workspace:^",
"@payloadcms/ui": "workspace:^",
"@payloadcms/translations": "workspace:*",
"@payloadcms/ui": "workspace:*",
"payload": "^2.4.0"
},
"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,
Pill,
SectionTitle,
createNestedFieldPath,
useDocumentInfo,
useFormSubmitted,
useTranslation,
@@ -21,7 +20,6 @@ import { $getNodeByKey } from 'lexical'
import React, { useCallback } from 'react'
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
import type { FieldProps } from '../../../../types'
import type { BlockFields, BlockNode } from '../nodes/BlocksNode'
import { FormSavePlugin } from './FormSavePlugin'
@@ -89,8 +87,6 @@ export const BlockContent: React.FC<Props> = (props) => {
.filter(Boolean)
.join(' ')
const path = '' as const
const onFormChange = useCallback(
({
fullFieldsWithValues,
@@ -101,9 +97,9 @@ export const BlockContent: React.FC<Props> = (props) => {
}) => {
newFormData = {
...newFormData,
id: formData.id, // TODO: Why does form updatee not include theeeeem
blockName: formData.blockName, // TODO: Why does form updatee not include theeeeem
blockType: formData.blockType, // TODO: Why does form updatee not include theeeeem
id: formData.id,
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,
}
// 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(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,
// which would trigger the "Leave without saving" dialog unnecessarily
if (!isDeepEqual(formData, newFormData)) {
@@ -136,7 +130,6 @@ export const BlockContent: React.FC<Props> = (props) => {
editor.update(() => {
const node: BlockNode = $getNodeByKey(nodeKey)
if (node) {
console.log('saving node data...', newFormData)
node.setFields(newFormData as BlockFields)
}
})
@@ -197,14 +190,14 @@ export const BlockContent: React.FC<Props> = (props) => {
? getTranslation(labels.singular, i18n)
: '[Singular Label]'}
</Pill>
<SectionTitle path={`${path}blockName`} readOnly={field?.admin?.readOnly} />
<SectionTitle path="blockName2" readOnly={field?.readOnly} />
{fieldHasErrors && <ErrorPill count={errorCount} i18n={i18n} withMessage />}
</div>
{editor.isEditable() && (
<Button
buttonStyle="icon-label"
className={`${baseClass}__removeButton`}
disabled={field?.admin?.readOnly}
disabled={field?.readOnly}
icon="x"
onClick={(e) => {
e.preventDefault()

View File

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

View File

@@ -4,15 +4,11 @@ import {
Form,
type FormProps,
type FormState,
buildInitialState,
buildStateFromSchema,
getFormState,
useConfig,
useDocumentInfo,
useFieldPath,
useFormSubmitted,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
@@ -21,6 +17,8 @@ const baseClass = 'lexical-block'
import type { Data } from 'payload/types'
import { v4 as uuid } from 'uuid'
import type { ReducedBlock } from '../../../../../../ui/src/utilities/buildComponentMap/types'
import type { ClientComponentProps } from '../../types'
import type { BlocksFeatureClientProps } from '../feature.client'
@@ -54,8 +52,6 @@ export const BlockComponent: React.FC<Props> = (props) => {
field: { richTextComponentMap },
} = useEditorConfigContext()
console.log('1. Loading node data', formData)
const componentMapRenderedFieldsPath = `feature.blocks.fields.${formData?.blockType}`
const schemaFieldsPath = `${schemaPath}.feature.blocks.${formData?.blockType}`
@@ -64,7 +60,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
?.clientFeatureProps as ClientComponentProps<BlocksFeatureClientProps>
)?.reducedBlocks?.find((block) => block.slug === formData?.blockType)
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath) // Field Schema
const fieldMap = richTextComponentMap.get(componentMapRenderedFieldsPath)
// Field Schema
useEffect(() => {
const awaitInitialState = async () => {
const state = await getFormState({
@@ -79,7 +76,15 @@ export const BlockComponent: React.FC<Props> = (props) => {
}) // Form 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(
async ({ formState: prevFormState }) => {
return await getFormState({
const formState = await getFormState({
apiRoute: config.routes.api,
body: {
id,
@@ -100,18 +105,21 @@ export const BlockComponent: React.FC<Props> = (props) => {
},
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
const formContent = useMemo(() => {
return (
@@ -123,6 +131,7 @@ export const BlockComponent: React.FC<Props> = (props) => {
initialState={initialState}
onChange={[onChange]}
submitted={submitted}
uuid={uuid()}
>
<BlockContent
baseClass={baseClass}
@@ -136,7 +145,16 @@ export const BlockComponent: React.FC<Props> = (props) => {
</FieldPathProvider>
)
)
}, [fieldMap, parentLexicalRichTextField, nodeKey, submitted, initialState, reducedBlock])
}, [
fieldMap,
parentLexicalRichTextField,
nodeKey,
submitted,
initialState,
reducedBlock,
blockFieldWrapperName,
onChange,
])
return <div className={baseClass}>{formContent}</div>
}

View File

@@ -14,8 +14,6 @@ import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
import ObjectID from 'bson-objectid'
import React from 'react'
import { transformInputFormData } from '../utils/transformInputFormData'
export type BlockFields = {
/** Block form data */
[key: string]: any
@@ -90,17 +88,7 @@ export class BlockNode extends DecoratorBlockNode {
return false
}
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
const blockFieldWrapperName = this.getFields().blockType + '-' + this.getFields().id
const transformedFormData = transformInputFormData(this.getFields(), blockFieldWrapperName)
return (
<BlockComponent
blockFieldWrapperName={blockFieldWrapperName}
formData={this.getFields()}
nodeKey={this.getKey()}
transformedFormData={transformedFormData}
/>
)
return <BlockComponent formData={this.getFields()} nodeKey={this.getKey()} />
}
exportDOM(): DOMExportOutput {
@@ -129,18 +117,7 @@ export class BlockNode extends DecoratorBlockNode {
}
setFields(fields: BlockFields): void {
let 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 fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
const writable = this.getWritable()
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 { 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 { HTMLConverter } from '../converter/types'
import type { HTMLConverterFeatureProps } from '../index'
import type { HTMLConverterFeatureProps } from '../feature.server'
import { convertLexicalToHTML } from '../converter'
import { defaultHTMLConverters } from '../converter/defaultConverters'
@@ -21,10 +21,11 @@ type Props = {
export const consolidateHTMLConverters = ({
editorConfig,
}: {
editorConfig: SanitizedEditorConfig
editorConfig: SanitizedServerEditorConfig
}) => {
const htmlConverterFeature = editorConfig.resolvedFeatureMap.get('htmlConverter')
const htmlConverterFeatureProps: HTMLConverterFeatureProps = htmlConverterFeature?.props
const htmlConverterFeatureProps: HTMLConverterFeatureProps =
htmlConverterFeature?.serverFeatureProps
const defaultConvertersWithConvertersFromFeatures = defaultHTMLConverters
@@ -55,7 +56,7 @@ export const lexicalHTML: (
) => TextField = (lexicalFieldName, props) => {
const { name = 'lexicalHTML' } = props
return {
name: name,
name,
type: 'text',
admin: {
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()
}}
title={isRecording ? 'Disable test recorder' : 'Enable test recorder'}
type="button"
>
{isRecording ? 'Disable test recorder' : 'Enable test recorder'}
</button>
@@ -404,6 +405,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault()
}}
title="Insert snapshot"
type="button"
>
Insert Snapshot
</button>
@@ -415,6 +417,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault()
}}
title="Copy to clipboard"
type="button"
>
Copy
</button>
@@ -426,6 +429,7 @@ ${steps.map(formatStep).join(`\n`)}
e.preventDefault()
}}
title="Download as a file"
type="button"
>
Download
</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'
export function TreeViewPlugin(): JSX.Element {
export function TreeViewPlugin(): React.ReactNode {
const [editor] = useLexicalComposerContext()
return (
<TreeView

View File

@@ -1,7 +1,10 @@
'use client'
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 {
BOLD_ITALIC_STAR,
@@ -10,9 +13,9 @@ import {
BOLD_UNDERSCORE,
} from './markdownTransformers'
export const BoldTextFeature = (): FeatureProvider => {
const BoldFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
dependenciesSoft: ['italic'],
clientFeatureProps: props,
feature: ({ featureProviderMap }) => {
const markdownTransformers = [BOLD_STAR, BOLD_UNDERSCORE]
if (featureProviderMap.get('italic')) {
@@ -20,13 +23,12 @@ export const BoldTextFeature = (): FeatureProvider => {
}
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Bold').then((module) => module.BoldIcon),
ChildComponent: BoldIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
return selection.hasFormat('bold')
@@ -42,10 +44,10 @@ export const BoldTextFeature = (): FeatureProvider => {
]),
],
},
markdownTransformers: markdownTransformers,
props: null,
markdownTransformers,
}
},
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 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 { INLINE_CODE } from './markdownTransformers'
export const InlineCodeTextFeature = (): FeatureProvider => {
const InlineCodeFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Code').then((module) => module.CodeIcon),
ChildComponent: CodeIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
return selection.hasFormat('code')
@@ -31,10 +35,11 @@ export const InlineCodeTextFeature = (): FeatureProvider => {
]),
],
},
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 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 { ITALIC_STAR, ITALIC_UNDERSCORE } from './markdownTransformers'
export const ItalicTextFeature = (): FeatureProvider => {
const ItalicFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Italic').then((module) => module.ItalicIcon),
ChildComponent: ItalicIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
return selection.hasFormat('italic')
@@ -32,9 +37,9 @@ export const ItalicTextFeature = (): FeatureProvider => {
],
},
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 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 { STRIKETHROUGH } from './markdownTransformers'
export const StrikethroughTextFeature = (): FeatureProvider => {
const StrikethroughFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Strikethrough').then(
(module) => module.StrikethroughIcon,
),
ChildComponent: StrikethroughIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
return selection.hasFormat('strikethrough')
@@ -34,9 +37,9 @@ export const StrikethroughTextFeature = (): FeatureProvider => {
],
},
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 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'
export const SubscriptTextFeature = (): FeatureProvider => {
const SubscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Subscript').then(
(module) => module.SubscriptIcon,
),
ChildComponent: SubscriptIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
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 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'
export const SuperscriptTextFeature = (): FeatureProvider => {
const SuperscriptFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Superscript').then(
(module) => module.SuperscriptIcon,
),
ChildComponent: SuperscriptIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
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 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'
export const UnderlineTextFeature = (): FeatureProvider => {
const UnderlineFeatureClient: FeatureProviderProviderClient<undefined> = (props) => {
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
SectionWithEntries([
{
ChildComponent: () =>
// @ts-expect-error-next-line
import('../../../lexical/ui/icons/Underline').then(
(module) => module.UnderlineIcon,
),
ChildComponent: UnderlineIcon,
isActive: ({ selection }) => {
if ($isRangeSelection(selection)) {
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 { $getSelection } from 'lexical'
import type { HTMLConverter } from '../converters/html/converter/types'
import type { FeatureProvider } from '../types'
import type { FeatureProviderProviderClient } from '../types'
import type { HeadingFeatureProps } from './feature.server'
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 { convertLexicalNodesToHTML } from '../converters/html/converter'
import { createClientComponent } from '../createClientComponent'
import { MarkdownTransformer } from './markdownTransformer'
const setHeading = (headingSize: HeadingTagType) => {
@@ -17,31 +26,23 @@ const setHeading = (headingSize: HeadingTagType) => {
$setBlocksType(selection, () => $createHeadingNode(headingSize))
}
type Props = {
enabledHeadingSizes?: HeadingTagType[]
}
const iconImports = {
// @ts-expect-error-next-line
h1: () => import('../../lexical/ui/icons/H1').then((module) => module.H1Icon),
// @ts-expect-error-next-line
h2: () => import('../../lexical/ui/icons/H2').then((module) => module.H2Icon),
// @ts-expect-error-next-line
h3: () => import('../../lexical/ui/icons/H3').then((module) => module.H3Icon),
// @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),
h1: H1Icon,
h2: H2Icon,
h3: H3Icon,
h4: H4Icon,
h5: H5Icon,
h6: H6Icon,
}
export const HeadingFeature = (props: Props): FeatureProvider => {
const HeadingFeatureClient: FeatureProviderProviderClient<HeadingFeatureProps> = (props) => {
const { enabledHeadingSizes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } = props
return {
clientFeatureProps: props,
feature: () => {
return {
clientFeatureProps: props,
floatingSelectToolbar: {
sections: [
...enabledHeadingSizes.map((headingSize, i) =>
@@ -63,30 +64,7 @@ export const HeadingFeature = (props: Props): FeatureProvider => {
],
},
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,
},
],
props,
nodes: [HeadingNode],
slashMenu: {
options: [
...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