Pārlūkot izejas kodu

[web] Move scopes `permissionsLevel`, `permissions` and `hasLintingError` to React context (#26607)

* Move `hasLintingError` from scope to react state

* Move `permissionsLevel` to IdeReactContext states

* Get `permissionsLevel` from `useIdeReactContext()`

* Set `permissionsLevel` in mocked `IdeReactProvider`

* Replace `permissions` scope by React state

* Fixup `permission` changes

* Remove redundant type

GitOrigin-RevId: 6203c61f9ac429789624196bf67e508310f4577f
Antoine Clausse 1 gadu atpakaļ
vecāks
revīzija
09a534f48b
24 mainītis faili ar 93 papildinājumiem un 75 dzēšanām
  1. 2 2
      services/web/frontend/js/features/editor-left-menu/components/settings/settings-spell-check-language.tsx
  2. 2 2
      services/web/frontend/js/features/editor-left-menu/hooks/use-root-doc-id.tsx
  3. 5 6
      services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.tsx
  4. 2 2
      services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx
  5. 2 3
      services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
  6. 20 9
      services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
  7. 1 3
      services/web/frontend/js/features/ide-react/context/metadata-context.tsx
  8. 15 7
      services/web/frontend/js/features/ide-react/context/permissions-context.tsx
  9. 1 3
      services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts
  10. 3 1
      services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx
  11. 3 1
      services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx
  12. 3 2
      services/web/frontend/js/features/review-panel-new/components/review-mode-switcher.tsx
  13. 3 1
      services/web/frontend/js/features/share-project-modal/components/editor-over-limit-modal.tsx
  14. 3 2
      services/web/frontend/js/features/share-project-modal/components/view-only-access-modal.tsx
  15. 2 1
      services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
  16. 1 14
      services/web/frontend/js/shared/context/editor-context.tsx
  17. 4 1
      services/web/frontend/js/shared/context/file-tree-data-context.tsx
  18. 2 3
      services/web/frontend/js/shared/context/local-compile-context.tsx
  19. 2 2
      services/web/frontend/js/shared/hooks/use-viewer-permissions.ts
  20. 3 1
      services/web/frontend/stories/decorators/scope.tsx
  21. 0 2
      services/web/test/frontend/components/editor-left-menu/scope.tsx
  22. 13 6
      services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx
  23. 0 1
      services/web/test/frontend/components/pdf-preview/scope.tsx
  24. 1 0
      services/web/test/frontend/helpers/editor-providers.jsx

+ 2 - 2
services/web/frontend/js/features/editor-left-menu/components/settings/settings-spell-check-language.tsx

@@ -4,7 +4,7 @@ import getMeta from '../../../../utils/meta'
 import { useProjectSettingsContext } from '../../context/project-settings-context'
 import SettingsMenuSelect from './settings-menu-select'
 import type { Optgroup } from './settings-menu-select'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { supportsWebAssembly } from '@/utils/wasm'
 
 export default function SettingsSpellCheckLanguage() {
@@ -12,7 +12,7 @@ export default function SettingsSpellCheckLanguage() {
 
   const { spellCheckLanguage, setSpellCheckLanguage } =
     useProjectSettingsContext()
-  const { permissionsLevel } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
 
   const optgroup: Optgroup = useMemo(() => {
     const options = (getMeta('ol-languages') ?? [])

+ 2 - 2
services/web/frontend/js/features/editor-left-menu/hooks/use-root-doc-id.tsx

@@ -1,5 +1,5 @@
 import { useCallback } from 'react'
-import { useEditorContext } from '../../../shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import useScopeValue from '../../../shared/hooks/use-scope-value'
 import type { ProjectSettings } from '../utils/api'
 import useSaveProjectSettings from './use-save-project-settings'
@@ -7,7 +7,7 @@ import useSaveProjectSettings from './use-save-project-settings'
 export default function useRootDocId() {
   const [rootDocId] =
     useScopeValue<ProjectSettings['rootDocId']>('project.rootDocId')
-  const { permissionsLevel } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const saveProjectSettings = useSaveProjectSettings()
 
   const setRootDocIdFunc = useCallback(

+ 5 - 6
services/web/frontend/js/features/editor-navigation-toolbar/components/editor-navigation-toolbar-root.tsx

@@ -1,6 +1,7 @@
 import React, { useState, useCallback } from 'react'
 import ToolbarHeader from './toolbar-header'
 import { useEditorContext } from '../../../shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useChatContext } from '../../chat/context/chat-context'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import { useProjectContext } from '../../../shared/context/project-context'
@@ -27,12 +28,10 @@ const EditorNavigationToolbarRoot = React.memo(
       features: { trackChangesVisible },
     } = useProjectContext()
 
-    const {
-      cobranding,
-      isRestrictedTokenMember,
-      renameProject,
-      permissionsLevel,
-    } = useEditorContext()
+    const { cobranding, isRestrictedTokenMember, renameProject } =
+      useEditorContext()
+
+    const { permissionsLevel } = useIdeReactContext()
 
     const {
       chatIsOpen,

+ 2 - 2
services/web/frontend/js/features/ide-react/components/unsaved-docs/unsaved-docs.tsx

@@ -1,5 +1,5 @@
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { FC, useCallback, useEffect, useRef, useState } from 'react'
 import { PermissionsLevel } from '@/features/ide-react/types/permissions'
 import { UnsavedDocsLockedAlert } from '@/features/ide-react/components/unsaved-docs/unsaved-docs-locked-alert'
@@ -12,7 +12,7 @@ const MAX_UNSAVED_SECONDS = 30 // lock the editor after this time if unsaved
 
 export const UnsavedDocs: FC = () => {
   const { openDocs, debugTimers } = useEditorManagerContext()
-  const { permissionsLevel, setPermissionsLevel } = useEditorContext()
+  const { permissionsLevel, setPermissionsLevel } = useIdeReactContext()
   const [isLocked, setIsLocked] = useState(false)
   const [unsavedDocs, setUnsavedDocs] = useState(new Map<string, number>())
   const globalAlertsContainer = useGlobalAlertsContainer()

+ 2 - 3
services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx

@@ -35,7 +35,6 @@ import { EditorType } from '@/features/ide-react/editor/types/editor-type'
 import { DocId } from '../../../../../types/project-settings'
 import { Update } from '@/features/history/services/types/update'
 import { useDebugDiffTracker } from '../hooks/use-debug-diff-tracker'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
 import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
 
@@ -89,8 +88,8 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
 }) => {
   const { t } = useTranslation()
   const { scopeStore } = useIdeContext()
-  const { reportError, eventEmitter, projectId } = useIdeReactContext()
-  const { setOutOfSync } = useEditorContext()
+  const { reportError, eventEmitter, projectId, setOutOfSync } =
+    useIdeReactContext()
   const { socket, closeConnection, connectionState } = useConnectionContext()
   const { view, setView, setOpenFile } = useLayoutContext()
   const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =

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

@@ -19,6 +19,7 @@ import { populateEditorScope } from '@/features/ide-react/scope-adapters/editor-
 import { postJSON } from '@/infrastructure/fetch-json'
 import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
 import getMeta from '@/utils/meta'
+import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
 
 const LOADED_AT = new Date()
 
@@ -31,6 +32,9 @@ type IdeReactContextValue = {
   >
   reportError: (error: any, meta?: Record<string, any>) => void
   projectJoined: boolean
+  permissionsLevel: PermissionsLevel
+  setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
+  setOutOfSync: (value: boolean) => void
 }
 
 export const IdeReactContext = createContext<IdeReactContextValue | undefined>(
@@ -43,13 +47,6 @@ function populateIdeReactScope(store: ReactScopeValueStore) {
 
 function populateProjectScope(store: ReactScopeValueStore) {
   store.allowNonExistentPath('project', true)
-  store.set('permissionsLevel', 'readOnly')
-  store.set('permissions', {
-    read: true,
-    write: false,
-    admin: false,
-    comment: true,
-  })
 }
 
 function populatePdfScope(store: ReactScopeValueStore) {
@@ -78,6 +75,9 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
   const projectId = getMeta('ol-project_id')
   const [scopeStore] = useState(() => createReactScopeValueStore(projectId))
   const [eventEmitter] = useState(createIdeEventEmitter)
+  const [permissionsLevel, setPermissionsLevel] =
+    useState<PermissionsLevel>('readOnly')
+  const [outOfSync, setOutOfSync] = useState(false)
   const [scopeEventEmitter] = useState(
     () => new ReactScopeEventEmitter(eventEmitter)
   )
@@ -137,7 +137,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
     }: JoinProjectPayload) {
       const project = { ..._project, rootDocId }
       scopeStore.set('project', project)
-      scopeStore.set('permissionsLevel', permissionsLevel)
+      setPermissionsLevel(permissionsLevel)
       // Make watchers update immediately
       scopeStore.flushUpdates()
       eventEmitter.emit('project:joined', { project, permissionsLevel })
@@ -173,11 +173,22 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
       eventEmitter,
       startedFreeTrial,
       setStartedFreeTrial,
+      permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
+      setPermissionsLevel,
+      setOutOfSync,
       projectId,
       reportError,
       projectJoined,
     }),
-    [eventEmitter, projectId, projectJoined, reportError, startedFreeTrial]
+    [
+      eventEmitter,
+      outOfSync,
+      permissionsLevel,
+      projectId,
+      projectJoined,
+      reportError,
+      startedFreeTrial,
+    ]
   )
 
   return (

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

@@ -13,7 +13,6 @@ import { useConnectionContext } from '@/features/ide-react/context/connection-co
 import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
 import { getJSON, postJSON } from '@/infrastructure/fetch-json'
 import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
-import { useEditorContext } from '@/shared/context/editor-context'
 import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
 import useEventListener from '@/shared/hooks/use-event-listener'
 import { useModalsContext } from '@/features/ide-react/context/modals-context'
@@ -49,10 +48,9 @@ export const MetadataContext = createContext<
 
 export const MetadataProvider: FC<React.PropsWithChildren> = ({ children }) => {
   const { t } = useTranslation()
-  const { eventEmitter, projectId } = useIdeReactContext()
+  const { eventEmitter, permissionsLevel, projectId } = useIdeReactContext()
   const { socket } = useConnectionContext()
   const { onlineUsersCount } = useOnlineUsersContext()
-  const { permissionsLevel } = useEditorContext()
   const permissions = usePermissionsContext()
   const { currentDocument } = useEditorOpenDocContext()
   const { showGenericMessageModal } = useModalsContext()

+ 15 - 7
services/web/frontend/js/features/ide-react/context/permissions-context.tsx

@@ -1,12 +1,11 @@
-import { createContext, useContext, useEffect } from 'react'
+import { createContext, useContext, useEffect, useState } from 'react'
 import { useConnectionContext } from '@/features/ide-react/context/connection-context'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import getMeta from '@/utils/meta'
 import {
   Permissions,
   PermissionsLevel,
 } from '@/features/ide-react/types/permissions'
-import useScopeValue from '@/shared/hooks/use-scope-value'
 import { DeepReadonly } from '../../../../../types/utils'
 import useViewerPermissions from '@/shared/hooks/use-viewer-permissions'
 import { useProjectContext } from '@/shared/context/project-context'
@@ -79,15 +78,24 @@ const noTrackChangesPermissionsMap: typeof permissionsMap = {
   owner: permissionsMap.owner,
 }
 
+const defaultPermissions: Permissions = {
+  read: true,
+  write: true,
+  admin: false,
+  comment: true,
+  resolveOwnComments: false,
+  resolveAllComments: false,
+  trackedWrite: true,
+  labelVersion: false,
+}
+
 export const PermissionsProvider: React.FC<React.PropsWithChildren> = ({
   children,
 }) => {
   const [permissions, setPermissions] =
-    useScopeValue<Readonly<Permissions>>('permissions')
+    useState<Permissions>(defaultPermissions)
   const { connectionState } = useConnectionContext()
-  const { permissionsLevel } = useEditorContext() as {
-    permissionsLevel: PermissionsLevel
-  }
+  const { permissionsLevel } = useIdeReactContext()
   const hasViewerPermissions = useViewerPermissions()
   const anonymous = getMeta('ol-anonymous')
   const project = useProjectContext()

+ 1 - 3
services/web/frontend/js/features/ide-react/hooks/use-socket-listeners.ts

@@ -12,14 +12,12 @@ import { debugConsole } from '@/utils/debugging'
 import { useCallback } from 'react'
 import { PublicAccessLevel } from '../../../../../types/public-access-level'
 import { useLocation } from '@/shared/hooks/use-location'
-import { useEditorContext } from '@/shared/context/editor-context'
 
 function useSocketListeners() {
   const { t } = useTranslation()
   const { socket } = useConnectionContext()
-  const { projectId } = useIdeReactContext()
+  const { permissionsLevel, projectId } = useIdeReactContext()
   const { showGenericMessageModal } = useModalsContext()
-  const { permissionsLevel } = useEditorContext()
   const [, setPublicAccessLevel] = useScopeValue('project.publicAccesLevel')
   const [, setProjectMembers] = useScopeValue('project.members')
   const [, setProjectInvites] = useScopeValue('project.invites')

+ 3 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/project-title.tsx

@@ -9,6 +9,7 @@ import { useProjectContext } from '@/shared/context/project-context'
 import { useTranslation } from 'react-i18next'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { DownloadProjectPDF, DownloadProjectZip } from './download-project'
 import { useCallback, useState } from 'react'
 import OLDropdownMenuItem from '@/features/ui/components/ol/ol-dropdown-menu-item'
@@ -21,7 +22,8 @@ const SubmitProjectButton = publishModalModules?.import.NewPublishDropdownButton
 export const ToolbarProjectTitle = () => {
   const { cobranding } = useEditorContext()
   const { t } = useTranslation()
-  const { permissionsLevel, renameProject } = useEditorContext()
+  const { renameProject } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const { name } = useProjectContext()
   const shouldDisplaySubmitButton =
     (permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&

+ 3 - 1
services/web/frontend/js/features/ide-redesign/components/toolbar/toolbar.tsx

@@ -14,13 +14,15 @@ import { useEditorContext } from '@/shared/context/editor-context'
 import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import UpgradeButton from './upgrade-button'
 import getMeta from '@/utils/meta'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 
 const [publishModalModules] = importOverleafModules('publishModal')
 const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
 
 export const Toolbar = () => {
   const { view, setView } = useLayoutContext()
-  const { cobranding, permissionsLevel } = useEditorContext()
+  const { cobranding } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const shouldDisplaySubmitButton =
     (permissionsLevel === 'owner' || permissionsLevel === 'readAndWrite') &&
     SubmitProjectButton

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

@@ -17,6 +17,7 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions-
 import usePersistedState from '@/shared/hooks/use-persisted-state'
 import { sendMB } from '@/infrastructure/event-tracking'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useProjectContext } from '@/shared/context/project-context'
 import UpgradeTrackChangesModal from './upgrade-track-changes-modal'
 import { ReviewModePromo } from '@/features/review-panel-new/components/review-mode-promo'
@@ -33,7 +34,7 @@ const useCurrentMode = (): Mode => {
     trackChanges?.onForEveryone ||
     (user?.id && trackChanges?.onForMembers[user.id]) ||
     (!user?.id && trackChanges?.onForGuests)
-  const { permissionsLevel } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
 
   if (permissionsLevel === 'readOnly') {
     return 'view'
@@ -52,7 +53,7 @@ function ReviewModeSwitcher() {
   const { saveTrackChangesForCurrentUser, saveTrackChanges } =
     useTrackChangesStateActionsContext()
   const mode = useCurrentMode()
-  const { permissionsLevel } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const { write, trackedWrite } = usePermissionsContext()
   const project = useProjectContext()
   const [showUpgradeModal, setShowUpgradeModal] = useState(false)

+ 3 - 1
services/web/frontend/js/features/share-project-modal/components/editor-over-limit-modal.tsx

@@ -3,13 +3,15 @@ import EditorOverLimitModalContent from './editor-over-limit-modal-content'
 import customLocalStorage from '@/infrastructure/local-storage'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import OLModal from '@/features/ui/components/ol/ol-modal'
 
 const EditorOverLimitModal = () => {
   const [show, setShow] = useState(false)
 
-  const { isProjectOwner, permissionsLevel } = useEditorContext()
+  const { isProjectOwner } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const { members, features, _id: projectId } = useProjectContext()
 
   const handleHide = () => {

+ 3 - 2
services/web/frontend/js/features/share-project-modal/components/view-only-access-modal.tsx

@@ -3,14 +3,15 @@ import ViewOnlyAccessModalContent from './view-only-access-modal-content'
 import customLocalStorage from '@/infrastructure/local-storage'
 import { useProjectContext } from '@/shared/context/project-context'
 import { useEditorContext } from '@/shared/context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { sendMB } from '@/infrastructure/event-tracking'
 import OLModal from '@/features/ui/components/ol/ol-modal'
 
 const ViewOnlyAccessModal = () => {
   const [show, setShow] = useState(false)
 
-  const { isProjectOwner, isPendingEditor, permissionsLevel } =
-    useEditorContext()
+  const { isProjectOwner, isPendingEditor } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   const { members, features, _id: projectId } = useProjectContext()
 
   const handleHide = () => {

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

@@ -58,11 +58,12 @@ import {
 import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
 import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
 import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
 
 function useCodeMirrorScope(view: EditorView) {
   const { fileTreeData } = useFileTreeData()
 
-  const [permissions] = useScopeValue<Permissions>('permissions')
+  const permissions: Permissions = usePermissionsContext()
 
   // set up scope listeners
 

+ 1 - 14
services/web/frontend/js/shared/context/editor-context.tsx

@@ -17,7 +17,6 @@ import { useDetachContext } from './detach-context'
 import getMeta from '../../utils/meta'
 import { useUserContext } from './user-context'
 import { saveProjectSettings } from '@/features/editor-left-menu/utils/api'
-import { PermissionsLevel } from '@/features/ide-react/types/permissions'
 import { useModalsContext } from '@/features/ide-react/context/modals-context'
 import { WritefullAPI } from './types/writefull-instance'
 import { Cobranding } from '../../../../types/cobranding'
@@ -28,19 +27,16 @@ export const EditorContext = createContext<
       cobranding?: Cobranding
       hasPremiumCompile?: boolean
       renameProject: (newName: string) => void
-      setPermissionsLevel: (permissionsLevel: PermissionsLevel) => void
       showSymbolPalette?: boolean
       toggleSymbolPalette?: () => void
       insertSymbol?: (symbol: SymbolWithCharacter) => void
       isProjectOwner: boolean
       isRestrictedTokenMember?: boolean
       isPendingEditor: boolean
-      permissionsLevel: PermissionsLevel
       deactivateTutorial: (tutorial: string) => void
       inactiveTutorials: string[]
       currentPopup: string | null
       setCurrentPopup: Dispatch<SetStateAction<string | null>>
-      setOutOfSync: (value: boolean) => void
       hasPremiumSuggestion: boolean
       setHasPremiumSuggestion: (value: boolean) => void
       setPremiumSuggestionResetDate: (date: Date) => void
@@ -78,9 +74,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
   }, [])
 
   const [projectName, setProjectName] = useScopeValue('project.name')
-  const [permissionsLevel, setPermissionsLevel] =
-    useScopeValue('permissionsLevel')
-  const [outOfSync, setOutOfSync] = useState(false)
   const [showSymbolPalette] = useScopeValue('editor.showSymbolPalette')
   const [toggleSymbolPalette] = useScopeValue('editor.toggleSymbolPalette')
 
@@ -187,8 +180,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       cobranding,
       hasPremiumCompile: features?.compileGroup === 'priority',
       renameProject,
-      permissionsLevel: outOfSync ? 'readOnly' : permissionsLevel,
-      setPermissionsLevel,
       isProjectOwner: owner?._id === userId,
       isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
       isPendingEditor,
@@ -199,7 +190,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       deactivateTutorial,
       currentPopup,
       setCurrentPopup,
-      setOutOfSync,
       hasPremiumSuggestion,
       setHasPremiumSuggestion,
       premiumSuggestionResetDate,
@@ -213,8 +203,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       owner,
       userId,
       renameProject,
-      permissionsLevel,
-      setPermissionsLevel,
       isPendingEditor,
       showSymbolPalette,
       toggleSymbolPalette,
@@ -223,8 +211,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       deactivateTutorial,
       currentPopup,
       setCurrentPopup,
-      outOfSync,
-      setOutOfSync,
       hasPremiumSuggestion,
       setHasPremiumSuggestion,
       premiumSuggestionResetDate,
@@ -238,6 +224,7 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
     <EditorContext.Provider value={value}>{children}</EditorContext.Provider>
   )
 }
+
 export function useEditorContext() {
   const context = useContext(EditorContext)
 

+ 4 - 1
services/web/frontend/js/shared/context/file-tree-data-context.tsx

@@ -28,6 +28,8 @@ import {
   useSnapshotContext,
 } from '@/features/ide-react/context/snapshot-context'
 import importOverleafModules from '../../../macros/import-overleaf-module.macro'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
+
 const { buildFileTree, createFolder } =
   (importOverleafModules('snapshotUtils')[0]
     ?.import as typeof StubSnapshotUtils) || StubSnapshotUtils
@@ -61,6 +63,7 @@ enum ACTION_TYPES {
   MOVE = 'MOVE',
   CREATE = 'CREATE',
 }
+
 /* eslint-enable no-unused-vars */
 
 type Action =
@@ -181,7 +184,7 @@ export const FileTreeDataProvider: FC<React.PropsWithChildren> = ({
 }) => {
   const [project] = useScopeValue<Project>('project')
   const { currentDocumentId, setOpenDocName } = useEditorOpenDocContext()
-  const [permissionsLevel] = useScopeValue('permissionsLevel')
+  const { permissionsLevel } = useIdeReactContext()
   const { fileTreeFromHistory, snapshot, snapshotVersion } =
     useSnapshotContext()
   const fileTreeReadOnly =

+ 2 - 3
services/web/frontend/js/shared/context/local-compile-context.tsx

@@ -10,7 +10,6 @@ import {
   Dispatch,
   SetStateAction,
 } from 'react'
-import useScopeValue from '../hooks/use-scope-value'
 import usePersistedState from '../hooks/use-persisted-state'
 import useAbortController from '../hooks/use-abort-controller'
 import DocumentCompiler from '../../features/pdf-preview/util/compiler'
@@ -85,7 +84,7 @@ export type CompileContext = {
   setAutoCompile: (value: boolean) => void
   setDraft: (value: any) => void
   setError: (value: any) => void
-  setHasLintingError: (value: any) => void // only for storybook
+  setHasLintingError: (value: boolean) => void // only for storybook
   setHighlights: (value: any) => void
   setPosition: Dispatch<SetStateAction<PdfScrollPosition>>
   setShowCompileTimeWarning: (value: any) => void
@@ -273,7 +272,7 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
   )
 
   // whether the editor linter found errors
-  const [hasLintingError, setHasLintingError] = useScopeValue('hasLintingError')
+  const [hasLintingError, setHasLintingError] = useState(false)
 
   // the timestamp that a doc was last changed
   const [changedAt, setChangedAt] = useState(0)

+ 2 - 2
services/web/frontend/js/shared/hooks/use-viewer-permissions.ts

@@ -1,7 +1,7 @@
-import { useEditorContext } from '../context/editor-context'
+import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 
 function useViewerPermissions() {
-  const { permissionsLevel } = useEditorContext()
+  const { permissionsLevel } = useIdeReactContext()
   return permissionsLevel === 'readOnly'
 }
 

+ 3 - 1
services/web/frontend/stories/decorators/scope.tsx

@@ -75,7 +75,6 @@ const initialScope = {
     },
     open_doc_name: 'testfile.tex',
   },
-  hasLintingError: false,
   permissionsLevel: 'owner',
 }
 
@@ -190,6 +189,9 @@ const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
     setStartedFreeTrial,
     reportError: () => {},
     projectJoined: true,
+    permissionsLevel: 'owner' as const,
+    setPermissionsLevel: () => {},
+    setOutOfSync: () => {},
   }))
 
   const [ideContextValue] = useState(() => {

+ 0 - 2
services/web/test/frontend/components/editor-left-menu/scope.tsx

@@ -12,7 +12,6 @@ type Scope = {
       getSnapshot?: () => string
     }
   }
-  hasLintingError?: boolean
   ui?: {
     view?: 'editor' | 'history' | 'file' | 'pdf'
     pdfLayout?: 'flat' | 'sideBySide' | 'split'
@@ -46,6 +45,5 @@ export const mockScope = (scope?: Scope) => ({
       getSnapshot: () => 'some doc content',
     },
   },
-  hasLintingError: false,
   ...scope,
 })

+ 13 - 6
services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx

@@ -7,7 +7,8 @@ import {
   IdeView,
   useLayoutContext,
 } from '../../../../frontend/js/shared/context/layout-context'
-import { FC, useEffect } from 'react'
+import { FC, PropsWithChildren, useEffect } from 'react'
+import { useLocalCompileContext } from '@/shared/context/local-compile-context'
 
 const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => {
   localStorage.setItem(key, value)
@@ -462,14 +463,20 @@ describe('<PdfPreview/>', function () {
     const scope = mockScope()
     // enable linting in the editor
     const userSettings = { syntaxValidation: true }
-    // mock a linting error
-    scope.hasLintingError = true
+
+    const WithLintingErrors: FC<PropsWithChildren> = ({ children }) => {
+      const { setHasLintingError } = useLocalCompileContext()
+      useEffect(() => setHasLintingError(true), [setHasLintingError])
+      return children
+    }
 
     cy.mount(
       <EditorProviders scope={scope} userSettings={userSettings}>
-        <div className="pdf-viewer">
-          <PdfPreview />
-        </div>
+        <WithLintingErrors>
+          <div className="pdf-viewer">
+            <PdfPreview />
+          </div>
+        </WithLintingErrors>
       </EditorProviders>
     )
 

+ 0 - 1
services/web/test/frontend/components/pdf-preview/scope.tsx

@@ -15,5 +15,4 @@ export const mockScope = () => ({
       doc: '\\documentclass{article}',
     }),
   },
-  hasLintingError: false,
 })

+ 1 - 0
services/web/test/frontend/helpers/editor-providers.jsx

@@ -204,6 +204,7 @@ const makeIdeReactProvider = (scope, socket) => {
       setStartedFreeTrial,
       reportError: () => {},
       projectJoined: true,
+      permissionsLevel: scope.permissionsLevel,
     }))
 
     const [ideContextValue] = useState(() => {