Selaa lähdekoodia

Merge pull request #34102 from overleaf/mj-document-import-errors

[web] Expose pandoc errors in import

GitOrigin-RevId: 55f89b91a52099a99a5d955bc05f3657b87b2cdc
Mathias Jakobsen 4 kuukautta sitten
vanhempi
sitoutus
cabe0046c5

+ 5 - 2
services/web/app/src/Features/Uploads/ProjectUploadController.mjs

@@ -188,7 +188,10 @@ async function importDocument(req, res, next) {
   const { path } = req.file
   const { path } = req.file
   const conversionType = req.query.type
   const conversionType = req.query.type
   if (!['docx', 'markdown'].includes(conversionType)) {
   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))
   const name = Path.basename(req.body.name, Path.extname(req.body.name))
   logger.debug({ path, userId, conversionType }, 'importing document file')
   logger.debug({ path, userId, conversionType }, 'importing document file')
@@ -239,7 +242,7 @@ async function importDocument(req, res, next) {
     ) {
     ) {
       return res.status(422).json({
       return res.status(422).json({
         success: false,
         success: false,
-        error: 'file_too_large',
+        error: req.i18n.translate('file_too_large'),
       })
       })
     }
     }
     if (error instanceof DocumentConversionError) {
     if (error instanceof DocumentConversionError) {

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

@@ -2488,6 +2488,7 @@
   "your_current_plan_gives_you": "",
   "your_current_plan_gives_you": "",
   "your_current_plan_supports_up_to_x_licenses": "",
   "your_current_plan_supports_up_to_x_licenses": "",
   "your_current_project_will_revert_to_the_version_from_time": "",
   "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_email_is_confirmed": "",
   "your_feedback_matters_answer_two_quick_questions": "",
   "your_feedback_matters_answer_two_quick_questions": "",
   "your_git_access_info": "",
   "your_git_access_info": "",

+ 48 - 3
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 { Dashboard } from '@uppy/react'
-import { useTranslation } from 'react-i18next'
+import { Trans, useTranslation } from 'react-i18next'
 import { useProjectUploader } from '../../hooks/use-project-uploader'
 import { useProjectUploader } from '../../hooks/use-project-uploader'
 import {
 import {
   OLModal,
   OLModal,
@@ -12,6 +12,7 @@ import {
 import OLButton from '@/shared/components/ol/ol-button'
 import OLButton from '@/shared/components/ol/ol-button'
 import '@uppy/core/dist/style.css'
 import '@uppy/core/dist/style.css'
 import '@uppy/dashboard/dist/style.css'
 import '@uppy/dashboard/dist/style.css'
+import OLNotification from '@/shared/components/ol/ol-notification'
 
 
 function ImportDocumentModal({
 function ImportDocumentModal({
   type,
   type,
@@ -23,6 +24,7 @@ function ImportDocumentModal({
   openProject: (id: string, convertedFrom?: string) => void
   openProject: (id: string, convertedFrom?: string) => void
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
+  const [error, setError] = useState<string | null>(null)
   const IMPORT_CONFIGS = useMemo(
   const IMPORT_CONFIGS = useMemo(
     () => ({
     () => ({
       docx: {
       docx: {
@@ -46,6 +48,18 @@ function ImportDocumentModal({
     endpoint: `/project/new/import-document?type=${type}`,
     endpoint: `/project/new/import-document?type=${type}`,
     allowedFileTypes: config.allowedFileTypes,
     allowedFileTypes: config.allowedFileTypes,
     onSuccess: (projectId: string) => openProject(projectId, type),
     onSuccess: (projectId: string) => openProject(projectId, type),
+    onError: response => {
+      const message = response?.body?.error
+      if (message) {
+        setError(message)
+      }
+    },
+    onFileAdded: () => {
+      setError(null)
+    },
+    onFileRemoved: () => {
+      setError(null)
+    },
   })
   })
 
 
   return (
   return (
@@ -56,11 +70,11 @@ function ImportDocumentModal({
       id="upload-project-modal"
       id="upload-project-modal"
       backdrop="static"
       backdrop="static"
     >
     >
-      {/* TODO: make necessary changes here for import document modal */}
       <OLModalHeader>
       <OLModalHeader>
         <OLModalTitle as="h3">{config.title}</OLModalTitle>
         <OLModalTitle as="h3">{config.title}</OLModalTitle>
       </OLModalHeader>
       </OLModalHeader>
       <OLModalBody>
       <OLModalBody>
+        {error && <ErrorNotification message={error} />}
         <p>{t('import_document_description')}</p>
         <p>{t('import_document_description')}</p>
         <Dashboard
         <Dashboard
           uppy={uppy}
           uppy={uppy}
@@ -87,4 +101,35 @@ function ImportDocumentModal({
   )
   )
 }
 }
 
 
+const ErrorNotification = ({ message }: { message: string }) => {
+  const { t } = useTranslation()
+  return (
+    <OLNotification
+      type="error"
+      className="import-error-notification"
+      content={
+        <div>
+          <Trans
+            i18nKey="your_document_couldnt_be_imported_check_our_guidance_or_expand_conversion_error_details"
+            components={[
+              // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
+              <a
+                href="https://docs.overleaf.com/managing-projects-and-files/importing-and-exporting-files#common-issues-and-how-to-address-them"
+                target="_BLANK"
+                rel="noopener noreferrer"
+              />,
+            ]}
+          />
+          {message && (
+            <details style={{ maxHeight: '200px', overflow: 'auto' }}>
+              <summary>{t('conversion_error_details')}</summary>
+              <code style={{ wordBreak: 'break-all' }}>{message}</code>
+            </details>
+          )}
+        </div>
+      }
+    />
+  )
+}
+
 export default ImportDocumentModal
 export default ImportDocumentModal

+ 15 - 2
services/web/frontend/js/features/project-list/hooks/use-project-uploader.tsx

@@ -1,5 +1,5 @@
 import { useEffect, useState } from 'react'
 import { useEffect, useState } from 'react'
-import Uppy from '@uppy/core'
+import Uppy, { ErrorResponse } from '@uppy/core'
 import XHRUpload from '@uppy/xhr-upload'
 import XHRUpload from '@uppy/xhr-upload'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
 
 
@@ -7,6 +7,9 @@ type UploaderConfig = {
   endpoint: string
   endpoint: string
   allowedFileTypes: string[]
   allowedFileTypes: string[]
   onSuccess: (projectId: string) => void
   onSuccess: (projectId: string) => void
+  onError?: (response: ErrorResponse | undefined) => void
+  onFileAdded?: () => void
+  onFileRemoved?: () => void
 }
 }
 
 
 type ImportResponse = {
 type ImportResponse = {
@@ -17,6 +20,9 @@ export function useProjectUploader({
   endpoint,
   endpoint,
   allowedFileTypes,
   allowedFileTypes,
   onSuccess,
   onSuccess,
+  onError,
+  onFileAdded,
+  onFileRemoved,
 }: UploaderConfig) {
 }: UploaderConfig) {
   const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings')
   const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings')
   const [ableToUpload, setAbleToUpload] = useState(false)
   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
         // the rest of the files will appear on the 'restriction-failed' event callback
         setAbleToUpload(true)
         setAbleToUpload(true)
       })
       })
-      .on('upload-error', () => {
+      .on('upload-error', (_file, _err, response) => {
         // refresh state so they can try uploading a new file
         // refresh state so they can try uploading a new file
         setAbleToUpload(false)
         setAbleToUpload(false)
+        onError?.(response)
       })
       })
       .on('upload-success', (_file, response) => {
       .on('upload-success', (_file, response) => {
         const { project_id: projectId }: ImportResponse = response.body
         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
         // reset state so they can try uploading a different file, etc
         setAbleToUpload(false)
         setAbleToUpload(false)
       })
       })
+      .on('file-added', () => {
+        onFileAdded?.()
+      })
+      .on('file-removed', () => {
+        onFileRemoved?.()
+      })
   })
   })
 
 
   useEffect(() => {
   useEffect(() => {

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

@@ -1283,6 +1283,7 @@
   "invalid_email": "An email address is invalid",
   "invalid_email": "An email address is invalid",
   "invalid_file_name": "Invalid File Name",
   "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_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_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__</0>, which is an unexpected domain that we do not recognise as belonging to it. Please <1>contact us</1> if you think this is in error.",
   "invalid_organization_email": "Your organization’s SSO service returned your email address as <0>__institutionEmail__</0>, which is an unexpected domain that we do not recognise as belonging to it. Please <1>contact us</1> if you think this is in error.",
   "invalid_password": "Invalid Password.",
   "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_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_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_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</0> or expand the conversion error details below.",
   "your_email_is_confirmed": "Your email is confirmed.",
   "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_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.",
   "your_feedback_matters_answer_two_quick_questions": "Your feedback matters! Answer two quick questions.",

+ 1 - 1
services/web/test/unit/src/Uploads/ProjectUploadController.test.mjs

@@ -636,7 +636,7 @@ describe('ProjectUploadController', function () {
           ctx.res.json = data => {
           ctx.res.json = data => {
             expect(data).to.deep.equal({
             expect(data).to.deep.equal({
               success: false,
               success: false,
-              error: 'invalid_type',
+              error: 'invalid_import_type',
             })
             })
             resolve()
             resolve()
           }
           }