diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 6dbdc904b5..6a4cb6ecdd 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -284,6 +284,7 @@ "choose_how_you_search_your_references": "", "choose_which_experiments": "", "citation": "", + "citations_and_cross_references_may_be_written_as_plain_text": "", "cite_a_reference_from_your_reference_manager_to_automatically_add_it_to_your_bib_file": "", "cite_directly_or_import_references": "", "cite_faster": "", @@ -473,6 +474,7 @@ "do_you_want_to_overwrite_it": "", "do_you_want_to_overwrite_it_plural": "", "do_you_want_to_overwrite_them": "", + "document_successfully_imported": "", "document_too_long": "", "document_too_long_detail": "", "document_too_long_tracked_deletes": "", @@ -1811,6 +1813,7 @@ "start_a_free_trial": "", "start_by_adding_your_email": "", "start_by_fixing_the_first_error_in_your_document": "", + "start_editing": "", "start_free_trial": "", "start_free_trial_without_exclamation": "", "start_new_chat": "", @@ -1914,6 +1917,7 @@ "the_code_editor_color_scheme": "", "the_code_editor_color_scheme_dark_mode": "", "the_code_editor_color_scheme_light_mode": "", + "the_document_has_been_converted_to": "", "the_following_files_already_exist_in_this_project": "", "the_following_files_and_folders_already_exist_in_this_project": "", "the_following_folder_already_exists_in_this_project": "", diff --git a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx index 2c2e715493..15820f4fbe 100644 --- a/services/web/frontend/js/features/ide-react/components/modals/modals.tsx +++ b/services/web/frontend/js/features/ide-react/components/modals/modals.tsx @@ -3,6 +3,7 @@ import ForceDisconnected from '@/features/ide-react/components/modals/force-disc import { UnsavedDocs } from '@/features/ide-react/components/unsaved-docs/unsaved-docs' import SystemMessages from '@/shared/components/system-messages' import ViewOnlyAccessModal from '@/features/share-project-modal/components/view-only-access-modal' +import ProjectConvertedFromDocxModal from '@/features/ide-react/components/modals/project-converted-from-docx-modal' export const Modals = memo(() => { return ( @@ -11,6 +12,7 @@ export const Modals = memo(() => { + ) }) diff --git a/services/web/frontend/js/features/ide-react/components/modals/project-converted-from-docx-modal.tsx b/services/web/frontend/js/features/ide-react/components/modals/project-converted-from-docx-modal.tsx new file mode 100644 index 0000000000..823a92ac34 --- /dev/null +++ b/services/web/frontend/js/features/ide-react/components/modals/project-converted-from-docx-modal.tsx @@ -0,0 +1,84 @@ +import { Trans, useTranslation } from 'react-i18next' +import { + OLModal, + OLModalBody, + OLModalFooter, + OLModalHeader, + OLModalTitle, +} from '@/shared/components/ol/ol-modal' +import OLButton from '@/shared/components/ol/ol-button' +import { useEffect, useState } from 'react' + +function ProjectConvertedFromDocxModal() { + const [ + showProjectConvertedFromDocxModal, + setShowProjectConvertedFromDocxModal, + ] = useState(false) + + useEffect(() => { + const query = window.location.search + const queryString = new URLSearchParams(query) + + if (queryString.get('converted-from-docx') === 'true') { + setShowProjectConvertedFromDocxModal(true) + + // Clean the URL immediately so a refresh doesn't trigger the modal again, + // but preserve other search params and the hash. + const url = new URL(window.location.href) + url.searchParams.delete('converted-from-docx') + window.history.replaceState(window.history.state, '', url.toString()) + } + }, []) + + return ( + <> + {showProjectConvertedFromDocxModal && ( + setShowProjectConvertedFromDocxModal(false)} + /> + )} + + ) +} + +function ProjectConvertedFromDocxModalContent({ + onHide, +}: { + onHide: () => void +}) { + const { t } = useTranslation() + + return ( + + + + {t('document_successfully_imported')} + + + +
    +
  • {t('the_document_has_been_converted_to')}
  • +
  • + }} + /> +
  • +
+
+ + + {t('start_editing')} + + +
+ ) +} + +export default ProjectConvertedFromDocxModal diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/import-docx-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/import-docx-modal.tsx index a2af5c7a98..37b596b70c 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button/import-docx-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button/import-docx-modal.tsx @@ -1,4 +1,6 @@ -import { useEffect, useState } from 'react' +import { Dashboard } from '@uppy/react' +import { useTranslation } from 'react-i18next' +import { useProjectUploader } from '../../hooks/use-project-uploader' import { OLModal, OLModalBody, @@ -7,86 +9,24 @@ import { OLModalTitle, } from '@/shared/components/ol/ol-modal' import OLButton from '@/shared/components/ol/ol-button' -import { useTranslation } from 'react-i18next' -import Uppy from '@uppy/core' -import { Dashboard } from '@uppy/react' -import XHRUpload from '@uppy/xhr-upload' -import getMeta from '../../../../utils/meta' - import '@uppy/core/dist/style.css' import '@uppy/dashboard/dist/style.css' -type ImportResponse = { - project_id: string -} - -type ImportDocxModalProps = { +function ImportDocxModal({ + onHide, + openProject, +}: { onHide: () => void - openProject: (projectId: string) => void -} - -function ImportDocxModal({ onHide, openProject }: ImportDocxModalProps) { + openProject: (id: string, isConvertedFromDocx?: boolean) => void +}) { const { t } = useTranslation() - const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings') - const [ableToUpload, setAbleToUpload] = useState(false) - const [uppy] = useState(() => { - return new Uppy({ - allowMultipleUploadBatches: false, - restrictions: { - maxNumberOfFiles: 1, - maxFileSize: maxUploadSize, - allowedFileTypes: ['.docx'], - }, - }) - .use(XHRUpload, { - endpoint: '/project/new/import-docx', - headers: { - 'X-CSRF-TOKEN': getMeta('ol-csrfToken'), - }, - limit: 1, - fieldName: 'qqfile', // "qqfile" is needed for our express multer middleware - timeout: projectUploadTimeout, - }) - .on('file-added', () => { - // this function can be invoked multiple times depending on maxNumberOfFiles - // in this case, since have maxNumberOfFiles = 1, this function will be invoked - // once if the correct file were added - // if user dragged more files than the maxNumberOfFiles allow, - // the rest of the files will appear on the 'restriction-failed' event callback - setAbleToUpload(true) - }) - .on('upload-error', () => { - // refresh state so they can try uploading a new zip - setAbleToUpload(false) - }) - .on('upload-success', async (file, response) => { - const { project_id: projectId }: ImportResponse = response.body - - if (projectId) { - openProject(projectId) - } - }) - .on('restriction-failed', () => { - // 'restriction-failed event will be invoked when one of the "restrictions" above - // is not complied: - // 1. maxNumberOfFiles: if the uploaded files is more than 1, the rest of the files will appear here - // for example, user drop 5 files to the uploader, this function will be invoked 4 times and the `file-added` event - // will be invoked once - // 2. maxFileSize: if the uploaded file has size > maxFileSize, it will appear here - // 3. allowedFileTypes: if the type is not .zip, it will also appear here - - // reset state so they can try uploading a different file, etc - setAbleToUpload(false) - }) + const uppy = useProjectUploader({ + endpoint: '/project/new/import-docx', + allowedFileTypes: ['.docx'], + onSuccess: (projectId: string) => openProject(projectId, true), }) - useEffect(() => { - if (ableToUpload) { - uppy.upload() - } - }, [ableToUpload, uppy]) - return ( { - location.assign(`/project/${projectId}`) + (projectId: string, isConvertedFromDocx: boolean = false) => { + const url = isConvertedFromDocx + ? `/project/${projectId}?converted-from-docx=true` + : `/project/${projectId}` + + location.assign(url) }, [location] ) diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx index a221f68037..b69273729c 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx @@ -1,4 +1,6 @@ -import { useEffect, useState } from 'react' +import { Dashboard } from '@uppy/react' +import { useTranslation } from 'react-i18next' +import { useProjectUploader } from '../../hooks/use-project-uploader' import { OLModal, OLModalBody, @@ -7,86 +9,25 @@ import { OLModalTitle, } from '@/shared/components/ol/ol-modal' import OLButton from '@/shared/components/ol/ol-button' -import { useTranslation } from 'react-i18next' -import Uppy from '@uppy/core' -import { Dashboard } from '@uppy/react' -import XHRUpload from '@uppy/xhr-upload' -import getMeta from '../../../../utils/meta' import '@uppy/core/dist/style.css' import '@uppy/dashboard/dist/style.css' -type UploadResponse = { - project_id: string -} - -type UploadProjectModalProps = { +function UploadProjectModal({ + onHide, + openProject, +}: { onHide: () => void - openProject: (projectId: string) => void -} - -function UploadProjectModal({ onHide, openProject }: UploadProjectModalProps) { + openProject: (id: string) => void +}) { const { t } = useTranslation() - const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings') - const [ableToUpload, setAbleToUpload] = useState(false) - const [uppy] = useState(() => { - return new Uppy({ - allowMultipleUploadBatches: false, - restrictions: { - maxNumberOfFiles: 1, - maxFileSize: maxUploadSize, - allowedFileTypes: ['.zip'], - }, - }) - .use(XHRUpload, { - endpoint: '/project/new/upload', - headers: { - 'X-CSRF-TOKEN': getMeta('ol-csrfToken'), - }, - limit: 1, - fieldName: 'qqfile', // "qqfile" is needed for our express multer middleware - timeout: projectUploadTimeout, - }) - .on('file-added', () => { - // this function can be invoked multiple times depending on maxNumberOfFiles - // in this case, since have maxNumberOfFiles = 1, this function will be invoked - // once if the correct file were added - // if user dragged more files than the maxNumberOfFiles allow, - // the rest of the files will appear on the 'restriction-failed' event callback - setAbleToUpload(true) - }) - .on('upload-error', () => { - // refresh state so they can try uploading a new zip - setAbleToUpload(false) - }) - .on('upload-success', async (file, response) => { - const { project_id: projectId }: UploadResponse = response.body - - if (projectId) { - openProject(projectId) - } - }) - .on('restriction-failed', () => { - // 'restriction-failed event will be invoked when one of the "restrictions" above - // is not complied: - // 1. maxNumberOfFiles: if the uploaded files is more than 1, the rest of the files will appear here - // for example, user drop 5 files to the uploader, this function will be invoked 4 times and the `file-added` event - // will be invoked once - // 2. maxFileSize: if the uploaded file has size > maxFileSize, it will appear here - // 3. allowedFileTypes: if the type is not .zip, it will also appear here - - // reset state so they can try uploading a different file, etc - setAbleToUpload(false) - }) + const uppy = useProjectUploader({ + endpoint: '/project/new/upload', + allowedFileTypes: ['.zip'], + onSuccess: openProject, }) - useEffect(() => { - if (ableToUpload) { - uppy.upload() - } - }, [ableToUpload, uppy]) - return ( void +} + +type ImportResponse = { + project_id?: string +} + +export function useProjectUploader({ + endpoint, + allowedFileTypes, + onSuccess, +}: UploaderConfig) { + const { maxUploadSize, projectUploadTimeout } = getMeta('ol-ExposedSettings') + const [ableToUpload, setAbleToUpload] = useState(false) + + const [uppy] = useState(() => { + return new Uppy({ + allowMultipleUploadBatches: false, + restrictions: { + maxNumberOfFiles: 1, + maxFileSize: maxUploadSize, + allowedFileTypes, + }, + }) + .use(XHRUpload, { + endpoint, + headers: { + 'X-CSRF-TOKEN': getMeta('ol-csrfToken'), + }, + limit: 1, + fieldName: 'qqfile', + timeout: projectUploadTimeout, + }) + .on('file-added', () => { + // this function can be invoked multiple times depending on maxNumberOfFiles + // in this case, since have maxNumberOfFiles = 1, this function will be invoked + // once if the correct file were added + // if user dragged more files than the maxNumberOfFiles allow, + // the rest of the files will appear on the 'restriction-failed' event callback + setAbleToUpload(true) + }) + .on('upload-error', () => { + // refresh state so they can try uploading a new file + setAbleToUpload(false) + }) + .on('upload-success', (_file, response) => { + const { project_id: projectId }: ImportResponse = response.body + + if (projectId) { + onSuccess(projectId) + } + }) + .on('restriction-failed', () => { + // 'restriction-failed event will be invoked when one of the "restrictions" above + // is not complied: + // 1. maxNumberOfFiles: if the uploaded files is more than 1, the rest of the files will appear here + // for example, user drop 5 files to the uploader, this function will be invoked 4 times and the `file-added` event + // will be invoked once + // 2. maxFileSize: if the uploaded file has size > maxFileSize, it will appear here + // 3. allowedFileTypes: if the file type is not allowed by allowedFileTypes, it will also appear here + + // reset state so they can try uploading a different file, etc + setAbleToUpload(false) + }) + }) + + useEffect(() => { + if (ableToUpload) { + uppy.upload() + } + }, [ableToUpload, uppy]) + + return uppy +} diff --git a/services/web/frontend/stylesheets/pages/editor/ide.scss b/services/web/frontend/stylesheets/pages/editor/ide.scss index 34f73323ea..a173c559d3 100644 --- a/services/web/frontend/stylesheets/pages/editor/ide.scss +++ b/services/web/frontend/stylesheets/pages/editor/ide.scss @@ -162,6 +162,12 @@ $editor-toggler-bg-dark-color: color.adjust( } } +.docx-modal-list { + list-style: none; + padding-left: 0; + margin-bottom: 0; +} + .out-of-sync-modal { .text-preview { margin-top: var(--spacing-05); diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 466abd828b..998f6a79a0 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -356,6 +356,7 @@ "choose_which_experiments": "Choose which experiments you’d like to try.", "choose_your_plan": "Choose your plan", "citation": "Citation", + "citations_and_cross_references_may_be_written_as_plain_text": "Citations and cross-references may be written as plain text. Consider changing these to use the appropriate \\cite{} and \\ref{} commands.", "cite_a_reference_from_your_reference_manager_to_automatically_add_it_to_your_bib_file": "Cite a reference from your reference manager to automatically add it to your .bib file.", "cite_directly_or_import_references": "Cite directly or import references", "cite_faster": "Cite faster", @@ -600,6 +601,7 @@ "do_you_want_to_overwrite_it": "Do you want to overwrite it?", "do_you_want_to_overwrite_it_plural": "Do you want to overwrite them?", "do_you_want_to_overwrite_them": "Do you want to overwrite them?", + "document_successfully_imported": "Document successfully imported", "document_too_long": "Document Too Long", "document_too_long_detail": "Sorry, this file is too long to be edited manually. Please try to split it into smaller files.", "document_too_long_tracked_deletes": "You can also accept pending deletions to reduce the size of the file.", @@ -2306,6 +2308,7 @@ "start_a_free_trial": "Start a free trial", "start_by_adding_your_email": "Start by adding your email address.", "start_by_fixing_the_first_error_in_your_document": "Start by fixing the first error in the document to avoid cascading problems later on.", + "start_editing": "Start editing", "start_free_trial": "Start Free Trial!", "start_free_trial_without_exclamation": "Start free trial", "start_new_chat": "Start new chat", @@ -2440,6 +2443,7 @@ "the_code_editor_color_scheme": "The code editor color scheme", "the_code_editor_color_scheme_dark_mode": "The code editor color scheme for dark mode", "the_code_editor_color_scheme_light_mode": "The code editor color scheme for light mode", + "the_document_has_been_converted_to": "The document has been converted to LaTeX. You may notice some places where the layout looks slightly different to your original document, such as in tables or figures. Check the output carefully.", "the_file_supplied_is_of_an_unsupported_type ": "The link to open this content on Overleaf pointed to the wrong kind of file. Valid file types are .tex documents and .zip files. If this keeps happening for links on a particular site, please report this to them.", "the_following_files_already_exist_in_this_project": "The following files already exist in this project:", "the_following_files_and_folders_already_exist_in_this_project": "The following files and folders already exist in this project:",