Files

75 lines
2.5 KiB
TypeScript

"use client"
// Download the approved lines as an Excel workbook. The procedure answers with the file as base64
// (a procedure's answer is JSON on every transport); the browser turns it into a download. With
// nothing approved the button is disabled and says why, in text a screen reader reads with it.
import { type ReactElement, useId, useState } from "react"
import type { ProjectSlugs } from "./slugs.js"
import { useTransport } from "./transport.js"
export interface ExportLabels {
readonly export: string
readonly hint: string
readonly nothing: string
/** Said after the workbook was handed to the browser. */
readonly exported: string
readonly failed: string
}
export function ExportButton({
slugs,
fileId,
approved,
labels,
}: {
slugs: ProjectSlugs
fileId: string
/** How many lines are approved: with none, there is nothing to export. */
approved: number
labels: ExportLabels
}): ReactElement {
const { call, describe } = useTransport()
const hintId = useId()
const [message, setMessage] = useState<{ ok: boolean; text: string } | undefined>(undefined)
const [busy, setBusy] = useState(false)
const run = async (): Promise<void> => {
setBusy(true)
setMessage(undefined)
try {
const made = (await call("documents.export.create", { ...slugs, fileId })) as {
filename: string
contentType: string
body: string
}
const bytes = Uint8Array.from(atob(made.body), (char) => char.charCodeAt(0))
const url = URL.createObjectURL(new Blob([bytes], { type: made.contentType }))
const link = document.createElement("a")
link.href = url
link.download = made.filename
// Attached while it is clicked, and the object URL kept for a while after: a browser starts the
// download after the click returns, and a URL revoked first is a download that never starts.
document.body.append(link)
link.click()
link.remove()
setTimeout(() => URL.revokeObjectURL(url), 60_000)
setMessage({ ok: true, text: labels.exported })
} catch (error) {
setMessage({ ok: false, text: labels.failed.replace("{detail}", describe(error)) })
} finally {
setBusy(false)
}
}
return (
<div>
<button type="button" disabled={busy || approved === 0} aria-describedby={hintId} onClick={run}>
{labels.export}
</button>
<p id={hintId}>{approved === 0 ? labels.nothing : labels.hint}</p>
<p role={message?.ok === false ? "alert" : "status"}>{message?.text ?? ""}</p>
</div>
)
}