Files
overleaf-cep/services/web/frontend/js/features/ide-react/components/modals/unable-to-sync-modal.tsx
T
Davinder Singh 95b373ed52 Merge pull request #36035 from overleaf/ds-unable-sync-modal-error
[WEB] Adding error message in the unable to sync modal

GitOrigin-RevId: b9d2cdc0db8c87e8f90b77d812dbb6d0c6695622
2026-09-17 11:43:49 +00:00

151 lines
4.3 KiB
TypeScript

import { Trans, useTranslation } from 'react-i18next'
import { memo, useCallback, useState } from 'react'
import OLButton from '@/shared/components/ol/ol-button'
import {
OLModal,
OLModalBody,
OLModalFooter,
OLModalHeader,
OLModalTitle,
} from '@/shared/components/ol/ol-modal'
import DiffViewer from './diff-viewer'
import getMeta from '@/utils/meta'
import { uploadBatch } from '@/infrastructure/batch-file-uploader'
import { debugConsole } from '@/utils/debugging'
import Notification from '@/shared/components/notification'
import { downloadFileContent } from '@/utils/download-file'
export type UnableToSyncModalProps = {
baseContent: string
targetContent: string
docName: string | null
rootFolderId: string | undefined
show: boolean
onHide: () => void
}
// Generates the offline copy filename with a timestamp to avoid duplicates.
// e.g. main.tex -> main(offline-2026-07-27-14-07).tex
function buildOfflineFilename(docName: string | null): string {
const now = new Date()
const stamp = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
].join('-')
const suffix = `(offline-${stamp})`
if (!docName) {
return `main${suffix}.tex`
}
const lastDot = docName.lastIndexOf('.')
if (lastDot === -1) {
return `${docName}${suffix}`
}
return `${docName.slice(0, lastDot)}${suffix}${docName.slice(lastDot)}`
}
function UnableToSyncModal({
baseContent,
targetContent,
docName,
rootFolderId,
show,
onHide,
}: UnableToSyncModalProps) {
const { t } = useTranslation()
const [saving, setSaving] = useState(false)
const [saveError, setSaveError] = useState(false)
const handleDownload = useCallback(() => {
downloadFileContent(targetContent, docName ?? 'document.txt')
}, [targetContent, docName])
const handleSaveAsNewFile = useCallback(async () => {
const projectId = getMeta('ol-project_id')
const filename = buildOfflineFilename(docName)
setSaving(true)
setSaveError(false)
try {
if (!rootFolderId) {
throw new Error('rootFolderId not available')
}
const [result] = await uploadBatch(
[
{
file: new Blob([targetContent], { type: 'text/plain' }),
name: filename,
},
],
{ projectId, folderId: rootFolderId }
)
if (result.status === 'error') {
throw new Error(result.error)
}
onHide()
} catch (error) {
debugConsole.error('Failed to save offline changes as new file', error)
setSaveError(true)
} finally {
setSaving(false)
}
}, [docName, targetContent, rootFolderId, onHide])
return (
<OLModal
show={show}
onHide={onHide}
className="unable-to-sync-modal"
backdrop="static"
keyboard={false}
centered
>
<OLModalHeader closeButton={false}>
<OLModalTitle>{t('your_offline_edits_couldnt_be_synced')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>
<p>{t('offline_edits_couldnt_combine')}</p>
<DiffViewer baseContent={baseContent} targetContent={targetContent} />
<p className="mt-2">{t('offline_save_explanation')}</p>
{saveError && (
<Notification
type="error"
content={
<Trans
i18nKey="unable_to_save_check_connection_or_download"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content,jsx-a11y/anchor-is-valid,react/jsx-key
<a
href="#"
onClick={e => {
e.preventDefault()
handleDownload()
}}
/>,
]}
/>
}
/>
)}
</OLModalBody>
<OLModalFooter>
<OLButton variant="danger-ghost" onClick={onHide}>
{t('discard_changes')}
</OLButton>
<OLButton
variant="primary"
onClick={handleSaveAsNewFile}
isLoading={saving}
loadingLabel={t('saving')}
>
{t('save_as_new_file')}
</OLButton>
</OLModalFooter>
</OLModal>
)
}
export default memo(UnableToSyncModal)