Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(next, ui): exclude expired locks for globals #8914

Merged
merged 9 commits into from
Oct 29, 2024
27 changes: 23 additions & 4 deletions packages/next/src/views/Dashboard/Default/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import './index.scss'
const baseClass = 'dashboard'

export type DashboardProps = {
globalData: Array<{ data: { _isLocked: boolean; _userEditing: ClientUser | null }; slug: string }>
globalData: Array<{
data: { _isLocked: boolean; _lastEditedAt: string; _userEditing: ClientUser | null }
lockDuration?: number
slug: string
}>
Link: React.ComponentType<any>
navGroups?: ReturnType<typeof groupNavItems>
permissions: Permissions
Expand Down Expand Up @@ -95,7 +99,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
let createHREF: string
let href: string
let hasCreatePermission: boolean
let lockStatus = null
let isLocked = null
let userEditing = null

if (type === EntityType.collection) {
Expand Down Expand Up @@ -130,17 +134,32 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
const globalLockData = globalData.find(
(global) => global.slug === entity.slug,
)

if (globalLockData) {
lockStatus = globalLockData.data._isLocked
isLocked = globalLockData.data._isLocked
userEditing = globalLockData.data._userEditing

// Check if the lock is expired
const lockDuration = globalLockData?.lockDuration
const lastEditedAt = new Date(
globalLockData.data?._lastEditedAt,
).getTime()

const lockDurationInMilliseconds = lockDuration * 1000
const lockExpirationTime = lastEditedAt + lockDurationInMilliseconds

if (new Date().getTime() > lockExpirationTime) {
isLocked = false
userEditing = null
}
}
}

return (
<li key={entityIndex}>
<Card
actions={
lockStatus && user?.id !== userEditing?.id ? (
isLocked && user?.id !== userEditing?.id ? (
<Locked className={`${baseClass}__locked`} user={userEditing} />
) : hasCreatePermission && type === EntityType.collection ? (
<Button
Expand Down
23 changes: 18 additions & 5 deletions packages/next/src/views/Dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
visibleEntities,
} = initPageResult

const lockDurationDefault = 300 // Default 5 minutes in seconds

const CustomDashboardComponent = config.admin.components?.views?.Dashboard

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

const globalSlugs = config.globals.map((global) => global.slug)
const globalConfigs = config.globals.map((global) => ({
slug: global.slug,
lockDuration:
global.lockDocuments === false
? null // Set lockDuration to null if locking is disabled
: typeof global.lockDocuments === 'object'
? global.lockDocuments.duration
: lockDurationDefault,
}))

// Filter the slugs based on permissions and visibility
const filteredGlobalSlugs = globalSlugs.filter(
(slug) =>
permissions?.globals?.[slug]?.read?.permission && visibleEntities.globals.includes(slug),
const filteredGlobalConfigs = globalConfigs.filter(
({ slug, lockDuration }) =>
lockDuration !== null && // Ensure lockDuration is valid
permissions?.globals?.[slug]?.read?.permission &&
visibleEntities.globals.includes(slug),
)

const globalData = await Promise.all(
filteredGlobalSlugs.map(async (slug) => {
filteredGlobalConfigs.map(async ({ slug, lockDuration }) => {
const data = await payload.findGlobal({
slug,
depth: 0,
Expand All @@ -67,6 +79,7 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
return {
slug,
data,
lockDuration,
}
}),
)
Expand Down
1 change: 1 addition & 0 deletions packages/payload/src/globals/operations/findOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
}

doc._isLocked = !!lockStatus
doc._lastEditedAt = lockStatus?.updatedAt ?? null
doc._userEditing = lockStatus?.user?.value ?? null
}

Expand Down
39 changes: 27 additions & 12 deletions packages/ui/src/utilities/buildFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const buildFormState = async ({
}: {
req: PayloadRequest
}): Promise<{
lockedState?: { isLocked: boolean; user: ClientUser | number | string }
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser | number | string }
state: FormState
}> => {
const reqData: BuildFormStateArgs = (req.data || {}) as BuildFormStateArgs
Expand Down Expand Up @@ -242,7 +242,7 @@ export const buildFormState = async ({
}
} else if (globalSlug) {
lockedDocumentQuery = {
globalSlug: { equals: globalSlug },
and: [{ globalSlug: { equals: globalSlug } }],
}
}

Expand Down Expand Up @@ -275,6 +275,7 @@ export const buildFormState = async ({
if (lockedDocument.docs && lockedDocument.docs.length > 0) {
const lockedState = {
isLocked: true,
lastEditedAt: lockedDocument.docs[0]?.updatedAt,
user: lockedDocument.docs[0]?.user?.value,
}

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

return { lockedState, state: result }
} else {
// 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(),
let deleteExpiredLocksQuery

if (collectionSlug) {
deleteExpiredLocksQuery = {
and: [
{ 'document.relationTo': { equals: collectionSlug } },
{
updatedAt: {
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
},
},
},
],
],
}
} else if (globalSlug) {
deleteExpiredLocksQuery = {
and: [
{ globalSlug: { equals: globalSlug } },
{
updatedAt: {
less_than: new Date(now - lockDurationInMilliseconds).toISOString(),
},
},
],
}
}

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

const lockedState = {
isLocked: true,
lastEditedAt: new Date().toISOString(),
user: req.user,
}

Expand Down
7 changes: 5 additions & 2 deletions packages/ui/src/utilities/getFormState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export const getFormState = async (args: {
serverURL: SanitizedConfig['serverURL']
signal?: AbortSignal
token?: string
}): Promise<{ lockedState?: { isLocked: boolean; user: ClientUser }; state: FormState }> => {
}): Promise<{
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
state: FormState
}> => {
const { apiRoute, body, onError, serverURL, signal, token } = args

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

const json = (await res.json()) as {
lockedState?: { isLocked: boolean; user: ClientUser }
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
state: FormState
}

Expand Down
22 changes: 22 additions & 0 deletions test/locked-documents/collections/Tests/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { CollectionConfig } from 'payload'

export const testsSlug = 'tests'

export const TestsCollection: CollectionConfig = {
slug: testsSlug,
admin: {
useAsTitle: 'text',
},
lockDocuments: {
duration: 5,
},
fields: [
{
name: 'text',
type: 'text',
},
],
versions: {
drafts: true,
},
}
6 changes: 4 additions & 2 deletions test/locked-documents/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser, regularUser } from '../credentials.js'
import { PagesCollection, pagesSlug } from './collections/Pages/index.js'
import { PostsCollection, postsSlug } from './collections/Posts/index.js'
import { TestsCollection } from './collections/Tests/index.js'
import { Users } from './collections/Users/index.js'
import { AdminGlobal } from './globals/Admin/index.js'
import { MenuGlobal } from './globals/Menu/index.js'

const filename = fileURLToPath(import.meta.url)
Expand All @@ -17,8 +19,8 @@ export default buildConfigWithDefaults({
baseDir: path.resolve(dirname),
},
},
collections: [PagesCollection, PostsCollection, Users],
globals: [MenuGlobal],
collections: [PagesCollection, PostsCollection, TestsCollection, Users],
globals: [AdminGlobal, MenuGlobal],
onInit: async (payload) => {
if (process.env.SEED_IN_CONFIG_ONINIT !== 'false') {
await payload.create({
Expand Down
Loading