From cabe0046c55c52292f8e0b2b21904fb0c44df598 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Thu, 4 Jun 2026 14:54:49 +0100 Subject: [PATCH] Merge pull request #34102 from overleaf/mj-document-import-errors [web] Expose pandoc errors in import GitOrigin-RevId: 55f89b91a52099a99a5d955bc05f3657b87b2cdc --- .../Uploads/ProjectUploadController.mjs | 7 ++- .../web/frontend/extracted-translations.json | 1 + .../import-document-modal.tsx | 51 +++++++++++++++++-- .../hooks/use-project-uploader.tsx | 17 ++++++- services/web/locales/en.json | 2 + .../Uploads/ProjectUploadController.test.mjs | 2 +- 6 files changed, 72 insertions(+), 8 deletions(-) diff --git a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs index 1cb914bfd0..cecbac5ea2 100644 --- a/services/web/app/src/Features/Uploads/ProjectUploadController.mjs +++ b/services/web/app/src/Features/Uploads/ProjectUploadController.mjs @@ -188,7 +188,10 @@ async function importDocument(req, res, next) { const { path } = req.file const conversionType = req.query.type if (!['docx', 'markdown'].includes(conversionType)) { - return res.status(400).json({ success: false, error: 'invalid_type' }) + return res.status(400).json({ + success: false, + error: req.i18n.translate('invalid_import_type'), + }) } const name = Path.basename(req.body.name, Path.extname(req.body.name)) logger.debug({ path, userId, conversionType }, 'importing document file') @@ -239,7 +242,7 @@ async function importDocument(req, res, next) { ) { return res.status(422).json({ success: false, - error: 'file_too_large', + error: req.i18n.translate('file_too_large'), }) } if (error instanceof DocumentConversionError) { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index becf381a1d..17330ded89 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -2488,6 +2488,7 @@ "your_current_plan_gives_you": "", "your_current_plan_supports_up_to_x_licenses": "", "your_current_project_will_revert_to_the_version_from_time": "", + "your_document_couldnt_be_imported_check_our_guidance_or_expand_conversion_error_details": "", "your_email_is_confirmed": "", "your_feedback_matters_answer_two_quick_questions": "", "your_git_access_info": "", diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/import-document-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/import-document-modal.tsx index 24d4d4f34b..2593628fa8 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button/import-document-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button/import-document-modal.tsx @@ -1,6 +1,6 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { Dashboard } from '@uppy/react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' import { useProjectUploader } from '../../hooks/use-project-uploader' import { OLModal, @@ -12,6 +12,7 @@ import { import OLButton from '@/shared/components/ol/ol-button' import '@uppy/core/dist/style.css' import '@uppy/dashboard/dist/style.css' +import OLNotification from '@/shared/components/ol/ol-notification' function ImportDocumentModal({ type, @@ -23,6 +24,7 @@ function ImportDocumentModal({ openProject: (id: string, convertedFrom?: string) => void }) { const { t } = useTranslation() + const [error, setError] = useState(null) const IMPORT_CONFIGS = useMemo( () => ({ docx: { @@ -46,6 +48,18 @@ function ImportDocumentModal({ endpoint: `/project/new/import-document?type=${type}`, allowedFileTypes: config.allowedFileTypes, onSuccess: (projectId: string) => openProject(projectId, type), + onError: response => { + const message = response?.body?.error + if (message) { + setError(message) + } + }, + onFileAdded: () => { + setError(null) + }, + onFileRemoved: () => { + setError(null) + }, }) return ( @@ -56,11 +70,11 @@ function ImportDocumentModal({ id="upload-project-modal" backdrop="static" > - {/* TODO: make necessary changes here for import document modal */} {config.title} + {error && }

{t('import_document_description')}

{ + const { t } = useTranslation() + return ( + + , + ]} + /> + {message && ( +
+ {t('conversion_error_details')} + {message} +
+ )} + + } + /> + ) +} + export default ImportDocumentModal diff --git a/services/web/frontend/js/features/project-list/hooks/use-project-uploader.tsx b/services/web/frontend/js/features/project-list/hooks/use-project-uploader.tsx index d13c517e07..c260e6f886 100644 --- a/services/web/frontend/js/features/project-list/hooks/use-project-uploader.tsx +++ b/services/web/frontend/js/features/project-list/hooks/use-project-uploader.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import Uppy from '@uppy/core' +import Uppy, { ErrorResponse } from '@uppy/core' import XHRUpload from '@uppy/xhr-upload' import getMeta from '@/utils/meta' @@ -7,6 +7,9 @@ type UploaderConfig = { endpoint: string allowedFileTypes: string[] onSuccess: (projectId: string) => void + onError?: (response: ErrorResponse | undefined) => void + onFileAdded?: () => void + onFileRemoved?: () => void } type ImportResponse = { @@ -17,6 +20,9 @@ export function useProjectUploader({ endpoint, allowedFileTypes, onSuccess, + onError, + onFileAdded, + onFileRemoved, }: UploaderConfig) { const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings') const [ableToUpload, setAbleToUpload] = useState(false) @@ -47,9 +53,10 @@ export function useProjectUploader({ // the rest of the files will appear on the 'restriction-failed' event callback setAbleToUpload(true) }) - .on('upload-error', () => { + .on('upload-error', (_file, _err, response) => { // refresh state so they can try uploading a new file setAbleToUpload(false) + onError?.(response) }) .on('upload-success', (_file, response) => { const { project_id: projectId }: ImportResponse = response.body @@ -70,6 +77,12 @@ export function useProjectUploader({ // reset state so they can try uploading a different file, etc setAbleToUpload(false) }) + .on('file-added', () => { + onFileAdded?.() + }) + .on('file-removed', () => { + onFileRemoved?.() + }) }) useEffect(() => { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index c0bdfa53be..30866ad14b 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1283,6 +1283,7 @@ "invalid_email": "An email address is invalid", "invalid_file_name": "Invalid File Name", "invalid_filename": "Upload failed: check that the file name doesn’t contain special characters, trailing/leading whitespace or more than __nameLimit__ characters", + "invalid_import_type": "Invalid import type", "invalid_institutional_email": "Your institution’s SSO service returned your email address as __email__, which is an unexpected domain that we do not recognise as belonging to it. You may be able to change your primary email address via your user profile at your institution to one at your institution’s domain. Please contact your IT department if you have any questions.", "invalid_organization_email": "Your organization’s SSO service returned your email address as <0>__institutionEmail__, which is an unexpected domain that we do not recognise as belonging to it. Please <1>contact us if you think this is in error.", "invalid_password": "Invalid Password.", @@ -3154,6 +3155,7 @@ "your_current_plan_gives_you": "By pausing your subscription, you’ll be able to access your premium features faster when you need them again.", "your_current_plan_supports_up_to_x_licenses": "Your current plan supports up to __users__ licenses.", "your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__", + "your_document_couldnt_be_imported_check_our_guidance_or_expand_conversion_error_details": "Your document couldn’t be imported. <0>Check our guidance on common import problems or expand the conversion error details below.", "your_email_is_confirmed": "Your email is confirmed.", "your_email_is_not_set_up_with_sso": "Your email is not set up with __institutionName__ SSO. Please log in with your existing login method to confirm your identity.", "your_feedback_matters_answer_two_quick_questions": "Your feedback matters! Answer two quick questions.", diff --git a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs index d8609dd1dd..b12630052c 100644 --- a/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs +++ b/services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs @@ -636,7 +636,7 @@ describe('ProjectUploadController', function () { ctx.res.json = data => { expect(data).to.deep.equal({ success: false, - error: 'invalid_type', + error: 'invalid_import_type', }) resolve() }