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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}>
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -187,6 +187,7 @@ export const findOperation = async <
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
limit: sanitizedLimit,
|
||||
overrideAccess: false,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
|
||||
@@ -139,6 +139,7 @@ export const findByIDOperation = async <
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
overrideAccess: false,
|
||||
pagination: false,
|
||||
req,
|
||||
where: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ export const clientTranslationKeys = createClientTranslationKeys([
|
||||
'general:addFilter',
|
||||
'general:adminTheme',
|
||||
'general:and',
|
||||
'general:anotherUser',
|
||||
'general:anotherUserTakenOver',
|
||||
'general:applyChanges',
|
||||
'general:ascending',
|
||||
|
||||
@@ -177,6 +177,7 @@ export const arTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'أضف فلتر',
|
||||
adminTheme: 'شكل واجهة المستخدم',
|
||||
and: 'و',
|
||||
anotherUser: 'مستخدم آخر',
|
||||
anotherUserTakenOver: 'قام مستخدم آخر بالاستيلاء على تحرير هذا المستند.',
|
||||
applyChanges: 'طبق التغييرات',
|
||||
ascending: 'تصاعدي',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -178,6 +178,7 @@ export const bgTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'Добави филтър',
|
||||
adminTheme: 'Цветова тема',
|
||||
and: 'И',
|
||||
anotherUser: 'Друг потребител',
|
||||
anotherUserTakenOver: 'Друг потребител пое редактирането на този документ.',
|
||||
applyChanges: 'Приложи промените',
|
||||
ascending: 'Възходящ',
|
||||
|
||||
@@ -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ě',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -177,6 +177,7 @@ export const faTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'افزودن علامت',
|
||||
adminTheme: 'پوسته پیشخوان',
|
||||
and: 'و',
|
||||
anotherUser: 'کاربر دیگر',
|
||||
anotherUserTakenOver: 'کاربر دیگری ویرایش این سند را به دست گرفته است.',
|
||||
applyChanges: 'اعمال تغییرات',
|
||||
ascending: 'صعودی',
|
||||
|
||||
@@ -185,6 +185,7 @@ export const frTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'Ajouter un filtre',
|
||||
adminTheme: 'Thème d’administration',
|
||||
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',
|
||||
|
||||
@@ -174,6 +174,7 @@ export const heTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'הוסף מסנן',
|
||||
adminTheme: 'ערכת נושא ממשק הניהול',
|
||||
and: 'וגם',
|
||||
anotherUser: 'משתמש אחר',
|
||||
anotherUserTakenOver: 'משתמש אחר השתלט על עריכת מסמך זה.',
|
||||
applyChanges: 'החל שינויים',
|
||||
ascending: 'בסדר עולה',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ő',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -179,6 +179,7 @@ export const jaTranslations: DefaultTranslationsObject = {
|
||||
addFilter: '絞り込みを追加',
|
||||
adminTheme: '管理画面のテーマ',
|
||||
and: 'かつ',
|
||||
anotherUser: '別のユーザー',
|
||||
anotherUserTakenOver: '別のユーザーがこのドキュメントの編集を引き継ぎました。',
|
||||
applyChanges: '変更を適用する',
|
||||
ascending: '昇順',
|
||||
|
||||
@@ -178,6 +178,7 @@ export const koTranslations: DefaultTranslationsObject = {
|
||||
addFilter: '필터 추가',
|
||||
adminTheme: '관리자 테마',
|
||||
and: '및',
|
||||
anotherUser: '다른 사용자',
|
||||
anotherUserTakenOver: '다른 사용자가 이 문서의 편집을 인수했습니다.',
|
||||
applyChanges: '변경 사항 적용',
|
||||
ascending: '오름차순',
|
||||
|
||||
@@ -180,6 +180,7 @@ export const myTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'ဇကာထည့်ပါ။',
|
||||
adminTheme: 'အက်ပ်ဒိုင်များစပ်စွာ',
|
||||
and: 'နှင့်',
|
||||
anotherUser: 'တစ်ခြားအသုံးပြုသူ',
|
||||
anotherUserTakenOver: 'တစ်ခြားအသုံးပြုသူသည်ဤစာရွက်စာတမ်းကိုပြင်ဆင်မှုကိုရယူလိုက်သည်။',
|
||||
applyChanges: 'ပြောင်းလဲမှုများ အသုံးပြုပါ',
|
||||
ascending: 'တက်နေသည်',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -178,6 +178,7 @@ export const rsTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'Додај филтер',
|
||||
adminTheme: 'Администраторска тема',
|
||||
and: 'И',
|
||||
anotherUser: 'Други корисник',
|
||||
anotherUserTakenOver: 'Други корисник је преузео уређивање овог документа.',
|
||||
applyChanges: 'Примени промене',
|
||||
ascending: 'Узлазно',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -180,6 +180,7 @@ export const ruTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'Добавить фильтр',
|
||||
adminTheme: 'Тема Панели',
|
||||
and: 'А также',
|
||||
anotherUser: 'Другой пользователь',
|
||||
anotherUserTakenOver: 'Другой пользователь взял на себя редактирование этого документа.',
|
||||
applyChanges: 'Применить изменения',
|
||||
ascending: 'Восходящий',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -175,6 +175,7 @@ export const thTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'เพิ่มการกรอง',
|
||||
adminTheme: 'ธีมผู้ดูแลระบบ',
|
||||
and: 'และ',
|
||||
anotherUser: 'ผู้ใช้อื่น',
|
||||
anotherUserTakenOver: 'ผู้ใช้อื่นเข้าครอบครองการแก้ไขเอกสารนี้แล้ว',
|
||||
applyChanges: 'ใช้การเปลี่ยนแปลง',
|
||||
ascending: 'น้อยไปมาก',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -179,6 +179,7 @@ export const ukTranslations: DefaultTranslationsObject = {
|
||||
addFilter: 'Додати фільтр',
|
||||
adminTheme: 'Тема адмін панелі',
|
||||
and: 'і',
|
||||
anotherUser: 'Інший користувач',
|
||||
anotherUserTakenOver: 'Інший користувач взяв на себе редагування цього документа.',
|
||||
applyChanges: 'Застосувати зміни',
|
||||
ascending: 'В порядку зростання',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -172,6 +172,7 @@ export const zhTranslations: DefaultTranslationsObject = {
|
||||
addFilter: '添加过滤器',
|
||||
adminTheme: '管理页面主题',
|
||||
and: '和',
|
||||
anotherUser: '另一位用户',
|
||||
anotherUserTakenOver: '另一位用户接管了此文档的编辑。',
|
||||
applyChanges: '应用更改',
|
||||
ascending: '升序',
|
||||
|
||||
@@ -172,6 +172,7 @@ export const zhTwTranslations: DefaultTranslationsObject = {
|
||||
addFilter: '新增過濾器',
|
||||
adminTheme: '管理頁面主題',
|
||||
and: '和',
|
||||
anotherUser: '另一位使用者',
|
||||
anotherUserTakenOver: '另一位使用者接管了此文件的編輯。',
|
||||
applyChanges: '套用更改',
|
||||
ascending: '升冪',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
5
packages/ui/src/utilities/isClientUserObject.ts
Normal file
5
packages/ui/src/utilities/isClientUserObject.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ClientUser } from 'payload'
|
||||
|
||||
export const isClientUserObject = (user): user is ClientUser => {
|
||||
return user && typeof user === 'object'
|
||||
}
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user