mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[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
This commit is contained in:
parent
a4ca117640
commit
23593f8650
24 changed files with 206 additions and 575 deletions
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
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 (selectedEntityCount > 1) {
|
||||
rightColumnContent = (
|
||||
<EditorAndPdf
|
||||
editorContent={
|
||||
<>
|
||||
{editorPane}
|
||||
<FileView file={fileViewFile(openEntity.entity)} />
|
||||
<MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (editorContent) {
|
||||
rightColumnContent = (
|
||||
<EditorAndPdf
|
||||
editorContent={editorContent}
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
/>
|
||||
)
|
||||
} else if (openEntity) {
|
||||
rightColumnContent = (
|
||||
<EditorAndPdf
|
||||
editorContent={
|
||||
<>
|
||||
{editorPane}
|
||||
{openEntity.type !== 'doc' && (
|
||||
<FileView file={fileViewFile(openEntity.entity)} />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TwoColumnMainContent
|
||||
leftColumnId="editor-left-column"
|
||||
leftColumnContent={leftColumnContent}
|
||||
leftColumnContent={editorSidebar}
|
||||
leftColumnDefaultSize={leftColumnDefaultSize}
|
||||
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
|
||||
rightColumnContent={rightColumnContent}
|
||||
leftColumnIsOpen={leftColumnIsOpen}
|
||||
setLeftColumnIsOpen={setLeftColumnIsOpen}
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
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'
|
||||
|
||||
const symbolPaletteComponents = importOverleafModules(
|
||||
'sourceEditorSymbolPalette'
|
||||
) as { import: { default: ElementType }; path: string }[]
|
||||
const SymbolPalettePane = lazy(
|
||||
() => import('@/features/ide-react/components/editor/symbol-palette-pane')
|
||||
)
|
||||
|
||||
export type EditorPaneProps = {
|
||||
shouldPersistLayout?: boolean
|
||||
show: boolean
|
||||
}
|
||||
|
||||
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" />
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
{t('loading')}…
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function PlaceholderChat() {
|
||||
return (
|
||||
<aside className="chat ide-react-placeholder-chat">Chat placeholder</aside>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function PlaceholderFile() {
|
||||
return <div className="file-view full-size">File placeholder</div>
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import React from 'react'
|
||||
|
||||
export default function PlaceholderPdf() {
|
||||
return <div>PDF</div>
|
||||
}
|
|
@ -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,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])
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue