fix(next, ui): only show locked docs that are not expired (#8899)
`Issue`: Previously, documents that were locked but expired would still show in the list view / render the `DocumentLocked` modal upon other users entering the document. The expected outcome should be having expired locked documents seen as unlocked to other users. I.e: - Removing the lock icon from expired locks in the list view. - Prevent the `DocumentLocked` modal from appearing for other users - requiring a take over. `Fix`: - Only query for locked documents that are not expired, aka their `updatedAt` dates are greater than the the current time minus the lock duration. - Performs a `deleteMany` on expired documents when any user edits any other document in the same collection. Fixes #8778 `TODO`: Add tests
This commit is contained in:
@@ -65,6 +65,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
initialState,
|
||||
isEditing,
|
||||
isInitializing,
|
||||
lastUpdateTime,
|
||||
onDelete,
|
||||
onDrawerCreate,
|
||||
onDuplicate,
|
||||
@@ -110,9 +111,13 @@ export const DefaultEditView: React.FC = () => {
|
||||
const docConfig = collectionConfig || globalConfig
|
||||
|
||||
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
|
||||
|
||||
const isLockingEnabled = lockDocumentsProp !== false
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
|
||||
let preventLeaveWithoutSaving = true
|
||||
|
||||
if (collectionConfig) {
|
||||
@@ -130,6 +135,12 @@ export const DefaultEditView: React.FC = () => {
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||
|
||||
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
||||
|
||||
const isLockExpired = Date.now() > lockExpiryTime
|
||||
|
||||
const documentLockStateRef = useRef<{
|
||||
hasShownLockedModal: boolean
|
||||
isLocked: boolean
|
||||
@@ -140,8 +151,6 @@ export const DefaultEditView: React.FC = () => {
|
||||
user: null,
|
||||
})
|
||||
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now())
|
||||
|
||||
const classes = [baseClass, (id || globalSlug) && `${baseClass}--is-editing`]
|
||||
|
||||
if (globalSlug) {
|
||||
@@ -230,12 +239,12 @@ export const DefaultEditView: React.FC = () => {
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - lastUpdateTime
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
|
||||
const updateLastEdited = isLockingEnabled && timeSinceLastUpdate >= 10000 // 10 seconds
|
||||
|
||||
if (updateLastEdited) {
|
||||
setLastUpdateTime(currentTime)
|
||||
setEditSessionStartTime(currentTime)
|
||||
}
|
||||
|
||||
const docPreferences = await getDocPreferences()
|
||||
@@ -283,6 +292,7 @@ export const DefaultEditView: React.FC = () => {
|
||||
[
|
||||
apiRoute,
|
||||
collectionSlug,
|
||||
editSessionStartTime,
|
||||
schemaPath,
|
||||
getDocPreferences,
|
||||
globalSlug,
|
||||
@@ -294,7 +304,6 @@ export const DefaultEditView: React.FC = () => {
|
||||
setCurrentEditor,
|
||||
isLockingEnabled,
|
||||
setDocumentIsLocked,
|
||||
lastUpdateTime,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -346,7 +355,8 @@ export const DefaultEditView: React.FC = () => {
|
||||
currentEditor.id !== user?.id &&
|
||||
!isReadOnlyForIncomingUser &&
|
||||
!showTakeOverModal &&
|
||||
!documentLockStateRef.current?.hasShownLockedModal
|
||||
!documentLockStateRef.current?.hasShownLockedModal &&
|
||||
!isLockExpired
|
||||
|
||||
return (
|
||||
<main className={classes.filter(Boolean).join(' ')}>
|
||||
|
||||
@@ -85,6 +85,7 @@ const PreviewView: React.FC<Props> = ({
|
||||
initialState,
|
||||
isEditing,
|
||||
isInitializing,
|
||||
lastUpdateTime,
|
||||
onSave: onSaveFromProps,
|
||||
setCurrentEditor,
|
||||
setDocumentIsLocked,
|
||||
@@ -109,12 +110,22 @@ const PreviewView: React.FC<Props> = ({
|
||||
const docConfig = collectionConfig || globalConfig
|
||||
|
||||
const lockDocumentsProp = docConfig?.lockDocuments !== undefined ? docConfig?.lockDocuments : true
|
||||
|
||||
const isLockingEnabled = lockDocumentsProp !== false
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
|
||||
const [isReadOnlyForIncomingUser, setIsReadOnlyForIncomingUser] = useState(false)
|
||||
const [showTakeOverModal, setShowTakeOverModal] = useState(false)
|
||||
|
||||
const [editSessionStartTime, setEditSessionStartTime] = useState(Date.now())
|
||||
|
||||
const lockExpiryTime = lastUpdateTime + lockDurationInMilliseconds
|
||||
|
||||
const isLockExpired = Date.now() > lockExpiryTime
|
||||
|
||||
const documentLockStateRef = useRef<{
|
||||
hasShownLockedModal: boolean
|
||||
isLocked: boolean
|
||||
@@ -125,8 +136,6 @@ const PreviewView: React.FC<Props> = ({
|
||||
user: null,
|
||||
})
|
||||
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState(Date.now())
|
||||
|
||||
const onSave = useCallback(
|
||||
(json) => {
|
||||
reportUpdate({
|
||||
@@ -170,12 +179,12 @@ const PreviewView: React.FC<Props> = ({
|
||||
const onChange: FormProps['onChange'][0] = useCallback(
|
||||
async ({ formState: prevFormState }) => {
|
||||
const currentTime = Date.now()
|
||||
const timeSinceLastUpdate = currentTime - lastUpdateTime
|
||||
const timeSinceLastUpdate = currentTime - editSessionStartTime
|
||||
|
||||
const updateLastEdited = isLockingEnabled && timeSinceLastUpdate >= 10000 // 10 seconds
|
||||
|
||||
if (updateLastEdited) {
|
||||
setLastUpdateTime(currentTime)
|
||||
setEditSessionStartTime(currentTime)
|
||||
}
|
||||
|
||||
const docPreferences = await getDocPreferences()
|
||||
@@ -222,12 +231,12 @@ const PreviewView: React.FC<Props> = ({
|
||||
},
|
||||
[
|
||||
collectionSlug,
|
||||
editSessionStartTime,
|
||||
globalSlug,
|
||||
serverURL,
|
||||
apiRoute,
|
||||
id,
|
||||
isLockingEnabled,
|
||||
lastUpdateTime,
|
||||
operation,
|
||||
schemaPath,
|
||||
getDocPreferences,
|
||||
@@ -286,7 +295,8 @@ const PreviewView: React.FC<Props> = ({
|
||||
!isReadOnlyForIncomingUser &&
|
||||
!showTakeOverModal &&
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
!documentLockStateRef.current?.hasShownLockedModal
|
||||
!documentLockStateRef.current?.hasShownLockedModal &&
|
||||
!isLockExpired
|
||||
|
||||
return (
|
||||
<OperationProvider operation={operation}>
|
||||
|
||||
@@ -158,6 +158,13 @@ export const findOperation = async <TSlug extends CollectionSlug>(
|
||||
|
||||
if (includeLockStatus) {
|
||||
try {
|
||||
const lockDocumentsProp = collectionConfig?.lockDocuments
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
|
||||
const lockedDocuments = await payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
@@ -176,14 +183,27 @@ export const findOperation = async <TSlug extends CollectionSlug>(
|
||||
in: result.docs.map((doc) => doc.id),
|
||||
},
|
||||
},
|
||||
// Query where the lock is newer than the current time minus lock time
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: new Date(new Date().getTime() - lockDurationInMilliseconds),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const now = new Date().getTime()
|
||||
const lockedDocs = Array.isArray(lockedDocuments?.docs) ? lockedDocuments.docs : []
|
||||
|
||||
// Filter out stale locks
|
||||
const validLockedDocs = lockedDocs.filter((lock) => {
|
||||
const lastEditedAt = new Date(lock?.updatedAt).getTime()
|
||||
return lastEditedAt + lockDurationInMilliseconds > now
|
||||
})
|
||||
|
||||
result.docs = result.docs.map((doc) => {
|
||||
const lockedDoc = lockedDocs.find((lock) => lock?.document?.value === doc.id)
|
||||
const lockedDoc = validLockedDocs.find((lock) => lock?.document?.value === doc.id)
|
||||
return {
|
||||
...doc,
|
||||
_isLocked: !!lockedDoc,
|
||||
|
||||
@@ -112,6 +112,13 @@ export const findByIDOperation = async <TSlug extends CollectionSlug>(
|
||||
let lockStatus = null
|
||||
|
||||
try {
|
||||
const lockDocumentsProp = collectionConfig?.lockDocuments
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
|
||||
const lockedDocument = await req.payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
@@ -130,6 +137,12 @@ export const findByIDOperation = async <TSlug extends CollectionSlug>(
|
||||
equals: id,
|
||||
},
|
||||
},
|
||||
// Query where the lock is newer than the current time minus lock time
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: new Date(new Date().getTime() - lockDurationInMilliseconds),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
@@ -106,6 +106,7 @@ const DocumentInfo: React.FC<
|
||||
|
||||
const [documentIsLocked, setDocumentIsLocked] = useState<boolean | undefined>(false)
|
||||
const [currentEditor, setCurrentEditor] = useState<ClientUser | null>(null)
|
||||
const [lastUpdateTime, setLastUpdateTime] = useState<number>(null)
|
||||
|
||||
const isInitializing = initialState === undefined || data === undefined
|
||||
const [unpublishedVersions, setUnpublishedVersions] =
|
||||
@@ -228,9 +229,11 @@ const DocumentInfo: React.FC<
|
||||
|
||||
if (docs.length > 0) {
|
||||
const newEditor = docs[0].user?.value
|
||||
const lastUpdatedAt = new Date(docs[0].updatedAt).getTime()
|
||||
if (newEditor && newEditor.id !== currentEditor?.id) {
|
||||
setCurrentEditor(newEditor)
|
||||
setDocumentIsLocked(true)
|
||||
setLastUpdateTime(lastUpdatedAt)
|
||||
}
|
||||
} else {
|
||||
setDocumentIsLocked(false)
|
||||
@@ -685,6 +688,7 @@ const DocumentInfo: React.FC<
|
||||
initialState,
|
||||
isInitializing,
|
||||
isLoading,
|
||||
lastUpdateTime,
|
||||
onSave,
|
||||
preferencesKey,
|
||||
publishedDoc,
|
||||
|
||||
@@ -67,6 +67,7 @@ export type DocumentInfoContext = {
|
||||
initialState?: FormState
|
||||
isInitializing: boolean
|
||||
isLoading: boolean
|
||||
lastUpdateTime?: number
|
||||
preferencesKey?: string
|
||||
publishedDoc?: { _status?: string } & TypeWithID & TypeWithTimestamps
|
||||
setCurrentEditor?: React.Dispatch<React.SetStateAction<ClientUser>>
|
||||
|
||||
@@ -246,7 +246,24 @@ export const buildFormState = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDocumentsProp = collectionSlug
|
||||
? req.payload.config.collections.find((c) => c.slug === collectionSlug)?.lockDocuments
|
||||
: req.payload.config.globals.find((g) => g.slug === globalSlug)?.lockDocuments
|
||||
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
const now = new Date().getTime()
|
||||
|
||||
if (lockedDocumentQuery) {
|
||||
// Query where the lock is newer than the current time minus the lock duration
|
||||
lockedDocumentQuery.and.push({
|
||||
updatedAt: {
|
||||
greater_than: new Date(now - lockDurationInMilliseconds).toISOString(),
|
||||
},
|
||||
})
|
||||
|
||||
const lockedDocument = await req.payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
@@ -272,7 +289,27 @@ export const buildFormState = async ({
|
||||
|
||||
return { lockedState, state: result }
|
||||
} else {
|
||||
// If no lock document exists, create it
|
||||
// Delete Many Locks that are older than their updatedAt + lockDuration
|
||||
// If NO ACTIVE lock document exists, first delete any expired locks and then create a fresh lock
|
||||
// Where updatedAt is older than the duration that is specified in the config
|
||||
const deleteExpiredLocksQuery = {
|
||||
and: [
|
||||
{ 'document.relationTo': { equals: collectionSlug } },
|
||||
{ 'document.value': { equals: id } },
|
||||
{
|
||||
updatedAt: {
|
||||
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await req.payload.db.deleteMany({
|
||||
collection: 'payload-locked-documents',
|
||||
req,
|
||||
where: deleteExpiredLocksQuery,
|
||||
})
|
||||
|
||||
await req.payload.db.create({
|
||||
collection: 'payload-locked-documents',
|
||||
data: {
|
||||
|
||||
Reference in New Issue
Block a user