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

Merge pull request #5201 from overleaf/msm-pdf-viewer-error-boundaries

Error boundaries for React PDF viewer

GitOrigin-RevId: 90052fc183f7ece8125ecfb0410a529cf905c13b
June Kelly 4 лет назад
Родитель
Сommit
03e1daa038

+ 1 - 0
services/web/config/settings.defaults.js

@@ -712,6 +712,7 @@ module.exports = {
     publishModal: [],
     tprLinkedFileInfo: [],
     tprLinkedFileRefreshError: [],
+    contactUsModal: [],
   },
 
   moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],

+ 9 - 0
services/web/frontend/extracted-translations.json

@@ -54,7 +54,9 @@
   "compiling": "",
   "conflicting_paths_found": "",
   "connected_users": "",
+  "contact_message_label": "",
   "continue_github_merge": "",
+  "contact_us": "",
   "copy": "",
   "copy_project": "",
   "copying": "",
@@ -179,6 +181,7 @@
   "log_hint_extra_info": "",
   "logs_pane_info_message": "",
   "logs_pane_info_message_popup": "",
+  "log_viewer_error": "",
   "main_file_not_found": "",
   "make_private": "",
   "manage_files_from_your_dropbox_folder": "",
@@ -231,7 +234,9 @@
   "pdf_compile_in_progress_error": "",
   "pdf_compile_rate_limit_hit": "",
   "pdf_compile_try_again": "",
+  "pdf_preview_error": "",
   "pdf_rendering_error": "",
+  "pdf_viewer_error": "",
   "please_compile_pdf_before_download": "",
   "please_refresh": "",
   "please_select_a_file": "",
@@ -252,6 +257,7 @@
   "project_too_large": "",
   "project_too_large_please_reduce": "",
   "project_too_much_editable_text": "",
+  "project_url" : "",
   "public": "",
   "pull_github_changes_into_sharelatex": "",
   "push_sharelatex_changes_to_github": "",
@@ -307,6 +313,7 @@
   "stop_compile": "",
   "stop_on_validation_error": "",
   "store_your_work": "",
+  "subject": "",
   "submit_title": "",
   "sure_you_want_to_delete": "",
   "sync_project_to_github_explanation": "",
@@ -328,6 +335,8 @@
   "too_recently_compiled": "",
   "total_words": "",
   "try_it_for_free": "",
+  "try_recompile_project": "",
+  "try_refresh_page": "",
   "turn_off_link_sharing": "",
   "turn_on_link_sharing": "",
   "unlimited_projects": "",

+ 1 - 10
services/web/frontend/js/features/chat/components/chat-pane.js

@@ -12,6 +12,7 @@ import { useUserContext } from '../../../shared/context/user-context'
 import withErrorBoundary from '../../../infrastructure/error-boundary'
 import { FetchError } from '../../../infrastructure/fetch-json'
 import { useChatContext } from '../context/chat-context'
+import LoadingSpinner from '../../../shared/components/loading-spinner'
 
 const ChatPane = React.memo(function ChatPane() {
   const { t } = useTranslation()
@@ -87,16 +88,6 @@ const ChatPane = React.memo(function ChatPane() {
   )
 })
 
-function LoadingSpinner() {
-  const { t } = useTranslation()
-  return (
-    <div className="loading">
-      <Icon type="fw" modifier="refresh" spin />
-      {`  ${t('loading')}…`}
-    </div>
-  )
-}
-
 function Placeholder() {
   const { t } = useTranslation()
   return (

+ 88 - 0
services/web/frontend/js/features/pdf-preview/components/error-boundary-fallback.js

@@ -0,0 +1,88 @@
+import PropTypes from 'prop-types'
+import { Alert } from 'react-bootstrap'
+import { Trans, useTranslation } from 'react-i18next'
+import { useState } from 'react'
+import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+
+const [contactUsModalModules] = importOverleafModules('contactUsModal')
+const ContactUsModal = contactUsModalModules?.import.default
+
+function ErrorBoundaryFallback({ type }) {
+  const { t } = useTranslation()
+
+  const [showContactUsModal, setShowContactUsModal] = useState(false)
+
+  function handleContactUsClick() {
+    setShowContactUsModal(true)
+  }
+
+  function handleContactUsModalHide() {
+    setShowContactUsModal(false)
+  }
+
+  if (!ContactUsModal) {
+    return (
+      <div className="pdf-error-alert">
+        <Alert bsStyle="danger">
+          {`${t('generic_something_went_wrong')}. ${t('please_refresh')}`}
+        </Alert>
+      </div>
+    )
+  }
+
+  // we create each instance of `<Trans/>` individually so `i18next-scanner` can detect hardcoded `i18nKey` values
+  let content
+  if (type === 'pdf') {
+    content = (
+      <>
+        <p>{t('pdf_viewer_error')}</p>
+        <p>
+          <Trans
+            i18nKey="try_recompile_project"
+            components={[<a href="#" onClick={handleContactUsClick} />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+          />
+        </p>
+      </>
+    )
+  } else if (type === 'logs') {
+    content = (
+      <>
+        <p>{t('log_viewer_error')}</p>
+        <p>
+          <Trans
+            i18nKey="try_recompile_project"
+            components={[<a href="#" onClick={handleContactUsClick} />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+          />
+        </p>
+      </>
+    )
+  } else {
+    content = (
+      <>
+        <p>{t('pdf_preview_error')}</p>
+        <p>
+          <Trans
+            i18nKey="try_refresh_page"
+            components={[<a href="#" onClick={handleContactUsClick} />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+          />
+        </p>
+      </>
+    )
+  }
+
+  return (
+    <div className="pdf-error-alert">
+      <Alert bsStyle="danger">{content}</Alert>
+      <ContactUsModal
+        show={showContactUsModal}
+        handleHide={handleContactUsModalHide}
+      />
+    </div>
+  )
+}
+
+ErrorBoundaryFallback.propTypes = {
+  type: PropTypes.oneOf(['preview', 'pdf', 'logs']).isRequired,
+}
+
+export default ErrorBoundaryFallback

+ 5 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js

@@ -8,6 +8,8 @@ import usePersistedState from '../../../shared/hooks/use-persisted-state'
 import useScopeValue from '../../../shared/context/util/scope-value-hook'
 import { buildHighlightElement } from '../util/highlights'
 import PDFJSWrapper from '../util/pdf-js-wrapper'
+import withErrorBoundary from '../../../infrastructure/error-boundary'
+import ErrorBoundaryFallback from './error-boundary-fallback'
 
 function PdfJsViewer({ url }) {
   const { _id: projectId } = useProjectContext()
@@ -248,4 +250,6 @@ PdfJsViewer.propTypes = {
   url: PropTypes.string.isRequired,
 }
 
-export default memo(PdfJsViewer)
+export default withErrorBoundary(memo(PdfJsViewer), () => (
+  <ErrorBoundaryFallback type="pdf" />
+))

+ 5 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js

@@ -9,6 +9,8 @@ import PdfPreviewError from './pdf-preview-error'
 import PdfClearCacheButton from './pdf-clear-cache-button'
 import PdfDownloadFilesButton from './pdf-download-files-button'
 import PdfLogsEntries from './pdf-logs-entries'
+import withErrorBoundary from '../../../infrastructure/error-boundary'
+import ErrorBoundaryFallback from './error-boundary-fallback'
 
 function PdfLogsViewer() {
   const {
@@ -67,4 +69,6 @@ function PdfLogsViewer() {
   )
 }
 
-export default memo(PdfLogsViewer)
+export default withErrorBoundary(memo(PdfLogsViewer), () => (
+  <ErrorBoundaryFallback type="logs" />
+))

+ 11 - 3
services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js

@@ -2,8 +2,8 @@ import { memo, Suspense } from 'react'
 import PdfLogsViewer from './pdf-logs-viewer'
 import PdfViewer from './pdf-viewer'
 import { usePdfPreviewContext } from '../contexts/pdf-preview-context'
-import withErrorBoundary from '../../../infrastructure/error-boundary'
 import PdfPreviewToolbar from './pdf-preview-toolbar'
+import LoadingSpinner from '../../../shared/components/loading-spinner'
 
 function PdfPreviewPane() {
   const { showLogs } = usePdfPreviewContext()
@@ -11,7 +11,7 @@ function PdfPreviewPane() {
   return (
     <div className="pdf full-size">
       <PdfPreviewToolbar />
-      <Suspense fallback={<div>Loading…</div>}>
+      <Suspense fallback={<LoadingPreview />}>
         <div className="pdf-viewer">
           <PdfViewer />
         </div>
@@ -21,4 +21,12 @@ function PdfPreviewPane() {
   )
 }
 
-export default memo(withErrorBoundary(PdfPreviewPane))
+function LoadingPreview() {
+  return (
+    <div className="pdf-loading-spinner-container">
+      <LoadingSpinner />
+    </div>
+  )
+}
+
+export default memo(PdfPreviewPane)

+ 5 - 1
services/web/frontend/js/features/pdf-preview/components/pdf-preview.js

@@ -1,6 +1,8 @@
 import PdfPreviewProvider from '../contexts/pdf-preview-context'
 import PdfPreviewPane from './pdf-preview-pane'
 import { memo } from 'react'
+import withErrorBoundary from '../../../infrastructure/error-boundary'
+import ErrorBoundaryFallback from './error-boundary-fallback'
 
 function PdfPreview() {
   return (
@@ -10,4 +12,6 @@ function PdfPreview() {
   )
 }
 
-export default memo(PdfPreview)
+export default withErrorBoundary(memo(PdfPreview), () => (
+  <ErrorBoundaryFallback type="preview" />
+))

+ 14 - 0
services/web/frontend/js/shared/components/loading-spinner.js

@@ -0,0 +1,14 @@
+import { useTranslation } from 'react-i18next'
+import Icon from './icon'
+
+function LoadingSpinner() {
+  const { t } = useTranslation()
+  return (
+    <div className="loading">
+      <Icon type="fw" modifier="refresh" spin />
+      {`  ${t('loading')}…`}
+    </div>
+  )
+}
+
+export default LoadingSpinner

+ 1 - 0
services/web/frontend/js/shared/context/user-context.js

@@ -8,6 +8,7 @@ UserContext.Provider.propTypes = {
   value: PropTypes.shape({
     user: PropTypes.shape({
       id: PropTypes.string,
+      email: PropTypes.string,
       allowedFreeTrial: PropTypes.boolean,
       first_name: PropTypes.string,
       last_name: PropTypes.string,

+ 35 - 0
services/web/frontend/stories/contact-us-modal.stories.js

@@ -0,0 +1,35 @@
+import { useState } from 'react'
+import useFetchMock from './hooks/use-fetch-mock'
+import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
+import { withContextRoot } from './utils/with-context-root'
+
+export const Generic = () => {
+  const [show, setShow] = useState(true)
+  const handleHide = () => setShow(false)
+
+  useFetchMock(fetchMock => {
+    fetchMock.post('express:/support', { status: 200 }, { delay: 1000 })
+  })
+
+  return withContextRoot(<ContactUsModal show={show} handleHide={handleHide} />)
+}
+
+export const RequestError = args => {
+  useFetchMock(fetchMock => {
+    fetchMock.post('express:/support', { status: 404 }, { delay: 250 })
+  })
+
+  return withContextRoot(<ContactUsModal {...args} />)
+}
+
+export default {
+  title: 'Modals / Contact Us',
+  component: ContactUsModal,
+  args: {
+    show: true,
+    handleHide: () => {},
+  },
+  argTypes: {
+    handleHide: { action: 'close modal' },
+  },
+}

+ 16 - 0
services/web/frontend/stylesheets/app/editor/pdf.less

@@ -538,3 +538,19 @@
     color: #fff;
   }
 }
+
+.pdf-error-alert {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background-color: @pdf-bg;
+  padding: @line-height-computed / 2;
+}
+
+.pdf-loading-spinner-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}

+ 5 - 0
services/web/locales/en.json

@@ -701,6 +701,11 @@
   "open_a_file_on_the_left": "Open a file on the left",
   "reference_error_relink_hint": "If this error persists, try re-linking your account here:",
   "pdf_rendering_error": "PDF Rendering Error",
+  "pdf_preview_error": "There was a problem displaying the compilation results for this project. This is an internal __appName__ issue, not a problem with your LaTeX code.",
+  "pdf_viewer_error": "There was a problem displaying this project PDF. This is an internal __appName__ issue, not a problem with your LaTeX code.",
+  "log_viewer_error": "There was a problem displaying this project compilation errors and logs. This is an internal __appName__ issue, not a problem with your LaTeX code.",
+  "try_recompile_project": "Please try recompiling the project. If the problem persists, <0>contact us</0>",
+  "try_refresh_page": "Please try refreshing this page. If the problem persists, <0>contact us</0>.",
   "something_went_wrong_rendering_pdf": "Something went wrong while rendering this PDF.",
   "mendeley_reference_loading_error_expired": "Mendeley token expired, please re-link your account",
   "zotero_reference_loading_error_expired": "Zotero token expired, please re-link your account",

+ 1 - 1
services/web/test/frontend/helpers/render-with-context.js

@@ -14,7 +14,7 @@ import { SplitTestProvider } from '../../../frontend/js/shared/context/split-tes
 import { CompileProvider } from '../../../frontend/js/shared/context/compile-context'
 
 export function EditorProviders({
-  user = { id: '123abd' },
+  user = { id: '123abd', email: 'testuser@example.com' },
   projectId = 'project123',
   socket = {
     on: sinon.stub(),