Explorar o código

Merge pull request #15829 from overleaf/ii-ide-page-prototype-permissions

[web] React ide page permissions

GitOrigin-RevId: 727d33e0654d4bfefe5e710e553895f793cacb82
ilkin-overleaf %!s(int64=2) %!d(string=hai) anos
pai
achega
e4f5afc0c0

+ 1 - 1
services/web/frontend/js/features/ide-react/connection/join-project-payload.ts

@@ -1,5 +1,5 @@
 import { Project } from '../../../../../types/project'
-import { PermissionsLevel } from '../types/permissions-level'
+import { PermissionsLevel } from '@/features/ide-react/types/permissions'
 
 export type JoinProjectPayloadProject = Pick<
   Project,

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

@@ -21,6 +21,7 @@ import useEventListener from '@/shared/hooks/use-event-listener'
 import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
 import { Project } from '../../../../../types/project'
 import { useModalsContext } from '@/features/ide-react/context/modals-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 import { useTranslation } from 'react-i18next'
 
 type DocumentMetadata = {
@@ -53,6 +54,7 @@ export const MetadataProvider: FC = ({ children }) => {
   const { socket } = useConnectionContext()
   const { onlineUsersCount } = useOnlineUsersContext()
   const { permissionsLevel } = useEditorContext()
+  const { permissions } = usePermissionsContext()
   const { currentDocument } = useEditorManagerContext()
   const { showGenericMessageModal } = useModalsContext()
 
@@ -190,7 +192,7 @@ export const MetadataProvider: FC = ({ children }) => {
         )
       }
       window.setTimeout(() => {
-        if (permissionsLevel !== 'readOnly') {
+        if (permissions.write) {
           loadProjectMetaFromServer()
         }
       }, 200)
@@ -204,7 +206,7 @@ export const MetadataProvider: FC = ({ children }) => {
   }, [
     eventEmitter,
     loadProjectMetaFromServer,
-    permissionsLevel,
+    permissions,
     showGenericMessageModal,
     t,
   ])

+ 117 - 0
services/web/frontend/js/features/ide-react/context/permissions-context.tsx

@@ -0,0 +1,117 @@
+import { createContext, useContext, useState, useEffect, useMemo } from 'react'
+import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import { useEditorContext } from '@/shared/context/editor-context'
+import getMeta from '@/utils/meta'
+import { Permissions } from '@/features/ide-react/types/permissions'
+
+type PermissionsContextValue = {
+  permissions: Permissions
+}
+
+const PermissionsContext = createContext<PermissionsContextValue | undefined>(
+  undefined
+)
+
+const readOnlyPermissions: Readonly<Permissions> = {
+  read: true,
+  write: false,
+  admin: false,
+  comment: true,
+}
+const readAndWritePermissions: Readonly<Permissions> = {
+  read: true,
+  write: true,
+  admin: false,
+  comment: true,
+}
+const ownerPermissions: Readonly<Permissions> = {
+  read: true,
+  write: true,
+  admin: true,
+  comment: true,
+}
+const permissionsMap = {
+  readOnly: readOnlyPermissions,
+  readAndWrite: readAndWritePermissions,
+  owner: ownerPermissions,
+  anonymous: {
+    readOnly: { ...readOnlyPermissions, comment: false },
+    readAndWrite: { ...readAndWritePermissions, comment: false },
+    owner: { ...ownerPermissions, comment: false },
+  },
+} as const
+
+export const PermissionsProvider: React.FC = ({ children }) => {
+  const [permissions, setPermissions] = useState<Permissions>({
+    read: false,
+    write: false,
+    admin: false,
+    comment: false,
+  })
+  const { connectionState } = useConnectionContext()
+  const { permissionsLevel } = useEditorContext()
+  const anonymous = getMeta('ol-anonymous') as boolean | undefined
+
+  useEffect(() => {
+    if (permissionsLevel === 'readOnly') {
+      if (anonymous) {
+        setPermissions(permissionsMap.anonymous.readOnly)
+      } else {
+        setPermissions(permissionsMap.readOnly)
+      }
+    }
+    if (permissionsLevel === 'readAndWrite') {
+      if (permissions.admin) {
+        if (anonymous) {
+          setPermissions(permissionsMap.anonymous.owner)
+        } else {
+          setPermissions(permissionsMap.owner)
+        }
+      } else {
+        if (anonymous) {
+          setPermissions(permissionsMap.anonymous.readAndWrite)
+        } else {
+          setPermissions(permissionsMap.readAndWrite)
+        }
+      }
+    }
+    if (permissionsLevel === 'owner') {
+      if (anonymous) {
+        setPermissions(permissionsMap.anonymous.owner)
+      } else {
+        setPermissions(permissionsMap.owner)
+      }
+    }
+  }, [anonymous, permissions, permissionsLevel])
+
+  useEffect(() => {
+    if (connectionState.forceDisconnected) {
+      setPermissions(prevState => ({ ...prevState, write: false }))
+    }
+  }, [connectionState.forceDisconnected])
+
+  const value = useMemo<PermissionsContextValue>(
+    () => ({
+      permissions,
+    }),
+    [permissions]
+  )
+
+  return (
+    <PermissionsContext.Provider value={value}>
+      {children}
+    </PermissionsContext.Provider>
+  )
+}
+
+export function usePermissionsContext(): PermissionsContextValue {
+  const context = useContext(PermissionsContext)
+
+  if (!context) {
+    throw new Error(
+      'usePermissionsContext is only available inside PermissionsProvider'
+    )
+  }
+
+  return context
+}

+ 22 - 19
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -19,6 +19,7 @@ import { SplitTestProvider } from '@/shared/context/split-test-context'
 import { ModalsContextProvider } from '@/features/ide-react/context/modals-context'
 import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
 import { UserSettingsProvider } from '@/shared/context/user-settings-context'
+import { PermissionsProvider } from '@/features/ide-react/context/permissions-context'
 
 export const ReactContextRoot: FC = ({ children }) => {
   return (
@@ -33,25 +34,27 @@ export const ReactContextRoot: FC = ({ children }) => {
                     <ReferencesProvider>
                       <DetachProvider>
                         <EditorProvider>
-                          <ProjectSettingsProvider>
-                            <LayoutProvider>
-                              <LocalCompileProvider>
-                                <DetachCompileProvider>
-                                  <ChatProvider>
-                                    <ModalsContextProvider>
-                                      <EditorManagerProvider>
-                                        <OnlineUsersProvider>
-                                          <MetadataProvider>
-                                            {children}
-                                          </MetadataProvider>
-                                        </OnlineUsersProvider>
-                                      </EditorManagerProvider>
-                                    </ModalsContextProvider>
-                                  </ChatProvider>
-                                </DetachCompileProvider>
-                              </LocalCompileProvider>
-                            </LayoutProvider>
-                          </ProjectSettingsProvider>
+                          <PermissionsProvider>
+                            <ProjectSettingsProvider>
+                              <LayoutProvider>
+                                <LocalCompileProvider>
+                                  <DetachCompileProvider>
+                                    <ChatProvider>
+                                      <ModalsContextProvider>
+                                        <EditorManagerProvider>
+                                          <OnlineUsersProvider>
+                                            <MetadataProvider>
+                                              {children}
+                                            </MetadataProvider>
+                                          </OnlineUsersProvider>
+                                        </EditorManagerProvider>
+                                      </ModalsContextProvider>
+                                    </ChatProvider>
+                                  </DetachCompileProvider>
+                                </LocalCompileProvider>
+                              </LayoutProvider>
+                            </ProjectSettingsProvider>
+                          </PermissionsProvider>
                         </EditorProvider>
                       </DetachProvider>
                     </ReferencesProvider>

+ 4 - 4
services/web/frontend/js/features/ide-react/context/review-panel/hooks/use-review-panel-state.ts

@@ -11,13 +11,13 @@ import { useLayoutContext } from '@/shared/context/layout-context'
 import { useUserContext } from '@/shared/context/user-context'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 import { debugConsole } from '@/utils/debugging'
 import { useEditorContext } from '@/shared/context/editor-context'
 import { getJSON, postJSON } from '@/infrastructure/fetch-json'
 import ColorManager from '@/ide/colors/ColorManager'
 // @ts-ignore
 import RangesTracker from '@overleaf/ranges-tracker'
-import { ReviewPanelStateReactIde } from '../types/review-panel-state'
 import * as ReviewPanel from '../types/review-panel-state'
 import {
   ReviewPanelCommentThreadMessage,
@@ -28,6 +28,7 @@ import {
 } from '../../../../../../../types/review-panel/review-panel'
 import { UserId } from '../../../../../../../types/user'
 import { PublicAccessLevel } from '../../../../../../../types/public-access-level'
+import { ReviewPanelStateReactIde } from '../types/review-panel-state'
 import {
   DeepReadonly,
   MergeAndOverride,
@@ -114,6 +115,8 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
     features: { trackChangesVisible, trackChanges },
   } = project
   const { isRestrictedTokenMember } = useEditorContext()
+  // TODO permissions to be removed from the review panel context. It currently acts just as a proxy.
+  const { permissions } = usePermissionsContext()
 
   // TODO `currentDocument` and `currentDocumentId` should be get from `useEditorManagerContext()` but that makes tests fail
   const [currentDocument] = useScopeValue<Document>('editor.sharejs_doc')
@@ -134,9 +137,6 @@ function useReviewPanelState(): ReviewPanelStateReactIde {
     ReviewPanel.Value<'commentThreads'>
   >({})
   const [entries, setEntries] = useState<ReviewPanel.Value<'entries'>>({})
-
-  const [permissions] =
-    useScopeValue<ReviewPanel.Value<'permissions'>>('permissions')
   const [users, setUsers] = useScopeValue<ReviewPanel.Value<'users'>>(
     'users',
     true

+ 1 - 1
services/web/frontend/js/features/ide-react/create-ide-event-emitter.ts

@@ -1,6 +1,6 @@
 import { Emitter } from 'strict-event-emitter'
 import { Project } from '../../../../types/project'
-import { PermissionsLevel } from '@/features/ide-react/types/permissions-level'
+import { PermissionsLevel } from '@/features/ide-react/types/permissions'
 import { ShareJsDoc } from '@/features/ide-react/editor/share-js-doc'
 import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
 import { CursorPosition } from '@/features/ide-react/types/cursor-position'

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

@@ -1 +0,0 @@
-export type PermissionsLevel = 'owner' | 'readAndWrite' | 'readOnly'

+ 8 - 0
services/web/frontend/js/features/ide-react/types/permissions.ts

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

+ 3 - 5
services/web/frontend/js/features/source-editor/components/review-panel/entries/comment-entry.tsx

@@ -8,10 +8,8 @@ import AutoExpandingTextArea from '../../../../../shared/components/auto-expandi
 import Icon from '../../../../../shared/components/icon'
 import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
 import classnames from 'classnames'
-import {
-  ReviewPanelPermissions,
-  ThreadId,
-} from '../../../../../../../types/review-panel/review-panel'
+import { ThreadId } from '../../../../../../../types/review-panel/review-panel'
+import { Permissions } from '@/features/ide-react/types/permissions'
 import { DocId } from '../../../../../../../types/project-settings'
 import { ReviewPanelCommentThread } from '../../../../../../../types/review-panel/comment-thread'
 import { ReviewPanelCommentEntry } from '../../../../../../../types/review-panel/entry'
@@ -24,7 +22,7 @@ type CommentEntryProps = {
   entryId: ThreadId
   thread: ReviewPanelCommentThread | undefined
   threadId: ReviewPanelCommentEntry['thread_id']
-  permissions: ReviewPanelPermissions
+  permissions: Permissions
 } & Pick<ReviewPanelCommentEntry, 'offset' | 'focused'>
 
 function CommentEntry({

+ 2 - 2
services/web/frontend/js/features/source-editor/components/review-panel/entries/resolved-comment-entry.tsx

@@ -4,7 +4,7 @@ import Linkify from 'react-linkify'
 import { formatTime } from '../../../../utils/format-date'
 import { useReviewPanelUpdaterFnsContext } from '../../../context/review-panel/review-panel-context'
 import { FilteredResolvedComments } from '../toolbar/resolved-comments-dropdown'
-import { ReviewPanelPermissions } from '../../../../../../../types/review-panel/review-panel'
+import { Permissions } from '@/features/ide-react/types/permissions'
 
 function LinkDecorator(
   decoratedHref: string,
@@ -20,7 +20,7 @@ function LinkDecorator(
 
 type ResolvedCommentEntryProps = {
   thread: FilteredResolvedComments
-  permissions: ReviewPanelPermissions
+  permissions: Permissions
   contentLimit?: number
 }
 

+ 2 - 2
services/web/frontend/js/features/source-editor/components/review-panel/toolbar/resolved-comments-scroller.tsx

@@ -2,11 +2,11 @@ import { useTranslation } from 'react-i18next'
 import { useMemo } from 'react'
 import ResolvedCommentEntry from '../entries/resolved-comment-entry'
 import { FilteredResolvedComments } from './resolved-comments-dropdown'
-import { ReviewPanelPermissions } from '../../../../../../../types/review-panel/review-panel'
+import { Permissions } from '@/features/ide-react/types/permissions'
 
 type ResolvedCommentsScrollerProps = {
   resolvedComments: FilteredResolvedComments[]
-  permissions: ReviewPanelPermissions
+  permissions: Permissions
 }
 
 function ResolvedCommentsScroller({

+ 2 - 2
services/web/frontend/js/features/source-editor/components/review-panel/types/base-change-entry-props.ts

@@ -1,16 +1,16 @@
 import { ReviewPanelChangeEntry } from '../../../../../../../types/review-panel/entry'
 import { DocId } from '../../../../../../../types/project-settings'
 import {
-  ReviewPanelPermissions,
   ReviewPanelUser,
   ThreadId,
 } from '../../../../../../../types/review-panel/review-panel'
+import { Permissions } from '@/features/ide-react/types/permissions'
 
 export interface BaseChangeEntryProps
   extends Pick<ReviewPanelChangeEntry, 'content' | 'offset' | 'focused'> {
   docId: DocId
   entryId: ThreadId
-  permissions: ReviewPanelPermissions
+  permissions: Permissions
   user: ReviewPanelUser | undefined
   timestamp: ReviewPanelChangeEntry['metadata']['ts']
   contentLimit?: number

+ 2 - 2
services/web/frontend/js/features/source-editor/context/review-panel/types/review-panel-state.ts

@@ -2,11 +2,11 @@ import {
   CommentId,
   ReviewPanelCommentThreads,
   ReviewPanelEntries,
-  ReviewPanelPermissions,
   ReviewPanelUsers,
   SubView,
   ThreadId,
 } from '../../../../../../../types/review-panel/review-panel'
+import { Permissions } from '@/features/ide-react/types/permissions'
 import { DocId } from '../../../../../../../types/project-settings'
 import { dispatchReviewPanelLayout } from '../../../extensions/changes/change-manager'
 import { UserId } from '../../../../../../../types/user'
@@ -21,7 +21,7 @@ export interface ReviewPanelState {
     isAddingComment: boolean
     loadingThreads: boolean
     nVisibleSelectedChanges: number
-    permissions: ReviewPanelPermissions
+    permissions: Permissions
     users: ReviewPanelUsers
     resolvedComments: ReviewPanelEntries
     shouldCollapse: boolean

+ 0 - 7
services/web/types/review-panel/review-panel.ts

@@ -10,13 +10,6 @@ import { ReviewPanelCommentThread } from './comment-thread'
 
 export type SubView = 'cur_file' | 'overview'
 
-export interface ReviewPanelPermissions {
-  read: boolean
-  write: boolean
-  admin: boolean
-  comment: boolean
-}
-
 export type ThreadId = Brand<string, 'ThreadId'>
 // Entries may contain `add-comment` and `bulk-actions` props along with DocIds
 // Ideally the `add-comment` and `bulk-actions` objects should not be within the entries object