mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-05 14:59:19 +00:00
Upgrade react-resizable-panels (#15998)
GitOrigin-RevId: af799f1a5b4945ad2acbb460806d559fae7416b9
This commit is contained in:
parent
df472b607e
commit
d5b3c10cb5
14 changed files with 438 additions and 424 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -34095,9 +34095,9 @@
|
|||
}
|
||||
},
|
||||
"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,
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
|
||||
|
@ -43709,7 +43709,7 @@
|
|||
"react-i18next": "^13.3.1",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-resizable-panels": "^0.0.63",
|
||||
"react2angular": "^4.0.6",
|
||||
"react2angular-shared-context": "^1.1.0",
|
||||
"requirejs": "^2.3.6",
|
||||
|
@ -52297,7 +52297,7 @@
|
|||
"react-i18next": "^13.3.1",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-resizable-panels": "^0.0.63",
|
||||
"react2angular": "^4.0.6",
|
||||
"react2angular-shared-context": "^1.1.0",
|
||||
"recurly": "^4.0.0",
|
||||
|
@ -73465,9 +73465,9 @@
|
|||
}
|
||||
},
|
||||
"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,
|
||||
"requires": {}
|
||||
},
|
||||
|
|
|
@ -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 { 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 classnames from 'classnames'
|
||||
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 type EditorProps = {
|
||||
editorContent: ReactNode
|
||||
}
|
||||
|
||||
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 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 (
|
||||
<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
|
||||
ref={pdfPanelRef}
|
||||
id="pdf"
|
||||
id="panel-pdf"
|
||||
order={2}
|
||||
defaultSize={50}
|
||||
minSize={5}
|
||||
defaultSizePercentage={50}
|
||||
minSizePercentage={25}
|
||||
collapsible
|
||||
onCollapse={collapsed => setPdfIsOpen(!collapsed)}
|
||||
onCollapse={handlePdfPaneCollapse}
|
||||
onExpand={handlePdfPaneExpand}
|
||||
className="ide-react-panel"
|
||||
>
|
||||
{pdfIsOpen || resizing ? <PdfPreview /> : null}
|
||||
{pdfIsOpen && <PdfPreview />}
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
)
|
||||
|
|
|
@ -27,11 +27,12 @@ export default function EditorSidebar({
|
|||
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
|
||||
onInit={onFileTreeInit}
|
||||
onSelect={onFileTreeSelect}
|
||||
|
@ -39,7 +40,7 @@ export default function EditorSidebar({
|
|||
/>
|
||||
</Panel>
|
||||
<VerticalResizeHandle />
|
||||
<Panel defaultSize={25}>
|
||||
<Panel defaultSizePercentage={25} id="panel-outline">
|
||||
<div className="outline-container" />
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
|
|
|
@ -22,33 +22,38 @@ export const EditorPane: FC<{ show: boolean }> = ({ show }) => {
|
|||
)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
|
||||
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'
|
||||
|
||||
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
|
||||
useEditingSessionHeartbeat() // send a batched event when user is active
|
||||
useRegisterUserActivity() // record activity and ensure connection when user is active
|
||||
|
|
|
@ -1,62 +1,149 @@
|
|||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
import { useState } from 'react'
|
||||
import { FC } 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'
|
||||
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()
|
||||
|
||||
useCollapsiblePanel(chatIsOpen, chatPanelRef)
|
||||
const {
|
||||
selectedEntityCount,
|
||||
openEntity,
|
||||
openDocId,
|
||||
handleFileTreeInit,
|
||||
handleFileTreeSelect,
|
||||
handleFileTreeDelete,
|
||||
} = useFileTree()
|
||||
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
// keep the editor pane open
|
||||
const editorPane = openDocId ? (
|
||||
<EditorPane
|
||||
show={openEntity?.type === 'doc' && selectedEntityCount === 1}
|
||||
/>
|
||||
) : null
|
||||
|
||||
return (
|
||||
<div className="ide-react-main">
|
||||
<EditorNavigationToolbar />
|
||||
<div className="ide-react-body">
|
||||
<PanelGroup
|
||||
autoSaveId="ide-react-chat-layout"
|
||||
autoSaveId="ide-outer-layout"
|
||||
direction="horizontal"
|
||||
onLayout={handleLayout}
|
||||
onLayout={handleSidebarLayout}
|
||||
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>
|
||||
{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>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{chatIsOpen && (
|
||||
<>
|
||||
<HorizontalResizeHandle onDragging={setChatResizing} />
|
||||
|
||||
{/* chat */}
|
||||
<Panel
|
||||
ref={chatPanelRef}
|
||||
id="panel-chat"
|
||||
order={2}
|
||||
defaultSizePixels={200}
|
||||
minSizePixels={150}
|
||||
collapsible
|
||||
>
|
||||
<ChatPane />
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
</PanelGroup>
|
||||
</Panel>
|
||||
</PanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 {
|
||||
FileTreeDeleteHandler,
|
||||
FileTreeDocumentFindResult,
|
||||
FileTreeFileRefFindResult,
|
||||
FileTreeFindResult,
|
||||
} 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 { 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 { eventEmitter } = useIdeReactContext()
|
||||
const {
|
||||
|
@ -62,7 +22,6 @@ export function EditorAndSidebar() {
|
|||
currentDocumentId: openDocId,
|
||||
openInitialDoc,
|
||||
} = useEditorManagerContext()
|
||||
const { view } = useLayoutContext()
|
||||
const { projectJoined } = useIdeReactContext()
|
||||
const { selectEntity } = useSelectFileTreeEntity()
|
||||
const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
|
||||
|
@ -145,93 +104,38 @@ 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 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,
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
export function fileViewFile(fileRef: FileRef) {
|
||||
return {
|
||||
...convertFileRefToBinaryFile(fileRef),
|
||||
created: fileRef.created,
|
||||
}
|
||||
|
||||
// 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)} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TwoColumnMainContent
|
||||
leftColumnId="editor-left-column"
|
||||
leftColumnContent={editorSidebar}
|
||||
leftColumnDefaultSize={leftColumnDefaultSize}
|
||||
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
|
||||
rightColumnContent={rightColumnContent}
|
||||
leftColumnIsOpen={leftColumnIsOpen}
|
||||
setLeftColumnIsOpen={setLeftColumnIsOpen}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,32 +1,21 @@
|
|||
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 fixedPanelWidthRef = useRef({ size: defaultSize, pixels: 0 })
|
||||
const fixedPanelSizeRef = useRef<number>(0)
|
||||
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)
|
||||
},
|
||||
[measureFixedPanelSizePixels]
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
|
@ -43,24 +32,26 @@ export default function useFixedSizeColumn(
|
|||
const fixedPanelElement = document.querySelector(
|
||||
`[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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return () => resizeObserver.unobserve(panelGroupElement)
|
||||
}, [fixedPanelRef, measureFixedPanelSizePixels, initialLayoutDone, isOpen])
|
||||
}, [fixedPanelRef, initialLayoutDone, isOpen])
|
||||
|
||||
return {
|
||||
fixedPanelRef,
|
||||
fixedPanelWidthRef,
|
||||
handleLayout,
|
||||
}
|
||||
return { fixedPanelRef, handleLayout }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -145,16 +145,12 @@
|
|||
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;
|
||||
|
||||
// Hide panel contents while resizing
|
||||
.ide-react-editor-content,
|
||||
.pdf {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@
|
|||
"react-i18next": "^13.3.1",
|
||||
"react-linkify": "^1.0.0-alpha",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-resizable-panels": "^0.0.63",
|
||||
"react2angular": "^4.0.6",
|
||||
"react2angular-shared-context": "^1.1.0",
|
||||
"requirejs": "^2.3.6",
|
||||
|
|
Loading…
Reference in a new issue