ソースを参照

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
David 1 年間 前
コミット
4a8163b3f3

+ 53 - 26
services/web/frontend/js/features/file-tree/contexts/file-tree-actionable.tsx

@@ -33,6 +33,7 @@ import {
 } from '../errors'
 } from '../errors'
 import { Folder } from '../../../../../types/folder'
 import { Folder } from '../../../../../types/folder'
 import { useReferencesContext } from '@/features/ide-react/context/references-context'
 import { useReferencesContext } from '@/features/ide-react/context/references-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 
 
 type DroppedFile = File & {
 type DroppedFile = File & {
   relativePath?: string
   relativePath?: string
@@ -220,6 +221,7 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
   const { _id: projectId } = useProjectContext()
   const { _id: projectId } = useProjectContext()
   const { fileTreeReadOnly } = useFileTreeData()
   const { fileTreeReadOnly } = useFileTreeData()
   const { indexAllReferences } = useReferencesContext()
   const { indexAllReferences } = useReferencesContext()
+  const { write } = usePermissionsContext()
 
 
   const [state, dispatch] = useReducer(
   const [state, dispatch] = useReducer(
     fileTreeReadOnly
     fileTreeReadOnly
@@ -501,32 +503,57 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
     }
     }
   }, [fileTreeData, projectId, selectedEntityIds])
   }, [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 (
   return (
     <FileTreeActionableContext.Provider value={value}>
     <FileTreeActionableContext.Provider value={value}>

+ 4 - 1
services/web/frontend/js/features/ide-react/context/metadata-context.tsx

@@ -185,7 +185,10 @@ export const MetadataProvider: FC = ({ children }) => {
         )
         )
       }
       }
       window.setTimeout(() => {
       window.setTimeout(() => {
-        if (permissionsRef.current.write) {
+        if (
+          permissionsRef.current.write ||
+          permissionsRef.current.trackedWrite
+        ) {
           loadProjectMetaFromServer()
           loadProjectMetaFromServer()
         }
         }
       }, 200)
       }, 200)

+ 9 - 5
services/web/frontend/js/features/ide-react/context/permissions-context.tsx

@@ -17,27 +17,31 @@ export const PermissionsContext = createContext<Permissions | undefined>(
 const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
 const permissionsMap: DeepReadonly<Record<PermissionsLevel, Permissions>> = {
   readOnly: {
   readOnly: {
     read: true,
     read: true,
+    comment: true,
+    trackedWrite: false,
     write: false,
     write: false,
     admin: false,
     admin: false,
-    comment: true,
   },
   },
   review: {
   review: {
     read: true,
     read: true,
-    write: true,
-    admin: false,
     comment: true,
     comment: true,
+    trackedWrite: true,
+    write: false,
+    admin: false,
   },
   },
   readAndWrite: {
   readAndWrite: {
     read: true,
     read: true,
+    comment: true,
+    trackedWrite: true,
     write: true,
     write: true,
     admin: false,
     admin: false,
-    comment: true,
   },
   },
   owner: {
   owner: {
     read: true,
     read: true,
+    comment: true,
+    trackedWrite: true,
     write: true,
     write: true,
     admin: true,
     admin: true,
-    comment: true,
   },
   },
 }
 }
 
 

+ 2 - 1
services/web/frontend/js/features/ide-react/types/permissions.ts

@@ -1,8 +1,9 @@
 export type Permissions = {
 export type Permissions = {
   read: boolean
   read: boolean
+  comment: boolean
+  trackedWrite: boolean
   write: boolean
   write: boolean
   admin: boolean
   admin: boolean
-  comment: boolean
 }
 }
 
 
 export type PermissionsLevel = 'owner' | 'readAndWrite' | 'review' | 'readOnly'
 export type PermissionsLevel = 'owner' | 'readAndWrite' | 'review' | 'readOnly'

+ 8 - 13
services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx

@@ -13,7 +13,6 @@ import {
 } from '../context/track-changes-state-context'
 } from '../context/track-changes-state-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 
 
 type Mode = 'viewing' | 'reviewing' | 'editing'
 type Mode = 'viewing' | 'reviewing' | 'editing'
@@ -24,11 +23,11 @@ const useCurrentMode = (): Mode => {
   const trackChangesForCurrentUser =
   const trackChangesForCurrentUser =
     trackChanges?.onForEveryone ||
     trackChanges?.onForEveryone ||
     (user && user.id && trackChanges?.onForMembers[user.id])
     (user && user.id && trackChanges?.onForMembers[user.id])
-  const { write } = usePermissionsContext()
+  const { write, trackedWrite } = usePermissionsContext()
 
 
   if (write && !trackChangesForCurrentUser) {
   if (write && !trackChangesForCurrentUser) {
     return 'editing'
     return 'editing'
-  } else if (write) {
+  } else if (trackedWrite) {
     return 'reviewing'
     return 'reviewing'
   }
   }
 
 
@@ -41,12 +40,8 @@ function ReviewModeSwitcher() {
     useTrackChangesStateActionsContext()
     useTrackChangesStateActionsContext()
   const mode = useCurrentMode()
   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 (
   return (
     <div className="review-mode-switcher-container">
     <div className="review-mode-switcher-container">
@@ -57,24 +52,24 @@ function ReviewModeSwitcher() {
         />
         />
         <DropdownMenu flip={false}>
         <DropdownMenu flip={false}>
           <OLDropdownMenuItem
           <OLDropdownMenuItem
-            disabled={!enableEditing}
+            disabled={!write}
             onClick={() => {
             onClick={() => {
               saveTrackChangesForCurrentUser(false)
               saveTrackChangesForCurrentUser(false)
             }}
             }}
             description={t('can_edit_content')}
             description={t('can_edit_content')}
             leadingIcon="edit"
             leadingIcon="edit"
-            active={enableEditing && mode === 'editing'}
+            active={write && mode === 'editing'}
           >
           >
             {t('editing')}
             {t('editing')}
           </OLDropdownMenuItem>
           </OLDropdownMenuItem>
           <OLDropdownMenuItem
           <OLDropdownMenuItem
-            disabled={!enableReviewing}
+            disabled={!trackedWrite}
             onClick={() => {
             onClick={() => {
               saveTrackChangesForCurrentUser(true)
               saveTrackChangesForCurrentUser(true)
             }}
             }}
             description={t('can_add_tracked_changes_and_comments')}
             description={t('can_add_tracked_changes_and_comments')}
             leadingIcon="rate_review"
             leadingIcon="rate_review"
-            active={enableReviewing && mode === 'reviewing'}
+            active={trackedWrite && mode === 'reviewing'}
           >
           >
             {t('reviewing')}
             {t('reviewing')}
           </OLDropdownMenuItem>
           </OLDropdownMenuItem>

+ 6 - 5
services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts

@@ -67,11 +67,12 @@ import { updateRanges } from '@/features/source-editor/extensions/ranges'
 import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
 import { useThreadsContext } from '@/features/review-panel-new/context/threads-context'
 import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
 import { useHunspell } from '@/features/source-editor/hooks/use-hunspell'
 import { isBootstrap5 } from '@/features/utils/bootstrap-5'
 import { isBootstrap5 } from '@/features/utils/bootstrap-5'
+import { Permissions } from '@/features/ide-react/types/permissions'
 
 
 function useCodeMirrorScope(view: EditorView) {
 function useCodeMirrorScope(view: EditorView) {
   const { fileTreeData } = useFileTreeData()
   const { fileTreeData } = useFileTreeData()
 
 
-  const [permissions] = useScopeValue<{ write: boolean }>('permissions')
+  const [permissions] = useScopeValue<Permissions>('permissions')
 
 
   // set up scope listeners
   // set up scope listeners
 
 
@@ -111,7 +112,7 @@ function useCodeMirrorScope(view: EditorView) {
 
 
   let [spellCheckLanguage] = useScopeValue<string>('project.spellCheckLanguage')
   let [spellCheckLanguage] = useScopeValue<string>('project.spellCheckLanguage')
   // spell check is off when read-only
   // spell check is off when read-only
-  if (!permissions.write) {
+  if (!permissions.write && !permissions.trackedWrite) {
     spellCheckLanguage = ''
     spellCheckLanguage = ''
   }
   }
 
 
@@ -288,7 +289,7 @@ function useCodeMirrorScope(view: EditorView) {
     }
     }
   }, [view, fileTreeData])
   }, [view, fileTreeData])
 
 
-  const editableRef = useRef(permissions.write)
+  const editableRef = useRef(permissions.write || permissions.trackedWrite)
 
 
   const { previewByPath } = useFileTreePathContext()
   const { previewByPath } = useFileTreePathContext()
 
 
@@ -409,11 +410,11 @@ function useCodeMirrorScope(view: EditorView) {
   }, [view, previewByPath])
   }, [view, previewByPath])
 
 
   useEffect(() => {
   useEffect(() => {
-    editableRef.current = permissions.write
+    editableRef.current = permissions.write || permissions.trackedWrite
     window.setTimeout(() => {
     window.setTimeout(() => {
       view.dispatch(setEditable(editableRef.current)) // the editor needs to be locked when there's a problem saving data
       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(() => {
   useEffect(() => {
     phrasesRef.current = phrases
     phrasesRef.current = phrases

+ 3 - 1
services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx

@@ -27,9 +27,10 @@ const PermissionsProvider: FC = ({ children }) => (
   <PermissionsContext.Provider
   <PermissionsContext.Provider
     value={{
     value={{
       read: true,
       read: true,
+      comment: true,
+      trackedWrite: false,
       write: false,
       write: false,
       admin: false,
       admin: false,
-      comment: true,
     }}
     }}
   >
   >
     {children}
     {children}
@@ -39,6 +40,7 @@ const PermissionsProvider: FC = ({ children }) => (
 const mountEditor = (content: string) => {
 const mountEditor = (content: string) => {
   const scope = mockScope(content)
   const scope = mockScope(content)
   scope.permissions.write = false
   scope.permissions.write = false
+  scope.permissions.trackedWrite = false
   scope.editor.showVisual = true
   scope.editor.showVisual = true
 
 
   cy.mount(
   cy.mount(

+ 1 - 0
services/web/test/frontend/features/source-editor/helpers/mock-scope.ts

@@ -62,6 +62,7 @@ export const mockScope = (content?: string) => {
     },
     },
     permissions: {
     permissions: {
       comment: true,
       comment: true,
+      trackedWrite: true,
       write: true,
       write: true,
     },
     },
     reviewPanel: {
     reviewPanel: {