## Description
Adds a new property to `collection` / `global` configs called
`lockDocuments`.
Set to `true` by default - the lock is automatically triggered when a
user begins editing a document within the Admin Panel and remains in
place until the user exits the editing view or the lock expires due to
inactivity.
Set to `false` to disable document locking entirely - i.e.
`lockDocuments: false`
You can pass an object to this property to configure the `duration` in
seconds, which defines how long the document remains locked without user
interaction. If no edits are made within the specified time (default:
300 seconds), the lock expires, allowing other users to edit / update or
delete the document.
```
lockDocuments: {
duration: 180, // 180 seconds or 3 minutes
}
```
- [x] I have read and understand the
[CONTRIBUTING.md](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md)
document in this repository.
## Type of change
- [x] New feature (non-breaking change which adds functionality)
## Checklist:
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] Existing test suite passes locally with my changes
- [x] I have made corresponding changes to the documentation
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import type { FormProps } from '@payloadcms/ui'
|
|
|
|
import {
|
|
Collapsible,
|
|
Form,
|
|
Pill,
|
|
RenderComponent,
|
|
SectionTitle,
|
|
ShimmerEffect,
|
|
useConfig,
|
|
useDocumentInfo,
|
|
useFieldProps,
|
|
useFormSubmitted,
|
|
useTranslation,
|
|
} from '@payloadcms/ui'
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
|
const baseClass = 'lexical-block'
|
|
import type { BlocksFieldClient, FormState } from 'payload'
|
|
|
|
import { getTranslation } from '@payloadcms/translations'
|
|
import { getFormState } from '@payloadcms/ui/shared'
|
|
import { v4 as uuid } from 'uuid'
|
|
|
|
import type { BlockFields } from '../../server/nodes/BlocksNode.js'
|
|
|
|
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
|
|
import { BlockContent } from './BlockContent.js'
|
|
import './index.scss'
|
|
|
|
type Props = {
|
|
readonly children?: React.ReactNode
|
|
readonly formData: BlockFields
|
|
readonly nodeKey?: string
|
|
}
|
|
|
|
export const BlockComponent: React.FC<Props> = (props) => {
|
|
const { formData, nodeKey } = props
|
|
const { config } = useConfig()
|
|
const submitted = useFormSubmitted()
|
|
const { id } = useDocumentInfo()
|
|
const { path, schemaPath } = useFieldProps()
|
|
const { field: parentLexicalRichTextField } = useEditorConfigContext()
|
|
|
|
const [initialState, setInitialState] = useState<false | FormState>(false)
|
|
const {
|
|
field: { richTextComponentMap },
|
|
} = useEditorConfigContext()
|
|
|
|
const schemaFieldsPath = `${schemaPath}.lexical_internal_feature.blocks.lexical_blocks.lexical_blocks.${formData.blockType}`
|
|
|
|
const componentMapRenderedBlockPath = `lexical_internal_feature.blocks.fields.lexical_blocks`
|
|
const blocksField: BlocksFieldClient = richTextComponentMap.get(componentMapRenderedBlockPath)[0]
|
|
|
|
const clientBlock = blocksField.blocks.find((block) => block.slug === formData.blockType)
|
|
|
|
// Field Schema
|
|
useEffect(() => {
|
|
const awaitInitialState = async () => {
|
|
const { state } = await getFormState({
|
|
apiRoute: config.routes.api,
|
|
body: {
|
|
id,
|
|
data: formData,
|
|
operation: 'update',
|
|
schemaPath: schemaFieldsPath,
|
|
},
|
|
serverURL: config.serverURL,
|
|
})
|
|
|
|
if (state) {
|
|
state.blockName = {
|
|
initialValue: '',
|
|
passesCondition: true,
|
|
valid: true,
|
|
value: formData.blockName,
|
|
}
|
|
|
|
setInitialState(state)
|
|
}
|
|
}
|
|
|
|
if (formData) {
|
|
void awaitInitialState()
|
|
}
|
|
}, [config.routes.api, config.serverURL, schemaFieldsPath, id]) // DO NOT ADD FORMDATA HERE! Adding formData will kick you out of sub block editors while writing.
|
|
|
|
const onChange: FormProps['onChange'][0] = useCallback(
|
|
async ({ formState: prevFormState }) => {
|
|
const { state: formState } = await getFormState({
|
|
apiRoute: config.routes.api,
|
|
body: {
|
|
id,
|
|
formState: prevFormState,
|
|
operation: 'update',
|
|
schemaPath: schemaFieldsPath,
|
|
},
|
|
serverURL: config.serverURL,
|
|
})
|
|
|
|
formState.blockName = {
|
|
initialValue: '',
|
|
passesCondition: true,
|
|
valid: true,
|
|
value: formData.blockName,
|
|
}
|
|
|
|
return formState
|
|
},
|
|
|
|
[config.routes.api, config.serverURL, id, schemaFieldsPath, formData.blockName],
|
|
)
|
|
const { i18n } = useTranslation()
|
|
|
|
const classNames = [`${baseClass}__row`, `${baseClass}__row--no-errors`].filter(Boolean).join(' ')
|
|
|
|
// Memoized Form JSX
|
|
const formContent = useMemo(() => {
|
|
return clientBlock && initialState !== false ? (
|
|
<Form
|
|
beforeSubmit={[onChange]}
|
|
fields={clientBlock.fields}
|
|
initialState={initialState}
|
|
onChange={[onChange]}
|
|
submitted={submitted}
|
|
uuid={uuid()}
|
|
>
|
|
<BlockContent
|
|
baseClass={baseClass}
|
|
clientBlock={clientBlock}
|
|
field={parentLexicalRichTextField}
|
|
formData={formData}
|
|
formSchema={clientBlock.fields}
|
|
nodeKey={nodeKey}
|
|
path={`${path}.lexical_internal_feature.blocks.${formData.blockType}`}
|
|
schemaPath={schemaFieldsPath}
|
|
/>
|
|
</Form>
|
|
) : (
|
|
<Collapsible
|
|
className={classNames}
|
|
collapsibleStyle="default"
|
|
header={
|
|
clientBlock?.admin?.components?.Label ? (
|
|
<RenderComponent
|
|
clientProps={{ blockKind: 'lexicalBlock', formData }}
|
|
mappedComponent={clientBlock.admin.components.Label}
|
|
/>
|
|
) : (
|
|
<div className={`${baseClass}__block-header`}>
|
|
<div>
|
|
<Pill
|
|
className={`${baseClass}__block-pill ${baseClass}__block-pill-${formData?.blockType}`}
|
|
pillStyle="white"
|
|
>
|
|
{clientBlock && typeof clientBlock.labels.singular === 'string'
|
|
? getTranslation(clientBlock.labels.singular, i18n)
|
|
: clientBlock.slug}
|
|
</Pill>
|
|
<SectionTitle
|
|
path="blockName"
|
|
readOnly={parentLexicalRichTextField?.admin?.readOnly}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
key={0}
|
|
>
|
|
<ShimmerEffect height="35vh" />
|
|
</Collapsible>
|
|
)
|
|
}, [
|
|
clientBlock,
|
|
initialState,
|
|
onChange,
|
|
submitted,
|
|
parentLexicalRichTextField,
|
|
nodeKey,
|
|
path,
|
|
schemaFieldsPath,
|
|
classNames,
|
|
i18n,
|
|
])
|
|
|
|
return <div className={baseClass + ' ' + baseClass + '-' + formData.blockType}>{formContent}</div>
|
|
}
|