Merge pull request #15376 from overleaf/td-ide-page-working-editor
React IDE page: working editor GitOrigin-RevId: 3ba8cb787a6f7f8435686d8962adb7444d09acb5
This commit is contained in:
@@ -3,6 +3,7 @@ import { LostConnectionAlert } from './lost-connection-alert'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { debugging } from '@/utils/debugging'
|
||||
import { Alert } from 'react-bootstrap'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
|
||||
// TODO SavingNotificationController, SystemMessagesController, out-of-sync modal
|
||||
export function Alerts() {
|
||||
@@ -15,8 +16,7 @@ export function Alerts() {
|
||||
secondsUntilReconnect,
|
||||
} = useConnectionContext()
|
||||
|
||||
// TODO: Get this from a context
|
||||
const synctexError = false
|
||||
const [synctexError] = useScopeValue('sync_tex_error')
|
||||
|
||||
return (
|
||||
<div className="global-alerts">
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
|
||||
import Editor from '@/features/ide-react/components/editor/editor'
|
||||
import EditorSidebar from '@/features/ide-react/components/editor-sidebar'
|
||||
import customLocalStorage from '@/infrastructure/local-storage'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
|
||||
|
||||
type EditorAndSidebarProps = {
|
||||
shouldPersistLayout: boolean
|
||||
leftColumnDefaultSize: number
|
||||
setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
|
||||
}
|
||||
|
||||
export function EditorAndSidebar({
|
||||
shouldPersistLayout,
|
||||
leftColumnDefaultSize,
|
||||
setLeftColumnDefaultSize,
|
||||
}: EditorAndSidebarProps) {
|
||||
const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
|
||||
const { rootDocId, _id: projectId } = useProjectContext()
|
||||
|
||||
const [openDocId, setOpenDocId] = useState(
|
||||
() => customLocalStorage.getItem(`doc.open_id.${projectId}`) || rootDocId
|
||||
)
|
||||
const [fileTreeReady, setFileTreeReady] = useState(false)
|
||||
|
||||
const handleFileTreeInit = useCallback(() => {
|
||||
setFileTreeReady(true)
|
||||
}, [])
|
||||
|
||||
const handleFileTreeSelect = useCallback(
|
||||
(selectedEntities: FileTreeFindResult[]) => {
|
||||
const firstDocId =
|
||||
selectedEntities.find(result => result.type === 'doc')?.entity._id ||
|
||||
null
|
||||
setOpenDocId(firstDocId)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const leftColumnContent = (
|
||||
<EditorSidebar
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
onFileTreeInit={handleFileTreeInit}
|
||||
onFileTreeSelect={handleFileTreeSelect}
|
||||
/>
|
||||
)
|
||||
const rightColumnContent = (
|
||||
<Editor
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
openDocId={openDocId}
|
||||
fileTreeReady={fileTreeReady}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<TwoColumnMainContent
|
||||
leftColumnId="editor-left-column"
|
||||
leftColumnContent={leftColumnContent}
|
||||
leftColumnDefaultSize={leftColumnDefaultSize}
|
||||
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
|
||||
rightColumnContent={rightColumnContent}
|
||||
leftColumnIsOpen={leftColumnIsOpen}
|
||||
setLeftColumnIsOpen={setLeftColumnIsOpen}
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
|
||||
import { FileTree } from '@/features/ide-react/components/file-tree'
|
||||
import { FileTreeSelectHandler } from '@/features/ide-react/types/file-tree'
|
||||
|
||||
type EditorSidebarProps = {
|
||||
shouldPersistLayout: boolean
|
||||
onFileTreeInit: () => void
|
||||
onFileTreeSelect: FileTreeSelectHandler
|
||||
}
|
||||
|
||||
export default function EditorSidebar({
|
||||
shouldPersistLayout,
|
||||
onFileTreeInit,
|
||||
onFileTreeSelect,
|
||||
}: EditorSidebarProps) {
|
||||
return (
|
||||
<aside className="ide-react-placeholder-editor-sidebar">
|
||||
<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} />
|
||||
</Panel>
|
||||
<VerticalResizeHandle />
|
||||
<Panel defaultSize={25}>File outline placeholder</Panel>
|
||||
</PanelGroup>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
|
||||
import React, { ElementType, useEffect } from 'react'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import SourceEditor from '@/features/source-editor/components/source-editor'
|
||||
import {
|
||||
EditorScopeValue,
|
||||
useEditorManagerContext,
|
||||
} from '@/features/ide-react/context/editor-manager-context'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import { EditorProps } from '@/features/ide-react/components/editor/editor'
|
||||
|
||||
const symbolPaletteComponents = importOverleafModules(
|
||||
'sourceEditorSymbolPalette'
|
||||
) as { import: { default: ElementType }; path: string }[]
|
||||
|
||||
export function EditorPane({
|
||||
shouldPersistLayout,
|
||||
openDocId,
|
||||
fileTreeReady,
|
||||
}: EditorProps) {
|
||||
const { openDocId: openDocWithId } = useEditorManagerContext()
|
||||
|
||||
const [editor] = useScopeValue<EditorScopeValue>('editor')
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileTreeReady || !openDocId) {
|
||||
return
|
||||
}
|
||||
openDocWithId(openDocId)
|
||||
}, [fileTreeReady, openDocId, openDocWithId])
|
||||
|
||||
return (
|
||||
<PanelGroup
|
||||
autoSaveId={
|
||||
shouldPersistLayout
|
||||
? 'ide-react-editor-and-symbol-palette-layout'
|
||||
: undefined
|
||||
}
|
||||
direction="vertical"
|
||||
units="pixels"
|
||||
>
|
||||
<Panel
|
||||
id="editor"
|
||||
order={1}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{!!editor.sharejs_doc &&
|
||||
!editor.opening &&
|
||||
editor.multiSelectedCount === 0 &&
|
||||
!editor.error_state ? (
|
||||
<SourceEditor />
|
||||
) : null}
|
||||
</Panel>
|
||||
{editor.showSymbolPalette ? (
|
||||
<>
|
||||
<VerticalResizeHandle id="editor-symbol-palette" />
|
||||
<Panel
|
||||
id="symbol-palette"
|
||||
order={2}
|
||||
defaultSize={250}
|
||||
minSize={250}
|
||||
maxSize={336}
|
||||
>
|
||||
<div className="ide-react-placeholder-symbol-palette">
|
||||
{symbolPaletteComponents.map(
|
||||
({ import: { default: Component }, path }) => (
|
||||
<Component key={path} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
) : null}
|
||||
</PanelGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useRef } 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 useCollapsiblePanel from '@/features/ide-react/hooks/use-collapsible-panel'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
|
||||
import PlaceholderFile from '@/features/ide-react/components/layout/placeholder/placeholder-file'
|
||||
import PlaceholderPdf from '@/features/ide-react/components/layout/placeholder/placeholder-pdf'
|
||||
|
||||
export type EditorProps = {
|
||||
shouldPersistLayout?: boolean
|
||||
openDocId: string | null
|
||||
fileTreeReady: boolean
|
||||
}
|
||||
|
||||
export default function Editor({
|
||||
shouldPersistLayout = false,
|
||||
openDocId,
|
||||
fileTreeReady,
|
||||
}: EditorProps) {
|
||||
const { t } = useTranslation()
|
||||
const { view, pdfLayout, changeLayout } = useLayoutContext()
|
||||
|
||||
const pdfPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
const isDualPane = pdfLayout === 'sideBySide'
|
||||
const editorIsVisible = isDualPane || view === 'editor'
|
||||
const pdfIsOpen = isDualPane || view === 'pdf'
|
||||
|
||||
useCollapsiblePanel(pdfIsOpen, pdfPanelRef)
|
||||
|
||||
if (view === 'file') {
|
||||
return <PlaceholderFile />
|
||||
}
|
||||
|
||||
if (view === 'history') {
|
||||
return null
|
||||
}
|
||||
|
||||
function setPdfIsOpen(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
changeLayout('sideBySide')
|
||||
} else {
|
||||
changeLayout('flat', 'editor')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelGroup
|
||||
autoSaveId={
|
||||
shouldPersistLayout ? 'ide-react-editor-and-pdf-layout' : undefined
|
||||
}
|
||||
direction="horizontal"
|
||||
>
|
||||
{editorIsVisible ? (
|
||||
<Panel id="editor" order={1} defaultSize={50}>
|
||||
<EditorPane
|
||||
shouldPersistLayout={shouldPersistLayout}
|
||||
openDocId={openDocId}
|
||||
fileTreeReady={fileTreeReady}
|
||||
/>
|
||||
</Panel>
|
||||
) : null}
|
||||
{isDualPane ? (
|
||||
<HorizontalResizeHandle>
|
||||
<HorizontalToggler
|
||||
id="editor-pdf"
|
||||
togglerType="east"
|
||||
isOpen={pdfIsOpen}
|
||||
setIsOpen={isOpen => setPdfIsOpen(isOpen)}
|
||||
tooltipWhenOpen={t('tooltip_hide_pdf')}
|
||||
tooltipWhenClosed={t('tooltip_show_pdf')}
|
||||
/>
|
||||
</HorizontalResizeHandle>
|
||||
) : null}
|
||||
{pdfIsOpen ? (
|
||||
<Panel
|
||||
ref={pdfPanelRef}
|
||||
id="pdf"
|
||||
order={2}
|
||||
defaultSize={50}
|
||||
minSize={5}
|
||||
collapsible
|
||||
onCollapse={collapsed => setPdfIsOpen(!collapsed)}
|
||||
>
|
||||
<PlaceholderPdf />
|
||||
</Panel>
|
||||
) : null}
|
||||
</PanelGroup>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import FileTreeRoot from '@/features/file-tree/components/file-tree-root'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
import { useReferencesContext } from '@/features/ide-react/context/references-context'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { FileTreeSelectHandler } from '@/features/ide-react/types/file-tree'
|
||||
import { RefProviders } from '../../../../../types/user'
|
||||
|
||||
type FileTreeProps = {
|
||||
onInit: () => void
|
||||
onSelect: FileTreeSelectHandler
|
||||
}
|
||||
|
||||
export function FileTree({ onInit, onSelect }: FileTreeProps) {
|
||||
const user = useUserContext()
|
||||
const { indexAllReferences } = useReferencesContext()
|
||||
const { setStartedFreeTrial } = useIdeReactContext()
|
||||
const { isConnected } = useConnectionContext()
|
||||
|
||||
const [refProviders, setRefProviders] = useState<RefProviders>(
|
||||
() => user.refProviders || {}
|
||||
)
|
||||
|
||||
function reindexReferences() {
|
||||
indexAllReferences(true)
|
||||
}
|
||||
|
||||
const setRefProviderEnabled = useCallback(
|
||||
(provider: keyof RefProviders, value = true) => {
|
||||
setRefProviders(refProviders => ({ ...refProviders, [provider]: value }))
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="file-tree">
|
||||
<FileTreeRoot
|
||||
refProviders={refProviders}
|
||||
reindexReferences={reindexReferences}
|
||||
setRefProviderEnabled={setRefProviderEnabled}
|
||||
setStartedFreeTrial={setStartedFreeTrial}
|
||||
isConnected={isConnected}
|
||||
onInit={onInit}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import ChatToggleButton from '@/features/editor-navigation-toolbar/components/chat-toggle-button'
|
||||
import HistoryToggleButton from '@/features/editor-navigation-toolbar/components/history-toggle-button'
|
||||
import LayoutDropdownButton from '@/features/editor-navigation-toolbar/components/layout-dropdown-button'
|
||||
import MenuButton from '@/features/editor-navigation-toolbar/components/menu-button'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import OnlineUsersWidget from '@/features/editor-navigation-toolbar/components/online-users-widget'
|
||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
|
||||
type HeaderProps = {
|
||||
chatIsOpen: boolean
|
||||
setChatIsOpen: (chatIsOpen: boolean) => void
|
||||
historyIsOpen: boolean
|
||||
setHistoryIsOpen: (historyIsOpen: boolean) => void
|
||||
}
|
||||
|
||||
export default function Header({
|
||||
chatIsOpen,
|
||||
setChatIsOpen,
|
||||
historyIsOpen,
|
||||
setHistoryIsOpen,
|
||||
}: HeaderProps) {
|
||||
const { setLeftMenuShown } = useLayoutContext()
|
||||
const { onlineUsersArray } = useOnlineUsersContext()
|
||||
|
||||
function toggleChatOpen() {
|
||||
setChatIsOpen(!chatIsOpen)
|
||||
}
|
||||
|
||||
function toggleHistoryOpen() {
|
||||
setHistoryIsOpen(!historyIsOpen)
|
||||
}
|
||||
|
||||
const handleShowLeftMenuClick = useCallback(() => {
|
||||
sendMB('navigation-clicked-menu')
|
||||
setLeftMenuShown(value => !value)
|
||||
}, [setLeftMenuShown])
|
||||
|
||||
return (
|
||||
<header className="toolbar toolbar-header">
|
||||
<div className="toolbar-left">
|
||||
<MenuButton onClick={handleShowLeftMenuClick} />
|
||||
</div>
|
||||
<div className="toolbar-right">
|
||||
<OnlineUsersWidget
|
||||
onlineUsers={onlineUsersArray}
|
||||
goToUser={() => alert('Not implemented')}
|
||||
/>
|
||||
<LayoutDropdownButton />
|
||||
<HistoryToggleButton
|
||||
historyIsOpen={historyIsOpen}
|
||||
onClick={toggleHistoryOpen}
|
||||
/>
|
||||
<ChatToggleButton
|
||||
chatIsOpen={chatIsOpen}
|
||||
onClick={toggleChatOpen}
|
||||
unreadMessageCount={0}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,22 @@
|
||||
import LayoutWithPlaceholders from '@/features/ide-react/components/layout/layout-with-placeholders'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Alerts } from '@/features/ide-react/components/alerts/alerts'
|
||||
import { useLayoutContext } from '@/shared/context/layout-context'
|
||||
import PlaceholderChat from '@/features/ide-react/components/layout/placeholder/placeholder-chat'
|
||||
import PlaceholderHistory from '@/features/ide-react/components/layout/placeholder/placeholder-history'
|
||||
import MainLayout from '@/features/ide-react/components/layout/main-layout'
|
||||
import { EditorAndSidebar } from '@/features/ide-react/components/editor-and-sidebar'
|
||||
import Header from '@/features/ide-react/components/header'
|
||||
import EditorLeftMenu from '@/features/editor-left-menu/components/editor-left-menu'
|
||||
import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
|
||||
|
||||
// This is filled with placeholder content while the real content is migrated
|
||||
// away from Angular
|
||||
export default function IdePage() {
|
||||
useLayoutEventTracking()
|
||||
|
||||
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
|
||||
const { registerUserActivity } = useConnectionContext()
|
||||
|
||||
// Inform the connection manager when the user is active
|
||||
@@ -22,11 +32,50 @@ export default function IdePage() {
|
||||
return () => document.body.removeEventListener('click', listener)
|
||||
}, [listener])
|
||||
|
||||
const { chatIsOpen, setChatIsOpen, view, setView } = useLayoutContext()
|
||||
const historyIsOpen = view === 'history'
|
||||
const setHistoryIsOpen = useCallback(
|
||||
(historyIsOpen: boolean) => {
|
||||
setView(historyIsOpen ? 'history' : 'editor')
|
||||
},
|
||||
[setView]
|
||||
)
|
||||
|
||||
const headerContent = (
|
||||
<Header
|
||||
chatIsOpen={chatIsOpen}
|
||||
setChatIsOpen={setChatIsOpen}
|
||||
historyIsOpen={historyIsOpen}
|
||||
setHistoryIsOpen={setHistoryIsOpen}
|
||||
/>
|
||||
)
|
||||
const chatContent = <PlaceholderChat />
|
||||
|
||||
const mainContent = historyIsOpen ? (
|
||||
<PlaceholderHistory
|
||||
shouldPersistLayout
|
||||
leftColumnDefaultSize={leftColumnDefaultSize}
|
||||
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
|
||||
/>
|
||||
) : (
|
||||
<EditorAndSidebar
|
||||
leftColumnDefaultSize={leftColumnDefaultSize}
|
||||
setLeftColumnDefaultSize={setLeftColumnDefaultSize}
|
||||
shouldPersistLayout
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alerts />
|
||||
{/* TODO: Left menu will go here */}
|
||||
<LayoutWithPlaceholders shouldPersistLayout />
|
||||
<EditorLeftMenu />
|
||||
<MainLayout
|
||||
headerContent={headerContent}
|
||||
chatContent={chatContent}
|
||||
mainContent={mainContent}
|
||||
chatIsOpen={chatIsOpen}
|
||||
shouldPersistLayout
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@ import React from 'react'
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels'
|
||||
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
|
||||
|
||||
type PlaceholderHeaderProps = {
|
||||
type PlaceholderEditorSidebarProps = {
|
||||
shouldPersistLayout: boolean
|
||||
}
|
||||
|
||||
export default function PlaceholderEditorSidebar({
|
||||
shouldPersistLayout,
|
||||
}: PlaceholderHeaderProps) {
|
||||
}: PlaceholderEditorSidebarProps) {
|
||||
return (
|
||||
<aside className="ide-react-placeholder-editor-sidebar">
|
||||
<PanelGroup
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function PlaceholderFile() {
|
||||
return <div className="file-view full-size">File placeholder</div>
|
||||
}
|
||||
+3
-1
@@ -1,12 +1,13 @@
|
||||
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'
|
||||
import LayoutDropdownButton from '@/features/editor-navigation-toolbar/components/layout-dropdown-button'
|
||||
|
||||
type PlaceholderHeaderProps = {
|
||||
chatIsOpen: boolean
|
||||
setChatIsOpen: (chatIsOpen: boolean) => void
|
||||
historyIsOpen: boolean
|
||||
setHistoryIsOpen: (chatIsOpen: boolean) => void
|
||||
setHistoryIsOpen: (historyIsOpen: boolean) => void
|
||||
}
|
||||
|
||||
export default function PlaceholderHeader({
|
||||
@@ -27,6 +28,7 @@ export default function PlaceholderHeader({
|
||||
<header className="toolbar toolbar-header">
|
||||
<div className="toolbar-left">Header placeholder</div>
|
||||
<div className="toolbar-right">
|
||||
<LayoutDropdownButton />
|
||||
<HistoryToggleButton
|
||||
historyIsOpen={historyIsOpen}
|
||||
onClick={toggleHistoryOpen}
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function PlaceholderPdf() {
|
||||
return <div>PDF</div>
|
||||
}
|
||||
Reference in New Issue
Block a user