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 ( +
+ {content} + +
+ ) +} + +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 (
- Loading…
}> + }>
@@ -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 us", + "try_refresh_page": "Please try refreshing this page. If the problem persists, <0>contact us.", "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(),