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.
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { Button, ComposedModal, InlineLoading, InlineNotification, ModalBody, ModalFooter, ModalHeader, Tag } from "@carbon/react";
|
|
import { reportToText, runSelfTest, type Check, type SelfTestReport } from "../lib/selfTest";
|
|
|
|
const STATUS_TAG: Record<Check["status"], { type: "green" | "red" | "gray"; text: string }> = {
|
|
pass: { type: "green", text: "Pass" },
|
|
fail: { type: "red", text: "Fail" },
|
|
info: { type: "gray", text: "Info" },
|
|
};
|
|
|
|
/** Renders the fixtures in this webview's real render client and reports what it found. */
|
|
export default function DiagnosticsPanel({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
const [report, setReport] = useState<SelfTestReport | null>(null);
|
|
const [fatal, setFatal] = useState<string | null>(null);
|
|
const [copied, setCopied] = useState<"ok" | "failed" | null>(null);
|
|
const runId = useRef(0);
|
|
|
|
const run = useCallback(async () => {
|
|
const id = ++runId.current;
|
|
setReport(null);
|
|
setFatal(null);
|
|
setCopied(null);
|
|
try {
|
|
// Loaded on demand: the fixtures, goldens and pdf.js are only needed here.
|
|
const { browserSelfTestDeps } = await import("../lib/selfTestBrowser");
|
|
await runSelfTest(browserSelfTestDeps(), (r) => {
|
|
if (runId.current === id) setReport(r);
|
|
});
|
|
} catch (e) {
|
|
if (runId.current === id) setFatal(e instanceof Error ? e.message : String(e));
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (open) void run();
|
|
else runId.current++;
|
|
}, [open, run]);
|
|
|
|
const copy = async () => {
|
|
if (!report) return;
|
|
try {
|
|
await navigator.clipboard.writeText(reportToText(report));
|
|
setCopied("ok");
|
|
} catch {
|
|
setCopied("failed");
|
|
}
|
|
};
|
|
|
|
const running = open && !fatal && !report?.done;
|
|
|
|
return (
|
|
<ComposedModal open={open} onClose={onClose} size="md" preventCloseOnClickOutside aria-label="Diagnostics">
|
|
<ModalHeader title="Diagnostics" label="Settings" />
|
|
<ModalBody hasScrollingContent>
|
|
<p className="voiced-muted" style={{ marginBottom: "1rem" }}>
|
|
Renders two sample invoices in this app's own renderer, reads them back and compares them with the reference layouts. Nothing is saved or sent.
|
|
</p>
|
|
{fatal ? <InlineNotification kind="error" lowContrast hideCloseButton title="The self-test could not run" subtitle={fatal} /> : null}
|
|
{report && report.done ? (
|
|
<InlineNotification
|
|
kind={report.passed ? "success" : "error"}
|
|
lowContrast
|
|
hideCloseButton
|
|
title={report.passed ? "All checks passed" : "Some checks failed"}
|
|
subtitle={report.passed ? undefined : "Copy the report and include it when you report the problem."}
|
|
/>
|
|
) : null}
|
|
{report?.sections.map((section) => (
|
|
<section key={section.title} style={{ marginTop: "1.5rem" }} aria-label={section.title}>
|
|
<h4 style={{ marginBottom: "0.5rem" }}>{section.title}</h4>
|
|
{section.checks.map((check, i) => (
|
|
<div key={`${check.label}-${i}`} className="voiced-diagnostics__check">
|
|
<div className="voiced-diagnostics__head">
|
|
<Tag type={STATUS_TAG[check.status].type} size="sm">
|
|
{STATUS_TAG[check.status].text}
|
|
</Tag>
|
|
<span className="voiced-diagnostics__label">{check.label}</span>
|
|
{check.ms !== undefined ? <span className="voiced-diagnostics__ms">{check.ms} ms</span> : null}
|
|
</div>
|
|
{check.detail ? <p className="voiced-diagnostics__detail">{check.detail}</p> : null}
|
|
</div>
|
|
))}
|
|
</section>
|
|
))}
|
|
{running ? (
|
|
<div style={{ marginTop: "1.5rem" }}>
|
|
<InlineLoading description="Running checks…" />
|
|
</div>
|
|
) : null}
|
|
{copied ? (
|
|
<div style={{ marginTop: "1rem" }}>
|
|
<InlineNotification
|
|
kind={copied === "ok" ? "success" : "warning"}
|
|
lowContrast
|
|
hideCloseButton
|
|
title={copied === "ok" ? "Report copied" : "Could not copy"}
|
|
subtitle={copied === "ok" ? undefined : "The clipboard is not available here."}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<Button kind="secondary" onClick={() => void run()} disabled={running}>
|
|
Run again
|
|
</Button>
|
|
<Button kind="secondary" onClick={() => void copy()} disabled={!report}>
|
|
Copy report
|
|
</Button>
|
|
<Button kind="primary" onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
</ModalFooter>
|
|
</ComposedModal>
|
|
);
|
|
}
|