Skip to content

Commit

Permalink
fix(next, ui): exclude expired locks for globals (#8914)
Browse files Browse the repository at this point in the history
Continued PR off of #8899
  • Loading branch information
PatrikKozak authored Oct 29, 2024
1 parent 1e002ac commit e74906f
Show file tree
Hide file tree
Showing 10 changed files with 426 additions and 57 deletions.
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

0 comments on commit e74906f

Please sign in to comment.