Presentation export: progress modal + inline failure log
Build and Deploy Verso / deploy (push) Has been cancelled
Build and Deploy Verso / deploy (push) Has been cancelled
The HTML/PDF export links were plain downloads that left the browser silently spinning during the server-side render and, on failure, saved an error page as pdf.txt/pdf.htm. Replace them with fetch-based downloads that show a modal: a spinner with a 'this can take up to a minute' message while compiling, and the actual compile log inline if the export fails. The user can dismiss at any time; a stale request that finishes after dismissal no longer reopens the modal. Adds the three i18n keys (en/fr + extracted). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1470,6 +1470,9 @@
|
||||
"premium_feature": "",
|
||||
"premium_plan_label": "",
|
||||
"preparing_for_export": "",
|
||||
"preparing_your_download": "",
|
||||
"presentation_export_can_take_a_moment": "",
|
||||
"presentation_export_failed": "",
|
||||
"presentation_link_members": "",
|
||||
"presentation_link_private": "",
|
||||
"presentation_link_public": "",
|
||||
|
||||
+141
-31
@@ -1,3 +1,4 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
@@ -12,6 +13,21 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownToggle,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
import {
|
||||
OLModal,
|
||||
OLModalBody,
|
||||
OLModalFooter,
|
||||
OLModalHeader,
|
||||
OLModalTitle,
|
||||
} from '@/shared/components/ol/ol-modal'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
|
||||
type ExportFormat = 'html' | 'pdf'
|
||||
|
||||
function filenameFromDisposition(disposition: string | null, ext: string) {
|
||||
const match = disposition?.match(/filename="?([^"]+)"?/)
|
||||
return match ? match[1] : `presentation.${ext}`
|
||||
}
|
||||
|
||||
function PdfHybridDownloadButton() {
|
||||
const { pdfDownloadUrl, pdfFile, showLogs } = useCompileContext()
|
||||
@@ -19,6 +35,63 @@ function PdfHybridDownloadButton() {
|
||||
const { projectId } = useProjectContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Standalone-HTML / slide-PDF exports trigger an on-demand server-side
|
||||
// render that can take several seconds; surface that wait (and any failure
|
||||
// log) in a modal instead of leaving the browser silently spinning.
|
||||
const [exporting, setExporting] = useState<ExportFormat | null>(null)
|
||||
const [exportError, setExportError] = useState<string | null>(null)
|
||||
// Bumped whenever the user dismisses the modal, so a request that finishes
|
||||
// after they've closed it doesn't pop the modal back open.
|
||||
const requestIdRef = useRef(0)
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
requestIdRef.current += 1
|
||||
setExporting(null)
|
||||
setExportError(null)
|
||||
}, [])
|
||||
|
||||
const startExport = useCallback(
|
||||
async (format: ExportFormat) => {
|
||||
const requestId = ++requestIdRef.current
|
||||
setExportError(null)
|
||||
setExporting(format)
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/project/${projectId}/presentation-export/${format}`,
|
||||
{ credentials: 'same-origin' }
|
||||
)
|
||||
if (requestId !== requestIdRef.current) return // dismissed meanwhile
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
if (requestId !== requestIdRef.current) return
|
||||
setExporting(null)
|
||||
setExportError(text || `Export failed (HTTP ${response.status})`)
|
||||
return
|
||||
}
|
||||
const blob = await response.blob()
|
||||
if (requestId !== requestIdRef.current) return
|
||||
const filename = filenameFromDisposition(
|
||||
response.headers.get('Content-Disposition'),
|
||||
format === 'pdf' ? 'pdf' : 'html'
|
||||
)
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
URL.revokeObjectURL(url)
|
||||
setExporting(null)
|
||||
} catch (err) {
|
||||
if (requestId !== requestIdRef.current) return
|
||||
setExporting(null)
|
||||
setExportError(err instanceof Error ? err.message : String(err))
|
||||
}
|
||||
},
|
||||
[projectId]
|
||||
)
|
||||
|
||||
if (showLogs) {
|
||||
return null
|
||||
}
|
||||
@@ -30,38 +103,75 @@ function PdfHybridDownloadButton() {
|
||||
|
||||
if (isPresentation) {
|
||||
return (
|
||||
<Dropdown align="end">
|
||||
<DropdownToggle
|
||||
id="download-presentation"
|
||||
variant="link"
|
||||
className="pdf-toolbar-btn"
|
||||
aria-label={t('download')}
|
||||
<>
|
||||
<Dropdown align="end">
|
||||
<DropdownToggle
|
||||
id="download-presentation"
|
||||
variant="link"
|
||||
className="pdf-toolbar-btn"
|
||||
aria-label={t('download')}
|
||||
>
|
||||
<MaterialIcon type="download" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
onClick={() => startExport('html')}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
{t('download_as_standalone_html')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
as="button"
|
||||
onClick={() => startExport('pdf')}
|
||||
disabled={exporting !== null}
|
||||
>
|
||||
{t('download_as_pdf_slides')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
<OLModal
|
||||
show={exporting !== null || exportError !== null}
|
||||
onHide={dismiss}
|
||||
>
|
||||
<MaterialIcon type="download" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
href={`/project/${projectId}/presentation-export/html`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
download
|
||||
>
|
||||
{t('download_as_standalone_html')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
<li role="none">
|
||||
<DropdownItem
|
||||
href={`/project/${projectId}/presentation-export/pdf`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
download
|
||||
>
|
||||
{t('download_as_pdf_slides')}
|
||||
</DropdownItem>
|
||||
</li>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<OLModalHeader closeButton>
|
||||
<OLModalTitle>
|
||||
{exportError
|
||||
? t('presentation_export_failed')
|
||||
: t('preparing_your_download')}
|
||||
</OLModalTitle>
|
||||
</OLModalHeader>
|
||||
<OLModalBody>
|
||||
{exporting !== null ? (
|
||||
<LoadingSpinner
|
||||
loadingText={t('presentation_export_can_take_a_moment')}
|
||||
/>
|
||||
) : (
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
maxHeight: '50vh',
|
||||
overflow: 'auto',
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
{exportError}
|
||||
</pre>
|
||||
)}
|
||||
</OLModalBody>
|
||||
<OLModalFooter>
|
||||
<OLButton variant="secondary" onClick={dismiss}>
|
||||
{t('close')}
|
||||
</OLButton>
|
||||
</OLModalFooter>
|
||||
</OLModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -692,6 +692,9 @@
|
||||
"download_all": "Download all",
|
||||
"download_as_pdf": "Download as PDF",
|
||||
"download_as_pdf_slides": "Download PDF slides",
|
||||
"preparing_your_download": "Preparing your download",
|
||||
"presentation_export_can_take_a_moment": "Compiling your presentation. This can take up to a minute for large decks…",
|
||||
"presentation_export_failed": "Presentation export failed",
|
||||
"download_as_source_zip": "Download as source (.zip)",
|
||||
"download_as_standalone_html": "Download standalone HTML",
|
||||
"download_csv": "Download CSV",
|
||||
|
||||
@@ -348,6 +348,9 @@
|
||||
"dont_have_account": "Vous n’avez pas de compte ?",
|
||||
"download": "Télécharger",
|
||||
"download_as_pdf_slides": "Télécharger les diapositives PDF",
|
||||
"preparing_your_download": "Préparation de votre téléchargement",
|
||||
"presentation_export_can_take_a_moment": "Compilation de votre présentation. Cela peut prendre jusqu'à une minute pour les présentations volumineuses…",
|
||||
"presentation_export_failed": "Échec de l'export de la présentation",
|
||||
"download_as_standalone_html": "Télécharger le HTML autonome",
|
||||
"download_pdf": "Télécharger le PDF",
|
||||
"download_zip_file": "Télécharger le fichier Zip",
|
||||
|
||||
Reference in New Issue
Block a user