Merge pull request #35855 from overleaf/ds-unable-to-sync-modal-improved

[WEB] Adding improved version of the unable to sync modal

GitOrigin-RevId: 1cfe2b3a2e8417028339efddb18385a5224e50c3
This commit is contained in:
Davinder Singh
2026-09-17 11:43:20 +00:00
committed by Copybot
parent 469f5629d2
commit 36d0a5c227
11 changed files with 241 additions and 72 deletions
@@ -0,0 +1,45 @@
import { memo, useCallback, useMemo } from 'react'
import { diffWordsWithSpace } from 'diff'
type DiffViewerProps = {
baseContent: string
targetContent: string
}
// Renders an inline diff between two document versions using <ins> and <del>
// tags. This mirrors the DOM-based rendering approach used by the workbench
// CodeDiff component (modules/workbench/frontend/js/components/code-diff.tsx).
// TODO: refactor both to share a common diff renderer from a shared location.
function DiffViewer({ baseContent, targetContent }: DiffViewerProps) {
const changes = useMemo(
() => diffWordsWithSpace(baseContent, targetContent),
[baseContent, targetContent]
)
const createDiff = useCallback(
(element: HTMLDivElement) => {
if (!element) return
element.replaceChildren()
for (const change of changes) {
if (change.added) {
const ins = document.createElement('ins')
ins.textContent = change.value
element.append(ins)
} else if (change.removed) {
const del = document.createElement('del')
del.textContent = change.value
element.append(del)
} else {
const text = document.createTextNode(change.value)
element.append(text)
}
}
},
[changes]
)
return <div className="diff-container" ref={createDiff} />
}
export default memo(DiffViewer)
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import { memo, useCallback, useState } from 'react'
import OLButton from '@/shared/components/ol/ol-button'
import {
OLModal,
@@ -8,42 +8,110 @@ import {
OLModalHeader,
OLModalTitle,
} from '@/shared/components/ol/ol-modal'
import { downloadFileContent } from '@/utils/download-file'
import DiffViewer from './diff-viewer'
import getMeta from '@/utils/meta'
import { uploadBatch } from '@/infrastructure/batch-file-uploader'
import { debugConsole } from '@/utils/debugging'
export type UnableToSyncModalProps = {
editorContent: string
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({
editorContent,
baseContent,
targetContent,
docName,
rootFolderId,
show,
onHide,
}: UnableToSyncModalProps) {
const { t } = useTranslation()
const [saving, setSaving] = useState(false)
const handleDownload = useCallback(() => {
downloadFileContent(editorContent, docName ?? 'document.txt')
}, [editorContent, docName])
const handleSaveAsNewFile = useCallback(async () => {
const projectId = getMeta('ol-project_id')
const filename = buildOfflineFilename(docName)
setSaving(true)
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)
} finally {
setSaving(false)
}
}, [docName, targetContent, rootFolderId, onHide])
return (
<OLModal
show={show}
onHide={onHide}
className="unable-to-sync-modal"
backdrop="static"
keyboard={false}
centered
>
<OLModalHeader>
<OLModalHeader closeButton={false}>
<OLModalTitle>{t('your_offline_edits_couldnt_be_synced')}</OLModalTitle>
</OLModalHeader>
<OLModalBody>{t('offline_edits_couldnt_be_synced_detail')}</OLModalBody>
<OLModalBody>
<p>{t('offline_edits_couldnt_combine')}</p>
<DiffViewer baseContent={baseContent} targetContent={targetContent} />
<p className="mt-2">{t('offline_save_explanation')}</p>
</OLModalBody>
<OLModalFooter>
<OLButton variant="primary" onClick={handleDownload}>
{t('download_local_version')}
<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>