Skip to content

Commit e74906f

Browse files
authored
fix(next, ui): exclude expired locks for globals (#8914)
Continued PR off of #8899
1 parent 1e002ac commit e74906f

File tree

10 files changed

+426
-57
lines changed

10 files changed

+426
-57
lines changed

packages/next/src/views/Dashboard/Default/index.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import './index.scss'
1616
const baseClass = 'dashboard'
1717

1818
export type DashboardProps = {
19-
globalData: Array<{ data: { _isLocked: boolean; _userEditing: ClientUser | null }; slug: string }>
19+
globalData: Array<{
20+
data: { _isLocked: boolean; _lastEditedAt: string; _userEditing: ClientUser | null }
21+
lockDuration?: number
22+
slug: string
23+
}>
2024
Link: React.ComponentType<any>
2125
navGroups?: ReturnType<typeof groupNavItems>
2226
permissions: Permissions
@@ -95,7 +99,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
9599
let createHREF: string
96100
let href: string
97101
let hasCreatePermission: boolean
98-
let lockStatus = null
102+
let isLocked = null
99103
let userEditing = null
100104

101105
if (type === EntityType.collection) {
@@ -130,17 +134,32 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
130134
const globalLockData = globalData.find(
131135
(global) => global.slug === entity.slug,
132136
)
137+
133138
if (globalLockData) {
134-
lockStatus = globalLockData.data._isLocked
139+
isLocked = globalLockData.data._isLocked
135140
userEditing = globalLockData.data._userEditing
141+
142+
// Check if the lock is expired
143+
const lockDuration = globalLockData?.lockDuration
144+
const lastEditedAt = new Date(
145+
globalLockData.data?._lastEditedAt,
146+
).getTime()
147+
148+
const lockDurationInMilliseconds = lockDuration * 1000
149+
const lockExpirationTime = lastEditedAt + lockDurationInMilliseconds
150+
151+
if (new Date().getTime() > lockExpirationTime) {
152+
isLocked = false
153+
userEditing = null
154+
}
136155
}
137156
}
138157

139158
return (
140159
<li key={entityIndex}>
141160
<Card
142161
actions={
143-
lockStatus && user?.id !== userEditing?.id ? (
162+
isLocked && user?.id !== userEditing?.id ? (
144163
<Locked className={`${baseClass}__locked`} user={userEditing} />
145164
) : hasCreatePermission && type === EntityType.collection ? (
146165
<Button

packages/next/src/views/Dashboard/index.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
3434
visibleEntities,
3535
} = initPageResult
3636

37+
const lockDurationDefault = 300 // Default 5 minutes in seconds
38+
3739
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
3840

3941
const collections = config.collections.filter(
@@ -48,16 +50,26 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
4850
visibleEntities.globals.includes(global.slug),
4951
)
5052

51-
const globalSlugs = config.globals.map((global) => global.slug)
53+
const globalConfigs = config.globals.map((global) => ({
54+
slug: global.slug,
55+
lockDuration:
56+
global.lockDocuments === false
57+
? null // Set lockDuration to null if locking is disabled
58+
: typeof global.lockDocuments === 'object'
59+
? global.lockDocuments.duration
60+
: lockDurationDefault,
61+
}))
5262

5363
// Filter the slugs based on permissions and visibility
54-
const filteredGlobalSlugs = globalSlugs.filter(
55-
(slug) =>
56-
permissions?.globals?.[slug]?.read?.permission && visibleEntities.globals.includes(slug),
64+
const filteredGlobalConfigs = globalConfigs.filter(
65+
({ slug, lockDuration }) =>
66+
lockDuration !== null && // Ensure lockDuration is valid
67+
permissions?.globals?.[slug]?.read?.permission &&
68+
visibleEntities.globals.includes(slug),
5769
)
5870

5971
const globalData = await Promise.all(
60-
filteredGlobalSlugs.map(async (slug) => {
72+
filteredGlobalConfigs.map(async ({ slug, lockDuration }) => {
6173
const data = await payload.findGlobal({
6274
slug,
6375
depth: 0,
@@ -67,6 +79,7 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
6779
return {
6880
slug,
6981
data,
82+
lockDuration,
7083
}
7184
}),
7285
)

packages/payload/src/globals/operations/findOne.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
8787
}
8888

8989
doc._isLocked = !!lockStatus
90+
doc._lastEditedAt = lockStatus?.updatedAt ?? null
9091
doc._userEditing = lockStatus?.user?.value ?? null
9192
}
9293

packages/ui/src/utilities/buildFormState.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const buildFormState = async ({
3939
}: {
4040
req: PayloadRequest
4141
}): Promise<{
42-
lockedState?: { isLocked: boolean; user: ClientUser | number | string }
42+
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser | number | string }
4343
state: FormState
4444
}> => {
4545
const reqData: BuildFormStateArgs = (req.data || {}) as BuildFormStateArgs
@@ -242,7 +242,7 @@ export const buildFormState = async ({
242242
}
243243
} else if (globalSlug) {
244244
lockedDocumentQuery = {
245-
globalSlug: { equals: globalSlug },
245+
and: [{ globalSlug: { equals: globalSlug } }],
246246
}
247247
}
248248

@@ -275,6 +275,7 @@ export const buildFormState = async ({
275275
if (lockedDocument.docs && lockedDocument.docs.length > 0) {
276276
const lockedState = {
277277
isLocked: true,
278+
lastEditedAt: lockedDocument.docs[0]?.updatedAt,
278279
user: lockedDocument.docs[0]?.user?.value,
279280
}
280281

@@ -289,19 +290,32 @@ export const buildFormState = async ({
289290

290291
return { lockedState, state: result }
291292
} else {
292-
// Delete Many Locks that are older than their updatedAt + lockDuration
293293
// If NO ACTIVE lock document exists, first delete any expired locks and then create a fresh lock
294294
// Where updatedAt is older than the duration that is specified in the config
295-
const deleteExpiredLocksQuery = {
296-
and: [
297-
{ 'document.relationTo': { equals: collectionSlug } },
298-
{ 'document.value': { equals: id } },
299-
{
300-
updatedAt: {
301-
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
295+
let deleteExpiredLocksQuery
296+
297+
if (collectionSlug) {
298+
deleteExpiredLocksQuery = {
299+
and: [
300+
{ 'document.relationTo': { equals: collectionSlug } },
301+
{
302+
updatedAt: {
303+
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
304+
},
302305
},
303-
},
304-
],
306+
],
307+
}
308+
} else if (globalSlug) {
309+
deleteExpiredLocksQuery = {
310+
and: [
311+
{ globalSlug: { equals: globalSlug } },
312+
{
313+
updatedAt: {
314+
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
315+
},
316+
},
317+
],
318+
}
305319
}
306320

307321
await req.payload.db.deleteMany({
@@ -330,6 +344,7 @@ export const buildFormState = async ({
330344

331345
const lockedState = {
332346
isLocked: true,
347+
lastEditedAt: new Date().toISOString(),
333348
user: req.user,
334349
}
335350

packages/ui/src/utilities/getFormState.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export const getFormState = async (args: {
99
serverURL: SanitizedConfig['serverURL']
1010
signal?: AbortSignal
1111
token?: string
12-
}): Promise<{ lockedState?: { isLocked: boolean; user: ClientUser }; state: FormState }> => {
12+
}): Promise<{
13+
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
14+
state: FormState
15+
}> => {
1316
const { apiRoute, body, onError, serverURL, signal, token } = args
1417

1518
const res = await fetch(`${serverURL}${apiRoute}/form-state`, {
@@ -24,7 +27,7 @@ export const getFormState = async (args: {
2427
})
2528

2629
const json = (await res.json()) as {
27-
lockedState?: { isLocked: boolean; user: ClientUser }
30+
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
2831
state: FormState
2932
}
3033

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const testsSlug = 'tests'
4+
5+
export const TestsCollection: CollectionConfig = {
6+
slug: testsSlug,
7+
admin: {
8+
useAsTitle: 'text',
9+
},
10+
lockDocuments: {
11+
duration: 5,
12+
},
13+
fields: [
14+
{
15+
name: 'text',
16+
type: 'text',
17+
},
18+
],
19+
versions: {
20+
drafts: true,
21+
},
22+
}

test/locked-documents/config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
55
import { devUser, regularUser } from '../credentials.js'
66
import { PagesCollection, pagesSlug } from './collections/Pages/index.js'
77
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
8+
import { TestsCollection } from './collections/Tests/index.js'
89
import { Users } from './collections/Users/index.js'
10+
import { AdminGlobal } from './globals/Admin/index.js'
911
import { MenuGlobal } from './globals/Menu/index.js'
1012

1113
const filename = fileURLToPath(import.meta.url)
@@ -17,8 +19,8 @@ export default buildConfigWithDefaults({
1719
baseDir: path.resolve(dirname),
1820
},
1921
},
20-
collections: [PagesCollection, PostsCollection, Users],
21-
globals: [MenuGlobal],
22+
collections: [PagesCollection, PostsCollection, TestsCollection, Users],
23+
globals: [AdminGlobal, MenuGlobal],
2224
onInit: async (payload) => {
2325
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
2426
await payload.create({

0 commit comments

Comments
 (0)