Merge pull request #15376 from overleaf/td-ide-page-working-editor

React IDE page: working editor

GitOrigin-RevId: 3ba8cb787a6f7f8435686d8962adb7444d09acb5
This commit is contained in:
Tim Down
2023-10-27 08:03:07 +00:00
committed by Copybot
parent a59e63846d
commit 01439641ca
48 changed files with 3622 additions and 62 deletions
@@ -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,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
@@ -0,0 +1,5 @@
import React from 'react'
export default function PlaceholderFile() {
return <div className="file-view full-size">File placeholder</div>
}
@@ -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}
@@ -0,0 +1,5 @@
import React from 'react'
export default function PlaceholderPdf() {
return <div>PDF</div>
}