Quellcode durchsuchen

[web] Move UI scope to React states (#26403)

* Move `ui.view` to setState

* Move `openFile` to setState

* Move `ui.leftMenuShown` to setState

* Move `ui.miniReviewPanelVisible` to setState

* Move `ui.pdfLayout` to setState

* Move `ui.chatOpen` to setState

* Move `ui.reviewPanelOpen` to setState

* Cleanup: remove layout-context-adapter and imports

* Replace `ui` scope by mocked `LayoutProvider` in tests

* Update test

* Remove unnecessary `scopeStore.set('ui.chatOpen' ...`

GitOrigin-RevId: 81578bfdc958239eac492905f714a6074c81d0f5
Antoine Clausse vor 1 Jahr
Ursprung
Commit
9e189e7d59
17 geänderte Dateien mit 406 neuen und 310 gelöschten Zeilen
  1. 1 5
      services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx
  2. 2 3
      services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx
  3. 0 2
      services/web/frontend/js/features/ide-react/context/ide-react-context.tsx
  4. 0 14
      services/web/frontend/js/features/ide-react/scope-adapters/layout-context-adapter.ts
  5. 22 10
      services/web/frontend/js/shared/context/layout-context.tsx
  6. 0 4
      services/web/frontend/stories/decorators/scope.tsx
  7. 128 157
      services/web/test/frontend/components/editor-left-menu/editor-left-menu.spec.tsx
  8. 0 5
      services/web/test/frontend/components/editor-left-menu/scope.tsx
  9. 0 4
      services/web/test/frontend/components/pdf-preview/scope.tsx
  10. 11 6
      services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx
  11. 2 0
      services/web/test/frontend/features/full-project-search/components/full-project-search.spec.tsx
  12. 82 78
      services/web/test/frontend/features/history/components/change-list.spec.tsx
  13. 6 9
      services/web/test/frontend/features/history/components/toolbar.spec.tsx
  14. 15 3
      services/web/test/frontend/features/layout/components/switch-to-editor-button.spec.tsx
  15. 7 3
      services/web/test/frontend/features/layout/components/switch-to-pdf-button.spec.tsx
  16. 0 3
      services/web/test/frontend/features/source-editor/helpers/mock-scope.ts
  17. 130 4
      services/web/test/frontend/helpers/editor-providers.jsx

+ 1 - 5
services/web/frontend/js/features/ide-react/context/editor-manager-context.tsx

@@ -36,8 +36,6 @@ 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 useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
-import { BinaryFile } from '@/features/file-view/types/binary-file'
 import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
 
 export interface GotoOffsetOptions {
@@ -97,7 +95,7 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
   const { reportError, eventEmitter, projectId } = useIdeReactContext()
   const { setOutOfSync } = useEditorContext()
   const { socket, closeConnection, connectionState } = useConnectionContext()
-  const { view, setView } = useLayoutContext()
+  const { view, setView, setOpenFile } = useLayoutContext()
   const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
     useModalsContext()
   const { id: userId } = useUserContext()
@@ -521,8 +519,6 @@ export const EditorManagerProvider: FC<React.PropsWithChildren> = ({
     [fileTreeData, openDoc]
   )
 
-  const [, setOpenFile] = useScopeValueSetterOnly<BinaryFile | null>('openFile')
-
   const openFileWithId = useCallback(
     (fileRefId: string) => {
       const fileRef = findFileRefEntityById(fileTreeData, fileRefId)

+ 2 - 3
services/web/frontend/js/features/ide-react/context/file-tree-open-context.tsx

@@ -11,8 +11,6 @@ import {
 import { useProjectContext } from '@/shared/context/project-context'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import useScopeValueSetterOnly from '@/shared/hooks/use-scope-value-setter-only'
-import { BinaryFile } from '@/features/file-view/types/binary-file'
 import {
   FileTreeDocumentFindResult,
   FileTreeFileRefFindResult,
@@ -22,6 +20,7 @@ import { debugConsole } from '@/utils/debugging'
 import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
 import { sendMB } from '@/infrastructure/event-tracking'
 import { FileRef } from '../../../../../types/file-ref'
+import { useLayoutContext } from '@/shared/context/layout-context'
 
 const FileTreeOpenContext = createContext<
   | {
@@ -43,7 +42,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
   const { eventEmitter, projectJoined } = useIdeReactContext()
   const { openDocWithId, currentDocumentId, openInitialDoc } =
     useEditorManagerContext()
-  const [, setOpenFile] = useScopeValueSetterOnly<BinaryFile | null>('openFile')
+  const { setOpenFile } = useLayoutContext()
   const [openEntity, setOpenEntity] = useState<
     FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
   >(null)

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

@@ -8,7 +8,6 @@ import React, {
   useCallback,
 } from 'react'
 import { ReactScopeValueStore } from '@/features/ide-react/scope-value-store/react-scope-value-store'
-import populateLayoutScope from '@/features/ide-react/scope-adapters/layout-context-adapter'
 import { IdeProvider } from '@/shared/context/ide-context'
 import {
   createIdeEventEmitter,
@@ -67,7 +66,6 @@ export function createReactScopeValueStore(projectId: string) {
   // necessary values in the store, but this is simpler for now
   populateIdeReactScope(scopeStore)
   populateEditorScope(scopeStore, projectId)
-  populateLayoutScope(scopeStore)
   populateProjectScope(scopeStore)
   populatePdfScope(scopeStore)
 

+ 0 - 14
services/web/frontend/js/features/ide-react/scope-adapters/layout-context-adapter.ts

@@ -1,14 +0,0 @@
-import { ReactScopeValueStore } from '../scope-value-store/react-scope-value-store'
-import getMeta from '@/utils/meta'
-
-const reviewPanelStorageKey = `ui.reviewPanelOpen.${getMeta('ol-project_id')}`
-
-export default function populateLayoutScope(store: ReactScopeValueStore) {
-  store.set('ui.view', 'editor')
-  store.set('openFile', null)
-  store.persisted('ui.chatOpen', false, 'ui.chatOpen')
-  store.persisted('ui.reviewPanelOpen', false, reviewPanelStorageKey)
-  store.set('ui.leftMenuShown', false)
-  store.set('ui.miniReviewPanelVisible', false)
-  store.set('ui.pdfLayout', 'sideBySide')
-}

+ 22 - 10
services/web/frontend/js/shared/context/layout-context.tsx

@@ -9,7 +9,6 @@ import {
   FC,
   useState,
 } from 'react'
-import useScopeValue from '../hooks/use-scope-value'
 import useDetachLayout from '../hooks/use-detach-layout'
 import localStorage from '../../infrastructure/local-storage'
 import getMeta from '../../utils/meta'
@@ -21,6 +20,7 @@ import useEventListener from '@/shared/hooks/use-event-listener'
 import { isMac } from '@/shared/utils/os'
 import { sendSearchEvent } from '@/features/event-tracking/search-events'
 import { useRailContext } from '@/features/ide-redesign/contexts/rail-context'
+import usePersistedState from '@/shared/hooks/use-persisted-state'
 
 export type IdeLayout = 'sideBySide' | 'flat'
 export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
@@ -55,6 +55,8 @@ export type LayoutContextValue = {
   pdfPreviewOpen: boolean
   projectSearchIsOpen: boolean
   setProjectSearchIsOpen: Dispatch<SetStateAction<boolean>>
+  openFile: BinaryFile | null
+  setOpenFile: Dispatch<SetStateAction<BinaryFile | null>>
 }
 
 const debugPdfDetach = getMeta('ol-debugPdfDetach')
@@ -70,10 +72,12 @@ function setLayoutInLocalStorage(pdfLayout: IdeLayout) {
   )
 }
 
+const reviewPanelStorageKey = `ui.reviewPanelOpen.${getMeta('ol-project_id')}`
+
 export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
   // what to show in the "flat" view (editor or pdf)
-  const [view, _setView] = useScopeValue<IdeView | null>('ui.view')
-  const [openFile] = useScopeValue<BinaryFile | null>('openFile')
+  const [view, _setView] = useState<IdeView | null>('editor')
+  const [openFile, setOpenFile] = useState<BinaryFile | null>(null)
   const historyToggleEmitter = useScopeEventEmitter('history:toggle', true)
   const { isOpen: railIsOpen, setIsOpen: setRailIsOpen } = useRailContext()
   const [prevRailIsOpen, setPrevRailIsOpen] = useState(railIsOpen)
@@ -118,19 +122,23 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
   )
 
   // whether the chat pane is open
-  const [chatIsOpen, setChatIsOpen] = useScopeValue<boolean>('ui.chatOpen')
+  const [chatIsOpen, setChatIsOpen] = usePersistedState<boolean>(
+    'ui.chatOpen',
+    false
+  )
 
   // whether the review pane is open
-  const [reviewPanelOpen, setReviewPanelOpen] =
-    useScopeValue<boolean>('ui.reviewPanelOpen')
+  const [reviewPanelOpen, setReviewPanelOpen] = usePersistedState<boolean>(
+    reviewPanelStorageKey,
+    false
+  )
 
   // whether the review pane is collapsed
   const [miniReviewPanelVisible, setMiniReviewPanelVisible] =
-    useScopeValue<boolean>('ui.miniReviewPanelVisible')
+    useState<boolean>(false)
 
   // whether the menu pane is open
-  const [leftMenuShown, setLeftMenuShown] =
-    useScopeValue<boolean>('ui.leftMenuShown')
+  const [leftMenuShown, setLeftMenuShown] = useState<boolean>(false)
 
   // whether the project search is open
   const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false)
@@ -173,7 +181,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
   )
 
   // whether to display the editor and preview side-by-side or full-width ("flat")
-  const [pdfLayout, setPdfLayout] = useScopeValue<IdeLayout>('ui.pdfLayout')
+  const [pdfLayout, setPdfLayout] = useState<IdeLayout>('sideBySide')
 
   // whether stylesheet on theme is loading
   const [loadingStyleSheet, setLoadingStyleSheet] = useState(false)
@@ -238,6 +246,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
       changeLayout,
       chatIsOpen,
       leftMenuShown,
+      openFile,
       pdfLayout,
       pdfPreviewOpen,
       projectSearchIsOpen,
@@ -247,6 +256,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
       loadingStyleSheet,
       setChatIsOpen,
       setLeftMenuShown,
+      setOpenFile,
       setPdfLayout,
       setReviewPanelOpen,
       setMiniReviewPanelVisible,
@@ -262,6 +272,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
       changeLayout,
       chatIsOpen,
       leftMenuShown,
+      openFile,
       pdfLayout,
       pdfPreviewOpen,
       projectSearchIsOpen,
@@ -271,6 +282,7 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
       loadingStyleSheet,
       setChatIsOpen,
       setLeftMenuShown,
+      setOpenFile,
       setPdfLayout,
       setReviewPanelOpen,
       setMiniReviewPanelVisible,

+ 0 - 4
services/web/frontend/stories/decorators/scope.tsx

@@ -59,10 +59,6 @@ const project: Project = {
 const initialScope = {
   user,
   project,
-  ui: {
-    chatOpen: true,
-    pdfLayout: 'flat',
-  },
   settings: {
     pdfViewer: 'js',
     syntaxValidation: true,

+ 128 - 157
services/web/test/frontend/components/editor-left-menu/editor-left-menu.spec.tsx

@@ -56,14 +56,10 @@ describe('<EditorLeftMenu />', function () {
     })
 
     it('render full menu', function () {
-      const scope = mockScope({
-        ui: {
-          leftMenuShown: true,
-        },
-      })
+      const scope = mockScope()
 
       cy.mount(
-        <EditorProviders scope={scope}>
+        <EditorProviders scope={scope} layoutContext={{ leftMenuShown: true }}>
           <EditorLeftMenu />
         </EditorProviders>
       )
@@ -110,14 +106,12 @@ describe('<EditorLeftMenu />', function () {
 
     describe('download menu', function () {
       it('have a correct source & pdf download url', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
-
+        const scope = mockScope()
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -142,14 +136,13 @@ describe('<EditorLeftMenu />', function () {
           },
         })
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -184,14 +177,13 @@ describe('<EditorLeftMenu />', function () {
           },
         }).as('wordCount')
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -217,9 +209,6 @@ describe('<EditorLeftMenu />', function () {
         })
 
         const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
           project: {
             members: [],
             owner: {
@@ -234,7 +223,10 @@ describe('<EditorLeftMenu />', function () {
         })
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -245,9 +237,6 @@ describe('<EditorLeftMenu />', function () {
 
       it('shows git modal correctly', function () {
         const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
           project: {
             owner: {
               _id: '123',
@@ -259,7 +248,10 @@ describe('<EditorLeftMenu />', function () {
         })
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -271,9 +263,6 @@ describe('<EditorLeftMenu />', function () {
 
       it('shows git modal paywall correctly', function () {
         const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
           project: {
             owner: {
               _id: '123',
@@ -285,7 +274,10 @@ describe('<EditorLeftMenu />', function () {
         })
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -304,14 +296,13 @@ describe('<EditorLeftMenu />', function () {
           enabled: false,
         }).as('project-status')
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -335,14 +326,13 @@ describe('<EditorLeftMenu />', function () {
 
     describe('settings menu', function () {
       it('shows compiler menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -369,14 +359,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows texlive version menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -410,14 +399,14 @@ describe('<EditorLeftMenu />', function () {
           folders: [],
         }
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope} rootFolder={[rootFolder as any]}>
+          <EditorProviders
+            layoutContext={{ leftMenuShown: true }}
+            scope={scope}
+            rootFolder={[rootFolder as any]}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -451,14 +440,13 @@ describe('<EditorLeftMenu />', function () {
 
         window.metaAttributesCache.set('ol-languages', languages)
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -475,14 +463,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows dictionary modal correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -493,14 +480,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows auto-complete menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -517,14 +503,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows auto-close brackets menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -541,14 +526,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows code check menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -579,14 +563,13 @@ describe('<EditorLeftMenu />', function () {
           legacyEditorThemes
         )
 
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -619,14 +602,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows overall theme menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -643,14 +625,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows keybindings menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -667,14 +648,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows font size menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -713,14 +693,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows font family menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -741,14 +720,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows line height menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -765,14 +743,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows pdf viewer menu correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -791,14 +768,13 @@ describe('<EditorLeftMenu />', function () {
 
     describe('help menu', function () {
       it('shows hotkeys modal correctly', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -808,14 +784,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows correct url for documentation', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -828,14 +803,13 @@ describe('<EditorLeftMenu />', function () {
       })
 
       it('shows correct contact us modal', function () {
-        const scope = mockScope({
-          ui: {
-            leftMenuShown: true,
-          },
-        })
+        const scope = mockScope()
 
         cy.mount(
-          <EditorProviders scope={scope}>
+          <EditorProviders
+            scope={scope}
+            layoutContext={{ leftMenuShown: true }}
+          >
             <EditorLeftMenu />
           </EditorProviders>
         )
@@ -848,16 +822,13 @@ describe('<EditorLeftMenu />', function () {
 
   describe('for anonymous users', function () {
     it('render minimal menu', function () {
-      const scope = mockScope({
-        ui: {
-          leftMenuShown: true,
-        },
-      })
+      const scope = mockScope()
+
       window.metaAttributesCache.set('ol-anonymous', true)
       Object.assign(getMeta('ol-ExposedSettings'), { ieeeBrandId: 123 })
 
       cy.mount(
-        <EditorProviders scope={scope}>
+        <EditorProviders scope={scope} layoutContext={{ leftMenuShown: true }}>
           <EditorLeftMenu />
         </EditorProviders>
       )

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

@@ -47,10 +47,5 @@ export const mockScope = (scope?: Scope) => ({
     },
   },
   hasLintingError: false,
-  ui: {
-    view: 'editor',
-    pdfLayout: 'sideBySide',
-    leftMenuShown: false,
-  },
   ...scope,
 })

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

@@ -17,8 +17,4 @@ export const mockScope = () => ({
     }),
   },
   hasLintingError: false,
-  ui: {
-    view: 'editor',
-    pdfLayout: 'sideBySide',
-  },
 })

+ 11 - 6
services/web/test/frontend/features/editor-navigation-toolbar/components/layout-dropdown-button.test.jsx

@@ -9,9 +9,11 @@ import * as eventTracking from '@/infrastructure/event-tracking'
 describe('<LayoutDropdownButton />', function () {
   let openStub
   let sendMBSpy
-  const defaultUi = {
+
+  const defaultLayout = {
     pdfLayout: 'flat',
     view: 'pdf',
+    chatIsOpen: false,
   }
 
   beforeEach(function () {
@@ -27,7 +29,9 @@ describe('<LayoutDropdownButton />', function () {
 
   it('should mark current layout option as selected', async function () {
     // Selected is aria-label, visually we show a checkmark
-    renderWithEditorContext(<LayoutDropdownButton />, { ui: defaultUi })
+    renderWithEditorContext(<LayoutDropdownButton />, {
+      layoutContext: defaultLayout,
+    })
 
     screen.getByRole('button', { name: 'Layout' }).click()
 
@@ -69,7 +73,7 @@ describe('<LayoutDropdownButton />', function () {
   it('should not select any option in history view', async function () {
     // Selected is aria-label, visually we show a checkmark
     renderWithEditorContext(<LayoutDropdownButton />, {
-      ui: { ...defaultUi, view: 'history' },
+      layoutContext: { ...defaultLayout, view: 'history' },
     })
 
     screen.getByRole('button', { name: 'Layout' }).click()
@@ -112,9 +116,10 @@ describe('<LayoutDropdownButton />', function () {
   it('should treat file and editor views the same way', async function () {
     // Selected is aria-label, visually we show a checkmark
     renderWithEditorContext(<LayoutDropdownButton />, {
-      ui: {
+      layoutContext: {
         pdfLayout: 'flat',
         view: 'file',
+        chatIsOpen: false,
       },
     })
 
@@ -161,7 +166,7 @@ describe('<LayoutDropdownButton />', function () {
       window.BroadcastChannel = originalBroadcastChannel || true // ensure that window.BroadcastChannel is truthy
 
       renderWithEditorContext(<LayoutDropdownButton />, {
-        ui: { ...defaultUi, view: 'editor' },
+        layoutContext: { ...defaultLayout, view: 'editor' },
       })
 
       screen.getByRole('button', { name: 'Layout' }).click()
@@ -192,7 +197,7 @@ describe('<LayoutDropdownButton />', function () {
     beforeEach(async function () {
       window.metaAttributesCache.set('ol-detachRole', 'detacher')
       renderWithEditorContext(<LayoutDropdownButton />, {
-        ui: { ...defaultUi, view: 'editor' },
+        layoutContext: { ...defaultLayout, view: 'editor' },
       })
 
       screen.getByRole('button', { name: 'Layout' }).click()

+ 2 - 0
services/web/test/frontend/features/full-project-search/components/full-project-search.spec.tsx

@@ -93,6 +93,8 @@ const createInitialValue = () =>
     pdfPreviewOpen: false,
     projectSearchIsOpen: true,
     setProjectSearchIsOpen: cy.stub(),
+    openFile: null,
+    setOpenFile: cy.stub(),
   }) satisfies LayoutContextValue
 
 const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {

+ 82 - 78
services/web/test/frontend/features/history/components/change-list.spec.tsx

@@ -14,11 +14,10 @@ import { withTestContainerErrorBoundary } from '../../../helpers/error-boundary'
 
 const TestContainerWithoutErrorBoundary: FC<{
   component: React.ReactNode
-  scope: Record<string, unknown>
   props: Record<string, unknown>
-}> = ({ component, scope, props }) => {
+}> = ({ component, props }) => {
   return (
-    <EditorProviders scope={scope} {...props}>
+    <EditorProviders {...props}>
       <HistoryProvider>
         <div style={{ display: 'flex', justifyContent: 'center' }}>
           <div className="history-react">{component}</div>
@@ -34,17 +33,12 @@ const TestContainer = withTestContainerErrorBoundary(
 
 const mountWithEditorProviders = (
   component: React.ReactNode,
-  scope: Record<string, unknown> = {},
   props: Record<string, unknown> = {}
 ) => {
-  cy.mount(<TestContainer component={component} scope={scope} props={props} />)
+  cy.mount(<TestContainer component={component} props={props} />)
 }
 
 describe('change list (Bootstrap 5)', function () {
-  const scope = {
-    ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
-  }
-
   const waitForData = () => {
     cy.wait('@updates')
     cy.wait('@labels')
@@ -101,7 +95,8 @@ describe('change list (Bootstrap 5)', function () {
 
   describe('tags', function () {
     it('renders tags', function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
+      mountWithEditorProviders(<ChangeList />, {
+        layoutContext: { view: 'history' },
         user: {
           id: USER_ID,
           email: USER_EMAIL,
@@ -173,13 +168,18 @@ describe('change list (Bootstrap 5)', function () {
     })
 
     it('deletes tag', function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: true,
-        },
-      })
+      mountWithEditorProviders(
+        <ChangeList />,
+
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: true,
+          },
+        }
+      )
       waitForData()
 
       cy.findByLabelText(/all history/i).click({ force: true })
@@ -235,7 +235,8 @@ describe('change list (Bootstrap 5)', function () {
     })
 
     it('verifies that selecting the same list item will not trigger a new diff', function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
+      mountWithEditorProviders(<ChangeList />, {
+        layoutContext: { view: 'history' },
         user: {
           id: USER_ID,
           email: USER_EMAIL,
@@ -257,13 +258,18 @@ describe('change list (Bootstrap 5)', function () {
 
   describe('all history', function () {
     beforeEach(function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: true,
-        },
-      })
+      mountWithEditorProviders(
+        <ChangeList />,
+
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: true,
+          },
+        }
+      )
       waitForData()
     })
 
@@ -318,13 +324,18 @@ describe('change list (Bootstrap 5)', function () {
 
   describe('labels only', function () {
     beforeEach(function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: true,
-        },
-      })
+      mountWithEditorProviders(
+        <ChangeList />,
+
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: true,
+          },
+        }
+      )
       waitForData()
       cy.findByLabelText(/labels/i).click({ force: true })
     })
@@ -399,13 +410,18 @@ describe('change list (Bootstrap 5)', function () {
 
   describe('compare mode', function () {
     beforeEach(function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: true,
-        },
-      })
+      mountWithEditorProviders(
+        <ChangeList />,
+
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: true,
+          },
+        }
+      )
       waitForData()
     })
 
@@ -435,13 +451,18 @@ describe('change list (Bootstrap 5)', function () {
 
   describe('dropdown', function () {
     beforeEach(function () {
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: true,
-        },
-      })
+      mountWithEditorProviders(
+        <ChangeList />,
+
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: true,
+          },
+        }
+      )
       waitForData()
     })
 
@@ -610,21 +631,18 @@ describe('change list (Bootstrap 5)', function () {
     })
 
     it('shows non-owner paywall', function () {
-      const scope = {
-        ui: {
-          view: 'history',
-          pdfLayout: 'sideBySide',
-          chatOpen: true,
-        },
-      }
+      mountWithEditorProviders(
+        <ChangeList />,
 
-      mountWithEditorProviders(<ChangeList />, scope, {
-        user: {
-          id: USER_ID,
-          email: USER_EMAIL,
-          isAdmin: false,
-        },
-      })
+        {
+          layoutContext: { view: 'history' },
+          user: {
+            id: USER_ID,
+            email: USER_EMAIL,
+            isAdmin: false,
+          },
+        }
+      )
 
       waitForData()
 
@@ -634,15 +652,8 @@ describe('change list (Bootstrap 5)', function () {
     })
 
     it('shows owner paywall', function () {
-      const scope = {
-        ui: {
-          view: 'history',
-          pdfLayout: 'sideBySide',
-          chatOpen: true,
-        },
-      }
-
-      mountWithEditorProviders(<ChangeList />, scope, {
+      mountWithEditorProviders(<ChangeList />, {
+        layoutContext: { view: 'history' },
         user: {
           id: USER_ID,
           email: USER_EMAIL,
@@ -662,15 +673,8 @@ describe('change list (Bootstrap 5)', function () {
     })
 
     it('shows all labels in free tier', function () {
-      const scope = {
-        ui: {
-          view: 'history',
-          pdfLayout: 'sideBySide',
-          chatOpen: true,
-        },
-      }
-
-      mountWithEditorProviders(<ChangeList />, scope, {
+      mountWithEditorProviders(<ChangeList />, {
+        layoutContext: { view: 'history' },
         user: {
           id: USER_ID,
           email: USER_EMAIL,

+ 6 - 9
services/web/test/frontend/features/history/components/toolbar.spec.tsx

@@ -5,14 +5,15 @@ import { Diff } from '../../../../../frontend/js/features/history/services/types
 import { EditorProviders } from '../../../helpers/editor-providers'
 import { FC } from 'react'
 import { withTestContainerErrorBoundary } from '../../../helpers/error-boundary'
+import { LayoutContextValue } from '@/shared/context/layout-context'
 
 const TestContainerWithoutErrorBoundary: FC<{
-  scope: Record<string, unknown>
+  layoutContext: LayoutContextValue
   diff: Diff
   selection: HistoryContextValue['selection']
-}> = ({ scope, diff, selection }) => {
+}> = ({ diff, selection, layoutContext }) => {
   return (
-    <EditorProviders scope={scope}>
+    <EditorProviders layoutContext={layoutContext}>
       <HistoryProvider>
         <div className="history-react">
           <Toolbar diff={diff} selection={selection} />
@@ -27,10 +28,6 @@ const TestContainer = withTestContainerErrorBoundary(
 )
 
 describe('history toolbar', function () {
-  const editorProvidersScope = {
-    ui: { view: 'history', pdfLayout: 'sideBySide', chatOpen: true },
-  }
-
   const diff: Diff = {
     binary: false,
     docDiff: {
@@ -81,7 +78,7 @@ describe('history toolbar', function () {
 
     cy.mount(
       <TestContainer
-        scope={editorProvidersScope}
+        layoutContext={{ view: 'history' }}
         diff={diff}
         selection={selection}
       />
@@ -129,7 +126,7 @@ describe('history toolbar', function () {
 
     cy.mount(
       <TestContainer
-        scope={editorProvidersScope}
+        layoutContext={{ view: 'history' }}
         diff={diff}
         selection={selection}
       />

+ 15 - 3
services/web/test/frontend/features/layout/components/switch-to-editor-button.spec.tsx

@@ -4,7 +4,9 @@ import SwitchToEditorButton from '@/features/pdf-preview/components/switch-to-ed
 describe('<SwitchToEditorButton />', function () {
   it('shows button in full screen pdf layout', function () {
     cy.mount(
-      <EditorProviders ui={{ view: 'pdf', pdfLayout: 'flat', chatOpen: false }}>
+      <EditorProviders
+        layoutContext={{ view: 'pdf', pdfLayout: 'flat', chatIsOpen: false }}
+      >
         <SwitchToEditorButton />
       </EditorProviders>
     )
@@ -15,7 +17,11 @@ describe('<SwitchToEditorButton />', function () {
   it('does not show button in split screen layout', function () {
     cy.mount(
       <EditorProviders
-        ui={{ view: 'pdf', pdfLayout: 'sideBySide', chatOpen: false }}
+        layoutContext={{
+          view: 'pdf',
+          pdfLayout: 'sideBySide',
+          chatIsOpen: false,
+        }}
       >
         <SwitchToEditorButton />
       </EditorProviders>
@@ -28,7 +34,13 @@ describe('<SwitchToEditorButton />', function () {
     window.metaAttributesCache.set('ol-detachRole', 'detacher')
 
     cy.mount(
-      <EditorProviders ui={{ view: 'pdf', pdfLayout: 'flat', chatOpen: false }}>
+      <EditorProviders
+        layoutContext={{
+          view: 'pdf',
+          pdfLayout: 'flat',
+          chatIsOpen: false,
+        }}
+      >
         <SwitchToEditorButton />
       </EditorProviders>
     )

+ 7 - 3
services/web/test/frontend/features/layout/components/switch-to-pdf-button.spec.tsx

@@ -5,7 +5,7 @@ describe('<SwitchToPDFButton />', function () {
   it('shows button in full screen editor layout', function () {
     cy.mount(
       <EditorProviders
-        ui={{ view: 'editor', pdfLayout: 'flat', chatOpen: false }}
+        layoutContext={{ view: 'editor', pdfLayout: 'flat', chatIsOpen: false }}
       >
         <SwitchToPDFButton />
       </EditorProviders>
@@ -17,7 +17,11 @@ describe('<SwitchToPDFButton />', function () {
   it('does not show button in split screen layout', function () {
     cy.mount(
       <EditorProviders
-        ui={{ view: 'editor', pdfLayout: 'sideBySide', chatOpen: false }}
+        layoutContext={{
+          view: 'editor',
+          pdfLayout: 'sideBySide',
+          chatIsOpen: false,
+        }}
       >
         <SwitchToPDFButton />
       </EditorProviders>
@@ -31,7 +35,7 @@ describe('<SwitchToPDFButton />', function () {
 
     cy.mount(
       <EditorProviders
-        ui={{ view: 'editor', pdfLayout: 'flat', chatOpen: false }}
+        layoutContext={{ view: 'editor', pdfLayout: 'flat', chatIsOpen: false }}
       >
         <SwitchToPDFButton />
       </EditorProviders>

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

@@ -81,9 +81,6 @@ export const mockScope = (
       write: true,
       ...permissions,
     },
-    ui: {
-      reviewPanelOpen: false,
-    },
     toggleReviewPanel: cy.stub(),
     toggleTrackChangesForEveryone: cy.stub(),
     refreshResolvedCommentsDropdown: cy.stub(() => sleep(1000)),

+ 130 - 4
services/web/test/frontend/helpers/editor-providers.jsx

@@ -3,7 +3,7 @@
 import { merge } from 'lodash'
 import { SocketIOMock } from '@/ide/connection/SocketIoShim'
 import { IdeContext } from '@/shared/context/ide-context'
-import React, { useEffect, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
 import {
   createReactScopeValueStore,
   IdeReactContext,
@@ -12,6 +12,9 @@ import { IdeEventEmitter } from '@/features/ide-react/create-ide-event-emitter'
 import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
 import { ConnectionContext } from '@/features/ide-react/context/connection-context'
 import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
+import useEventListener from '@/shared/hooks/use-event-listener'
+import useDetachLayout from '@/shared/hooks/use-detach-layout'
+import { LayoutContext } from '@/shared/context/layout-context'
 
 // these constants can be imported in tests instead of
 // using magic strings
@@ -35,6 +38,22 @@ const defaultUserSettings = {
   mathPreview: true,
 }
 
+/**
+ * @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue
+ * @type Partial<LayoutContextValue>
+ */
+const layoutContextDefault = {
+  view: 'editor',
+  openFile: null,
+  chatIsOpen: true, // false in the application, true in tests
+  reviewPanelOpen: false,
+  miniReviewPanelVisible: false,
+  leftMenuShown: false,
+  projectSearchIsOpen: false,
+  pdfLayout: 'sideBySide',
+  loadingStyleSheet: false,
+}
+
 export function EditorProviders({
   user = { id: USER_ID, email: USER_EMAIL },
   projectId = PROJECT_ID,
@@ -68,7 +87,8 @@ export function EditorProviders({
       fileRefs: [],
     },
   ],
-  ui = { view: 'editor', pdfLayout: 'sideBySide', chatOpen: true },
+  /** @type {Partial<LayoutContext>} */
+  layoutContext = layoutContextDefault,
   userSettings = {},
   providers = {},
 }) {
@@ -110,7 +130,6 @@ export function EditorProviders({
         imageName,
         compiler,
       },
-      ui,
       permissionsLevel,
     },
     defaultScope
@@ -125,6 +144,7 @@ export function EditorProviders({
       providers={{
         ConnectionProvider: makeConnectionProvider(socket),
         IdeReactProvider: makeIdeReactProvider(scope, socket),
+        LayoutProvider: makeLayoutProvider(layoutContext),
         ...providers,
       }}
     >
@@ -183,7 +203,6 @@ const makeIdeReactProvider = (scope, socket) => {
         scopeStore.set(key, value)
       }
       scopeStore.set('editor.sharejs_doc', scope.editor.sharejs_doc)
-      scopeStore.set('ui.chatOpen', scope.ui.chatOpen)
       const scopeEventEmitter = new ReactScopeEventEmitter(
         new IdeEventEmitter()
       )
@@ -215,3 +234,110 @@ const makeIdeReactProvider = (scope, socket) => {
   }
   return IdeReactProvider
 }
+
+const makeLayoutProvider = layoutContextOverrides => {
+  const layout = {
+    ...layoutContextDefault,
+    ...layoutContextOverrides,
+  }
+  const LayoutProvider = ({ children }) => {
+    const [view, setView] = useState(layout.view)
+    const [openFile, setOpenFile] = useState(layout.openFile)
+    const [chatIsOpen, setChatIsOpen] = useState(layout.chatIsOpen)
+    const [reviewPanelOpen, setReviewPanelOpen] = useState(
+      layout.reviewPanelOpen
+    )
+    const [miniReviewPanelVisible, setMiniReviewPanelVisible] = useState(
+      layout.miniReviewPanelVisible
+    )
+    const [leftMenuShown, setLeftMenuShown] = useState(layout.leftMenuShown)
+    const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(
+      layout.projectSearchIsOpen
+    )
+    const [pdfLayout, setPdfLayout] = useState(layout.pdfLayout)
+    const [loadingStyleSheet, setLoadingStyleSheet] = useState(
+      layout.loadingStyleSheet
+    )
+
+    useEventListener(
+      'ui.toggle-review-panel',
+      useCallback(() => {
+        setReviewPanelOpen(open => !open)
+      }, [setReviewPanelOpen])
+    )
+    const changeLayout = useCallback(
+      (newLayout, newView = 'editor') => {
+        setPdfLayout(newLayout)
+        setView(newLayout === 'sideBySide' ? 'editor' : newView)
+      },
+      [setPdfLayout, setView]
+    )
+    const {
+      reattach,
+      detach,
+      isLinked: detachIsLinked,
+      role: detachRole,
+    } = useDetachLayout()
+    const pdfPreviewOpen =
+      pdfLayout === 'sideBySide' || view === 'pdf' || detachRole === 'detacher'
+    const value = useMemo(
+      () => ({
+        reattach,
+        detach,
+        detachIsLinked,
+        detachRole,
+        changeLayout,
+        chatIsOpen,
+        leftMenuShown,
+        openFile,
+        pdfLayout,
+        pdfPreviewOpen,
+        projectSearchIsOpen,
+        setProjectSearchIsOpen,
+        reviewPanelOpen,
+        miniReviewPanelVisible,
+        loadingStyleSheet,
+        setChatIsOpen,
+        setLeftMenuShown,
+        setOpenFile,
+        setPdfLayout,
+        setReviewPanelOpen,
+        setMiniReviewPanelVisible,
+        setLoadingStyleSheet,
+        setView,
+        view,
+      }),
+      [
+        reattach,
+        detach,
+        detachIsLinked,
+        detachRole,
+        changeLayout,
+        chatIsOpen,
+        leftMenuShown,
+        openFile,
+        pdfLayout,
+        pdfPreviewOpen,
+        projectSearchIsOpen,
+        setProjectSearchIsOpen,
+        reviewPanelOpen,
+        miniReviewPanelVisible,
+        loadingStyleSheet,
+        setChatIsOpen,
+        setLeftMenuShown,
+        setOpenFile,
+        setPdfLayout,
+        setReviewPanelOpen,
+        setMiniReviewPanelVisible,
+        setLoadingStyleSheet,
+        setView,
+        view,
+      ]
+    )
+
+    return (
+      <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>
+    )
+  }
+  return LayoutProvider
+}