Ver Fonte

[ide-react] Tidy IDE page layout components (#15953)

* Defer script loading
* Refactor loading
* Wait for project:joined
* Only mount IdePage once everything has connected
* Add useConnectionState hook and comments
* Remove placeholder components
* Move props into EditorAndSidebar
* Move props into MainLayout
* Tidy editor and sidebar components
* Lazy-load the symbol palette pane and separate the loading pane

GitOrigin-RevId: 4b721a06d6aba0ae0ec91768e6a6e29cf15e2083
Alf Eaton há 2 anos atrás
pai
commit
23593f8650
24 ficheiros alterados com 204 adições e 573 exclusões
  1. 2 8
      services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx
  2. 67 74
      services/web/frontend/js/features/ide-react/components/editor-and-sidebar.tsx
  3. 23 29
      services/web/frontend/js/features/ide-react/components/editor-sidebar.tsx
  4. 23 46
      services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx
  5. 15 0
      services/web/frontend/js/features/ide-react/components/editor/loading-pane.tsx
  6. 20 0
      services/web/frontend/js/features/ide-react/components/editor/symbol-palette-pane.tsx
  7. 9 52
      services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx
  8. 0 49
      services/web/frontend/js/features/ide-react/components/layout/layout-with-placeholders.tsx
  9. 12 20
      services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx
  10. 0 7
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-chat.tsx
  11. 0 90
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-and-pdf.tsx
  12. 0 38
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-main-content.tsx
  13. 0 26
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-sidebar.tsx
  14. 0 5
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-file.tsx
  15. 0 42
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx
  16. 0 38
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-history.tsx
  17. 0 5
      services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-pdf.tsx
  18. 1 5
      services/web/frontend/js/features/ide-react/components/layout/two-column-main-content.tsx
  19. 6 8
      services/web/frontend/js/features/ide-react/hooks/use-collapsible-panel.ts
  20. 19 0
      services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts
  21. 2 2
      services/web/frontend/js/features/ide-react/hooks/use-fixed-size-column.ts
  22. 0 2
      services/web/frontend/js/features/ide-react/hooks/use-open-file.ts
  23. 0 27
      services/web/frontend/stories/ide-page/layout.stories.tsx
  24. 5 0
      services/web/frontend/stylesheets/app/editor/ide-react.less

+ 2 - 8
services/web/frontend/js/features/ide-react/components/editor-and-pdf.tsx

@@ -14,14 +14,10 @@ import { DefaultSynctexControl } from '@/features/pdf-preview/components/detach-
 import classnames from 'classnames'
 
 export type EditorProps = {
-  shouldPersistLayout?: boolean
   editorContent: ReactNode
 }
 
-export default function EditorAndPdf({
-  shouldPersistLayout = false,
-  editorContent,
-}: EditorProps) {
+export default function EditorAndPdf({ editorContent }: EditorProps) {
   const { t } = useTranslation()
   const { view, pdfLayout, changeLayout, detachRole, reattach } =
     useLayoutContext()
@@ -49,9 +45,7 @@ export default function EditorAndPdf({
 
   return (
     <PanelGroup
-      autoSaveId={
-        shouldPersistLayout ? 'ide-react-editor-and-pdf-layout' : undefined
-      }
+      autoSaveId="ide-react-editor-and-pdf-layout"
       direction="horizontal"
       className={classnames('ide-react-editor-and-pdf', {
         hide: historyIsOpen,

+ 67 - 74
services/web/frontend/js/features/ide-react/components/editor-and-sidebar.tsx

@@ -26,12 +26,6 @@ import { BinaryFile } from '@/features/file-view/types/binary-file'
 import useScopeValue from '@/shared/hooks/use-scope-value'
 import { useSelectFileTreeEntity } from '@/features/ide-react/hooks/use-select-file-tree-entity'
 
-type EditorAndSidebarProps = {
-  shouldPersistLayout: boolean
-  leftColumnDefaultSize: number
-  setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
-}
-
 function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
   return {
     _id: fileRef._id,
@@ -58,11 +52,8 @@ function fileViewFile(fileRef: FileRef) {
   }
 }
 
-export function EditorAndSidebar({
-  shouldPersistLayout,
-  leftColumnDefaultSize,
-  setLeftColumnDefaultSize,
-}: EditorAndSidebarProps) {
+export function EditorAndSidebar() {
+  const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
   const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
   const { rootDocId } = useProjectContext()
   const { eventEmitter } = useIdeReactContext()
@@ -75,9 +66,6 @@ export function EditorAndSidebar({
   const { projectJoined } = useIdeReactContext()
   const { selectEntity } = useSelectFileTreeEntity()
   const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
-
-  const historyIsOpen = view === 'history'
-
   const [openEntity, setOpenEntity] = useState<
     FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
   >(null)
@@ -157,88 +145,93 @@ export function EditorAndSidebar({
     }
   }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
 
-  // Keep the editor file tree around so that it is available and up to date
-  // when restoring a file
-  const leftColumnContent = (
-    <>
-      <EditorSidebar
-        shouldShow={!historyIsOpen}
-        shouldPersistLayout={shouldPersistLayout}
-        onFileTreeInit={handleFileTreeInit}
-        onFileTreeSelect={handleFileTreeSelect}
-        onFileTreeDelete={handleFileTreeDelete}
+  // Keep the editor file tree around so that it is available and up to date when restoring a file
+  const editorSidebar = (
+    <EditorSidebar
+      shouldShow={view !== 'history'}
+      onFileTreeInit={handleFileTreeInit}
+      onFileTreeSelect={handleFileTreeSelect}
+      onFileTreeDelete={handleFileTreeDelete}
+    />
+  )
+
+  if (view === 'history') {
+    return (
+      <TwoColumnMainContent
+        leftColumnId="editor-left-column"
+        leftColumnContent={
+          <>
+            {editorSidebar}
+            <HistorySidebar />
+          </>
+        }
+        leftColumnDefaultSize={leftColumnDefaultSize}
+        setLeftColumnDefaultSize={setLeftColumnDefaultSize}
+        rightColumnContent={
+          <HistoryProvider>
+            <History />
+          </HistoryProvider>
+        }
+        leftColumnIsOpen={leftColumnIsOpen}
+        setLeftColumnIsOpen={setLeftColumnIsOpen}
       />
-      {historyIsOpen ? <HistorySidebar /> : null}
-    </>
+    )
+  }
+
+  // Always have the editor mounted when not in history view, and hide and
+  // show it as necessary
+  const editorPane = (
+    <EditorPane
+      show={openEntity?.type === 'doc' && selectedEntityCount === 1}
+    />
   )
 
   let rightColumnContent
 
-  if (historyIsOpen) {
+  if (openDocId === undefined) {
+    rightColumnContent = <NoOpenDocPane />
+  } else if (selectedEntityCount === 0) {
     rightColumnContent = (
-      <HistoryProvider>
-        <History />
-      </HistoryProvider>
+      <>
+        {editorPane}
+        <NoSelectionPane />
+      </>
     )
-  } else {
-    let editorContent = null
-
-    // Always have the editor mounted when not in history view, and hide and
-    // show it as necessary
-    const editorPane = (
-      <EditorPane
-        shouldPersistLayout={shouldPersistLayout}
-        show={openEntity?.type === 'doc' && selectedEntityCount === 1}
+  } else if (selectedEntityCount > 1) {
+    rightColumnContent = (
+      <EditorAndPdf
+        editorContent={
+          <>
+            {editorPane}
+            <MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
+          </>
+        }
       />
     )
-    if (openDocId === undefined) {
-      rightColumnContent = <NoOpenDocPane />
-    } else if (selectedEntityCount === 0) {
-      rightColumnContent = (
-        <>
-          {editorPane}
-          <NoSelectionPane />
-        </>
-      )
-    } else if (selectedEntityCount > 1) {
-      editorContent = (
-        <>
-          {editorPane}
-          <MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
-        </>
-      )
-    } else if (openEntity) {
-      editorContent =
-        openEntity.type === 'doc' ? (
-          editorPane
-        ) : (
+  } else if (openEntity) {
+    rightColumnContent = (
+      <EditorAndPdf
+        editorContent={
           <>
             {editorPane}
-            <FileView file={fileViewFile(openEntity.entity)} />
+            {openEntity.type !== 'doc' && (
+              <FileView file={fileViewFile(openEntity.entity)} />
+            )}
           </>
-        )
-    }
-
-    if (editorContent) {
-      rightColumnContent = (
-        <EditorAndPdf
-          editorContent={editorContent}
-          shouldPersistLayout={shouldPersistLayout}
-        />
-      )
-    }
+        }
+      />
+    )
   }
 
   return (
     <TwoColumnMainContent
       leftColumnId="editor-left-column"
-      leftColumnContent={leftColumnContent}
+      leftColumnContent={editorSidebar}
       leftColumnDefaultSize={leftColumnDefaultSize}
       setLeftColumnDefaultSize={setLeftColumnDefaultSize}
       rightColumnContent={rightColumnContent}
       leftColumnIsOpen={leftColumnIsOpen}
       setLeftColumnIsOpen={setLeftColumnIsOpen}
-      shouldPersistLayout={shouldPersistLayout}
     />
   )
 }

+ 23 - 29
services/web/frontend/js/features/ide-react/components/editor-sidebar.tsx

@@ -9,46 +9,40 @@ import {
 import classNames from 'classnames'
 
 type EditorSidebarProps = {
-  shouldShow: boolean
-  shouldPersistLayout: boolean
+  shouldShow?: boolean
   onFileTreeInit: () => void
   onFileTreeSelect: FileTreeSelectHandler
   onFileTreeDelete: FileTreeDeleteHandler
 }
 
 export default function EditorSidebar({
-  shouldShow,
-  shouldPersistLayout,
+  shouldShow = false,
   onFileTreeInit,
   onFileTreeSelect,
   onFileTreeDelete,
 }: EditorSidebarProps) {
   return (
-    <>
-      <aside
-        className={classNames('ide-react-editor-sidebar', {
-          hidden: !shouldShow,
-        })}
+    <aside
+      className={classNames('ide-react-editor-sidebar', {
+        hidden: !shouldShow,
+      })}
+    >
+      <PanelGroup
+        autoSaveId="ide-react-editor-sidebar-layout"
+        direction="vertical"
       >
-        <PanelGroup
-          autoSaveId={
-            shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined
-          }
-          direction="vertical"
-        >
-          <Panel defaultSize={75} className="ide-react-file-tree-panel">
-            <FileTree
-              onInit={onFileTreeInit}
-              onSelect={onFileTreeSelect}
-              onDelete={onFileTreeDelete}
-            />
-          </Panel>
-          <VerticalResizeHandle />
-          <Panel defaultSize={25}>
-            <div className="outline-container" />
-          </Panel>
-        </PanelGroup>
-      </aside>
-    </>
+        <Panel defaultSize={75} className="ide-react-file-tree-panel">
+          <FileTree
+            onInit={onFileTreeInit}
+            onSelect={onFileTreeSelect}
+            onDelete={onFileTreeDelete}
+          />
+        </Panel>
+        <VerticalResizeHandle />
+        <Panel defaultSize={25}>
+          <div className="outline-container" />
+        </Panel>
+      </PanelGroup>
+    </aside>
   )
 }

+ 23 - 46
services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx

@@ -1,58 +1,39 @@
 import { Panel, PanelGroup } from 'react-resizable-panels'
-import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
-import React, { ElementType } from 'react'
+import React, { FC, lazy, Suspense } from 'react'
 import useScopeValue from '@/shared/hooks/use-scope-value'
 import SourceEditor from '@/features/source-editor/components/source-editor'
 import { EditorScopeValue } from '@/features/ide-react/context/editor-manager-context'
-import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
 import classNames from 'classnames'
-import { useTranslation } from 'react-i18next'
-
-const symbolPaletteComponents = importOverleafModules(
-  'sourceEditorSymbolPalette'
-) as { import: { default: ElementType }; path: string }[]
+import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane'
+import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
+import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
 
-export type EditorPaneProps = {
-  shouldPersistLayout?: boolean
-  show: boolean
-}
+const SymbolPalettePane = lazy(
+  () => import('@/features/ide-react/components/editor/symbol-palette-pane')
+)
 
-export function EditorPane({ shouldPersistLayout, show }: EditorPaneProps) {
-  const { t } = useTranslation()
+export const EditorPane: FC<{ show: boolean }> = ({ show }) => {
   const [editor] = useScopeValue<EditorScopeValue>('editor')
 
+  const isLoading = Boolean(
+    (!editor.sharejs_doc || editor.opening) &&
+      !editor.error_state &&
+      editor.open_doc_id
+  )
+
   return (
     <PanelGroup
-      autoSaveId={
-        shouldPersistLayout
-          ? 'ide-react-editor-and-symbol-palette-layout'
-          : undefined
-      }
+      autoSaveId="ide-react-editor-and-symbol-palette-layout"
       direction="vertical"
       units="pixels"
       className={classNames({ hidden: !show })}
     >
-      <Panel
-        id="sourceEditor"
-        order={1}
-        style={{
-          display: 'flex',
-          flexDirection: 'column',
-        }}
-      >
+      <Panel id="sourceEditor" order={1} className="ide-react-editor-panel">
         <SourceEditor />
-        {(!editor.sharejs_doc || editor.opening) &&
-        !editor.error_state &&
-        !!editor.open_doc_id ? (
-          <div className="loading-panel">
-            <span>
-              <i className="fa fa-spin fa-refresh" />
-              &nbsp;&nbsp;{t('loading')}…
-            </span>
-          </div>
-        ) : null}
+        {isLoading && <LoadingPane />}
       </Panel>
-      {editor.showSymbolPalette ? (
+
+      {editor.showSymbolPalette && (
         <>
           <VerticalResizeHandle id="editor-symbol-palette" />
           <Panel
@@ -62,16 +43,12 @@ export function EditorPane({ shouldPersistLayout, show }: EditorPaneProps) {
             minSize={250}
             maxSize={336}
           >
-            <div className="ide-react-symbol-palette">
-              {symbolPaletteComponents.map(
-                ({ import: { default: Component }, path }) => (
-                  <Component key={path} />
-                )
-              )}
-            </div>
+            <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
+              <SymbolPalettePane />
+            </Suspense>
           </Panel>
         </>
-      ) : null}
+      )}
     </PanelGroup>
   )
 }

+ 15 - 0
services/web/frontend/js/features/ide-react/components/editor/loading-pane.tsx

@@ -0,0 +1,15 @@
+import { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+
+export const LoadingPane: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className="loading-panel">
+      <span>
+        <i className="fa fa-spin fa-refresh" />
+        &nbsp;&nbsp;{t('loading')}…
+      </span>
+    </div>
+  )
+}

+ 20 - 0
services/web/frontend/js/features/ide-react/components/editor/symbol-palette-pane.tsx

@@ -0,0 +1,20 @@
+import React, { ElementType, FC } from 'react'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+
+const symbolPaletteComponents = importOverleafModules(
+  'sourceEditorSymbolPalette'
+) as { import: { default: ElementType }; path: string }[]
+
+const SymbolPalettePane: FC = () => {
+  return (
+    <div className="ide-react-symbol-palette">
+      {symbolPaletteComponents.map(
+        ({ import: { default: Component }, path }) => (
+          <Component key={path} />
+        )
+      )}
+    </div>
+  )
+}
+
+export default SymbolPalettePane

+ 9 - 52
services/web/frontend/js/features/ide-react/components/layout/ide-page.tsx

@@ -1,71 +1,28 @@
-import { useConnectionContext } from '@/features/ide-react/context/connection-context'
-import { useEffect, useState } from 'react'
 import { Alerts } from '@/features/ide-react/components/alerts/alerts'
-import { useLayoutContext } from '@/shared/context/layout-context'
 import MainLayout from '@/features/ide-react/components/layout/main-layout'
-import { EditorAndSidebar } from '@/features/ide-react/components/editor-and-sidebar'
 import EditorLeftMenu from '@/features/editor-left-menu/components/editor-left-menu'
-import EditorNavigationToolbar from '@/features/ide-react/components/editor-navigation-toolbar'
-import ChatPane from '@/features/chat/components/chat-pane'
 import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
 import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
-import { useModalsContext } from '@/features/ide-react/context/modals-context'
 import { useOpenFile } from '@/features/ide-react/hooks/use-open-file'
 import { useEditingSessionHeartbeat } from '@/features/ide-react/hooks/use-editing-session-heartbeat'
 import { useRegisterUserActivity } from '@/features/ide-react/hooks/use-register-user-activity'
 import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-error'
+import { useConnectionState } from '@/features/ide-react/hooks/use-connection-state'
 
-// This is filled with placeholder content while the real content is migrated
-// away from Angular
 export default function IdePage() {
-  useLayoutEventTracking()
-  useSocketListeners()
-  useEditingSessionHeartbeat()
-  useRegisterUserActivity()
-  useHasLintingError()
-
-  // This returns a function to open a binary file but for now we just use the
-  // fact that it also patches in ide.binaryFilesManager. Once Angular is gone,
-  // we can remove this hook from here and use it in the history file restore
-  // component instead.
-  useOpenFile()
-
-  const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
-  const { connectionState } = useConnectionContext()
-  const { showLockEditorMessageModal } = useModalsContext()
-
-  // Show modal when editor is forcefully disconnected
-  useEffect(() => {
-    if (connectionState.forceDisconnected) {
-      showLockEditorMessageModal(connectionState.forcedDisconnectDelay)
-    }
-  }, [
-    connectionState.forceDisconnected,
-    connectionState.forcedDisconnectDelay,
-    showLockEditorMessageModal,
-  ])
-
-  const { chatIsOpen } = useLayoutContext()
-
-  const mainContent = (
-    <EditorAndSidebar
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-      shouldPersistLayout
-    />
-  )
+  useLayoutEventTracking() // send event when the layout changes
+  useSocketListeners() // listen for project-related websocket messages
+  useEditingSessionHeartbeat() // send a batched event when user is active
+  useRegisterUserActivity() // record activity and ensure connection when user is active
+  useHasLintingError() // pass editor:lint hasLintingError to the compiler
+  useOpenFile() // create ide.binaryFilesManager (TODO: move to the history file restore component)
+  useConnectionState() // show modal when editor is forcefully disconnected
 
   return (
     <>
       <Alerts />
       <EditorLeftMenu />
-      <MainLayout
-        headerContent={<EditorNavigationToolbar />}
-        chatContent={<ChatPane />}
-        mainContent={mainContent}
-        chatIsOpen={chatIsOpen}
-        shouldPersistLayout
-      />
+      <MainLayout />
     </>
   )
 }

+ 0 - 49
services/web/frontend/js/features/ide-react/components/layout/layout-with-placeholders.tsx

@@ -1,49 +0,0 @@
-import { useState } from 'react'
-import PlaceholderHeader from '@/features/ide-react/components/layout/placeholder/placeholder-header'
-import PlaceholderChat from '@/features/ide-react/components/layout/placeholder/placeholder-chat'
-import PlaceholderHistory from '@/features/ide-react/components/layout/placeholder/placeholder-history'
-import PlaceholderEditorMainContent from '@/features/ide-react/components/layout/placeholder/placeholder-editor-main-content'
-import MainLayout from '@/features/ide-react/components/layout/main-layout'
-
-export default function LayoutWithPlaceholders({
-  shouldPersistLayout,
-}: {
-  shouldPersistLayout: boolean
-}) {
-  const [chatIsOpen, setChatIsOpen] = useState(false)
-  const [historyIsOpen, setHistoryIsOpen] = useState(false)
-  const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
-
-  const headerContent = (
-    <PlaceholderHeader
-      chatIsOpen={chatIsOpen}
-      setChatIsOpen={setChatIsOpen}
-      historyIsOpen={historyIsOpen}
-      setHistoryIsOpen={setHistoryIsOpen}
-    />
-  )
-  const chatContent = <PlaceholderChat />
-  const mainContent = historyIsOpen ? (
-    <PlaceholderHistory
-      shouldPersistLayout
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-    />
-  ) : (
-    <PlaceholderEditorMainContent
-      shouldPersistLayout
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-    />
-  )
-
-  return (
-    <MainLayout
-      headerContent={headerContent}
-      chatContent={chatContent}
-      mainContent={mainContent}
-      chatIsOpen={chatIsOpen}
-      shouldPersistLayout={shouldPersistLayout}
-    />
-  )
-}

+ 12 - 20
services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx

@@ -1,31 +1,23 @@
 import { Panel, PanelGroup } from 'react-resizable-panels'
-import { ReactNode, useState } from 'react'
+import { useState } from 'react'
 import { HorizontalResizeHandle } from '../resize/horizontal-resize-handle'
 import useFixedSizeColumn from '@/features/ide-react/hooks/use-fixed-size-column'
 import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
 import classNames from 'classnames'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import EditorNavigationToolbar from '@/features/ide-react/components/editor-navigation-toolbar'
+import ChatPane from '@/features/chat/components/chat-pane'
+import { EditorAndSidebar } from '@/features/ide-react/components/editor-and-sidebar'
 
 const CHAT_DEFAULT_SIZE = 20
 
-type PageProps = {
-  headerContent: ReactNode
-  chatContent: ReactNode
-  mainContent: ReactNode
-  chatIsOpen: boolean
-  shouldPersistLayout: boolean
-}
-
 // The main area below the header is split into two: the main content and chat.
 // The reason for not splitting the left column containing the file tree and
 // outline here is that the history view has its own file tree, so it is more
 // convenient to replace the whole of the main content when in history view.
-export default function MainLayout({
-  headerContent,
-  chatContent,
-  mainContent,
-  chatIsOpen,
-  shouldPersistLayout,
-}: PageProps) {
+export default function MainLayout() {
+  const { chatIsOpen } = useLayoutContext()
+
   const { fixedPanelRef: chatPanelRef, handleLayout } = useFixedSizeColumn(
     CHAT_DEFAULT_SIZE,
     chatIsOpen
@@ -37,10 +29,10 @@ export default function MainLayout({
 
   return (
     <div className="ide-react-main">
-      {headerContent}
+      <EditorNavigationToolbar />
       <div className="ide-react-body">
         <PanelGroup
-          autoSaveId={shouldPersistLayout ? 'ide-react-chat-layout' : undefined}
+          autoSaveId="ide-react-chat-layout"
           direction="horizontal"
           onLayout={handleLayout}
           className={classNames({
@@ -48,7 +40,7 @@ export default function MainLayout({
           })}
         >
           <Panel id="main" order={1}>
-            {mainContent}
+            <EditorAndSidebar />
           </Panel>
           {chatIsOpen ? (
             <>
@@ -61,7 +53,7 @@ export default function MainLayout({
                 minSize={5}
                 collapsible
               >
-                {chatContent}
+                <ChatPane />
               </Panel>
             </>
           ) : null}

+ 0 - 7
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-chat.tsx

@@ -1,7 +0,0 @@
-import React from 'react'
-
-export default function PlaceholderChat() {
-  return (
-    <aside className="chat ide-react-placeholder-chat">Chat placeholder</aside>
-  )
-}

+ 0 - 90
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-and-pdf.tsx

@@ -1,90 +0,0 @@
-import React, { useRef, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import {
-  ImperativePanelHandle,
-  Panel,
-  PanelGroup,
-} from 'react-resizable-panels'
-import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
-import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
-import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
-import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
-
-type PlaceholderEditorAndPdfProps = {
-  shouldPersistLayout?: boolean
-}
-
-export default function PlaceholderEditorAndPdf({
-  shouldPersistLayout = false,
-}: PlaceholderEditorAndPdfProps) {
-  const { t } = useTranslation()
-  const [pdfIsOpen, setPdfIsOpen] = useState(false)
-  const [symbolPaletteIsOpen, setSymbolPaletteIsOpen] = useState(false)
-
-  const pdfPanelRef = useRef<ImperativePanelHandle>(null)
-  useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
-
-  return (
-    <PanelGroup
-      autoSaveId={
-        shouldPersistLayout ? 'ide-react-editor-and-pdf-layout' : undefined
-      }
-      direction="horizontal"
-    >
-      <Panel defaultSize={50}>
-        <PanelGroup
-          autoSaveId={
-            shouldPersistLayout
-              ? 'ide-react-editor-and-symbol-palette-layout'
-              : undefined
-          }
-          direction="vertical"
-          units="pixels"
-        >
-          <Panel id="editor" order={1}>
-            Editor placeholder
-            <br />
-            <button onClick={() => setSymbolPaletteIsOpen(value => !value)}>
-              Toggle symbol palette
-            </button>
-          </Panel>
-          {symbolPaletteIsOpen ? (
-            <>
-              <VerticalResizeHandle id="editor-symbol-palette" />
-              <Panel
-                id="symbol-palette"
-                order={2}
-                defaultSize={250}
-                minSize={250}
-                maxSize={336}
-              >
-                <div className="ide-react-symbol-palette ">
-                  Symbol palette placeholder
-                </div>
-              </Panel>
-            </>
-          ) : null}
-        </PanelGroup>
-      </Panel>
-      <HorizontalResizeHandle onDoubleClick={() => setPdfIsOpen(!pdfIsOpen)}>
-        <HorizontalToggler
-          id="editor-pdf"
-          togglerType="east"
-          isOpen={pdfIsOpen}
-          setIsOpen={setPdfIsOpen}
-          tooltipWhenOpen={t('tooltip_hide_pdf')}
-          tooltipWhenClosed={t('tooltip_show_pdf')}
-        />
-      </HorizontalResizeHandle>
-      <Panel
-        ref={pdfPanelRef}
-        defaultSize={50}
-        minSize={5}
-        collapsible
-        onCollapse={collapsed => setPdfIsOpen(!collapsed)}
-      >
-        PDF placeholder
-      </Panel>
-    </PanelGroup>
-  )
-}

+ 0 - 38
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-main-content.tsx

@@ -1,38 +0,0 @@
-import React, { useState } from 'react'
-import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
-import PlaceholderEditorAndPdf from '@/features/ide-react/components/layout/placeholder/placeholder-editor-and-pdf'
-import PlaceholderEditorSidebar from '@/features/ide-react/components/layout/placeholder/placeholder-editor-sidebar'
-
-type PlaceholderEditorMainContentProps = {
-  shouldPersistLayout: boolean
-  leftColumnDefaultSize: number
-  setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
-}
-
-export default function PlaceholderEditorMainContent({
-  shouldPersistLayout,
-  leftColumnDefaultSize,
-  setLeftColumnDefaultSize,
-}: PlaceholderEditorMainContentProps) {
-  const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
-
-  const leftColumnContent = (
-    <PlaceholderEditorSidebar shouldPersistLayout={shouldPersistLayout} />
-  )
-  const rightColumnContent = (
-    <PlaceholderEditorAndPdf shouldPersistLayout={shouldPersistLayout} />
-  )
-
-  return (
-    <TwoColumnMainContent
-      leftColumnId="editor-left-column"
-      leftColumnContent={leftColumnContent}
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-      rightColumnContent={rightColumnContent}
-      leftColumnIsOpen={leftColumnIsOpen}
-      setLeftColumnIsOpen={setLeftColumnIsOpen}
-      shouldPersistLayout={shouldPersistLayout}
-    />
-  )
-}

+ 0 - 26
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-editor-sidebar.tsx

@@ -1,26 +0,0 @@
-import React from 'react'
-import { Panel, PanelGroup } from 'react-resizable-panels'
-import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
-
-type PlaceholderEditorSidebarProps = {
-  shouldPersistLayout: boolean
-}
-
-export default function PlaceholderEditorSidebar({
-  shouldPersistLayout,
-}: PlaceholderEditorSidebarProps) {
-  return (
-    <aside className="ide-react-editor-sidebar">
-      <PanelGroup
-        autoSaveId={
-          shouldPersistLayout ? 'ide-react-editor-sidebar-layout' : undefined
-        }
-        direction="vertical"
-      >
-        <Panel defaultSize={75}>File tree placeholder</Panel>
-        <VerticalResizeHandle />
-        <Panel defaultSize={25}>File outline placeholder</Panel>
-      </PanelGroup>
-    </aside>
-  )
-}

+ 0 - 5
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-file.tsx

@@ -1,5 +0,0 @@
-import React from 'react'
-
-export default function PlaceholderFile() {
-  return <div className="file-view full-size">File placeholder</div>
-}

+ 0 - 42
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-header.tsx

@@ -1,42 +0,0 @@
-import React from 'react'
-import ChatToggleButton from '@/features/editor-navigation-toolbar/components/chat-toggle-button'
-import HistoryToggleButton from '@/features/editor-navigation-toolbar/components/history-toggle-button'
-
-type PlaceholderHeaderProps = {
-  chatIsOpen: boolean
-  setChatIsOpen: (chatIsOpen: boolean) => void
-  historyIsOpen: boolean
-  setHistoryIsOpen: (historyIsOpen: boolean) => void
-}
-
-export default function PlaceholderHeader({
-  chatIsOpen,
-  setChatIsOpen,
-  historyIsOpen,
-  setHistoryIsOpen,
-}: PlaceholderHeaderProps) {
-  function toggleChatOpen() {
-    setChatIsOpen(!chatIsOpen)
-  }
-
-  function toggleHistoryOpen() {
-    setHistoryIsOpen(!historyIsOpen)
-  }
-
-  return (
-    <header className="toolbar toolbar-header">
-      <div className="toolbar-left">Header placeholder</div>
-      <div className="toolbar-right">
-        <HistoryToggleButton
-          historyIsOpen={historyIsOpen}
-          onClick={toggleHistoryOpen}
-        />
-        <ChatToggleButton
-          chatIsOpen={chatIsOpen}
-          onClick={toggleChatOpen}
-          unreadMessageCount={0}
-        />
-      </div>
-    </header>
-  )
-}

+ 0 - 38
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-history.tsx

@@ -1,38 +0,0 @@
-import React, { useState } from 'react'
-import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
-
-type PlaceholderHistoryProps = {
-  shouldPersistLayout: boolean
-  leftColumnDefaultSize: number
-  setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
-}
-
-export default function PlaceholderHistory({
-  shouldPersistLayout,
-  leftColumnDefaultSize,
-  setLeftColumnDefaultSize,
-}: PlaceholderHistoryProps) {
-  const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
-
-  const leftColumnContent = (
-    <aside className="ide-react-editor-sidebar history-file-tree">
-      History file tree placeholder
-    </aside>
-  )
-  const rightColumnContent = (
-    <div>History document diff viewer and versions list placeholder</div>
-  )
-
-  return (
-    <TwoColumnMainContent
-      leftColumnId="editor-left-column"
-      leftColumnContent={leftColumnContent}
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-      rightColumnContent={rightColumnContent}
-      leftColumnIsOpen={leftColumnIsOpen}
-      setLeftColumnIsOpen={setLeftColumnIsOpen}
-      shouldPersistLayout={shouldPersistLayout}
-    />
-  )
-}

+ 0 - 5
services/web/frontend/js/features/ide-react/components/layout/placeholder/placeholder-pdf.tsx

@@ -1,5 +0,0 @@
-import React from 'react'
-
-export default function PlaceholderPdf() {
-  return <div>PDF</div>
-}

+ 1 - 5
services/web/frontend/js/features/ide-react/components/layout/two-column-main-content.tsx

@@ -17,7 +17,6 @@ type TwoColumnMainContentProps = {
   setLeftColumnIsOpen: (
     leftColumnIsOpen: TwoColumnMainContentProps['leftColumnIsOpen']
   ) => void
-  shouldPersistLayout?: boolean
 }
 
 export default function TwoColumnMainContent({
@@ -28,7 +27,6 @@ export default function TwoColumnMainContent({
   rightColumnContent,
   leftColumnIsOpen,
   setLeftColumnIsOpen,
-  shouldPersistLayout = false,
 }: TwoColumnMainContentProps) {
   const { t } = useTranslation()
 
@@ -52,9 +50,7 @@ export default function TwoColumnMainContent({
 
   return (
     <PanelGroup
-      autoSaveId={
-        shouldPersistLayout ? 'ide-react-main-content-layout' : undefined
-      }
+      autoSaveId="ide-react-main-content-layout"
       direction="horizontal"
       onLayout={handleLayout}
       className={classNames({

+ 6 - 8
services/web/frontend/js/features/ide-react/hooks/use-collapsible-panel.ts

@@ -6,14 +6,12 @@ export default function useCollapsiblePanel(
   panelRef: RefObject<ImperativePanelHandle>
 ) {
   useEffect(() => {
-    const panel = panelRef.current
-    if (!panel) {
-      return
-    }
-    if (panelIsOpen) {
-      panel.expand()
-    } else {
-      panel.collapse()
+    if (panelRef.current) {
+      if (panelIsOpen) {
+        panelRef.current.expand()
+      } else {
+        panelRef.current.collapse()
+      }
     }
   }, [panelIsOpen, panelRef])
 }

+ 19 - 0
services/web/frontend/js/features/ide-react/hooks/use-connection-state.ts

@@ -0,0 +1,19 @@
+import { useEffect } from 'react'
+import { useConnectionContext } from '@/features/ide-react/context/connection-context'
+import { useModalsContext } from '@/features/ide-react/context/modals-context'
+
+export const useConnectionState = () => {
+  const { connectionState } = useConnectionContext()
+  const { showLockEditorMessageModal } = useModalsContext()
+
+  // Show modal when editor is forcefully disconnected
+  useEffect(() => {
+    if (connectionState.forceDisconnected) {
+      showLockEditorMessageModal(connectionState.forcedDisconnectDelay)
+    }
+  }, [
+    connectionState.forceDisconnected,
+    connectionState.forcedDisconnectDelay,
+    showLockEditorMessageModal,
+  ])
+}

+ 2 - 2
services/web/frontend/js/features/ide-react/hooks/use-fixed-size-column.ts

@@ -15,7 +15,7 @@ export default function useFixedSizeColumn(
     return fixedPanelRef.current?.getSize('pixels') || 0
   }, [fixedPanelRef])
 
-  const handleLayout = useCallback(
+  const handleLayout: PanelGroupOnLayout = useCallback(
     sizes => {
       // Measure the pixel width here because it's not always up to date in the
       // panel's onResize
@@ -26,7 +26,7 @@ export default function useFixedSizeColumn(
       setInitialLayoutDone(true)
     },
     [measureFixedPanelSizePixels]
-  ) as PanelGroupOnLayout
+  )
 
   useEffect(() => {
     if (!isOpen) {

+ 0 - 2
services/web/frontend/js/features/ide-react/hooks/use-open-file.ts

@@ -21,6 +21,4 @@ export function useOpenFile() {
   // Expose BinaryFilesManager via ide object solely for the benefit of the file
   // restore feature in history. This can be removed once Angular is gone.
   ide.binaryFilesManager = { openFileWithId }
-
-  return openFileWithId
 }

+ 0 - 27
services/web/frontend/stories/ide-page/layout.stories.tsx

@@ -1,27 +0,0 @@
-import LayoutWithPlaceholders from '@/features/ide-react/components/layout/layout-with-placeholders'
-
-export default {
-  title: 'Editor / Page Layout',
-  component: LayoutWithPlaceholders,
-  decorators: [
-    (Story: any) => (
-      <div
-        style={{ position: 'absolute', inset: '1em', border: 'solid #ccc 1px' }}
-      >
-        <Story />
-      </div>
-    ),
-  ],
-}
-
-export const Persisted = {
-  args: {
-    shouldPersistLayout: true,
-  },
-}
-
-export const Unpersisted = {
-  args: {
-    shouldPersistLayout: false,
-  },
-}

+ 5 - 0
services/web/frontend/stylesheets/app/editor/ide-react.less

@@ -128,6 +128,11 @@
   }
 }
 
+.ide-react-editor-panel {
+  display: flex;
+  flex-direction: column;
+}
+
 // Ensure an element with class "full-size", such as the binary file view, stays within the bounds of the panel
 .ide-react-panel {
   position: relative;