diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index 0bf0c52560..332f72ce4c 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -712,6 +712,7 @@ module.exports = {
publishModal: [],
tprLinkedFileInfo: [],
tprLinkedFileRefreshError: [],
+ contactUsModal: [],
},
moduleImportSequence: ['launchpad', 'server-ce-scripts', 'user-activate'],
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 1fde7d0468..45ec81982a 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/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": "",
diff --git a/services/web/frontend/js/features/chat/components/chat-pane.js b/services/web/frontend/js/features/chat/components/chat-pane.js
index 215af12117..1343369354 100644
--- a/services/web/frontend/js/features/chat/components/chat-pane.js
+++ b/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 (
-
-
- {` ${t('loading')}…`}
-
- )
-}
-
function Placeholder() {
const { t } = useTranslation()
return (
diff --git a/services/web/frontend/js/features/pdf-preview/components/error-boundary-fallback.js b/services/web/frontend/js/features/pdf-preview/components/error-boundary-fallback.js
new file mode 100644
index 0000000000..eb0a1dd00c
--- /dev/null
+++ b/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 (
+
+
+ {`${t('generic_something_went_wrong')}. ${t('please_refresh')}`}
+
+
+ )
+ }
+
+ // we create each instance of `` individually so `i18next-scanner` can detect hardcoded `i18nKey` values
+ let content
+ if (type === 'pdf') {
+ content = (
+ <>
+ {t('pdf_viewer_error')}
+
+ ]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+ />
+
+ >
+ )
+ } else if (type === 'logs') {
+ content = (
+ <>
+ {t('log_viewer_error')}
+
+ ]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+ />
+
+ >
+ )
+ } else {
+ content = (
+ <>
+ {t('pdf_preview_error')}
+
+ ]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content, jsx-a11y/anchor-is-valid
+ />
+
+ >
+ )
+ }
+
+ return (
+
+ )
+}
+
+ErrorBoundaryFallback.propTypes = {
+ type: PropTypes.oneOf(['preview', 'pdf', 'logs']).isRequired,
+}
+
+export default ErrorBoundaryFallback
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js
index 2f6c30d69c..26377cdb90 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.js
+++ b/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), () => (
+
+))
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js
index e782d445c1..88e18eff18 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.js
+++ b/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), () => (
+
+))
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js
index 19999f8d38..456a13dcfe 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.js
+++ b/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 (
}>
+ }>
@@ -21,4 +21,12 @@ function PdfPreviewPane() {
)
}
-export default memo(withErrorBoundary(PdfPreviewPane))
+function LoadingPreview() {
+ return (
+
+
+
+ )
+}
+
+export default memo(PdfPreviewPane)
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js b/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js
index a68e33f76b..e671b06034 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview.js
+++ b/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), () => (
+
+))
diff --git a/services/web/frontend/js/shared/components/loading-spinner.js b/services/web/frontend/js/shared/components/loading-spinner.js
new file mode 100644
index 0000000000..f019c6715a
--- /dev/null
+++ b/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 (
+
+
+ {` ${t('loading')}…`}
+
+ )
+}
+
+export default LoadingSpinner
diff --git a/services/web/frontend/js/shared/context/user-context.js b/services/web/frontend/js/shared/context/user-context.js
index a122078216..b3113bc7f1 100644
--- a/services/web/frontend/js/shared/context/user-context.js
+++ b/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,
diff --git a/services/web/frontend/stories/contact-us-modal.stories.js b/services/web/frontend/stories/contact-us-modal.stories.js
new file mode 100644
index 0000000000..354d0899e2
--- /dev/null
+++ b/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()
+}
+
+export const RequestError = args => {
+ useFetchMock(fetchMock => {
+ fetchMock.post('express:/support', { status: 404 }, { delay: 250 })
+ })
+
+ return withContextRoot()
+}
+
+export default {
+ title: 'Modals / Contact Us',
+ component: ContactUsModal,
+ args: {
+ show: true,
+ handleHide: () => {},
+ },
+ argTypes: {
+ handleHide: { action: 'close modal' },
+ },
+}
diff --git a/services/web/frontend/stylesheets/app/editor/pdf.less b/services/web/frontend/stylesheets/app/editor/pdf.less
index f6080fce63..b395a05a0c 100644
--- a/services/web/frontend/stylesheets/app/editor/pdf.less
+++ b/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;
+}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index d21003bced..5b3300088a 100644
--- a/services/web/locales/en.json
+++ b/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 us0>",
+ "try_refresh_page": "Please try refreshing this page. If the problem persists, <0>contact us0>.",
"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",
diff --git a/services/web/test/frontend/helpers/render-with-context.js b/services/web/test/frontend/helpers/render-with-context.js
index 2057657273..ac7d5ee35d 100644
--- a/services/web/test/frontend/helpers/render-with-context.js
+++ b/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(),