浏览代码

Upgrade react-resizable-panels (#15998)

GitOrigin-RevId: af799f1a5b4945ad2acbb460806d559fae7416b9
Alf Eaton 2 年之前
父节点
当前提交
d5b3c10cb5

+ 8 - 8
package-lock.json

@@ -34095,9 +34095,9 @@
       }
       }
     },
     },
     "node_modules/react-resizable-panels": {
     "node_modules/react-resizable-panels": {
-      "version": "0.0.55",
-      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.55.tgz",
-      "integrity": "sha512-J/LTFzUEjJiqwSjVh8gjUXkQDA8MRPjARASfn++d2+KOgA+9UcRYUfE3QBJixer2vkk+ffQ4cq3QzWzzHgqYpQ==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.63.tgz",
+      "integrity": "sha512-AfA8b6kouhL4rBvgUGs17uzWVlYPaJIwwTCVeWRxNpUHJlCG1h9RIMlzA1849AZGsaNJO3j/SNdI5SS4GZDE3g==",
       "dev": true,
       "dev": true,
       "peerDependencies": {
       "peerDependencies": {
         "react": "^16.14.0 || ^17.0.0 || ^18.0.0",
         "react": "^16.14.0 || ^17.0.0 || ^18.0.0",
@@ -43709,7 +43709,7 @@
         "react-i18next": "^13.3.1",
         "react-i18next": "^13.3.1",
         "react-linkify": "^1.0.0-alpha",
         "react-linkify": "^1.0.0-alpha",
         "react-refresh": "^0.14.0",
         "react-refresh": "^0.14.0",
-        "react-resizable-panels": "^0.0.55",
+        "react-resizable-panels": "^0.0.63",
         "react2angular": "^4.0.6",
         "react2angular": "^4.0.6",
         "react2angular-shared-context": "^1.1.0",
         "react2angular-shared-context": "^1.1.0",
         "requirejs": "^2.3.6",
         "requirejs": "^2.3.6",
@@ -52297,7 +52297,7 @@
         "react-i18next": "^13.3.1",
         "react-i18next": "^13.3.1",
         "react-linkify": "^1.0.0-alpha",
         "react-linkify": "^1.0.0-alpha",
         "react-refresh": "^0.14.0",
         "react-refresh": "^0.14.0",
-        "react-resizable-panels": "^0.0.55",
+        "react-resizable-panels": "^0.0.63",
         "react2angular": "^4.0.6",
         "react2angular": "^4.0.6",
         "react2angular-shared-context": "^1.1.0",
         "react2angular-shared-context": "^1.1.0",
         "recurly": "^4.0.0",
         "recurly": "^4.0.0",
@@ -73465,9 +73465,9 @@
       }
       }
     },
     },
     "react-resizable-panels": {
     "react-resizable-panels": {
-      "version": "0.0.55",
-      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.55.tgz",
-      "integrity": "sha512-J/LTFzUEjJiqwSjVh8gjUXkQDA8MRPjARASfn++d2+KOgA+9UcRYUfE3QBJixer2vkk+ffQ4cq3QzWzzHgqYpQ==",
+      "version": "0.0.63",
+      "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-0.0.63.tgz",
+      "integrity": "sha512-AfA8b6kouhL4rBvgUGs17uzWVlYPaJIwwTCVeWRxNpUHJlCG1h9RIMlzA1849AZGsaNJO3j/SNdI5SS4GZDE3g==",
       "dev": true,
       "dev": true,
       "requires": {}
       "requires": {}
     },
     },

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

@@ -1,99 +1,103 @@
-import { ReactNode, useRef, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import {
-  ImperativePanelHandle,
-  Panel,
-  PanelGroup,
-} from 'react-resizable-panels'
+import React, { FC, useState } from 'react'
+import { Panel, PanelGroup } from 'react-resizable-panels'
+import NoSelectionPane from '@/features/ide-react/components/editor/no-selection-pane'
+import FileView from '@/features/file-view/components/file-view'
+import { fileViewFile } from '@/features/ide-react/hooks/use-file-tree'
+import MultipleSelectionPane from '@/features/ide-react/components/editor/multiple-selection-pane'
 import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
 import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
 import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
 import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
-import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
-import { useLayoutContext } from '@/shared/context/layout-context'
-import PdfPreview from '@/features/pdf-preview/components/pdf-preview'
 import { DefaultSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
 import { DefaultSynctexControl } from '@/features/pdf-preview/components/detach-synctex-control'
-import classnames from 'classnames'
-
-export type EditorProps = {
-  editorContent: ReactNode
-}
+import PdfPreview from '@/features/pdf-preview/components/pdf-preview'
+import { usePdfPane } from '@/features/ide-react/hooks/use-pdf-pane'
+import { useLayoutContext } from '@/shared/context/layout-context'
+import { useTranslation } from 'react-i18next'
+import classNames from 'classnames'
 
 
-export default function EditorAndPdf({ editorContent }: EditorProps) {
-  const { t } = useTranslation()
-  const { view, pdfLayout, changeLayout, detachRole, reattach } =
-    useLayoutContext()
+export const EditorAndPdf: FC<{
+  editorPane: React.ReactNode
+  selectedEntityCount: number
+  openEntity: any // TODO
+}> = ({ editorPane, selectedEntityCount, openEntity }) => {
   const [resizing, setResizing] = useState(false)
   const [resizing, setResizing] = useState(false)
 
 
-  const pdfPanelRef = useRef<ImperativePanelHandle>(null)
-  const isDualPane = pdfLayout === 'sideBySide'
-  const editorIsVisible = isDualPane || view === 'editor' || view === 'file'
-  const pdfIsOpen = isDualPane || view === 'pdf'
+  const { t } = useTranslation()
 
 
-  useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
+  const {
+    togglePdfPane,
+    handlePdfPaneExpand,
+    handlePdfPaneCollapse,
+    setPdfIsOpen,
+    pdfIsOpen,
+    pdfPanelRef,
+  } = usePdfPane()
 
 
-  const historyIsOpen = view === 'history'
+  const { view, pdfLayout } = useLayoutContext()
 
 
-  function setPdfIsOpen(isOpen: boolean) {
-    if (isOpen) {
-      if (detachRole === 'detacher') {
-        reattach()
-      }
-      changeLayout('sideBySide')
-    } else {
-      changeLayout('flat', 'editor')
-    }
-  }
+  const editorIsOpen =
+    view === 'editor' || view === 'file' || pdfLayout === 'sideBySide'
 
 
   return (
   return (
-    <PanelGroup
-      autoSaveId="ide-react-editor-and-pdf-layout"
-      direction="horizontal"
-      className={classnames('ide-react-editor-and-pdf', {
-        hide: historyIsOpen,
-        'ide-react-editor-and-pdf-resizing': resizing,
-      })}
-    >
-      {editorIsVisible ? (
-        <Panel
-          id="editor"
-          order={1}
-          defaultSize={50}
-          className="ide-react-panel"
-        >
-          <div className="ide-react-editor-content full-size">
-            {editorContent}
-          </div>
-        </Panel>
-      ) : null}
-      <HorizontalResizeHandle
-        resizable={isDualPane}
-        onDoubleClick={() => setPdfIsOpen(!pdfIsOpen)}
-        onDragging={setResizing}
-      >
-        <HorizontalToggler
-          id="editor-pdf"
-          togglerType="east"
-          isOpen={pdfIsOpen}
-          setIsOpen={setPdfIsOpen}
-          tooltipWhenOpen={t('tooltip_hide_pdf')}
-          tooltipWhenClosed={t('tooltip_show_pdf')}
-        />
-        {isDualPane ? (
-          <div className="synctex-controls">
-            <DefaultSynctexControl />
-          </div>
-        ) : null}
-      </HorizontalResizeHandle>
+    <PanelGroup autoSaveId="ide-editor-pdf-layout" direction="horizontal">
+      {/* main */}
+      {editorIsOpen && (
+        <>
+          <Panel
+            id="panel-main"
+            order={1}
+            defaultSizePercentage={50}
+            minSizePercentage={25}
+            className={classNames('ide-react-panel', {
+              'ide-panel-group-resizing': resizing,
+            })}
+          >
+            {selectedEntityCount === 0 && <NoSelectionPane />}
+            {selectedEntityCount === 1 && openEntity?.type === 'fileRef' && (
+              <FileView file={fileViewFile(openEntity.entity)} />
+            )}
+            {selectedEntityCount === 1 && editorPane}
+            {selectedEntityCount > 1 && (
+              <MultipleSelectionPane
+                selectedEntityCount={selectedEntityCount}
+              />
+            )}
+          </Panel>
+
+          <HorizontalResizeHandle
+            resizable={pdfLayout === 'sideBySide'}
+            onDoubleClick={togglePdfPane}
+            onDragging={setResizing}
+          >
+            <HorizontalToggler
+              id="editor-pdf"
+              togglerType="east"
+              isOpen={pdfIsOpen}
+              setIsOpen={setPdfIsOpen}
+              tooltipWhenOpen={t('tooltip_hide_pdf')}
+              tooltipWhenClosed={t('tooltip_show_pdf')}
+            />
+
+            {pdfLayout === 'sideBySide' && (
+              <div className="synctex-controls">
+                <DefaultSynctexControl />
+              </div>
+            )}
+          </HorizontalResizeHandle>
+        </>
+      )}
+
+      {/* pdf */}
       <Panel
       <Panel
         ref={pdfPanelRef}
         ref={pdfPanelRef}
-        id="pdf"
+        id="panel-pdf"
         order={2}
         order={2}
-        defaultSize={50}
-        minSize={5}
+        defaultSizePercentage={50}
+        minSizePercentage={25}
         collapsible
         collapsible
-        onCollapse={collapsed => setPdfIsOpen(!collapsed)}
+        onCollapse={handlePdfPaneCollapse}
+        onExpand={handlePdfPaneExpand}
         className="ide-react-panel"
         className="ide-react-panel"
       >
       >
-        {pdfIsOpen || resizing ? <PdfPreview /> : null}
+        {pdfIsOpen && <PdfPreview />}
       </Panel>
       </Panel>
     </PanelGroup>
     </PanelGroup>
   )
   )

+ 7 - 6
services/web/frontend/js/features/ide-react/components/editor-sidebar.tsx

@@ -27,11 +27,12 @@ export default function EditorSidebar({
         hidden: !shouldShow,
         hidden: !shouldShow,
       })}
       })}
     >
     >
-      <PanelGroup
-        autoSaveId="ide-react-editor-sidebar-layout"
-        direction="vertical"
-      >
-        <Panel defaultSize={75} className="ide-react-file-tree-panel">
+      <PanelGroup autoSaveId="ide-editor-sidebar-layout" direction="vertical">
+        <Panel
+          defaultSizePercentage={75}
+          className="ide-react-file-tree-panel"
+          id="panel-file-tree"
+        >
           <FileTree
           <FileTree
             onInit={onFileTreeInit}
             onInit={onFileTreeInit}
             onSelect={onFileTreeSelect}
             onSelect={onFileTreeSelect}
@@ -39,7 +40,7 @@ export default function EditorSidebar({
           />
           />
         </Panel>
         </Panel>
         <VerticalResizeHandle />
         <VerticalResizeHandle />
-        <Panel defaultSize={25}>
+        <Panel defaultSizePercentage={25} id="panel-outline">
           <div className="outline-container" />
           <div className="outline-container" />
         </Panel>
         </Panel>
       </PanelGroup>
       </PanelGroup>

+ 32 - 27
services/web/frontend/js/features/ide-react/components/editor/editor-pane.tsx

@@ -22,33 +22,38 @@ export const EditorPane: FC<{ show: boolean }> = ({ show }) => {
   )
   )
 
 
   return (
   return (
-    <PanelGroup
-      autoSaveId="ide-react-editor-and-symbol-palette-layout"
-      direction="vertical"
-      units="pixels"
-      className={classNames({ hidden: !show })}
-    >
-      <Panel id="sourceEditor" order={1} className="ide-react-editor-panel">
-        <SourceEditor />
-        {isLoading && <LoadingPane />}
-      </Panel>
+    <div className="ide-react-editor-content full-size">
+      <PanelGroup
+        autoSaveId="ide-editor-layout"
+        direction="vertical"
+        className={classNames({ hidden: !show })}
+      >
+        <Panel
+          id="panel-source-editor"
+          order={1}
+          className="ide-react-editor-panel"
+        >
+          <SourceEditor />
+          {isLoading && <LoadingPane />}
+        </Panel>
 
 
-      {editor.showSymbolPalette && (
-        <>
-          <VerticalResizeHandle id="editor-symbol-palette" />
-          <Panel
-            id="symbol-palette"
-            order={2}
-            defaultSize={250}
-            minSize={250}
-            maxSize={336}
-          >
-            <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
-              <SymbolPalettePane />
-            </Suspense>
-          </Panel>
-        </>
-      )}
-    </PanelGroup>
+        {editor.showSymbolPalette && (
+          <>
+            <VerticalResizeHandle id="editor-symbol-palette" />
+            <Panel
+              id="panel-symbol-palette"
+              order={2}
+              defaultSizePixels={250}
+              minSizePixels={250}
+              maxSizePixels={336}
+            >
+              <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
+                <SymbolPalettePane />
+              </Suspense>
+            </Panel>
+          </>
+        )}
+      </PanelGroup>
+    </div>
   )
   )
 }
 }

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

@@ -1,5 +1,5 @@
 import { Alerts } from '@/features/ide-react/components/alerts/alerts'
 import { Alerts } from '@/features/ide-react/components/alerts/alerts'
-import MainLayout from '@/features/ide-react/components/layout/main-layout'
+import { MainLayout } from '@/features/ide-react/components/layout/main-layout'
 import EditorLeftMenu from '@/features/editor-left-menu/components/editor-left-menu'
 import EditorLeftMenu from '@/features/editor-left-menu/components/editor-left-menu'
 import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
 import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
 import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
 import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
@@ -10,7 +10,7 @@ import { useHasLintingError } from '@/features/ide-react/hooks/use-has-linting-e
 import { useConnectionState } from '@/features/ide-react/hooks/use-connection-state'
 import { useConnectionState } from '@/features/ide-react/hooks/use-connection-state'
 
 
 export default function IdePage() {
 export default function IdePage() {
-  useLayoutEventTracking() // send event when the layout changes
+  useLayoutEventTracking() // sent event when the layout changes
   useSocketListeners() // listen for project-related websocket messages
   useSocketListeners() // listen for project-related websocket messages
   useEditingSessionHeartbeat() // send a batched event when user is active
   useEditingSessionHeartbeat() // send a batched event when user is active
   useRegisterUserActivity() // record activity and ensure connection when user is active
   useRegisterUserActivity() // record activity and ensure connection when user is active

+ 123 - 36
services/web/frontend/js/features/ide-react/components/layout/main-layout.tsx

@@ -1,62 +1,149 @@
 import { Panel, PanelGroup } from 'react-resizable-panels'
 import { Panel, PanelGroup } from 'react-resizable-panels'
-import { useState } from 'react'
+import { FC } from 'react'
 import { HorizontalResizeHandle } from '../resize/horizontal-resize-handle'
 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 classNames from 'classnames'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import { useLayoutContext } from '@/shared/context/layout-context'
 import EditorNavigationToolbar from '@/features/ide-react/components/editor-navigation-toolbar'
 import EditorNavigationToolbar from '@/features/ide-react/components/editor-navigation-toolbar'
 import ChatPane from '@/features/chat/components/chat-pane'
 import ChatPane from '@/features/chat/components/chat-pane'
-import { EditorAndSidebar } from '@/features/ide-react/components/editor-and-sidebar'
+import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
+import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
+import { HistoryProvider } from '@/features/history/context/history-context'
+import History from '@/features/ide-react/components/history'
+import EditorSidebar from '@/features/ide-react/components/editor-sidebar'
+import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
+import { useFileTree } from '@/features/ide-react/hooks/use-file-tree'
+import { useTranslation } from 'react-i18next'
+import { useSidebarPane } from '@/features/ide-react/hooks/use-sidebar-pane'
+import { useChatPane } from '@/features/ide-react/hooks/use-chat-pane'
+import { EditorAndPdf } from '@/features/ide-react/components/editor-and-pdf'
 
 
-const CHAT_DEFAULT_SIZE = 20
+export const MainLayout: FC = () => {
+  const { view } = useLayoutContext()
 
 
-// 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() {
-  const { chatIsOpen } = useLayoutContext()
+  const {
+    isOpen: sidebarIsOpen,
+    setIsOpen: setSidebarIsOpen,
+    fixedPanelRef: sidebarPanelRef,
+    handleLayout: handleSidebarLayout,
+    togglePane: toggleSidebar,
+    handlePaneExpand: handleSidebarExpand,
+    handlePaneCollapse: handleSidebarCollapse,
+    resizing: sidebarResizing,
+    setResizing: setSidebarResizing,
+  } = useSidebarPane()
 
 
-  const { fixedPanelRef: chatPanelRef, handleLayout } = useFixedSizeColumn(
-    CHAT_DEFAULT_SIZE,
-    chatIsOpen
-  )
+  const {
+    isOpen: chatIsOpen,
+    fixedPanelRef: chatPanelRef,
+    handleLayout: handleChatLayout,
+    resizing: chatResizing,
+    setResizing: setChatResizing,
+  } = useChatPane()
+
+  const {
+    selectedEntityCount,
+    openEntity,
+    openDocId,
+    handleFileTreeInit,
+    handleFileTreeSelect,
+    handleFileTreeDelete,
+  } = useFileTree()
 
 
-  useCollapsiblePanel(chatIsOpen, chatPanelRef)
+  const { t } = useTranslation()
 
 
-  const [resizing, setResizing] = useState(false)
+  // keep the editor pane open
+  const editorPane = openDocId ? (
+    <EditorPane
+      show={openEntity?.type === 'doc' && selectedEntityCount === 1}
+    />
+  ) : null
 
 
   return (
   return (
     <div className="ide-react-main">
     <div className="ide-react-main">
       <EditorNavigationToolbar />
       <EditorNavigationToolbar />
       <div className="ide-react-body">
       <div className="ide-react-body">
         <PanelGroup
         <PanelGroup
-          autoSaveId="ide-react-chat-layout"
+          autoSaveId="ide-outer-layout"
           direction="horizontal"
           direction="horizontal"
-          onLayout={handleLayout}
+          onLayout={handleSidebarLayout}
           className={classNames({
           className={classNames({
-            'ide-react-main-resizing': resizing,
+            'ide-panel-group-resizing': sidebarResizing || chatResizing,
           })}
           })}
         >
         >
-          <Panel id="main" order={1}>
-            <EditorAndSidebar />
+          {/* sidebar */}
+          <Panel
+            ref={sidebarPanelRef}
+            id="panel-sidebar"
+            order={1}
+            defaultSizePixels={200}
+            minSizePixels={150}
+            collapsible
+            onCollapse={handleSidebarCollapse}
+            onExpand={handleSidebarExpand}
+          >
+            <EditorSidebar
+              shouldShow={view !== 'history'}
+              onFileTreeInit={handleFileTreeInit}
+              onFileTreeSelect={handleFileTreeSelect}
+              onFileTreeDelete={handleFileTreeDelete}
+            />
+            {view === 'history' && <HistorySidebar />}
           </Panel>
           </Panel>
-          {chatIsOpen ? (
-            <>
-              <HorizontalResizeHandle onDragging={setResizing} />
-              <Panel
-                ref={chatPanelRef}
-                id="chat"
-                order={2}
-                defaultSize={CHAT_DEFAULT_SIZE}
-                minSize={5}
-                collapsible
-              >
-                <ChatPane />
+
+          <HorizontalResizeHandle
+            onDoubleClick={toggleSidebar}
+            resizable={sidebarIsOpen}
+            onDragging={setSidebarResizing}
+          >
+            <HorizontalToggler
+              id="panel-sidebar"
+              togglerType="west"
+              isOpen={sidebarIsOpen}
+              setIsOpen={setSidebarIsOpen}
+              tooltipWhenOpen={t('tooltip_hide_filetree')}
+              tooltipWhenClosed={t('tooltip_show_filetree')}
+            />
+          </HorizontalResizeHandle>
+
+          <Panel id="panel-outer-main" order={2}>
+            <PanelGroup
+              autoSaveId="ide-inner-layout"
+              direction="horizontal"
+              onLayout={handleChatLayout}
+            >
+              <Panel className="ide-react-panel" id="panel-main" order={1}>
+                {view === 'history' ? (
+                  <HistoryProvider>
+                    <History />
+                  </HistoryProvider>
+                ) : (
+                  <EditorAndPdf
+                    editorPane={editorPane}
+                    selectedEntityCount={selectedEntityCount}
+                    openEntity={openEntity}
+                  />
+                )}
               </Panel>
               </Panel>
-            </>
-          ) : null}
+
+              {chatIsOpen && (
+                <>
+                  <HorizontalResizeHandle onDragging={setChatResizing} />
+
+                  {/* chat */}
+                  <Panel
+                    ref={chatPanelRef}
+                    id="panel-chat"
+                    order={2}
+                    defaultSizePixels={200}
+                    minSizePixels={150}
+                    collapsible
+                  >
+                    <ChatPane />
+                  </Panel>
+                </>
+              )}
+            </PanelGroup>
+          </Panel>
         </PanelGroup>
         </PanelGroup>
       </div>
       </div>
     </div>
     </div>

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

@@ -1,86 +0,0 @@
-import React, { ReactNode, useEffect, useState } from 'react'
-import { Panel, PanelGroup } from 'react-resizable-panels'
-import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'
-import { useTranslation } from 'react-i18next'
-import { HorizontalToggler } from '@/features/ide-react/components/resize/horizontal-toggler'
-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'
-
-type TwoColumnMainContentProps = {
-  leftColumnId: string
-  leftColumnContent: ReactNode
-  leftColumnDefaultSize: number
-  setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
-  rightColumnContent: ReactNode
-  leftColumnIsOpen: boolean
-  setLeftColumnIsOpen: (
-    leftColumnIsOpen: TwoColumnMainContentProps['leftColumnIsOpen']
-  ) => void
-}
-
-export default function TwoColumnMainContent({
-  leftColumnId,
-  leftColumnContent,
-  leftColumnDefaultSize,
-  setLeftColumnDefaultSize,
-  rightColumnContent,
-  leftColumnIsOpen,
-  setLeftColumnIsOpen,
-}: TwoColumnMainContentProps) {
-  const { t } = useTranslation()
-
-  const {
-    fixedPanelRef: leftColumnPanelRef,
-    fixedPanelWidthRef: leftColumnWidthRef,
-    handleLayout,
-  } = useFixedSizeColumn(leftColumnDefaultSize, leftColumnIsOpen)
-
-  useCollapsiblePanel(leftColumnIsOpen, leftColumnPanelRef)
-
-  const [resizing, setResizing] = useState(false)
-
-  // Update the left column default size on unmount rather than doing it on
-  // every resize, which causes ResizeObserver errors
-  useEffect(() => {
-    if (leftColumnWidthRef.current) {
-      setLeftColumnDefaultSize(leftColumnWidthRef.current.size)
-    }
-  }, [leftColumnWidthRef, setLeftColumnDefaultSize])
-
-  return (
-    <PanelGroup
-      autoSaveId="ide-react-main-content-layout"
-      direction="horizontal"
-      onLayout={handleLayout}
-      className={classNames({
-        'ide-react-main-content-resizing': resizing,
-      })}
-    >
-      <Panel
-        ref={leftColumnPanelRef}
-        defaultSize={leftColumnDefaultSize}
-        minSize={5}
-        collapsible
-        onCollapse={collapsed => setLeftColumnIsOpen(!collapsed)}
-      >
-        {leftColumnContent}
-      </Panel>
-      <HorizontalResizeHandle
-        onDoubleClick={() => setLeftColumnIsOpen(!leftColumnIsOpen)}
-        resizable={leftColumnIsOpen}
-        onDragging={setResizing}
-      >
-        <HorizontalToggler
-          id={leftColumnId}
-          togglerType="west"
-          isOpen={leftColumnIsOpen}
-          setIsOpen={setLeftColumnIsOpen}
-          tooltipWhenOpen={t('tooltip_hide_filetree')}
-          tooltipWhenClosed={t('tooltip_show_filetree')}
-        />
-      </HorizontalResizeHandle>
-      <Panel className="ide-react-panel">{rightColumnContent}</Panel>
-    </PanelGroup>
-  )
-}

+ 13 - 0
services/web/frontend/js/features/ide-react/hooks/use-chat-pane.ts

@@ -0,0 +1,13 @@
+import { useLayoutContext } from '@/shared/context/layout-context'
+import useFixedSizeColumn from '@/features/ide-react/hooks/use-fixed-size-column'
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+import { useState } from 'react'
+
+export const useChatPane = () => {
+  const { chatIsOpen: isOpen } = useLayoutContext()
+  const [resizing, setResizing] = useState(false)
+  const { fixedPanelRef, handleLayout } = useFixedSizeColumn(isOpen)
+  useCollapsiblePanel(isOpen, fixedPanelRef)
+
+  return { isOpen, fixedPanelRef, handleLayout, resizing, setResizing }
+}

+ 38 - 134
services/web/frontend/js/features/ide-react/components/editor-and-sidebar.tsx → services/web/frontend/js/features/ide-react/hooks/use-file-tree.ts

@@ -1,60 +1,20 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react'
-import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
-import EditorSidebar from '@/features/ide-react/components/editor-sidebar'
-import History from '@/features/ide-react/components/history'
-import { HistoryProvider } from '@/features/history/context/history-context'
-import { useProjectContext } from '@/shared/context/project-context'
-import { useLayoutContext } from '@/shared/context/layout-context'
+import { FileRef } from '../../../../../types/file-ref'
+import { BinaryFile } from '@/features/file-view/types/binary-file'
+import { useSelectFileTreeEntity } from '@/features/ide-react/hooks/use-select-file-tree-entity'
+import useScopeValue from '@/shared/hooks/use-scope-value'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import {
 import {
   FileTreeDeleteHandler,
   FileTreeDeleteHandler,
   FileTreeDocumentFindResult,
   FileTreeDocumentFindResult,
   FileTreeFileRefFindResult,
   FileTreeFileRefFindResult,
   FileTreeFindResult,
   FileTreeFindResult,
 } from '@/features/ide-react/types/file-tree'
 } from '@/features/ide-react/types/file-tree'
-import FileView from '@/features/file-view/components/file-view'
-import { FileRef } from '../../../../../types/file-ref'
-import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
-import EditorAndPdf from '@/features/ide-react/components/editor-and-pdf'
-import MultipleSelectionPane from '@/features/ide-react/components/editor/multiple-selection-pane'
-import NoSelectionPane from '@/features/ide-react/components/editor/no-selection-pane'
+import { debugConsole } from '@/utils/debugging'
+import { useProjectContext } from '@/shared/context/project-context'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
 import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
-import NoOpenDocPane from '@/features/ide-react/components/editor/no-open-doc-pane'
-import { debugConsole } from '@/utils/debugging'
-import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
-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'
-
-function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
-  return {
-    _id: fileRef._id,
-    name: fileRef.name,
-    id: fileRef._id,
-    type: 'file',
-    selected: true,
-    linkedFileData: fileRef.linkedFileData,
-    created: fileRef.created ? new Date(fileRef.created) : new Date(),
-  }
-}
-
-// `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
-// `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
-// into which `FileViewHeader` passes its BinaryFile, expects a file object with
-// `created` property of type `string`, which is a mismatch. `TPRFileViewInfo`
-// is the only one making runtime complaints and it seems that other uses of
-// `FileViewHeader` pass in a string for `created`, so that's what this function
-// does too.
-function fileViewFile(fileRef: FileRef) {
-  return {
-    ...convertFileRefToBinaryFile(fileRef),
-    created: fileRef.created,
-  }
-}
 
 
-export function EditorAndSidebar() {
-  const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
-  const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
+export const useFileTree = () => {
   const { rootDocId } = useProjectContext()
   const { rootDocId } = useProjectContext()
   const { eventEmitter } = useIdeReactContext()
   const { eventEmitter } = useIdeReactContext()
   const {
   const {
@@ -62,7 +22,6 @@ export function EditorAndSidebar() {
     currentDocumentId: openDocId,
     currentDocumentId: openDocId,
     openInitialDoc,
     openInitialDoc,
   } = useEditorManagerContext()
   } = useEditorManagerContext()
-  const { view } = useLayoutContext()
   const { projectJoined } = useIdeReactContext()
   const { projectJoined } = useIdeReactContext()
   const { selectEntity } = useSelectFileTreeEntity()
   const { selectEntity } = useSelectFileTreeEntity()
   const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
   const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
@@ -145,93 +104,38 @@ export function EditorAndSidebar() {
     }
     }
   }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
   }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
 
 
-  // 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}
-      />
-    )
+  return {
+    selectedEntityCount,
+    openEntity,
+    openDocId,
+    handleFileTreeInit,
+    handleFileTreeSelect,
+    handleFileTreeDelete,
   }
   }
+}
 
 
-  // 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 (openDocId === undefined) {
-    rightColumnContent = <NoOpenDocPane />
-  } else if (selectedEntityCount === 0) {
-    rightColumnContent = (
-      <>
-        {editorPane}
-        <NoSelectionPane />
-      </>
-    )
-  } else if (selectedEntityCount > 1) {
-    rightColumnContent = (
-      <EditorAndPdf
-        editorContent={
-          <>
-            {editorPane}
-            <MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
-          </>
-        }
-      />
-    )
-  } else if (openEntity) {
-    rightColumnContent = (
-      <EditorAndPdf
-        editorContent={
-          <>
-            {editorPane}
-            {openEntity.type !== 'doc' && (
-              <FileView file={fileViewFile(openEntity.entity)} />
-            )}
-          </>
-        }
-      />
-    )
+function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
+  return {
+    _id: fileRef._id,
+    name: fileRef.name,
+    id: fileRef._id,
+    type: 'file',
+    selected: true,
+    linkedFileData: fileRef.linkedFileData,
+    created: fileRef.created ? new Date(fileRef.created) : new Date(),
   }
   }
+}
 
 
-  return (
-    <TwoColumnMainContent
-      leftColumnId="editor-left-column"
-      leftColumnContent={editorSidebar}
-      leftColumnDefaultSize={leftColumnDefaultSize}
-      setLeftColumnDefaultSize={setLeftColumnDefaultSize}
-      rightColumnContent={rightColumnContent}
-      leftColumnIsOpen={leftColumnIsOpen}
-      setLeftColumnIsOpen={setLeftColumnIsOpen}
-    />
-  )
+// `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
+// `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
+// into which `FileViewHeader` passes its BinaryFile, expects a file object with
+// `created` property of type `string`, which is a mismatch. `TPRFileViewInfo`
+// is the only one making runtime complaints and it seems that other uses of
+// `FileViewHeader` pass in a string for `created`, so that's what this function
+// does too.
+export function fileViewFile(fileRef: FileRef) {
+  return {
+    ...convertFileRefToBinaryFile(fileRef),
+    created: fileRef.created,
+  }
 }
 }

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

@@ -1,32 +1,21 @@
 import { useCallback, useEffect, useRef, useState } from 'react'
 import { useCallback, useEffect, useRef, useState } from 'react'
-import { ImperativePanelHandle } from 'react-resizable-panels'
-import { PanelGroupOnLayout } from 'react-resizable-panels/src/types'
+import {
+  ImperativePanelHandle,
+  PanelGroupOnLayout,
+} from 'react-resizable-panels'
 
 
-export default function useFixedSizeColumn(
-  defaultSize: number,
-  isOpen: boolean
-) {
+export default function useFixedSizeColumn(isOpen: boolean) {
   const fixedPanelRef = useRef<ImperativePanelHandle>(null)
   const fixedPanelRef = useRef<ImperativePanelHandle>(null)
 
 
-  const fixedPanelWidthRef = useRef({ size: defaultSize, pixels: 0 })
+  const fixedPanelSizeRef = useRef<number>(0)
   const [initialLayoutDone, setInitialLayoutDone] = useState(false)
   const [initialLayoutDone, setInitialLayoutDone] = useState(false)
 
 
-  const measureFixedPanelSizePixels = useCallback(() => {
-    return fixedPanelRef.current?.getSize('pixels') || 0
-  }, [fixedPanelRef])
-
-  const handleLayout: PanelGroupOnLayout = useCallback(
-    sizes => {
-      // Measure the pixel width here because it's not always up to date in the
-      // panel's onResize
-      fixedPanelWidthRef.current = {
-        size: sizes[0],
-        pixels: measureFixedPanelSizePixels(),
-      }
+  const handleLayout: PanelGroupOnLayout = useCallback(() => {
+    if (fixedPanelRef.current) {
+      fixedPanelSizeRef.current = fixedPanelRef.current.getSize().sizePixels
       setInitialLayoutDone(true)
       setInitialLayoutDone(true)
-    },
-    [measureFixedPanelSizePixels]
-  )
+    }
+  }, [])
 
 
   useEffect(() => {
   useEffect(() => {
     if (!isOpen) {
     if (!isOpen) {
@@ -43,24 +32,26 @@ export default function useFixedSizeColumn(
     const fixedPanelElement = document.querySelector(
     const fixedPanelElement = document.querySelector(
       `[data-panel-id="${fixedPanelRef.current.getId()}"]`
       `[data-panel-id="${fixedPanelRef.current.getId()}"]`
     )
     )
+    if (!fixedPanelElement) {
+      return
+    }
 
 
-    const panelGroupElement = fixedPanelElement?.closest('[data-panel-group]')
-    if (!panelGroupElement || !fixedPanelElement) {
+    const panelGroupElement = fixedPanelElement.closest('[data-panel-group]')
+    if (!panelGroupElement) {
       return
       return
     }
     }
 
 
     const resizeObserver = new ResizeObserver(() => {
     const resizeObserver = new ResizeObserver(() => {
-      fixedPanelRef.current?.resize(fixedPanelWidthRef.current.pixels, 'pixels')
+      // when the panel group resizes, set the size of this panel to the previous size, in pixels
+      fixedPanelRef.current?.resize({
+        sizePixels: fixedPanelSizeRef.current,
+      })
     })
     })
 
 
     resizeObserver.observe(panelGroupElement)
     resizeObserver.observe(panelGroupElement)
 
 
     return () => resizeObserver.unobserve(panelGroupElement)
     return () => resizeObserver.unobserve(panelGroupElement)
-  }, [fixedPanelRef, measureFixedPanelSizePixels, initialLayoutDone, isOpen])
+  }, [fixedPanelRef, initialLayoutDone, isOpen])
 
 
-  return {
-    fixedPanelRef,
-    fixedPanelWidthRef,
-    handleLayout,
-  }
+  return { fixedPanelRef, handleLayout }
 }
 }

+ 65 - 0
services/web/frontend/js/features/ide-react/hooks/use-pdf-pane.ts

@@ -0,0 +1,65 @@
+import { useCallback, useEffect, useRef } from 'react'
+import { ImperativePanelHandle } from 'react-resizable-panels'
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+import { useLayoutContext } from '@/shared/context/layout-context'
+
+export const usePdfPane = () => {
+  const { view, pdfLayout, changeLayout, detachRole, reattach } =
+    useLayoutContext()
+
+  const pdfPanelRef = useRef<ImperativePanelHandle>(null)
+  const pdfIsOpen = pdfLayout === 'sideBySide' || view === 'pdf'
+
+  useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
+
+  // close a detached PDF when the detacher PDF view opens
+  useEffect(() => {
+    if (pdfIsOpen && detachRole === 'detacher') {
+      reattach()
+    }
+  }, [detachRole, pdfIsOpen, reattach])
+
+  // triggered by a double-click on the resizer
+  const togglePdfPane = useCallback(() => {
+    if (pdfIsOpen) {
+      changeLayout('flat', 'editor')
+    } else {
+      changeLayout('sideBySide')
+    }
+  }, [changeLayout, pdfIsOpen])
+
+  // triggered by a click on the toggle button
+  const setPdfIsOpen = useCallback(
+    (value: boolean) => {
+      if (value) {
+        changeLayout('sideBySide')
+      } else {
+        changeLayout('flat', 'editor')
+      }
+    },
+    [changeLayout]
+  )
+
+  // triggered when the PDF pane becomes open
+  const handlePdfPaneExpand = useCallback(() => {
+    if (pdfLayout === 'flat' && view === 'editor') {
+      changeLayout('sideBySide', 'editor')
+    }
+  }, [changeLayout, pdfLayout, view])
+
+  // triggered when the PDF pane becomes closed (either by dragging or toggling)
+  const handlePdfPaneCollapse = useCallback(() => {
+    if (pdfLayout === 'sideBySide') {
+      changeLayout('flat', 'editor')
+    }
+  }, [changeLayout, pdfLayout])
+
+  return {
+    togglePdfPane,
+    handlePdfPaneExpand,
+    handlePdfPaneCollapse,
+    setPdfIsOpen,
+    pdfIsOpen,
+    pdfPanelRef,
+  }
+}

+ 34 - 0
services/web/frontend/js/features/ide-react/hooks/use-sidebar-pane.ts

@@ -0,0 +1,34 @@
+import { useCallback, useState } from 'react'
+import useFixedSizeColumn from '@/features/ide-react/hooks/use-fixed-size-column'
+import useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
+
+export const useSidebarPane = () => {
+  const [isOpen, setIsOpen] = useState(true)
+  const [resizing, setResizing] = useState(false)
+  const { fixedPanelRef, handleLayout } = useFixedSizeColumn(isOpen)
+  useCollapsiblePanel(isOpen, fixedPanelRef)
+
+  const togglePane = useCallback(() => {
+    setIsOpen(value => !value)
+  }, [])
+
+  const handlePaneExpand = useCallback(() => {
+    setIsOpen(true)
+  }, [])
+
+  const handlePaneCollapse = useCallback(() => {
+    setIsOpen(false)
+  }, [])
+
+  return {
+    isOpen,
+    setIsOpen,
+    fixedPanelRef,
+    handleLayout,
+    togglePane,
+    handlePaneExpand,
+    handlePaneCollapse,
+    resizing,
+    setResizing,
+  }
+}

+ 7 - 11
services/web/frontend/stylesheets/app/editor/ide-react.less

@@ -145,16 +145,12 @@
   height: 100%;
   height: 100%;
 }
 }
 
 
-// Hide panel contents while resizing
-.ide-react-main-resizing .ide-react-editor-and-pdf,
-.ide-react-main-content-resizing .ide-react-editor-and-pdf,
-.ide-react-editor-and-pdf-resizing .ide-react-editor-content,
-.ide-react-editor-and-pdf-resizing .pdf {
-  display: none !important;
-}
-
-.ide-react-main-resizing,
-.ide-react-main-content-resizing,
-.ide-react-editor-and-pdf-resizing {
+.ide-panel-group-resizing {
   background-color: white;
   background-color: white;
+
+  // Hide panel contents while resizing
+  .ide-react-editor-content,
+  .pdf {
+    display: none !important;
+  }
 }
 }

+ 1 - 1
services/web/package.json

@@ -321,7 +321,7 @@
     "react-i18next": "^13.3.1",
     "react-i18next": "^13.3.1",
     "react-linkify": "^1.0.0-alpha",
     "react-linkify": "^1.0.0-alpha",
     "react-refresh": "^0.14.0",
     "react-refresh": "^0.14.0",
-    "react-resizable-panels": "^0.0.55",
+    "react-resizable-panels": "^0.0.63",
     "react2angular": "^4.0.6",
     "react2angular": "^4.0.6",
     "react2angular-shared-context": "^1.1.0",
     "react2angular-shared-context": "^1.1.0",
     "requirejs": "^2.3.6",
     "requirejs": "^2.3.6",