Add searchable and flattened export, history re-export and diagnostics self-test
issueAndArchive keeps an issued invoice even when rendering or archiving fails (Issued - not archived, with a Render & archive action). The export menu offers a searchable PDF or a 300 DPI image-only PDF whose metadata is the invoice number alone; flatten failures offer Retry or searchable export and never silently lower the resolution. History re-exports the original as issued by default and warns when a re-render's layout fingerprint differs. The render client gains a FIFO assemble lane that previews cannot supersede. A Diagnostics panel runs fixtures in the real worker and compares against goldens generated from Node.
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, InlineLoading, InlineNotification } from "@carbon/react";
|
||||
import { api } from "../lib/api";
|
||||
import type { InvoiceExport } from "../hooks/useInvoiceExport";
|
||||
import FlattenProgressModal from "./FlattenProgressModal";
|
||||
|
||||
/** Progress modal, spinner, success and error notices for one `useInvoiceExport`. */
|
||||
export default function ExportFeedback({ exp }: { exp: InvoiceExport }) {
|
||||
const { busy, progress, request, error, result } = exp;
|
||||
const [actionError, setActionError] = useState<string | null>(null);
|
||||
useEffect(() => setActionError(null), [result]);
|
||||
|
||||
const flattenFailed = request?.mode === "flattened" && error?.flatten === true;
|
||||
const modalOpen = request?.mode === "flattened" && ((busy && progress?.phase !== "saving") || flattenFailed);
|
||||
|
||||
const run = async (action: () => Promise<void>) => {
|
||||
setActionError(null);
|
||||
try {
|
||||
await action();
|
||||
} catch (e) {
|
||||
setActionError(String(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlattenProgressModal
|
||||
open={modalOpen}
|
||||
progress={progress}
|
||||
error={flattenFailed ? error!.message : null}
|
||||
onCancel={exp.cancel}
|
||||
onRetry={exp.retry}
|
||||
onExportSearchable={exp.exportSearchableInstead}
|
||||
onClose={exp.dismiss}
|
||||
/>
|
||||
|
||||
{busy && !modalOpen ? <InlineLoading description={progress?.phase === "saving" ? "Waiting for the save dialog…" : "Preparing the PDF…"} /> : null}
|
||||
|
||||
{error && !flattenFailed ? (
|
||||
<InlineNotification kind="error" lowContrast title="The PDF was not exported" subtitle={error.message} onCloseButtonClick={exp.dismiss}>
|
||||
<div className="voiced-notice-actions">
|
||||
<Button kind="ghost" size="sm" onClick={exp.retry}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</InlineNotification>
|
||||
) : null}
|
||||
|
||||
{result ? (
|
||||
<>
|
||||
<InlineNotification
|
||||
kind="success"
|
||||
lowContrast
|
||||
title={`Saved to ${result.path}`}
|
||||
subtitle={result.identicalToIssued === true ? "The layout is identical to the issued invoice." : undefined}
|
||||
onCloseButtonClick={exp.dismiss}
|
||||
>
|
||||
<div className="voiced-notice-actions">
|
||||
<Button kind="ghost" size="sm" onClick={() => void run(() => api.openFile(result.path))}>
|
||||
Open file
|
||||
</Button>
|
||||
<Button kind="ghost" size="sm" onClick={() => void run(() => api.revealInFolder(result.path))}>
|
||||
Show in folder
|
||||
</Button>
|
||||
</div>
|
||||
</InlineNotification>
|
||||
{result.identicalToIssued === false ? (
|
||||
<InlineNotification
|
||||
kind="warning"
|
||||
lowContrast
|
||||
hideCloseButton
|
||||
title="Layout differs from the issued invoice"
|
||||
subtitle="The current template lays this invoice out differently from the PDF issued. The archived original is unchanged; use Export PDF to get it."
|
||||
/>
|
||||
) : null}
|
||||
{result.archiveError ? (
|
||||
<InlineNotification kind="warning" lowContrast hideCloseButton title="This invoice is still not archived" subtitle={result.archiveError} />
|
||||
) : null}
|
||||
{actionError ? <InlineNotification kind="error" lowContrast title="Could not open the file" subtitle={actionError} onCloseButtonClick={() => setActionError(null)} /> : null}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user