fix: lock documents using the live-preview view (#8343)
Updates: - Exports `handleGoBack`, `handleBackToDashboard`, & `handleTakeOver` functions to consolidate logic in default edit view & live-preview edit view. - Only unlock document on navigation away from edit view entirely (aka do not unlock document if switching between tabs like `edit` --> `live-preview` --> `versions` --> `api`
This commit is contained in:
@@ -17,7 +17,13 @@ import {
|
|||||||
useEditDepth,
|
useEditDepth,
|
||||||
useUploadEdits,
|
useUploadEdits,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { formatAdminURL, getFormState } from '@payloadcms/ui/shared'
|
import {
|
||||||
|
formatAdminURL,
|
||||||
|
getFormState,
|
||||||
|
handleBackToDashboard,
|
||||||
|
handleGoBack,
|
||||||
|
handleTakeOver,
|
||||||
|
} from '@payloadcms/ui/shared'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
@@ -151,89 +157,6 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleTakeOver = useCallback(() => {
|
|
||||||
if (!isLockingEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call updateDocumentEditor to update the document's owner to the current user
|
|
||||||
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
|
|
||||||
|
|
||||||
documentLockStateRef.current.hasShownLockedModal = true
|
|
||||||
|
|
||||||
// Update the locked state to reflect the current user as the owner
|
|
||||||
documentLockStateRef.current = {
|
|
||||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
|
|
||||||
isLocked: true,
|
|
||||||
user,
|
|
||||||
}
|
|
||||||
setCurrentEditor(user)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Error during document takeover:', error)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
updateDocumentEditor,
|
|
||||||
id,
|
|
||||||
collectionSlug,
|
|
||||||
globalSlug,
|
|
||||||
user,
|
|
||||||
setCurrentEditor,
|
|
||||||
isLockingEnabled,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleTakeOverWithinDoc = useCallback(() => {
|
|
||||||
if (!isLockingEnabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Call updateDocumentEditor to update the document's owner to the current user
|
|
||||||
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
|
|
||||||
|
|
||||||
// Update the locked state to reflect the current user as the owner
|
|
||||||
documentLockStateRef.current = {
|
|
||||||
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
|
|
||||||
isLocked: true,
|
|
||||||
user,
|
|
||||||
}
|
|
||||||
setCurrentEditor(user)
|
|
||||||
|
|
||||||
// Ensure the document is editable for the incoming user
|
|
||||||
setIsReadOnlyForIncomingUser(false)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('Error during document takeover:', error)
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
updateDocumentEditor,
|
|
||||||
id,
|
|
||||||
collectionSlug,
|
|
||||||
globalSlug,
|
|
||||||
user,
|
|
||||||
setCurrentEditor,
|
|
||||||
isLockingEnabled,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleGoBack = useCallback(() => {
|
|
||||||
const redirectRoute = formatAdminURL({
|
|
||||||
adminRoute,
|
|
||||||
path: collectionSlug ? `/collections/${collectionSlug}` : '/',
|
|
||||||
})
|
|
||||||
router.push(redirectRoute)
|
|
||||||
}, [adminRoute, collectionSlug, router])
|
|
||||||
|
|
||||||
const handleBackToDashboard = useCallback(() => {
|
|
||||||
setShowTakeOverModal(false)
|
|
||||||
const redirectRoute = formatAdminURL({
|
|
||||||
adminRoute,
|
|
||||||
path: '/',
|
|
||||||
})
|
|
||||||
|
|
||||||
router.push(redirectRoute)
|
|
||||||
}, [adminRoute, router])
|
|
||||||
|
|
||||||
const onSave = useCallback(
|
const onSave = useCallback(
|
||||||
(json) => {
|
(json) => {
|
||||||
reportUpdate({
|
reportUpdate({
|
||||||
@@ -373,7 +296,19 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((id || globalSlug) && documentIsLocked) {
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
|
const documentId = id || globalSlug
|
||||||
|
|
||||||
|
// Routes where we do NOT want to unlock the document
|
||||||
|
const stayWithinDocumentPaths = ['preview', 'api', 'versions']
|
||||||
|
|
||||||
|
const isStayingWithinDocument = stayWithinDocumentPaths.some((path) =>
|
||||||
|
currentPath.includes(path),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unlock the document only if we're actually navigating away from the document
|
||||||
|
if (documentId && documentIsLocked && !isStayingWithinDocument) {
|
||||||
// Check if this user is still the current editor
|
// Check if this user is still the current editor
|
||||||
if (documentLockStateRef.current?.user?.id === user.id) {
|
if (documentLockStateRef.current?.user?.id === user.id) {
|
||||||
void unlockDocument(id, collectionSlug ?? globalSlug)
|
void unlockDocument(id, collectionSlug ?? globalSlug)
|
||||||
@@ -421,20 +356,32 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
{BeforeDocument}
|
{BeforeDocument}
|
||||||
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
|
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
|
||||||
<DocumentLocked
|
<DocumentLocked
|
||||||
handleGoBack={handleGoBack}
|
handleGoBack={() => handleGoBack({ adminRoute, collectionSlug, router })}
|
||||||
isActive={shouldShowDocumentLockedModal}
|
isActive={shouldShowDocumentLockedModal}
|
||||||
onReadOnly={() => {
|
onReadOnly={() => {
|
||||||
setIsReadOnlyForIncomingUser(true)
|
setIsReadOnlyForIncomingUser(true)
|
||||||
setShowTakeOverModal(false)
|
setShowTakeOverModal(false)
|
||||||
}}
|
}}
|
||||||
onTakeOver={handleTakeOver}
|
onTakeOver={() =>
|
||||||
|
handleTakeOver(
|
||||||
|
id,
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
user,
|
||||||
|
false,
|
||||||
|
updateDocumentEditor,
|
||||||
|
setCurrentEditor,
|
||||||
|
documentLockStateRef,
|
||||||
|
isLockingEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
updatedAt={lastUpdateTime}
|
updatedAt={lastUpdateTime}
|
||||||
user={currentEditor}
|
user={currentEditor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isLockingEnabled && showTakeOverModal && (
|
{isLockingEnabled && showTakeOverModal && (
|
||||||
<DocumentTakeOver
|
<DocumentTakeOver
|
||||||
handleBackToDashboard={handleBackToDashboard}
|
handleBackToDashboard={() => handleBackToDashboard({ adminRoute, router })}
|
||||||
isActive={showTakeOverModal}
|
isActive={showTakeOverModal}
|
||||||
onReadOnly={() => {
|
onReadOnly={() => {
|
||||||
setIsReadOnlyForIncomingUser(true)
|
setIsReadOnlyForIncomingUser(true)
|
||||||
@@ -469,7 +416,20 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
onDrawerCreate={onDrawerCreate}
|
onDrawerCreate={onDrawerCreate}
|
||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onTakeOver={handleTakeOverWithinDoc}
|
onTakeOver={() =>
|
||||||
|
handleTakeOver(
|
||||||
|
id,
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
user,
|
||||||
|
true,
|
||||||
|
updateDocumentEditor,
|
||||||
|
setCurrentEditor,
|
||||||
|
documentLockStateRef,
|
||||||
|
isLockingEnabled,
|
||||||
|
setIsReadOnlyForIncomingUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
permissions={docPermissions}
|
permissions={docPermissions}
|
||||||
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
|
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
|
||||||
redirectAfterDelete={redirectAfterDelete}
|
redirectAfterDelete={redirectAfterDelete}
|
||||||
@@ -494,6 +454,7 @@ export const DefaultEditView: React.FC = () => {
|
|||||||
requirePassword={!id}
|
requirePassword={!id}
|
||||||
setSchemaPath={setSchemaPath}
|
setSchemaPath={setSchemaPath}
|
||||||
setValidateBeforeSubmit={setValidateBeforeSubmit}
|
setValidateBeforeSubmit={setValidateBeforeSubmit}
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
useAPIKey={auth.useAPIKey}
|
useAPIKey={auth.useAPIKey}
|
||||||
username={data?.username}
|
username={data?.username}
|
||||||
verify={auth.verify}
|
verify={auth.verify}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
ClientConfig,
|
ClientConfig,
|
||||||
ClientField,
|
ClientField,
|
||||||
ClientGlobalConfig,
|
ClientGlobalConfig,
|
||||||
|
ClientUser,
|
||||||
Data,
|
Data,
|
||||||
LivePreviewConfig,
|
LivePreviewConfig,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
@@ -21,9 +22,17 @@ import {
|
|||||||
useDocumentInfo,
|
useDocumentInfo,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
} from '@payloadcms/ui'
|
} from '@payloadcms/ui'
|
||||||
import { getFormState } from '@payloadcms/ui/shared'
|
import {
|
||||||
import React, { Fragment, useCallback } from 'react'
|
getFormState,
|
||||||
|
handleBackToDashboard,
|
||||||
|
handleGoBack,
|
||||||
|
handleTakeOver,
|
||||||
|
} from '@payloadcms/ui/shared'
|
||||||
|
import { useRouter } from 'next/navigation.js'
|
||||||
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { DocumentLocked } from '../../elements/DocumentLocked/index.js'
|
||||||
|
import { DocumentTakeOver } from '../../elements/DocumentTakeOver/index.js'
|
||||||
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
import { LeaveWithoutSaving } from '../../elements/LeaveWithoutSaving/index.js'
|
||||||
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
import { SetDocumentStepNav } from '../Edit/Default/SetDocumentStepNav/index.js'
|
||||||
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
|
import { SetDocumentTitle } from '../Edit/Default/SetDocumentTitle/index.js'
|
||||||
@@ -63,9 +72,11 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
BeforeDocument,
|
BeforeDocument,
|
||||||
BeforeFields,
|
BeforeFields,
|
||||||
collectionSlug,
|
collectionSlug,
|
||||||
|
currentEditor,
|
||||||
disableActions,
|
disableActions,
|
||||||
disableLeaveWithoutSaving,
|
disableLeaveWithoutSaving,
|
||||||
docPermissions,
|
docPermissions,
|
||||||
|
documentIsLocked,
|
||||||
getDocPreferences,
|
getDocPreferences,
|
||||||
globalSlug,
|
globalSlug,
|
||||||
hasPublishPermission,
|
hasPublishPermission,
|
||||||
@@ -75,6 +86,10 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
isEditing,
|
isEditing,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
onSave: onSaveFromProps,
|
onSave: onSaveFromProps,
|
||||||
|
setCurrentEditor,
|
||||||
|
setDocumentIsLocked,
|
||||||
|
unlockDocument,
|
||||||
|
updateDocumentEditor,
|
||||||
} = useDocumentInfo()
|
} = useDocumentInfo()
|
||||||
|
|
||||||
const operation = id ? 'update' : 'create'
|
const operation = id ? 'update' : 'create'
|
||||||
@@ -82,13 +97,36 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
const {
|
const {
|
||||||
config: {
|
config: {
|
||||||
admin: { user: userSlug },
|
admin: { user: userSlug },
|
||||||
|
routes: { admin: adminRoute },
|
||||||
},
|
},
|
||||||
} = useConfig()
|
} = useConfig()
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { previewWindowType } = useLivePreviewContext()
|
const { previewWindowType } = useLivePreviewContext()
|
||||||
const { refreshCookieAsync, user } = useAuth()
|
const { refreshCookieAsync, user } = useAuth()
|
||||||
const { reportUpdate } = useDocumentEvents()
|
const { reportUpdate } = useDocumentEvents()
|
||||||
|
|
||||||
|
const docConfig = collectionConfig || globalConfig
|
||||||
|
|
||||||
|
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
|
||||||
|
|
||||||
|
const isLockingEnabled = lockDocumentsProp !== false
|
||||||
|
|
||||||
|
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||||
|
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||||
|
|
||||||
|
const documentLockStateRef = useRef<{
|
||||||
|
hasShownLockedModal: boolean
|
||||||
|
isLocked: boolean
|
||||||
|
user: ClientUser
|
||||||
|
} | null>({
|
||||||
|
hasShownLockedModal: false,
|
||||||
|
isLocked: false,
|
||||||
|
user: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now())
|
||||||
|
|
||||||
const onSave = useCallback(
|
const onSave = useCallback(
|
||||||
(json) => {
|
(json) => {
|
||||||
reportUpdate({
|
reportUpdate({
|
||||||
@@ -103,6 +141,11 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
void refreshCookieAsync()
|
void refreshCookieAsync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlock the document after save
|
||||||
|
if ((id || globalSlug) && isLockingEnabled) {
|
||||||
|
setDocumentIsLocked(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof onSaveFromProps === 'function') {
|
if (typeof onSaveFromProps === 'function') {
|
||||||
void onSaveFromProps({
|
void onSaveFromProps({
|
||||||
...json,
|
...json,
|
||||||
@@ -110,47 +153,194 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[collectionSlug, id, onSaveFromProps, refreshCookieAsync, reportUpdate, user, userSlug],
|
[
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
id,
|
||||||
|
isLockingEnabled,
|
||||||
|
onSaveFromProps,
|
||||||
|
refreshCookieAsync,
|
||||||
|
reportUpdate,
|
||||||
|
setDocumentIsLocked,
|
||||||
|
user,
|
||||||
|
userSlug,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onChange: FormProps['onChange'][0] = useCallback(
|
const onChange: FormProps['onChange'][0] = useCallback(
|
||||||
async ({ formState: prevFormState }) => {
|
async ({ formState: prevFormState }) => {
|
||||||
|
const currentTime = Date.now()
|
||||||
|
const timeSinceLastUpdate = currentTime - lastUpdateTime
|
||||||
|
|
||||||
|
const updateLastEdited = isLockingEnabled && timeSinceLastUpdate >= 10000 // 10 seconds
|
||||||
|
|
||||||
|
if (updateLastEdited) {
|
||||||
|
setLastUpdateTime(currentTime)
|
||||||
|
}
|
||||||
|
|
||||||
const docPreferences = await getDocPreferences()
|
const docPreferences = await getDocPreferences()
|
||||||
|
|
||||||
const { state } = await getFormState({
|
const { lockedState, state } = await getFormState({
|
||||||
apiRoute,
|
apiRoute,
|
||||||
body: {
|
body: {
|
||||||
id,
|
id,
|
||||||
|
collectionSlug,
|
||||||
docPreferences,
|
docPreferences,
|
||||||
formState: prevFormState,
|
formState: prevFormState,
|
||||||
|
globalSlug,
|
||||||
operation,
|
operation,
|
||||||
|
returnLockStatus: isLockingEnabled ? true : false,
|
||||||
schemaPath,
|
schemaPath,
|
||||||
|
updateLastEdited,
|
||||||
},
|
},
|
||||||
serverURL,
|
serverURL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setDocumentIsLocked(true)
|
||||||
|
|
||||||
|
if (isLockingEnabled) {
|
||||||
|
const previousOwnerId = documentLockStateRef.current?.user?.id
|
||||||
|
|
||||||
|
if (lockedState) {
|
||||||
|
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
|
||||||
|
if (previousOwnerId === user.id && lockedState.user.id !== user.id) {
|
||||||
|
setShowTakeOverModal(true)
|
||||||
|
documentLockStateRef.current.hasShownLockedModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
documentLockStateRef.current = documentLockStateRef.current = {
|
||||||
|
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal || false,
|
||||||
|
isLocked: true,
|
||||||
|
user: lockedState.user,
|
||||||
|
}
|
||||||
|
setCurrentEditor(lockedState.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return state
|
return state
|
||||||
},
|
},
|
||||||
[serverURL, apiRoute, id, operation, schemaPath, getDocPreferences],
|
[
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
serverURL,
|
||||||
|
apiRoute,
|
||||||
|
id,
|
||||||
|
isLockingEnabled,
|
||||||
|
lastUpdateTime,
|
||||||
|
operation,
|
||||||
|
schemaPath,
|
||||||
|
getDocPreferences,
|
||||||
|
setCurrentEditor,
|
||||||
|
setDocumentIsLocked,
|
||||||
|
user,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Clean up when the component unmounts or when the document is unlocked
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (!isLockingEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
|
const documentId = id || globalSlug
|
||||||
|
|
||||||
|
// Routes where we do NOT want to unlock the document
|
||||||
|
const stayWithinDocumentPaths = ['preview', 'api', 'versions']
|
||||||
|
|
||||||
|
const isStayingWithinDocument = stayWithinDocumentPaths.some((path) =>
|
||||||
|
currentPath.includes(path),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unlock the document only if we're actually navigating away from the document
|
||||||
|
if (documentId && documentIsLocked && !isStayingWithinDocument) {
|
||||||
|
// Check if this user is still the current editor
|
||||||
|
if (documentLockStateRef.current?.user?.id === user.id) {
|
||||||
|
void unlockDocument(id, collectionSlug ?? globalSlug)
|
||||||
|
setDocumentIsLocked(false)
|
||||||
|
setCurrentEditor(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowTakeOverModal(false)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
id,
|
||||||
|
unlockDocument,
|
||||||
|
user.id,
|
||||||
|
setCurrentEditor,
|
||||||
|
isLockingEnabled,
|
||||||
|
documentIsLocked,
|
||||||
|
setDocumentIsLocked,
|
||||||
|
])
|
||||||
|
|
||||||
|
const shouldShowDocumentLockedModal =
|
||||||
|
documentIsLocked &&
|
||||||
|
currentEditor &&
|
||||||
|
currentEditor.id !== user.id &&
|
||||||
|
!isReadOnlyForIncomingUser &&
|
||||||
|
!showTakeOverModal &&
|
||||||
|
// eslint-disable-next-line react-compiler/react-compiler
|
||||||
|
!documentLockStateRef.current?.hasShownLockedModal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OperationProvider operation={operation}>
|
<OperationProvider operation={operation}>
|
||||||
<Form
|
<Form
|
||||||
action={action}
|
action={action}
|
||||||
className={`${baseClass}__form`}
|
className={`${baseClass}__form`}
|
||||||
disabled={!hasSavePermission}
|
disabled={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||||
initialState={initialState}
|
initialState={initialState}
|
||||||
isInitializing={isInitializing}
|
isInitializing={isInitializing}
|
||||||
method={id ? 'PATCH' : 'POST'}
|
method={id ? 'PATCH' : 'POST'}
|
||||||
onChange={[onChange]}
|
onChange={[onChange]}
|
||||||
onSuccess={onSave}
|
onSuccess={onSave}
|
||||||
>
|
>
|
||||||
|
{isLockingEnabled && shouldShowDocumentLockedModal && !isReadOnlyForIncomingUser && (
|
||||||
|
<DocumentLocked
|
||||||
|
handleGoBack={() => handleGoBack({ adminRoute, collectionSlug, router })}
|
||||||
|
isActive={shouldShowDocumentLockedModal}
|
||||||
|
onReadOnly={() => {
|
||||||
|
setIsReadOnlyForIncomingUser(true)
|
||||||
|
setShowTakeOverModal(false)
|
||||||
|
}}
|
||||||
|
onTakeOver={() =>
|
||||||
|
handleTakeOver(
|
||||||
|
id,
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
user,
|
||||||
|
false,
|
||||||
|
updateDocumentEditor,
|
||||||
|
setCurrentEditor,
|
||||||
|
documentLockStateRef,
|
||||||
|
isLockingEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updatedAt={lastUpdateTime}
|
||||||
|
user={currentEditor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isLockingEnabled && showTakeOverModal && (
|
||||||
|
<DocumentTakeOver
|
||||||
|
handleBackToDashboard={() => handleBackToDashboard({ adminRoute, router })}
|
||||||
|
isActive={showTakeOverModal}
|
||||||
|
onReadOnly={() => {
|
||||||
|
setIsReadOnlyForIncomingUser(true)
|
||||||
|
setShowTakeOverModal(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{((collectionConfig &&
|
{((collectionConfig &&
|
||||||
!(collectionConfig.versions?.drafts && collectionConfig.versions?.drafts?.autosave)) ||
|
!(collectionConfig.versions?.drafts && collectionConfig.versions?.drafts?.autosave)) ||
|
||||||
(globalConfig &&
|
(globalConfig &&
|
||||||
!(globalConfig.versions?.drafts && globalConfig.versions?.drafts?.autosave))) &&
|
!(globalConfig.versions?.drafts && globalConfig.versions?.drafts?.autosave))) &&
|
||||||
!disableLeaveWithoutSaving && <LeaveWithoutSaving />}
|
!disableLeaveWithoutSaving &&
|
||||||
|
!isReadOnlyForIncomingUser && <LeaveWithoutSaving />}
|
||||||
<SetDocumentStepNav
|
<SetDocumentStepNav
|
||||||
collectionSlug={collectionSlug}
|
collectionSlug={collectionSlug}
|
||||||
globalLabel={globalConfig?.label}
|
globalLabel={globalConfig?.label}
|
||||||
@@ -174,8 +364,24 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
hasSavePermission={hasSavePermission}
|
hasSavePermission={hasSavePermission}
|
||||||
id={id}
|
id={id}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
|
onTakeOver={() =>
|
||||||
|
handleTakeOver(
|
||||||
|
id,
|
||||||
|
collectionSlug,
|
||||||
|
globalSlug,
|
||||||
|
user,
|
||||||
|
true,
|
||||||
|
updateDocumentEditor,
|
||||||
|
setCurrentEditor,
|
||||||
|
documentLockStateRef,
|
||||||
|
isLockingEnabled,
|
||||||
|
setIsReadOnlyForIncomingUser,
|
||||||
|
)
|
||||||
|
}
|
||||||
permissions={docPermissions}
|
permissions={docPermissions}
|
||||||
|
readOnlyForIncomingUser={isReadOnlyForIncomingUser}
|
||||||
slug={collectionConfig?.slug || globalConfig?.slug}
|
slug={collectionConfig?.slug || globalConfig?.slug}
|
||||||
|
user={currentEditor}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
|
className={[baseClass, previewWindowType === 'popup' && `${baseClass}--detached`]
|
||||||
@@ -197,7 +403,7 @@ const PreviewView: React.FC<Props> = ({
|
|||||||
docPermissions={docPermissions}
|
docPermissions={docPermissions}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
forceSidebarWrap
|
forceSidebarWrap
|
||||||
readOnly={!hasSavePermission}
|
readOnly={isReadOnlyForIncomingUser || !hasSavePermission}
|
||||||
schemaPath={collectionSlug || globalSlug}
|
schemaPath={collectionSlug || globalSlug}
|
||||||
/>
|
/>
|
||||||
{AfterDocument}
|
{AfterDocument}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export const DocumentControls: React.FC<{
|
|||||||
<Button
|
<Button
|
||||||
buttonStyle="secondary"
|
buttonStyle="secondary"
|
||||||
id="take-over"
|
id="take-over"
|
||||||
onClick={() => void onTakeOver()}
|
onClick={onTakeOver}
|
||||||
size="medium"
|
size="medium"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,5 +20,8 @@ export {
|
|||||||
type Group,
|
type Group,
|
||||||
groupNavItems,
|
groupNavItems,
|
||||||
} from '../../utilities/groupNavItems.js'
|
} from '../../utilities/groupNavItems.js'
|
||||||
|
export { handleBackToDashboard } from '../../utilities/handleBackToDashboard.js'
|
||||||
|
export { handleGoBack } from '../../utilities/handleGoBack.js'
|
||||||
|
export { handleTakeOver } from '../../utilities/handleTakeOver.js'
|
||||||
export { hasSavePermission } from '../../utilities/hasSavePermission.js'
|
export { hasSavePermission } from '../../utilities/hasSavePermission.js'
|
||||||
export { isEditing } from '../../utilities/isEditing.js'
|
export { isEditing } from '../../utilities/isEditing.js'
|
||||||
|
|||||||
16
packages/ui/src/utilities/handleBackToDashboard.tsx
Normal file
16
packages/ui/src/utilities/handleBackToDashboard.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
|
||||||
|
|
||||||
|
import { formatAdminURL } from './formatAdminURL.js'
|
||||||
|
|
||||||
|
type BackToDashboardProps = {
|
||||||
|
adminRoute: string
|
||||||
|
router: AppRouterInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleBackToDashboard = ({ adminRoute, router }: BackToDashboardProps) => {
|
||||||
|
const redirectRoute = formatAdminURL({
|
||||||
|
adminRoute,
|
||||||
|
path: '/',
|
||||||
|
})
|
||||||
|
router.push(redirectRoute)
|
||||||
|
}
|
||||||
17
packages/ui/src/utilities/handleGoBack.tsx
Normal file
17
packages/ui/src/utilities/handleGoBack.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
|
||||||
|
|
||||||
|
import { formatAdminURL } from './formatAdminURL.js'
|
||||||
|
|
||||||
|
type GoBackProps = {
|
||||||
|
adminRoute: string
|
||||||
|
collectionSlug: string
|
||||||
|
router: AppRouterInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleGoBack = ({ adminRoute, collectionSlug, router }: GoBackProps) => {
|
||||||
|
const redirectRoute = formatAdminURL({
|
||||||
|
adminRoute,
|
||||||
|
path: collectionSlug ? `/collections/${collectionSlug}` : '/',
|
||||||
|
})
|
||||||
|
router.push(redirectRoute)
|
||||||
|
}
|
||||||
47
packages/ui/src/utilities/handleTakeOver.tsx
Normal file
47
packages/ui/src/utilities/handleTakeOver.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { ClientUser } from 'payload'
|
||||||
|
|
||||||
|
export const handleTakeOver = (
|
||||||
|
id: number | string,
|
||||||
|
collectionSlug: string,
|
||||||
|
globalSlug: string,
|
||||||
|
user: ClientUser,
|
||||||
|
isWithinDoc: boolean,
|
||||||
|
updateDocumentEditor: (docId: number | string, slug: string, user: ClientUser) => Promise<void>,
|
||||||
|
setCurrentEditor: (value: React.SetStateAction<ClientUser>) => void,
|
||||||
|
documentLockStateRef: React.RefObject<{
|
||||||
|
hasShownLockedModal: boolean
|
||||||
|
isLocked: boolean
|
||||||
|
user: ClientUser
|
||||||
|
}>,
|
||||||
|
isLockingEnabled: boolean,
|
||||||
|
setIsReadOnlyForIncomingUser?: (value: React.SetStateAction<boolean>) => void,
|
||||||
|
): void => {
|
||||||
|
if (!isLockingEnabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call updateDocumentEditor to update the document's owner to the current user
|
||||||
|
void updateDocumentEditor(id, collectionSlug ?? globalSlug, user)
|
||||||
|
|
||||||
|
if (!isWithinDoc) {
|
||||||
|
documentLockStateRef.current.hasShownLockedModal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the locked state to reflect the current user as the owner
|
||||||
|
documentLockStateRef.current = {
|
||||||
|
hasShownLockedModal: documentLockStateRef.current?.hasShownLockedModal,
|
||||||
|
isLocked: true,
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
setCurrentEditor(user)
|
||||||
|
|
||||||
|
// If this is a takeover within the document, ensure the document is editable
|
||||||
|
if (isWithinDoc && setIsReadOnlyForIncomingUser) {
|
||||||
|
setIsReadOnlyForIncomingUser(false)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Error during document takeover:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -292,6 +292,51 @@ describe('locked documents', () => {
|
|||||||
|
|
||||||
expect(unlockedDocs.docs.length).toBe(0)
|
expect(unlockedDocs.docs.length).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should keep document locked when navigating to other tabs i.e. api', async () => {
|
||||||
|
await page.goto(postsUrl.edit(postDoc.id))
|
||||||
|
await page.waitForURL(postsUrl.edit(postDoc.id))
|
||||||
|
|
||||||
|
const textInput = page.locator('#field-text')
|
||||||
|
await textInput.fill('testing tab navigation...')
|
||||||
|
|
||||||
|
// eslint-disable-next-line payload/no-wait-function
|
||||||
|
await wait(500)
|
||||||
|
|
||||||
|
const lockedDocs = await payload.find({
|
||||||
|
collection: lockedDocumentCollection,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
where: {
|
||||||
|
'document.value': { equals: postDoc.id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lockedDocs.docs.length).toBe(1)
|
||||||
|
|
||||||
|
await page.locator('li[aria-label="API"] a').click()
|
||||||
|
|
||||||
|
// Locate the modal container
|
||||||
|
const modalContainer = page.locator('.payload__modal-container')
|
||||||
|
await expect(modalContainer).toBeVisible()
|
||||||
|
|
||||||
|
// Click the "Leave anyway" button
|
||||||
|
await page.locator('.leave-without-saving__controls .btn--style-primary').click()
|
||||||
|
|
||||||
|
// eslint-disable-next-line payload/no-wait-function
|
||||||
|
await wait(500)
|
||||||
|
|
||||||
|
const unlockedDocs = await payload.find({
|
||||||
|
collection: lockedDocumentCollection,
|
||||||
|
limit: 1,
|
||||||
|
pagination: false,
|
||||||
|
where: {
|
||||||
|
'document.value': { equals: postDoc.id },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(unlockedDocs.docs.length).toBe(1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('document locking - incoming user', () => {
|
describe('document locking - incoming user', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user