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>
This commit is contained in:
Suphonchai Phoonsawat 2026-09-08 14:01:13 +07:00
parent 618328ffa7
commit 39d610a781
39 changed files with 1387 additions and 212 deletions

View file

@ -9,7 +9,11 @@ import {
makeStyles,
tokens,
} from "@fluentui/react-components";
import { ArrowDownload24Regular, History24Regular } from "@fluentui/react-icons";
import {
ArrowDownload24Regular,
DocumentPdf24Regular,
History24Regular,
} from "@fluentui/react-icons";
import { VersionRecord } from "../../models/versionRecord";
interface VersionHistoryPanelProps {
@ -18,6 +22,8 @@ interface VersionHistoryPanelProps {
error: string | null;
/** versionId ของ version ที่กำลังดาวน์โหลดอยู่ (แสดง spinner ที่ปุ่ม) */
downloadingVersionId: string | null;
downloadingPdfVersionId: string | null;
onDownloadPdf: (version: VersionRecord) => void;
onDownload: (version: VersionRecord) => void;
onRetry: () => void;
}
@ -46,6 +52,7 @@ const useStyles = makeStyles({
backgroundColor: tokens.colorNeutralBackground1Hover,
},
},
actions: { display: "flex", flexDirection: "column", flexShrink: 0 },
itemInfo: {
display: "flex",
flexDirection: "column",
@ -76,6 +83,8 @@ export const VersionHistoryPanel: React.FC<VersionHistoryPanelProps> = ({
loading,
error,
downloadingVersionId,
downloadingPdfVersionId,
onDownloadPdf,
onDownload,
onRetry,
}) => {
@ -115,19 +124,30 @@ export const VersionHistoryPanel: React.FC<VersionHistoryPanelProps> = ({
</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>
{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>
<Button
icon={<ArrowDownload24Regular />}
size="small"
appearance="subtle"
disabled={downloadingVersionId !== null}
onClick={() => onDownload(version)}
>
{downloadingVersionId === version.versionId ? "กำลังดาวน์โหลด…" : "ดาวน์โหลด"}
</Button>
</div>
))}
</div>