eCourt-Doc-Edit/addin/src/taskpane/components/VersionHistoryPanel.tsx
Suphonchai Phoonsawat 39d610a781 Phase 11: version PDF (FR-15)
- เลข version ใหม่: เว็บ-generate = v0 ฉบับตั้งต้น (ไม่มี PDF), add-in save (Start Versioning + Save & Version) = v1+ มี PDF เสมอ
- PDF ผลิตจาก Word จริงผ่าน task pane เท่านั้น (getFileAsync Pdf + runtime guard, manifest ไม่เพิ่ม requirement); capture/upload เป็น best-effort — docx ผ่านแม้ PDF ล้ม + MessageBar เตือนตาม hasPdf
- Backend: DocumentVersion.PdfObjectKey (nullable, migration AddVersionPdf) + sibling .pdf ใน bucket docx-versions · multipart part pdf (ผิด type → 400) · /download + /download-link รับ ?format=pdf (ไม่มี → 404) · presigned ฝังชื่อไฟล์ไทย + _vN (RFC 5987)
- Web: เปิดไฟล์แทน blob-download — PDF แท็บใหม่, docx ผ่าน ms-word:ofe|u| พร้อม heuristic fallback ลิงก์สำรอง (useVersionOpen, versionDownload) · ปุ่ม "เปิดใน Word" ที่หน้า success ของฟอร์มสร้างเอกสาร
- Spec v7 + CONTEXT.md + ADR 0001 + แผน docs/plans/version-pdf.md
- Automated QA: backend 68 / add-in 63 / web 55 ผ่าน · manual acceptance ผ่านบน Word for Mac (2026-09-08) รวม fidelity + track changes — ค้างเฉพาะ Windows / Word on the web

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-08 14:01:13 +07:00

157 lines
4.8 KiB
TypeScript

import * as React from "react";
import {
Badge,
Button,
MessageBar,
MessageBarBody,
Spinner,
Text,
makeStyles,
tokens,
} from "@fluentui/react-components";
import {
ArrowDownload24Regular,
DocumentPdf24Regular,
History24Regular,
} from "@fluentui/react-icons";
import { VersionRecord } from "../../models/versionRecord";
interface VersionHistoryPanelProps {
versions: VersionRecord[];
loading: boolean;
error: string | null;
/** versionId ของ version ที่กำลังดาวน์โหลดอยู่ (แสดง spinner ที่ปุ่ม) */
downloadingVersionId: string | null;
downloadingPdfVersionId: string | null;
onDownloadPdf: (version: VersionRecord) => void;
onDownload: (version: VersionRecord) => void;
onRetry: () => void;
}
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
gap: tokens.spacingVerticalS,
},
header: {
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalXS,
marginTop: tokens.spacingVerticalL,
marginBottom: tokens.spacingVerticalXXS,
},
item: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: tokens.spacingHorizontalS,
padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalS}`,
borderRadius: tokens.borderRadiusMedium,
":hover": {
backgroundColor: tokens.colorNeutralBackground1Hover,
},
},
actions: { display: "flex", flexDirection: "column", flexShrink: 0 },
itemInfo: {
display: "flex",
flexDirection: "column",
minWidth: 0,
},
metaRow: {
color: tokens.colorNeutralForeground3,
fontSize: tokens.fontSizeBase200,
},
messageRow: {
color: tokens.colorNeutralForeground2,
fontSize: tokens.fontSizeBase200,
whiteSpace: "pre-wrap",
overflowWrap: "anywhere",
},
});
function formatTimestamp(iso: string): string {
const date = new Date(iso);
return Number.isNaN(date.getTime())
? iso
: date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
}
/** FR-4 — แสดง version history (เลข version / เวลา / ผู้สร้าง) + ปุ่มดาวน์โหลด version นั้น */
export const VersionHistoryPanel: React.FC<VersionHistoryPanelProps> = ({
versions,
loading,
error,
downloadingVersionId,
downloadingPdfVersionId,
onDownloadPdf,
onDownload,
onRetry,
}) => {
const styles = useStyles();
return (
<div className={styles.root}>
<div className={styles.header}>
<History24Regular />
<Text weight="semibold">Version history</Text>
</div>
{loading ? <Spinner size="small" label="กำลังโหลด…" /> : null}
{error ? (
<MessageBar intent="error">
<MessageBarBody>
{error}{" "}
<Button size="small" appearance="subtle" onClick={onRetry}>
ลองใหม่
</Button>
</MessageBarBody>
</MessageBar>
) : null}
{!loading && !error && versions.length === 0 ? (
<Text size={200} style={{ color: tokens.colorNeutralForeground3 }}>
ยังไม่มี version ในระบบ
</Text>
) : null}
{versions.map((version) => (
<div className={styles.item} key={version.versionId}>
<div className={styles.itemInfo}>
<Badge appearance="filled" color="brand">
v{version.versionNumber}
</Badge>
<span className={styles.metaRow}>{formatTimestamp(version.createdAt)}</span>
<span className={styles.metaRow}>โดย {version.createdByUserId}</span>
{version.message ? <span className={styles.messageRow}>{version.message}</span> : null}
</div>
<div className={styles.actions}>
<Button
icon={<ArrowDownload24Regular />}
size="small"
appearance="subtle"
disabled={downloadingVersionId !== null || downloadingPdfVersionId !== null}
onClick={() => onDownload(version)}
>
{downloadingVersionId === version.versionId ? "กำลังดาวน์โหลด…" : "ดาวน์โหลด"}
</Button>
{version.hasPdf ? (
<Button
icon={<DocumentPdf24Regular />}
size="small"
appearance="subtle"
disabled={downloadingVersionId !== null || downloadingPdfVersionId !== null}
onClick={() => onDownloadPdf(version)}
>
{downloadingPdfVersionId === version.versionId ? "กำลังเปิด…" : "PDF"}
</Button>
) : null}
</div>
</div>
))}
</div>
);
};
export default VersionHistoryPanel;