Creating useProjectUploader hook for project upload modal & Display modal when a project is successfully converted from Docx (#32676)

* creating useProjectUploader hook

* removing a comment and polishing some code

* removing doc from the array

* rewording comments

* creating the modal that appears after docs conversion is completed

* moving the file

* renaming translations

* moving into modals

* adding content within a list

* removing the newUrl variable

* changing translation name

* adding a button in the test

GitOrigin-RevId: f08ec6b02874f93a79039b831afad820239e094c
This commit is contained in:
Davinder Singh
2026-04-09 08:07:12 +00:00
committed by Copybot
parent 8dce78cd97
commit 470a05c23d
9 changed files with 214 additions and 147 deletions
@@ -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": "",
@@ -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(() => {
<UnsavedDocs />
<SystemMessages />
<ViewOnlyAccessModal />
<ProjectConvertedFromDocxModal />
</>
)
})
@@ -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 && (
<ProjectConvertedFromDocxModalContent
onHide={() => setShowProjectConvertedFromDocxModal(false)}
/>
)}
</>
)
}
function ProjectConvertedFromDocxModalContent({
onHide,
}: {
onHide: () => void
}) {
const { t } = useTranslation()
return (
<OLModal
show
animation
onHide={onHide}
id="converted-from-docx-modal"
backdrop="static"
>
<OLModalHeader>
<OLModalTitle as="h3">
{t('document_successfully_imported')}
</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<ul className="docx-modal-list">
<li>{t('the_document_has_been_converted_to')}</li>
<li>
<Trans
i18nKey="citations_and_cross_references_may_be_written_as_plain_text"
components={{ code: <code /> }}
/>
</li>
</ul>
</OLModalBody>
<OLModalFooter>
<OLButton variant="primary" onClick={onHide}>
{t('start_editing')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
export default ProjectConvertedFromDocxModal
@@ -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 (
<OLModal
show
@@ -32,8 +32,12 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) {
const location = useLocation()
const openProject = useCallback(
(projectId: string) => {
location.assign(`/project/${projectId}`)
(projectId: string, isConvertedFromDocx: boolean = false) => {
const url = isConvertedFromDocx
? `/project/${projectId}?converted-from-docx=true`
: `/project/${projectId}`
location.assign(url)
},
[location]
)
@@ -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 (
<OLModal
show
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react'
import Uppy from '@uppy/core'
import XHRUpload from '@uppy/xhr-upload'
import getMeta from '@/utils/meta'
type UploaderConfig = {
endpoint: string
allowedFileTypes: string[]
onSuccess: (projectId: string) => 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
}
@@ -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);
+4
View File
@@ -356,6 +356,7 @@
"choose_which_experiments": "Choose which experiments youd 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 <code>\\cite{}</code> and <code>\\ref{}</code> 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:",