mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #15271 from overleaf/jpa-lazy-loading
[web] lazy load big optional UI elements GitOrigin-RevId: 18d723c66834be3984b74c3c89cfb46e2fffbfc1
This commit is contained in:
parent
0e52c245ce
commit
83cf21d8cf
18 changed files with 186 additions and 101 deletions
|
@ -1,8 +1,7 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import React, { lazy, Suspense, useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import MessageList from './message-list'
|
||||
import MessageInput from './message-input'
|
||||
import InfiniteScroll from './infinite-scroll'
|
||||
import ChatFallbackError from './chat-fallback-error'
|
||||
|
@ -14,6 +13,8 @@ import { FetchError } from '../../../infrastructure/fetch-json'
|
|||
import { useChatContext } from '../context/chat-context'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
|
||||
const MessageList = lazy(() => import('./message-list'))
|
||||
|
||||
const ChatPane = React.memo(function ChatPane() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -48,6 +49,14 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
0
|
||||
)
|
||||
|
||||
// Keep the chat pane in the DOM to avoid resetting the form input and re-rendering MathJax content.
|
||||
const [chatOpenedOnce, setChatOpenedOnce] = useState(chatIsOpen)
|
||||
useEffect(() => {
|
||||
if (chatIsOpen) {
|
||||
setChatOpenedOnce(true)
|
||||
}
|
||||
}, [chatIsOpen])
|
||||
|
||||
if (error) {
|
||||
// let user try recover from fetch errors
|
||||
if (error instanceof FetchError) {
|
||||
|
@ -59,6 +68,9 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
if (!user) {
|
||||
return null
|
||||
}
|
||||
if (!chatOpenedOnce) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="chat">
|
||||
|
@ -71,13 +83,15 @@ const ChatPane = React.memo(function ChatPane() {
|
|||
>
|
||||
<div>
|
||||
<h2 className="sr-only">{t('chat')}</h2>
|
||||
{status === 'pending' && <LoadingSpinner delay={500} />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
userId={user.id}
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
/>
|
||||
<Suspense fallback={<LoadingSpinner delay={500} />}>
|
||||
{status === 'pending' && <LoadingSpinner delay={500} />}
|
||||
{shouldDisplayPlaceholder && <Placeholder />}
|
||||
<MessageList
|
||||
messages={messages}
|
||||
userId={user.id}
|
||||
resetUnreadMessages={markMessagesAsRead}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
<MessageInput
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import DownloadMenu from './download-menu'
|
||||
import ActionsMenu from './actions-menu'
|
||||
import HelpMenu from './help-menu'
|
||||
import SyncMenu from './sync-menu'
|
||||
import SettingsMenu from './settings-menu'
|
||||
|
||||
export default function EditorLeftMenuBody() {
|
||||
return (
|
||||
<>
|
||||
<DownloadMenu />
|
||||
<ActionsMenu />
|
||||
<SyncMenu />
|
||||
<SettingsMenu />
|
||||
<HelpMenu />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
import DownloadMenu from './download-menu'
|
||||
import ActionsMenu from './actions-menu'
|
||||
import HelpMenu from './help-menu'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import SyncMenu from './sync-menu'
|
||||
import SettingsMenu from './settings-menu'
|
||||
import LeftMenuMask from './left-menu-mask'
|
||||
import AccessibleModal from '../../../shared/components/accessible-modal'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import classNames from 'classnames'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
const EditorLeftMenuBody = lazy(() => import('./editor-left-menu-body'))
|
||||
|
||||
export default function EditorLeftMenu() {
|
||||
const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
|
||||
|
@ -29,11 +27,9 @@ export default function EditorLeftMenu() {
|
|||
className={classNames('full-size', { shown: leftMenuShown })}
|
||||
id="left-menu"
|
||||
>
|
||||
<DownloadMenu />
|
||||
<ActionsMenu />
|
||||
<SyncMenu />
|
||||
<SettingsMenu />
|
||||
<HelpMenu />
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<EditorLeftMenuBody />
|
||||
</Suspense>
|
||||
</Modal.Body>
|
||||
</AccessibleModal>
|
||||
{leftMenuShown && <LeftMenuMask />}
|
||||
|
|
|
@ -2,16 +2,19 @@ import { useTranslation } from 'react-i18next'
|
|||
import FileTreeCreateNewDoc from './modes/file-tree-create-new-doc'
|
||||
import FileTreeImportFromUrl from './modes/file-tree-import-from-url'
|
||||
import FileTreeImportFromProject from './modes/file-tree-import-from-project'
|
||||
import FileTreeUploadDoc from './modes/file-tree-upload-doc'
|
||||
import FileTreeModalCreateFileMode from './file-tree-modal-create-file-mode'
|
||||
import FileTreeCreateNameProvider from '../../contexts/file-tree-create-name'
|
||||
import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
|
||||
import { useFileTreeData } from '../../../../shared/context/file-tree-data-context'
|
||||
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
|
||||
const createFileModeModules = importOverleafModules('createFileModes')
|
||||
|
||||
const FileTreeUploadDoc = lazy(() => import('./modes/file-tree-upload-doc'))
|
||||
|
||||
export default function FileTreeModalCreateFileBody() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -86,7 +89,11 @@ export default function FileTreeModalCreateFileBody() {
|
|||
</FileTreeCreateNameProvider>
|
||||
)}
|
||||
|
||||
{newFileCreateMode === 'upload' && <FileTreeUploadDoc />}
|
||||
{newFileCreateMode === 'upload' && (
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<FileTreeUploadDoc />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
{createFileModeModules.map(
|
||||
({ import: { CreateFilePane }, path }) => (
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { useHistoryContext } from '@/features/history/context/history-context'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
import DiffView from '@/features/history/components/diff-view/diff-view'
|
||||
import ChangeList from '@/features/history/components/change-list/change-list'
|
||||
import { createPortal } from 'react-dom'
|
||||
import HistoryFileTree from '@/features/history/components/history-file-tree'
|
||||
|
||||
const fileTreeContainer = document.getElementById('history-file-tree')
|
||||
|
||||
export default function HistoryContent() {
|
||||
const { updatesInfo } = useHistoryContext()
|
||||
|
||||
let content
|
||||
if (updatesInfo.loadingState === 'loadingInitial') {
|
||||
content = <LoadingSpinner />
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<DiffView />
|
||||
<ChangeList />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileTreeContainer
|
||||
? createPortal(<HistoryFileTree />, fileTreeContainer)
|
||||
: null}
|
||||
<div className="history-react">{content}</div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,42 +1,23 @@
|
|||
import ChangeList from './change-list/change-list'
|
||||
import DiffView from './diff-view/diff-view'
|
||||
import { HistoryProvider, useHistoryContext } from '../context/history-context'
|
||||
import { HistoryProvider } from '../context/history-context'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import { createPortal } from 'react-dom'
|
||||
import HistoryFileTree from './history-file-tree'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
||||
import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { lazy, Suspense } from 'react'
|
||||
|
||||
const fileTreeContainer = document.getElementById('history-file-tree')
|
||||
const HistoryContent = lazy(() => import('./history-content'))
|
||||
|
||||
function Main() {
|
||||
const { view } = useLayoutContext()
|
||||
const { updatesInfo } = useHistoryContext()
|
||||
|
||||
if (view !== 'history') {
|
||||
return null
|
||||
}
|
||||
|
||||
let content
|
||||
if (updatesInfo.loadingState === 'loadingInitial') {
|
||||
content = <LoadingSpinner />
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<DiffView />
|
||||
<ChangeList />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileTreeContainer
|
||||
? createPortal(<HistoryFileTree />, fileTreeContainer)
|
||||
: null}
|
||||
<div className="history-react">{content}</div>
|
||||
</>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<HistoryContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { memo, Suspense } from 'react'
|
|||
import classNames from 'classnames'
|
||||
import PdfLogsViewer from './pdf-logs-viewer'
|
||||
import PdfViewer from './pdf-viewer'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
||||
import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import FasterCompilesFeedback from './faster-compiles-feedback'
|
||||
|
@ -27,7 +27,7 @@ function PdfPreviewPane() {
|
|||
<CompileTimeWarning />
|
||||
)}
|
||||
</PdfPreviewMessages>
|
||||
<Suspense fallback={<LoadingPreview />}>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<div className="pdf-viewer">
|
||||
<PdfViewer />
|
||||
<FasterCompilesFeedback />
|
||||
|
@ -39,12 +39,4 @@ function PdfPreviewPane() {
|
|||
)
|
||||
}
|
||||
|
||||
function LoadingPreview() {
|
||||
return (
|
||||
<div className="pdf-loading-spinner-container">
|
||||
<LoadingSpinner delay={500} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PdfPreviewPane)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import BlankProjectModal from './blank-project-modal'
|
||||
import ExampleProjectModal from './example-project-modal'
|
||||
import UploadProjectModal from './upload-project-modal'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import { JSXElementConstructor, lazy, Suspense } from 'react'
|
||||
import { Nullable } from '../../../../../../types/utils'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
|
||||
const UploadProjectModal = lazy(() => import('./upload-project-modal'))
|
||||
|
||||
export type NewProjectButtonModalVariant =
|
||||
| 'blank_project'
|
||||
|
@ -30,7 +32,11 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
|
|||
case 'example_project':
|
||||
return <ExampleProjectModal onHide={onHide} />
|
||||
case 'upload_project':
|
||||
return <UploadProjectModal onHide={onHide} />
|
||||
return (
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<UploadProjectModal onHide={onHide} />
|
||||
</Suspense>
|
||||
)
|
||||
case 'import_from_github':
|
||||
return <ImportProjectFromGithubModalWrapper onHide={onHide} />
|
||||
default:
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import { Button, Modal, Grid } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import ShareModalBody from './share-modal-body'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import AccessibleModal from '../../../shared/components/accessible-modal'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReadOnlyTokenLink } from './link-sharing'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
|
||||
const ReadOnlyTokenLink = lazy(() =>
|
||||
import('./link-sharing').then(({ ReadOnlyTokenLink }) => ({
|
||||
// re-export as default -- lazy can only handle default exports.
|
||||
default: ReadOnlyTokenLink,
|
||||
}))
|
||||
)
|
||||
|
||||
const ShareModalBody = lazy(() => import('./share-modal-body'))
|
||||
|
||||
export default function ShareProjectModalContent({
|
||||
show,
|
||||
|
@ -28,7 +37,13 @@ export default function ShareProjectModalContent({
|
|||
|
||||
<Modal.Body className="modal-body-share">
|
||||
<Grid fluid>
|
||||
{isRestrictedTokenMember ? <ReadOnlyTokenLink /> : <ShareModalBody />}
|
||||
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
|
||||
{isRestrictedTokenMember ? (
|
||||
<ReadOnlyTokenLink />
|
||||
) : (
|
||||
<ShareModalBody />
|
||||
)}
|
||||
</Suspense>
|
||||
</Grid>
|
||||
</Modal.Body>
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ const sourceModes = new Map([
|
|||
[FigureModalSource.EDIT_FIGURE, FigureModalEditFigureSource],
|
||||
])
|
||||
|
||||
export const FigureModalBody = () => {
|
||||
export default function FigureModalBody() {
|
||||
const { source, helpShown, sourcePickerShown, error, dispatch } =
|
||||
useFigureModalContext()
|
||||
const Body = sourceModes.get(source)
|
||||
|
|
|
@ -6,9 +6,8 @@ import {
|
|||
useFigureModalContext,
|
||||
useFigureModalExistingFigureContext,
|
||||
} from './figure-modal-context'
|
||||
import { FigureModalBody } from './figure-modal-body'
|
||||
import { FigureModalFooter } from './figure-modal-footer'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { lazy, memo, Suspense, useCallback, useEffect } from 'react'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
||||
import { ChangeSpec } from '@codemirror/state'
|
||||
import {
|
||||
|
@ -22,6 +21,9 @@ import { useTranslation } from 'react-i18next'
|
|||
import useEventListener from '../../../../shared/hooks/use-event-listener'
|
||||
import { prepareLines } from '../../utils/prepare-lines'
|
||||
import { FeedbackBadge } from '@/shared/components/feedback-badge'
|
||||
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
|
||||
|
||||
const FigureModalBody = lazy(() => import('./figure-modal-body'))
|
||||
|
||||
export const FigureModal = memo(function FigureModal() {
|
||||
return (
|
||||
|
@ -277,7 +279,9 @@ const FigureModalContent = () => {
|
|||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<FigureModalBody />
|
||||
<Suspense fallback={<FullSizeLoadingSpinner minHeight="15rem" />}>
|
||||
<FigureModalBody />
|
||||
</Suspense>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { lazy, memo, Suspense } from 'react'
|
||||
import LoadingSpinner from '../../../shared/components/loading-spinner'
|
||||
import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner'
|
||||
import withErrorBoundary from '../../../infrastructure/error-boundary'
|
||||
import { ErrorBoundaryFallback } from '../../../shared/components/error-boundary-fallback'
|
||||
|
||||
|
@ -10,13 +10,7 @@ const CodeMirrorEditor = lazy(
|
|||
|
||||
function SourceEditor() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="pdf-loading-spinner-container">
|
||||
<LoadingSpinner delay={500} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
|
||||
<CodeMirrorEditor />
|
||||
</Suspense>
|
||||
)
|
||||
|
|
|
@ -36,3 +36,22 @@ LoadingSpinner.propTypes = {
|
|||
}
|
||||
|
||||
export default LoadingSpinner
|
||||
|
||||
export function FullSizeLoadingSpinner({
|
||||
delay = 0,
|
||||
minHeight,
|
||||
}: {
|
||||
delay?: number
|
||||
minHeight?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="full-size-loading-spinner-container" style={{ minHeight }}>
|
||||
<LoadingSpinner delay={delay} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
FullSizeLoadingSpinner.propTypes = {
|
||||
delay: PropTypes.number,
|
||||
minHeight: PropTypes.string,
|
||||
}
|
|
@ -41,6 +41,7 @@
|
|||
@import 'components/notifications.less';
|
||||
@import 'components/pager.less';
|
||||
@import 'components/labels.less';
|
||||
@import 'components/loading-spinner';
|
||||
//@import "components/jumbotron.less";
|
||||
@import 'components/thumbnails.less';
|
||||
@import 'components/alerts.less';
|
||||
|
|
|
@ -582,14 +582,6 @@
|
|||
);
|
||||
}
|
||||
|
||||
.pdf-loading-spinner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-secondary-compile-timeout-override {
|
||||
color: #1b222c;
|
||||
background-color: #ffffff;
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.full-size-loading-spinner-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
|
@ -55,6 +55,7 @@
|
|||
@import 'components/footer.less';
|
||||
@import 'components/notifications.less';
|
||||
@import 'components/labels.less';
|
||||
@import 'components/loading-spinner';
|
||||
@import 'components/thumbnails.less';
|
||||
@import 'components/alerts.less';
|
||||
@import 'components/progress-bars.less';
|
||||
|
|
|
@ -382,14 +382,16 @@ describe('<FileTreeModalCreateFile/>', function () {
|
|||
// the submit button should not be present
|
||||
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
|
||||
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
await waitFor(() => {
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
|
||||
expect(dropzone).not.to.be.null
|
||||
expect(dropzone).not.to.be.null
|
||||
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
|
||||
},
|
||||
fireEvent.drop(dropzone, {
|
||||
dataTransfer: {
|
||||
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => expect(requests).to.have.length(1))
|
||||
|
@ -415,14 +417,16 @@ describe('<FileTreeModalCreateFile/>', function () {
|
|||
// the submit button should not be present
|
||||
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
|
||||
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
await waitFor(() => {
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
|
||||
expect(dropzone).not.to.be.null
|
||||
expect(dropzone).not.to.be.null
|
||||
|
||||
fireEvent.paste(dropzone, {
|
||||
clipboardData: {
|
||||
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
|
||||
},
|
||||
fireEvent.paste(dropzone, {
|
||||
clipboardData: {
|
||||
files: [new File(['test'], 'test.tex', { type: 'text/plain' })],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => expect(requests).to.have.length(1))
|
||||
|
@ -448,14 +452,16 @@ describe('<FileTreeModalCreateFile/>', function () {
|
|||
// the submit button should not be present
|
||||
expect(screen.queryByRole('button', { name: 'Create' })).to.be.null
|
||||
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
await waitFor(() => {
|
||||
const dropzone = screen.getByLabelText('File Uploader')
|
||||
|
||||
expect(dropzone).not.to.be.null
|
||||
expect(dropzone).not.to.be.null
|
||||
|
||||
fireEvent.paste(dropzone, {
|
||||
clipboardData: {
|
||||
files: [new File(['test'], 'tes!t.tex', { type: 'text/plain' })],
|
||||
},
|
||||
fireEvent.paste(dropzone, {
|
||||
clipboardData: {
|
||||
files: [new File(['test'], 'tes!t.tex', { type: 'text/plain' })],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => expect(requests).to.have.length(1))
|
||||
|
|
Loading…
Reference in a new issue