fix: locked documents with read access for users (#8950)

### What?

When read access is restricted on the `users` collection - restricted
users would not have access to other users complete user data object
only their IDs when accessing `user.value`.

### Why?

This is problematic when determining the lock status of a document from
a restricted users perspective as `user.id` would not exist - the user
data would not be an object in this case but instead a `string` or
`number` value for user ID

### How?

This PR properly handles both cases now and checks if the incoming user
data is an object or just a `string` / `number`.
This commit is contained in:
Patrik
2024-10-31 09:23:18 -04:00
committed by GitHub
parent b417c1f61a
commit 55ce8e68fc
56 changed files with 189 additions and 64 deletions

View File

@@ -2,6 +2,7 @@
import type { ClientUser } from 'payload'
import { Button, Modal, useModal, useTranslation } from '@payloadcms/ui'
import { isClientUserObject } from '@payloadcms/ui/shared'
import React, { useEffect } from 'react'
import './index.scss'
@@ -30,7 +31,7 @@ export const DocumentLocked: React.FC<{
onReadOnly: () => void
onTakeOver: () => void
updatedAt?: null | number
user?: ClientUser
user?: ClientUser | number | string
}> = ({ handleGoBack, isActive, onReadOnly, onTakeOver, updatedAt, user }) => {
const { closeModal, openModal } = useModal()
const { t } = useTranslation()
@@ -49,7 +50,10 @@ export const DocumentLocked: React.FC<{
<div className={`${baseClass}__content`}>
<h1>{t('general:documentLocked')}</h1>
<p>
<strong>{user?.email ?? user?.id}</strong> {t('general:currentlyEditing')}
<strong>
{isClientUserObject(user) ? (user.email ?? user.id) : `${t('general:user')}: ${user}`}
</strong>{' '}
{t('general:currentlyEditing')}
</p>
<p>
{t('general:editedSince')} <strong>{formatDate(updatedAt)}</strong>

View File

@@ -17,7 +17,7 @@ const baseClass = 'dashboard'
export type DashboardProps = {
globalData: Array<{
data: { _isLocked: boolean; _lastEditedAt: string; _userEditing: ClientUser | null }
data: { _isLocked: boolean; _lastEditedAt: string; _userEditing: ClientUser | number | string }
lockDuration?: number
slug: string
}>

View File

@@ -31,11 +31,10 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
payload,
user,
},
req,
visibleEntities,
} = initPageResult
const lockDurationDefault = 300 // Default 5 minutes in seconds
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
const collections = config.collections.filter(
@@ -50,39 +49,44 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
visibleEntities.globals.includes(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'
// Query locked global documents only if there are globals in the config
let globalData = []
if (config.globals.length > 0) {
const lockedDocuments = await payload.find({
collection: 'payload-locked-documents',
depth: 1,
overrideAccess: false,
pagination: false,
req,
where: {
globalSlug: {
exists: true,
},
},
})
// Map over globals to include `lockDuration` and lock data for each global slug
globalData = config.globals.map((global) => {
const lockDurationDefault = 300
const lockDuration =
typeof global.lockDocuments === 'object'
? global.lockDocuments.duration
: lockDurationDefault,
}))
: lockDurationDefault
// Filter the slugs based on permissions and visibility
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(
filteredGlobalConfigs.map(async ({ slug, lockDuration }) => {
const data = await payload.findGlobal({
slug,
depth: 0,
includeLockStatus: true,
})
const lockedDoc = lockedDocuments.docs.find((doc) => doc.globalSlug === global.slug)
return {
slug,
data,
slug: global.slug,
data: {
_isLocked: !!lockedDoc,
_lastEditedAt: lockedDoc?.updatedAt ?? null,
_userEditing: lockedDoc?.user?.value ?? null,
},
lockDuration,
}
}),
)
})
}
const navGroups = groupNavItems(
[

View File

@@ -144,7 +144,7 @@ export const DefaultEditView: React.FC = () => {
const documentLockStateRef = useRef<{
hasShownLockedModal: boolean
isLocked: boolean
user: ClientUser
user: ClientUser | number | string
} | null>({
hasShownLockedModal: false,
isLocked: false,
@@ -268,7 +268,10 @@ export const DefaultEditView: React.FC = () => {
setDocumentIsLocked(true)
if (isLockingEnabled) {
const previousOwnerId = documentLockStateRef.current?.user?.id
const previousOwnerId =
typeof documentLockStateRef.current?.user === 'object'
? documentLockStateRef.current?.user?.id
: documentLockStateRef.current?.user
if (lockedState) {
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
@@ -328,7 +331,11 @@ export const DefaultEditView: React.FC = () => {
// 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) {
if (
typeof documentLockStateRef.current?.user === 'object'
? documentLockStateRef.current?.user?.id === user?.id
: documentLockStateRef.current?.user === user?.id
) {
void unlockDocument(id, collectionSlug ?? globalSlug)
setDocumentIsLocked(false)
setCurrentEditor(null)
@@ -352,7 +359,9 @@ export const DefaultEditView: React.FC = () => {
const shouldShowDocumentLockedModal =
documentIsLocked &&
currentEditor &&
currentEditor.id !== user?.id &&
(typeof currentEditor === 'object'
? currentEditor.id !== user?.id
: currentEditor !== user?.id) &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
!documentLockStateRef.current?.hasShownLockedModal &&

View File

@@ -129,7 +129,7 @@ const PreviewView: React.FC<Props> = ({
const documentLockStateRef = useRef<{
hasShownLockedModal: boolean
isLocked: boolean
user: ClientUser
user: ClientUser | number | string
} | null>({
hasShownLockedModal: false,
isLocked: false,
@@ -208,7 +208,10 @@ const PreviewView: React.FC<Props> = ({
setDocumentIsLocked(true)
if (isLockingEnabled) {
const previousOwnerId = documentLockStateRef.current?.user?.id
const previousOwnerId =
typeof documentLockStateRef.current?.user === 'object'
? documentLockStateRef.current?.user?.id
: documentLockStateRef.current?.user
if (lockedState) {
if (!documentLockStateRef.current || lockedState.user.id !== previousOwnerId) {
@@ -267,7 +270,11 @@ const PreviewView: React.FC<Props> = ({
// 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) {
if (
typeof documentLockStateRef.current?.user === 'object'
? documentLockStateRef.current?.user?.id === user?.id
: documentLockStateRef.current?.user === user?.id
) {
void unlockDocument(id, collectionSlug ?? globalSlug)
setDocumentIsLocked(false)
setCurrentEditor(null)
@@ -291,7 +298,9 @@ const PreviewView: React.FC<Props> = ({
const shouldShowDocumentLockedModal =
documentIsLocked &&
currentEditor &&
currentEditor.id !== user.id &&
(typeof currentEditor === 'object'
? currentEditor.id !== user?.id
: currentEditor !== user?.id) &&
!isReadOnlyForIncomingUser &&
!showTakeOverModal &&
// eslint-disable-next-line react-compiler/react-compiler

View File

@@ -187,6 +187,7 @@ export const findOperation = async <
collection: 'payload-locked-documents',
depth: 1,
limit: sanitizedLimit,
overrideAccess: false,
pagination: false,
req,
where: {

View File

@@ -139,6 +139,7 @@ export const findByIDOperation = async <
collection: 'payload-locked-documents',
depth: 1,
limit: 1,
overrideAccess: false,
pagination: false,
req,
where: {

View File

@@ -65,21 +65,37 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
// /////////////////////////////////////
// Include Lock Status if required
// /////////////////////////////////////
if (includeLockStatus && slug) {
let lockStatus = null
try {
const lockDocumentsProp = globalConfig?.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,
limit: 1,
overrideAccess: false,
pagination: false,
req,
where: {
globalSlug: {
equals: slug,
},
and: [
{
globalSlug: {
equals: slug,
},
},
{
updatedAt: {
greater_than: new Date(new Date().getTime() - lockDurationInMilliseconds),
},
},
],
},
})
@@ -91,7 +107,6 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
}
doc._isLocked = !!lockStatus
doc._lastEditedAt = lockStatus?.updatedAt ?? null
doc._userEditing = lockStatus?.user?.value ?? null
}

View File

@@ -127,6 +127,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
'general:addFilter',
'general:adminTheme',
'general:and',
'general:anotherUser',
'general:anotherUserTakenOver',
'general:applyChanges',
'general:ascending',

View File

@@ -177,6 +177,7 @@ export const arTranslations: DefaultTranslationsObject = {
addFilter: 'أضف فلتر',
adminTheme: 'شكل واجهة المستخدم',
and: 'و',
anotherUser: 'مستخدم آخر',
anotherUserTakenOver: 'قام مستخدم آخر بالاستيلاء على تحرير هذا المستند.',
applyChanges: 'طبق التغييرات',
ascending: 'تصاعدي',

View File

@@ -178,6 +178,7 @@ export const azTranslations: DefaultTranslationsObject = {
addFilter: 'Filter əlavə et',
adminTheme: 'Admin Mövzusu',
and: 'Və',
anotherUser: 'Başqa bir istifadəçi',
anotherUserTakenOver: 'Başqa bir istifadəçi bu sənədin redaktəsini ələ keçirdi.',
applyChanges: 'Dəyişiklikləri Tətbiq Edin',
ascending: 'Artan',

View File

@@ -178,6 +178,7 @@ export const bgTranslations: DefaultTranslationsObject = {
addFilter: 'Добави филтър',
adminTheme: 'Цветова тема',
and: 'И',
anotherUser: 'Друг потребител',
anotherUserTakenOver: 'Друг потребител пое редактирането на този документ.',
applyChanges: 'Приложи промените',
ascending: 'Възходящ',

View File

@@ -178,6 +178,7 @@ export const csTranslations: DefaultTranslationsObject = {
addFilter: 'Přidat filtr',
adminTheme: 'Motiv administračního rozhraní',
and: 'a',
anotherUser: 'Jiný uživatel',
anotherUserTakenOver: 'Jiný uživatel převzal úpravy tohoto dokumentu.',
applyChanges: 'Použít změny',
ascending: 'Vzestupně',

View File

@@ -177,6 +177,7 @@ export const daTranslations: DefaultTranslationsObject = {
addFilter: 'Tilføj filter',
adminTheme: 'Admin tema',
and: 'Og',
anotherUser: 'En anden bruger',
anotherUserTakenOver: 'En anden bruger har overtaget denne ressource.',
applyChanges: 'Tilføj ændringer',
ascending: 'Stigende',

View File

@@ -182,6 +182,7 @@ export const deTranslations: DefaultTranslationsObject = {
addFilter: 'Filter hinzufügen',
adminTheme: 'Admin-Farbthema',
and: 'Und',
anotherUser: 'Ein anderer Benutzer',
anotherUserTakenOver: 'Ein anderer Benutzer hat die Bearbeitung dieses Dokuments übernommen.',
applyChanges: 'Änderungen anwenden',
ascending: 'Aufsteigend',

View File

@@ -179,6 +179,7 @@ export const enTranslations = {
addFilter: 'Add Filter',
adminTheme: 'Admin Theme',
and: 'And',
anotherUser: 'Another user',
anotherUserTakenOver: 'Another user has taken over editing this document.',
applyChanges: 'Apply Changes',
ascending: 'Ascending',

View File

@@ -182,6 +182,7 @@ export const esTranslations: DefaultTranslationsObject = {
addFilter: 'Añadir filtro',
adminTheme: 'Tema del admin',
and: 'Y',
anotherUser: 'Otro usuario',
anotherUserTakenOver: 'Otro usuario ha tomado el control de la edición de este documento.',
applyChanges: 'Aplicar Cambios',
ascending: 'Ascendente',

View File

@@ -177,6 +177,7 @@ export const faTranslations: DefaultTranslationsObject = {
addFilter: 'افزودن علامت',
adminTheme: 'پوسته پیشخوان',
and: 'و',
anotherUser: 'کاربر دیگر',
anotherUserTakenOver: 'کاربر دیگری ویرایش این سند را به دست گرفته است.',
applyChanges: 'اعمال تغییرات',
ascending: 'صعودی',

View File

@@ -185,6 +185,7 @@ export const frTranslations: DefaultTranslationsObject = {
addFilter: 'Ajouter un filtre',
adminTheme: 'Thème dadministration',
and: 'Et',
anotherUser: 'Un autre utilisateur',
anotherUserTakenOver: 'Un autre utilisateur a pris en charge la modification de ce document.',
applyChanges: 'Appliquer les modifications',
ascending: 'Ascendant',

View File

@@ -174,6 +174,7 @@ export const heTranslations: DefaultTranslationsObject = {
addFilter: 'הוסף מסנן',
adminTheme: 'ערכת נושא ממשק הניהול',
and: 'וגם',
anotherUser: 'משתמש אחר',
anotherUserTakenOver: 'משתמש אחר השתלט על עריכת מסמך זה.',
applyChanges: 'החל שינויים',
ascending: 'בסדר עולה',

View File

@@ -179,6 +179,7 @@ export const hrTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filter',
adminTheme: 'Administratorska tema',
and: 'i',
anotherUser: 'Drugi korisnik',
anotherUserTakenOver: 'Drugi korisnik je preuzeo uređivanje ovog dokumenta.',
applyChanges: 'Primijeni promjene',
ascending: 'Uzlazno',

View File

@@ -180,6 +180,7 @@ export const huTranslations: DefaultTranslationsObject = {
addFilter: 'Szűrő hozzáadása',
adminTheme: 'Admin téma',
and: 'És',
anotherUser: 'Egy másik felhasználó',
anotherUserTakenOver: 'Egy másik felhasználó átvette ennek a dokumentumnak a szerkesztését.',
applyChanges: 'Változtatások alkalmazása',
ascending: 'Növekvő',

View File

@@ -181,6 +181,7 @@ export const itTranslations: DefaultTranslationsObject = {
addFilter: 'Aggiungi Filtro',
adminTheme: 'Tema Admin',
and: 'E',
anotherUser: 'Un altro utente',
anotherUserTakenOver:
'Un altro utente ha preso il controllo della modifica di questo documento.',
applyChanges: 'Applica modifiche',

View File

@@ -179,6 +179,7 @@ export const jaTranslations: DefaultTranslationsObject = {
addFilter: '絞り込みを追加',
adminTheme: '管理画面のテーマ',
and: 'かつ',
anotherUser: '別のユーザー',
anotherUserTakenOver: '別のユーザーがこのドキュメントの編集を引き継ぎました。',
applyChanges: '変更を適用する',
ascending: '昇順',

View File

@@ -178,6 +178,7 @@ export const koTranslations: DefaultTranslationsObject = {
addFilter: '필터 추가',
adminTheme: '관리자 테마',
and: '및',
anotherUser: '다른 사용자',
anotherUserTakenOver: '다른 사용자가 이 문서의 편집을 인수했습니다.',
applyChanges: '변경 사항 적용',
ascending: '오름차순',

View File

@@ -180,6 +180,7 @@ export const myTranslations: DefaultTranslationsObject = {
addFilter: 'ဇကာထည့်ပါ။',
adminTheme: 'အက်ပ်ဒိုင်များစပ်စွာ',
and: 'နှင့်',
anotherUser: 'တစ်ခြားအသုံးပြုသူ',
anotherUserTakenOver: 'တစ်ခြားအသုံးပြုသူသည်ဤစာရွက်စာတမ်းကိုပြင်ဆင်မှုကိုရယူလိုက်သည်။',
applyChanges: 'ပြောင်းလဲမှုများ အသုံးပြုပါ',
ascending: 'တက်နေသည်',

View File

@@ -178,6 +178,7 @@ export const nbTranslations: DefaultTranslationsObject = {
addFilter: 'Legg til filter',
adminTheme: 'Admin-tema',
and: 'Og',
anotherUser: 'En annen bruker',
anotherUserTakenOver: 'En annen bruker har tatt over redigeringen av dette dokumentet.',
applyChanges: 'Bruk endringer',
ascending: 'Stigende',

View File

@@ -180,6 +180,7 @@ export const nlTranslations: DefaultTranslationsObject = {
addFilter: 'Filter toevoegen',
adminTheme: 'Adminthema',
and: 'En',
anotherUser: 'Een andere gebruiker',
anotherUserTakenOver: 'Een andere gebruiker heeft de bewerking van dit document overgenomen.',
applyChanges: 'Breng wijzigingen aan',
ascending: 'Oplopend',

View File

@@ -178,6 +178,7 @@ export const plTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filtr',
adminTheme: 'Motyw administratora',
and: 'i',
anotherUser: 'Inny użytkownik',
anotherUserTakenOver: 'Inny użytkownik przejął edycję tego dokumentu.',
applyChanges: 'Zastosuj zmiany',
ascending: 'Rosnąco',

View File

@@ -179,6 +179,7 @@ export const ptTranslations: DefaultTranslationsObject = {
addFilter: 'Adicionar Filtro',
adminTheme: 'Tema do Admin',
and: 'E',
anotherUser: 'Outro usuário',
anotherUserTakenOver: 'Outro usuário assumiu a edição deste documento.',
applyChanges: 'Aplicar alterações',
ascending: 'Ascendente',

View File

@@ -182,6 +182,7 @@ export const roTranslations: DefaultTranslationsObject = {
addFilter: 'Adaugă filtru',
adminTheme: 'Tema Admin',
and: 'Şi',
anotherUser: 'Un alt utilizator',
anotherUserTakenOver: 'Un alt utilizator a preluat editarea acestui document.',
applyChanges: 'Aplicați modificările',
ascending: 'Ascendant',

View File

@@ -178,6 +178,7 @@ export const rsTranslations: DefaultTranslationsObject = {
addFilter: 'Додај филтер',
adminTheme: 'Администраторска тема',
and: 'И',
anotherUser: 'Други корисник',
anotherUserTakenOver: 'Други корисник је преузео уређивање овог документа.',
applyChanges: 'Примени промене',
ascending: 'Узлазно',

View File

@@ -178,6 +178,7 @@ export const rsLatinTranslations: DefaultTranslationsObject = {
addFilter: 'Dodaj filter',
adminTheme: 'Administratorska tema',
and: 'I',
anotherUser: 'Drugi korisnik',
anotherUserTakenOver: 'Drugi korisnik je preuzeo uređivanje ovog dokumenta.',
applyChanges: 'Primeni promene',
ascending: 'Uzlazno',

View File

@@ -180,6 +180,7 @@ export const ruTranslations: DefaultTranslationsObject = {
addFilter: 'Добавить фильтр',
adminTheme: 'Тема Панели',
and: 'А также',
anotherUser: 'Другой пользователь',
anotherUserTakenOver: 'Другой пользователь взял на себя редактирование этого документа.',
applyChanges: 'Применить изменения',
ascending: 'Восходящий',

View File

@@ -180,6 +180,7 @@ export const skTranslations: DefaultTranslationsObject = {
addFilter: 'Pridať filter',
adminTheme: 'Motív administračného rozhrania',
and: 'a',
anotherUser: 'Iný používateľ',
anotherUserTakenOver: 'Iný používateľ prevzal úpravy tohto dokumentu.',
applyChanges: 'Použiť zmeny',
ascending: 'Vzostupne',

View File

@@ -178,6 +178,7 @@ export const slTranslations = {
addFilter: 'Dodaj filter',
adminTheme: 'Tema skrbnika',
and: 'In',
anotherUser: 'Drug uporabnik',
anotherUserTakenOver: 'Drug uporabnik je prevzel urejanje tega dokumenta.',
applyChanges: 'Uporabi spremembe',
ascending: 'Naraščajoče',

View File

@@ -178,6 +178,7 @@ export const svTranslations: DefaultTranslationsObject = {
addFilter: 'Lägg Till Filter',
adminTheme: 'Admin Tema',
and: 'Och',
anotherUser: 'En annan användare',
anotherUserTakenOver: 'En annan användare har tagit över redigeringen av detta dokument.',
applyChanges: 'Verkställ ändringar',
ascending: 'Stigande',

View File

@@ -175,6 +175,7 @@ export const thTranslations: DefaultTranslationsObject = {
addFilter: 'เพิ่มการกรอง',
adminTheme: 'ธีมผู้ดูแลระบบ',
and: 'และ',
anotherUser: 'ผู้ใช้อื่น',
anotherUserTakenOver: 'ผู้ใช้อื่นเข้าครอบครองการแก้ไขเอกสารนี้แล้ว',
applyChanges: 'ใช้การเปลี่ยนแปลง',
ascending: 'น้อยไปมาก',

View File

@@ -181,6 +181,7 @@ export const trTranslations: DefaultTranslationsObject = {
addFilter: 'Filtre ekle',
adminTheme: 'Admin arayüzü',
and: 've',
anotherUser: 'Başka bir kullanıcı',
anotherUserTakenOver: 'Başka bir kullanıcı bu belgenin düzenlemesini devraldı.',
applyChanges: 'Değişiklikleri Uygula',
ascending: 'artan',

View File

@@ -179,6 +179,7 @@ export const ukTranslations: DefaultTranslationsObject = {
addFilter: 'Додати фільтр',
adminTheme: 'Тема адмін панелі',
and: 'і',
anotherUser: 'Інший користувач',
anotherUserTakenOver: 'Інший користувач взяв на себе редагування цього документа.',
applyChanges: 'Застосувати зміни',
ascending: 'В порядку зростання',

View File

@@ -177,6 +177,7 @@ export const viTranslations: DefaultTranslationsObject = {
addFilter: 'Thêm bộ lọc',
adminTheme: 'Giao diện bảng điều khiển',
and: 'Và',
anotherUser: 'Người dùng khác',
anotherUserTakenOver: 'Người dùng khác đã tiếp quản việc chỉnh sửa tài liệu này.',
applyChanges: 'Áp dụng Thay đổi',
ascending: 'Sắp xếp theo thứ tự tăng dần',

View File

@@ -172,6 +172,7 @@ export const zhTranslations: DefaultTranslationsObject = {
addFilter: '添加过滤器',
adminTheme: '管理页面主题',
and: '和',
anotherUser: '另一位用户',
anotherUserTakenOver: '另一位用户接管了此文档的编辑。',
applyChanges: '应用更改',
ascending: '升序',

View File

@@ -172,6 +172,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
addFilter: '新增過濾器',
adminTheme: '管理頁面主題',
and: '和',
anotherUser: '另一位使用者',
anotherUserTakenOver: '另一位使用者接管了此文件的編輯。',
applyChanges: '套用更改',
ascending: '升冪',

View File

@@ -55,7 +55,7 @@ export const DocumentControls: React.FC<{
readonly redirectAfterDelete?: boolean
readonly redirectAfterDuplicate?: boolean
readonly slug: SanitizedCollectionConfig['slug']
readonly user?: ClientUser
readonly user?: ClientUser | number | string
}> = (props) => {
const {
id,

View File

@@ -3,20 +3,22 @@ import type { ClientUser } from 'payload'
import React, { useState } from 'react'
import { useTableCell } from '../../elements/Table/TableCellProvider/index.js'
import { LockIcon } from '../../icons/Lock/index.js'
import { useTranslation } from '../../providers/Translation/index.js'
import { isClientUserObject } from '../../utilities/isClientUserObject.js'
import { Tooltip } from '../Tooltip/index.js'
import './index.scss'
const baseClass = 'locked'
export const Locked: React.FC<{ className?: string; user: ClientUser }> = ({ className, user }) => {
const { rowData } = useTableCell()
export const Locked: React.FC<{ className?: string; user: ClientUser | number | string }> = ({
className,
user,
}) => {
const [hovered, setHovered] = useState(false)
const { t } = useTranslation()
const userToUse = user ? (user?.email ?? user?.id) : rowData?.id
const userToUse = isClientUserObject(user) ? (user.email ?? user.id) : t('general:anotherUser')
return (
<div

View File

@@ -24,4 +24,5 @@ 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 { isClientUserObject } from '../../utilities/isClientUserObject.js'
export { isEditing } from '../../utilities/isEditing.js'

View File

@@ -105,7 +105,7 @@ const DocumentInfo: React.FC<
)
const [documentIsLocked, setDocumentIsLocked] = useState<boolean | undefined>(false)
const [currentEditor, setCurrentEditor] = useState<ClientUser | null>(null)
const [currentEditor, setCurrentEditor] = useState<ClientUser | null | number | string>(null)
const [lastUpdateTime, setLastUpdateTime] = useState<number>(null)
const isInitializing = initialState === undefined || data === undefined
@@ -175,7 +175,7 @@ const DocumentInfo: React.FC<
)
const updateDocumentEditor = useCallback(
async (docId: number | string, slug: string, user: ClientUser) => {
async (docId: number | string, slug: string, user: ClientUser | number | string) => {
try {
const isGlobal = slug === globalSlug
@@ -191,10 +191,15 @@ const DocumentInfo: React.FC<
if (docs.length > 0) {
const lockId = docs[0].id
const userData =
typeof user === 'object'
? { relationTo: user.collection, value: user.id }
: { relationTo: 'users', value: user }
// Send a patch request to update the _lastEdited info
await requests.patch(`${serverURL}${api}/payload-locked-documents/${lockId}`, {
body: JSON.stringify({
user: { relationTo: user?.collection, value: user?.id },
user: userData,
}),
headers: {
'Content-Type': 'application/json',
@@ -230,7 +235,12 @@ 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) {
if (
newEditor && typeof newEditor === 'object' && typeof currentEditor === 'object'
? newEditor.id !== currentEditor?.id
: newEditor !== currentEditor
) {
setCurrentEditor(newEditor)
setDocumentIsLocked(true)
setLastUpdateTime(lastUpdatedAt)
@@ -238,6 +248,7 @@ const DocumentInfo: React.FC<
} else {
setDocumentIsLocked(false)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
// swallow error
}

View File

@@ -57,7 +57,7 @@ export type DocumentInfoProps = {
}
export type DocumentInfoContext = {
currentEditor?: ClientUser
currentEditor?: ClientUser | null | number | string
docConfig?: ClientCollectionConfig | ClientGlobalConfig
documentIsLocked?: boolean
getDocPermissions: (data?: Data) => Promise<void>

View File

@@ -39,7 +39,7 @@ export const buildFormState = async ({
}: {
req: PayloadRequest
}): Promise<{
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser | number | string }
lockedState?: { isLocked: boolean; user: ClientUser | number | string }
state: FormState
}> => {
const reqData: BuildFormStateArgs = (req.data || {}) as BuildFormStateArgs
@@ -275,7 +275,6 @@ 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,
}
@@ -344,7 +343,6 @@ export const buildFormState = async ({
const lockedState = {
isLocked: true,
lastEditedAt: new Date().toISOString(),
user: req.user,
}

View File

@@ -10,7 +10,7 @@ export const getFormState = async (args: {
signal?: AbortSignal
token?: string
}): Promise<{
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
lockedState?: { isLocked: boolean; user: ClientUser }
state: FormState
}> => {
const { apiRoute, body, onError, serverURL, signal, token } = args
@@ -27,7 +27,7 @@ export const getFormState = async (args: {
})
const json = (await res.json()) as {
lockedState?: { isLocked: boolean; lastEditedAt: string; user: ClientUser }
lockedState?: { isLocked: boolean; user: ClientUser }
state: FormState
}

View File

@@ -4,14 +4,18 @@ export const handleTakeOver = (
id: number | string,
collectionSlug: string,
globalSlug: string,
user: ClientUser,
user: ClientUser | number | string,
isWithinDoc: boolean,
updateDocumentEditor: (docId: number | string, slug: string, user: ClientUser) => Promise<void>,
setCurrentEditor: (value: React.SetStateAction<ClientUser>) => void,
updateDocumentEditor: (
docId: number | string,
slug: string,
user: ClientUser | number | string,
) => Promise<void>,
setCurrentEditor: (value: React.SetStateAction<ClientUser | number | string>) => void,
documentLockStateRef: React.RefObject<{
hasShownLockedModal: boolean
isLocked: boolean
user: ClientUser
user: ClientUser | number | string
}>,
isLockingEnabled: boolean,
setIsReadOnlyForIncomingUser?: (value: React.SetStateAction<boolean>) => void,

View File

@@ -0,0 +1,5 @@
import type { ClientUser } from 'payload'
export const isClientUserObject = (user): user is ClientUser => {
return user && typeof user === 'object'
}

View File

@@ -6,10 +6,26 @@ export const Users: CollectionConfig = {
useAsTitle: 'name',
},
auth: true,
access: {
read: ({ req: { user }, id }) => {
// Allow access if the user has the 'is_admin' role or if they are reading their own record
return Boolean(user?.roles?.includes('is_admin') || user?.id === id)
},
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'roles',
type: 'select',
hasMany: true,
// required: true,
options: [
{ label: 'User', value: 'is_user' },
{ label: 'Admin', value: 'is_admin' },
],
},
],
}

View File

@@ -29,6 +29,7 @@ export default buildConfigWithDefaults({
email: devUser.email,
password: devUser.password,
name: 'Admin',
roles: ['is_admin', 'is_user'],
},
})
@@ -38,6 +39,7 @@ export default buildConfigWithDefaults({
email: regularUser.email,
password: regularUser.password,
name: 'Dev',
roles: ['is_user'],
},
})

View File

@@ -106,6 +106,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
@@ -349,6 +350,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
@@ -650,6 +652,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
@@ -810,6 +813,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
@@ -899,6 +903,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
@@ -989,6 +994,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})
})
@@ -1174,6 +1180,7 @@ describe('locked documents', () => {
data: {
email: 'user2@payloadcms.com',
password: '1234',
roles: ['is_user'],
},
})

View File

@@ -89,6 +89,7 @@ export interface Test {
export interface User {
id: string;
name?: string | null;
roles?: ('is_user' | 'is_admin')[] | null;
updatedAt: string;
createdAt: string;
email: string;