Просмотр исходного кода

Merge pull request #15271 from overleaf/jpa-lazy-loading

[web] lazy load big optional UI elements

GitOrigin-RevId: 18d723c66834be3984b74c3c89cfb46e2fffbfc1
Jakob Ackermann 2 лет назад
Родитель
Сommit
83cf21d8cf
18 измененных файлов с 186 добавлено и 101 удалено
  1. 23 9
      services/web/frontend/js/features/chat/components/chat-pane.jsx
  2. 17 0
      services/web/frontend/js/features/editor-left-menu/components/editor-left-menu-body.tsx
  3. 6 10
      services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx
  4. 9 2
      services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx
  5. 33 0
      services/web/frontend/js/features/history/components/history-content.tsx
  6. 7 26
      services/web/frontend/js/features/history/components/history-root.tsx
  7. 2 10
      services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx
  8. 9 3
      services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx
  9. 18 3
      services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx
  10. 1 1
      services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx
  11. 7 3
      services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx
  12. 2 8
      services/web/frontend/js/features/source-editor/components/source-editor.tsx
  13. 19 0
      services/web/frontend/js/shared/components/loading-spinner.tsx
  14. 1 0
      services/web/frontend/stylesheets/_style_includes.less
  15. 0 8
      services/web/frontend/stylesheets/app/editor/pdf.less
  16. 7 0
      services/web/frontend/stylesheets/components/loading-spinner.less
  17. 1 0
      services/web/frontend/stylesheets/main-style.less
  18. 24 18
      services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.jsx

+ 23 - 9
services/web/frontend/js/features/chat/components/chat-pane.jsx

@@ -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} />}
-          {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>
         </div>
       </InfiniteScroll>
       </InfiniteScroll>
       <MessageInput
       <MessageInput

+ 17 - 0
services/web/frontend/js/features/editor-left-menu/components/editor-left-menu-body.tsx

@@ -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 />
+    </>
+  )
+}

+ 6 - 10
services/web/frontend/js/features/editor-left-menu/components/editor-left-menu.tsx

@@ -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 />
-          <ActionsMenu />
-          <SyncMenu />
-          <SettingsMenu />
-          <HelpMenu />
+          <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
+            <EditorLeftMenuBody />
+          </Suspense>
         </Modal.Body>
         </Modal.Body>
       </AccessibleModal>
       </AccessibleModal>
       {leftMenuShown && <LeftMenuMask />}
       {leftMenuShown && <LeftMenuMask />}

+ 9 - 2
services/web/frontend/js/features/file-tree/components/file-tree-create/file-tree-modal-create-file-body.jsx

@@ -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 }) => (

+ 33 - 0
services/web/frontend/js/features/history/components/history-content.tsx

@@ -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>
+    </>
+  )
+}

+ 7 - 26
services/web/frontend/js/features/history/components/history-root.tsx

@@ -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 { 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 { 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 (
-    <>
-      {fileTreeContainer
-        ? createPortal(<HistoryFileTree />, fileTreeContainer)
-        : null}
-      <div className="history-react">{content}</div>
-    </>
+    <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
+      <HistoryContent />
+    </Suspense>
   )
   )
 }
 }
 
 

+ 2 - 10
services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx

@@ -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)

+ 9 - 3
services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx

@@ -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:

+ 18 - 3
services/web/frontend/js/features/share-project-modal/components/share-project-modal-content.jsx

@@ -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>
 
 

+ 1 - 1
services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal-body.tsx

@@ -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)

+ 7 - 3
services/web/frontend/js/features/source-editor/components/figure-modal/figure-modal.tsx

@@ -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>

+ 2 - 8
services/web/frontend/js/features/source-editor/components/source-editor.tsx

@@ -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
-      fallback={
-        <div className="pdf-loading-spinner-container">
-          <LoadingSpinner delay={500} />
-        </div>
-      }
-    >
+    <Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
       <CodeMirrorEditor />
       <CodeMirrorEditor />
     </Suspense>
     </Suspense>
   )
   )

+ 19 - 0
services/web/frontend/js/shared/components/loading-spinner.jsx → services/web/frontend/js/shared/components/loading-spinner.tsx

@@ -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,
+}

+ 1 - 0
services/web/frontend/stylesheets/_style_includes.less

@@ -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';

+ 0 - 8
services/web/frontend/stylesheets/app/editor/pdf.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;

+ 7 - 0
services/web/frontend/stylesheets/components/loading-spinner.less

@@ -0,0 +1,7 @@
+.full-size-loading-spinner-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}

+ 1 - 0
services/web/frontend/stylesheets/main-style.less

@@ -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';

+ 24 - 18
services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.jsx

@@ -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, {
-      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))
     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, {
-      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))
     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, {
-      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))
     await waitFor(() => expect(requests).to.have.length(1))