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