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:
parent
618328ffa7
commit
39d610a781
39 changed files with 1387 additions and 212 deletions
|
|
@ -16,7 +16,7 @@ import * as authService from "../../services/authService";
|
|||
import { AuthRequiredError } from "../../services/apiClient";
|
||||
import { getErrorMessage } from "../../services/errors";
|
||||
import * as metadataService from "../../services/metadataService";
|
||||
import { downloadVersion, getDownloadLink, getVersions } from "../../services/versioningService";
|
||||
import { getDownloadLink, getVersions } from "../../services/versioningService";
|
||||
import { saveAndVersion, startVersioning } from "../../services/versionFlow";
|
||||
import { VersionRecord } from "../../models/versionRecord";
|
||||
import ConfirmVersionDialog from "./ConfirmVersionDialog";
|
||||
|
|
@ -89,6 +89,8 @@ const App: React.FC = () => {
|
|||
const [confirmOpen, setConfirmOpen] = React.useState(false);
|
||||
const [startConfirmOpen, setStartConfirmOpen] = React.useState(false);
|
||||
const [flowError, setFlowError] = React.useState<string | null>(null);
|
||||
const [flowWarning, setFlowWarning] = React.useState<string | null>(null);
|
||||
const [downloadingPdfVersionId, setDownloadingPdfVersionId] = React.useState<string | null>(null);
|
||||
const [flowSuccess, setFlowSuccess] = React.useState<string | null>(null);
|
||||
|
||||
const [downloadingVersionId, setDownloadingVersionId] = React.useState<string | null>(null);
|
||||
|
|
@ -215,11 +217,16 @@ const App: React.FC = () => {
|
|||
setStarting(true);
|
||||
setFlowError(null);
|
||||
setFlowSuccess(null);
|
||||
setFlowWarning(null);
|
||||
try {
|
||||
const result = await startVersioning(message, setProgressMessage);
|
||||
setDocumentId(result.documentId);
|
||||
setPhase("tracking");
|
||||
setFlowSuccess(`เริ่มติดตามเอกสารนี้แล้ว — สร้างเป็น version 1`);
|
||||
if (!result.hasPdf)
|
||||
setFlowWarning(
|
||||
"บันทึกแล้ว แต่สร้าง PDF ไม่สำเร็จ — version นี้ไม่มีสำเนา PDF (กด Save & Version อีกครั้งเพื่อสร้าง version ใหม่ที่มี PDF)"
|
||||
);
|
||||
await refreshHistory(result.documentId);
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
|
|
@ -240,10 +247,20 @@ const App: React.FC = () => {
|
|||
setSaving(true);
|
||||
setFlowError(null);
|
||||
setFlowSuccess(null);
|
||||
setFlowWarning(null);
|
||||
try {
|
||||
const latestVersionNumber = versions[0]?.versionNumber ?? 0;
|
||||
const result = await saveAndVersion(documentId, latestVersionNumber, message, setProgressMessage);
|
||||
const result = await saveAndVersion(
|
||||
documentId,
|
||||
latestVersionNumber,
|
||||
message,
|
||||
setProgressMessage
|
||||
);
|
||||
setFlowSuccess(`สร้าง version ${result.versionNumber} เรียบร้อย`);
|
||||
if (!result.hasPdf)
|
||||
setFlowWarning(
|
||||
"บันทึกแล้ว แต่สร้าง PDF ไม่สำเร็จ — version นี้ไม่มีสำเนา PDF (กด Save & Version อีกครั้งเพื่อสร้าง version ใหม่ที่มี PDF)"
|
||||
);
|
||||
await refreshHistory(documentId);
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
|
|
@ -258,36 +275,33 @@ const App: React.FC = () => {
|
|||
/**
|
||||
* FR-4 — ดาวน์โหลด version ที่เลือก: ขอ presigned URL อายุสั้นจาก backend แล้วเปิดใน
|
||||
* เบราว์เซอร์ของเครื่อง (webview ของ Word — โดยเฉพาะ WKWebView บน Mac — ทิ้ง blob download
|
||||
* แบบเงียบๆ ไฟล์ไม่เคยถูกเขียนที่ไหนเลย) · host ไหนไม่มี openBrowserWindow จะย้อนกลับไปใช้ blob
|
||||
* แบบเงียบๆ ไฟล์ไม่เคยถูกเขียนที่ไหนเลย)
|
||||
*/
|
||||
const handleDownload = async (version: VersionRecord) => {
|
||||
const handleDownload = async (version: VersionRecord, format: "docx" | "pdf" = "docx") => {
|
||||
if (!documentId) {
|
||||
return;
|
||||
}
|
||||
setDownloadingVersionId(version.versionId);
|
||||
const setDownloading = format === "pdf" ? setDownloadingPdfVersionId : setDownloadingVersionId;
|
||||
setDownloading(version.versionId);
|
||||
setFlowError(null);
|
||||
try {
|
||||
const link = await getDownloadLink(documentId, version.versionId);
|
||||
const link = await getDownloadLink(documentId, version.versionId, format);
|
||||
if (typeof Office.context.ui.openBrowserWindow === "function") {
|
||||
Office.context.ui.openBrowserWindow(link.url);
|
||||
setFlowSuccess(`กำลังดาวน์โหลด v${version.versionNumber} ผ่านเบราว์เซอร์ของคุณ — ดูไฟล์ที่โฟลเดอร์ Downloads`);
|
||||
setFlowSuccess(
|
||||
format === "pdf"
|
||||
? `กำลังเปิด PDF ของ v${version.versionNumber} ในเบราว์เซอร์ของคุณ`
|
||||
: `กำลังดาวน์โหลด v${version.versionNumber} ผ่านเบราว์เซอร์ของคุณ — ดูไฟล์ที่โฟลเดอร์ Downloads`
|
||||
);
|
||||
} else {
|
||||
const blob = await downloadVersion(documentId, version.versionId);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `document_v${version.versionNumber}.docx`;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.setTimeout(() => URL.revokeObjectURL(url), 60_000);
|
||||
throw new Error("Word นี้ไม่รองรับการเปิดเบราว์เซอร์ กรุณาเปิดไฟล์จาก Web Portal");
|
||||
}
|
||||
} catch (error) {
|
||||
if (!handleAuthRequired(error)) {
|
||||
setFlowError(getErrorMessage(error, "ดาวน์โหลด version ไม่สำเร็จ"));
|
||||
}
|
||||
} finally {
|
||||
setDownloadingVersionId(null);
|
||||
setDownloading(null);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -379,6 +393,11 @@ const App: React.FC = () => {
|
|||
</MessageBar>
|
||||
) : null}
|
||||
|
||||
{flowWarning ? (
|
||||
<MessageBar intent="warning">
|
||||
<MessageBarBody>{flowWarning}</MessageBarBody>
|
||||
</MessageBar>
|
||||
) : null}
|
||||
{flowSuccess ? (
|
||||
<MessageBar intent="success">
|
||||
<MessageBarBody>{flowSuccess}</MessageBarBody>
|
||||
|
|
@ -415,6 +434,8 @@ const App: React.FC = () => {
|
|||
loading={historyLoading}
|
||||
error={historyError}
|
||||
downloadingVersionId={downloadingVersionId}
|
||||
downloadingPdfVersionId={downloadingPdfVersionId}
|
||||
onDownloadPdf={(version) => void handleDownload(version, "pdf")}
|
||||
onDownload={(version) => void handleDownload(version)}
|
||||
onRetry={() => documentId && void refreshHistory(documentId)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue