Add new trackedWrite frontend permission for review panel (#22486)

* Add new trackedWrite frontend permission for review panel

* Fix test lint

* Remove comment

* Fix codemirror tests

* hide file actions for reviewer

---------

Co-authored-by: Domagoj Kriskovic <dom.kriskovic@overleaf.com>
GitOrigin-RevId: 6aab5c662287a02750d81b3960bbdd9c98b325db
This commit is contained in:
David
2024-12-17 09:04:48 +00:00
committed by Copybot
co-authored by Domagoj Kriskovic
parent 4607e8fe06
commit 43d291a9ac
8 changed files with 86 additions and 52 deletions
@@ -33,6 +33,7 @@ import {
} from '../errors'
import { Folder } from '../../../../../types/folder'
import { useReferencesContext } from '@/features/ide-react/context/references-context'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
type DroppedFile = File & {
relativePath?: string
@@ -220,6 +221,7 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
const { _id: projectId } = useProjectContext()
const { fileTreeReadOnly } = useFileTreeData()
const { indexAllReferences } = useReferencesContext()
const { write } = usePermissionsContext()
const [state, dispatch] = useReducer(
fileTreeReadOnly
@@ -501,32 +503,57 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
}
}, [fileTreeData, projectId, selectedEntityIds])
// TODO: wrap in useMemo
const value = {
canDelete: selectedEntityIds.size > 0 && !isRootFolderSelected,
canRename: selectedEntityIds.size === 1 && !isRootFolderSelected,
canCreate: selectedEntityIds.size < 2,
...state,
parentFolderId,
selectedFileName,
isDuplicate,
startRenaming,
finishRenaming,
startDeleting,
finishDeleting,
finishMoving,
startCreatingFile,
startCreatingFolder,
finishCreatingFolder,
startCreatingDocOrFile,
startUploadingDocOrFile,
finishCreatingDoc,
finishCreatingLinkedFile,
cancel,
droppedFiles,
setDroppedFiles,
downloadPath,
}
const value = useMemo(
() => ({
canDelete: write && selectedEntityIds.size > 0 && !isRootFolderSelected,
canRename: write && selectedEntityIds.size === 1 && !isRootFolderSelected,
canCreate: write && selectedEntityIds.size < 2,
...state,
parentFolderId,
selectedFileName,
isDuplicate,
startRenaming,
finishRenaming,
startDeleting,
finishDeleting,
finishMoving,
startCreatingFile,
startCreatingFolder,
finishCreatingFolder,
startCreatingDocOrFile,
startUploadingDocOrFile,
finishCreatingDoc,
finishCreatingLinkedFile,
cancel,
droppedFiles,
setDroppedFiles,
downloadPath,
}),
[
cancel,
downloadPath,
droppedFiles,
finishCreatingDoc,
finishCreatingFolder,
finishCreatingLinkedFile,
finishDeleting,
finishMoving,
finishRenaming,
isDuplicate,
isRootFolderSelected,
parentFolderId,
selectedEntityIds.size,
selectedFileName,
startCreatingDocOrFile,
startCreatingFile,
startCreatingFolder,
startDeleting,
startRenaming,
startUploadingDocOrFile,
state,
write,
]
)
return (
<FileTreeActionableContext.Provider value={value}>
@@ -185,7 +185,10 @@ export const MetadataProvider: FC = ({ children }) => {
)
}
window.setTimeout(() => {
if (permissionsRef.current.write) {
if (
permissionsRef.current.write ||
permissionsRef.current.trackedWrite
) {
loadProjectMetaFromServer()
}
}, 200)
@@ -17,27 +17,31 @@ export const PermissionsContext = createContext<Permissions | undefined>(
const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
readOnly: {
read: true,
comment: true,
trackedWrite: false,
write: false,
admin: false,
comment: true,
},
review: {
read: true,
write: true,
admin: false,
comment: true,
trackedWrite: true,
write: false,
admin: false,
},
readAndWrite: {
read: true,
comment: true,
trackedWrite: true,
write: true,
admin: false,
comment: true,
},
owner: {
read: true,
comment: true,
trackedWrite: true,
write: true,
admin: true,
comment: true,
},
}
@@ -1,8 +1,9 @@
export type Permissions = {
read: boolean
comment: boolean
trackedWrite: boolean
write: boolean
admin: boolean
comment: boolean
}
export type PermissionsLevel = 'owner' | 'readAndWrite' | 'review' | 'readOnly'
@@ -13,7 +13,6 @@ import {
} from '../context/track-changes-state-context'
import { useUserContext } from '@/shared/context/user-context'
import { useTranslation } from 'react-i18next'
import { useEditorContext } from '@/shared/context/editor-context'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
type Mode = 'viewing' | 'reviewing' | 'editing'
@@ -24,11 +23,11 @@ const useCurrentMode = (): Mode => {
const trackChangesForCurrentUser =
trackChanges?.onForEveryone ||
(user && user.id && trackChanges?.onForMembers[user.id])
const { write } = usePermissionsContext()
const { write, trackedWrite } = usePermissionsContext()
if (write && !trackChangesForCurrentUser) {
return 'editing'
} else if (write) {
} else if (trackedWrite) {
return 'reviewing'
}
@@ -41,12 +40,8 @@ function ReviewModeSwitcher() {
useTrackChangesStateActionsContext()
const mode = useCurrentMode()
const { permissionsLevel } = useEditorContext()
const enableEditing =
permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite'
const enableReviewing = enableEditing || permissionsLevel === 'review'
const showViewOption = !enableReviewing
const { write, trackedWrite } = usePermissionsContext()
const showViewOption = !trackedWrite
return (
<div className="review-mode-switcher-container">
@@ -57,24 +52,24 @@ function ReviewModeSwitcher() {
/>
<DropdownMenu flip={false}>
<OLDropdownMenuItem
disabled={!enableEditing}
disabled={!write}
onClick={() => {
saveTrackChangesForCurrentUser(false)
}}
description={t('can_edit_content')}
leadingIcon="edit"
active={enableEditing && mode === 'editing'}
active={write && mode === 'editing'}
>
{t('editing')}
</OLDropdownMenuItem>
<OLDropdownMenuItem
disabled={!enableReviewing}
disabled={!trackedWrite}
onClick={() => {
saveTrackChangesForCurrentUser(true)
}}
description={t('can_add_tracked_changes_and_comments')}
leadingIcon="rate_review"
active={enableReviewing && mode === 'reviewing'}
active={trackedWrite && mode === 'reviewing'}
>
{t('reviewing')}
</OLDropdownMenuItem>
@@ -67,11 +67,12 @@ import { updateRanges } from '@/features/source-editor/extensions/ranges'
import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
import { isBootstrap5 } from '@/features/utils/bootstrap-5'
import { Permissions } from '@/features/ide-react/types/permissions'
function useCodeMirrorScope(view: EditorView) {
const { fileTreeData } = useFileTreeData()
const [permissions] = useScopeValue<{ write: boolean }>('permissions')
const [permissions] = useScopeValue<Permissions>('permissions')
// set up scope listeners
@@ -111,7 +112,7 @@ function useCodeMirrorScope(view: EditorView) {
let [spellCheckLanguage] = useScopeValue<string>('project.spellCheckLanguage')
// spell check is off when read-only
if (!permissions.write) {
if (!permissions.write && !permissions.trackedWrite) {
spellCheckLanguage = ''
}
@@ -288,7 +289,7 @@ function useCodeMirrorScope(view: EditorView) {
}
}, [view, fileTreeData])
const editableRef = useRef(permissions.write)
const editableRef = useRef(permissions.write || permissions.trackedWrite)
const { previewByPath } = useFileTreePathContext()
@@ -409,11 +410,11 @@ function useCodeMirrorScope(view: EditorView) {
}, [view, previewByPath])
useEffect(() => {
editableRef.current = permissions.write
editableRef.current = permissions.write || permissions.trackedWrite
window.setTimeout(() => {
view.dispatch(setEditable(editableRef.current)) // the editor needs to be locked when there's a problem saving data
})
}, [view, permissions.write])
}, [view, permissions.write, permissions.trackedWrite])
useEffect(() => {
phrasesRef.current = phrases
@@ -27,9 +27,10 @@ const PermissionsProvider: FC = ({ children }) => (
<PermissionsContext.Provider
value={{
read: true,
comment: true,
trackedWrite: false,
write: false,
admin: false,
comment: true,
}}
>
{children}
@@ -39,6 +40,7 @@ const PermissionsProvider: FC = ({ children }) => (
const mountEditor = (content: string) => {
const scope = mockScope(content)
scope.permissions.write = false
scope.permissions.trackedWrite = false
scope.editor.showVisual = true
cy.mount(
@@ -62,6 +62,7 @@ export const mockScope = (content?: string) => {
},
permissions: {
comment: true,
trackedWrite: true,
write: true,
},
reviewPanel: {