พอร์ต taskpane ของ Word add-in "e-Court Documents Versioning" จาก D:\word-addin-poc มาเป็น sibling app ที่ route /docedit/wordaddin: - wordaddin/ (client-only zone): services พอร์ตดิบทั้งชุด (authService/versioningService เป็น mock รอ backend จริง) + models + config - components/wordaddin/: WordAddinGate ฉีด office.js self-host (singleton กัน StrictMode double-inject + fallback timer ครอบเคส onReady เงียบ) และ TaskpaneApp state machine แปลง JSX เป็น Genesis design system - scripts/fetch-office-js.mjs (postinstall ตัวที่สาม): ดาวน์โหลด Office.js runtime ล่าสุดจาก CDN Microsoft ไป public/wordaddin/office-js/ (gitignore สร้างใหม่ได้ — npm @microsoft/office-js ตกรุ่นไม่มี Document.close) - middleware/basePath/root page: เปิด /wordaddin เป็น public (auth แยกจาก แอป note), root /docedit redirect ไป taskpane ยกเว้นลิงก์ e-Court N8 (?fileUrl=&fileId=&typeId=) ที่ยังส่งต่อ /note ตามเดิม - docker/web.Dockerfile รัน fetch-office-js ตอน build + npm scripts dev:https สำหรับ sideload ตาม docs/WORDADDIN-DEV.md Co-Authored-By: Claude Code <noreply@anthropic.com>
149 lines
6.4 KiB
TypeScript
149 lines
6.4 KiB
TypeScript
import { Download, FileText, LoaderCircle } from "lucide-react";
|
|
|
|
import { VersionRecord } from "@/wordaddin/models/versionRecord";
|
|
|
|
import { MessageBar } from "./MessageBar";
|
|
import { Spinner } from "./Spinner";
|
|
|
|
/**
|
|
* FR-4 — แสดง version history (เลข version / เวลา / ผู้สร้าง) + ปุ่มดาวน์โหลด version นั้น
|
|
* ชั้นภาพ: timeline แนวตั้ง — จุดโหนดเท่าทึบคือเวอร์ชันล่าสุด จุดกลวงคือเก่ากว่า
|
|
* (เลข version เป็น static → ใช้ ink ตามกฎ "blue เฉพาะ interactive" ของ DESIGN.md)
|
|
*/
|
|
|
|
function formatTimestamp(iso: string): string {
|
|
const date = new Date(iso);
|
|
return Number.isNaN(date.getTime())
|
|
? iso
|
|
: date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
}
|
|
|
|
/** px-2 py-1.5 + text-xs ≈ สูง 30px — พ้นเกณฑ์ touch target 24px ของ WCAG 2.5.8 โดยยัง dense พอสำหรับ list */
|
|
const BTN_ACTION =
|
|
"focus-ring inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-xs text-ink-soft transition hover:bg-primary/[0.06] hover:text-primary disabled:pointer-events-none disabled:opacity-50";
|
|
|
|
export function VersionHistoryPanel({
|
|
versions,
|
|
loading,
|
|
error,
|
|
downloadingVersionId,
|
|
downloadingPdfVersionId,
|
|
onDownloadPdf,
|
|
onDownload,
|
|
onRetry,
|
|
}: {
|
|
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 anyDownloading = downloadingVersionId !== null || downloadingPdfVersionId !== null;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="mt-4 mb-2 flex items-baseline justify-between">
|
|
<h3 className="font-display text-sm font-semibold text-ink">Version history</h3>
|
|
{versions.length > 0 ? <span className="text-xs text-ink-soft">{versions.length} เวอร์ชัน</span> : null}
|
|
</div>
|
|
|
|
{loading ? <Spinner label="กำลังโหลด…" /> : null}
|
|
|
|
{error ? (
|
|
<MessageBar
|
|
intent="error"
|
|
action={
|
|
<button
|
|
type="button"
|
|
className="focus-ring rounded-md border border-error/40 bg-white px-2 py-1.5 text-xs text-error-ink"
|
|
onClick={onRetry}
|
|
>
|
|
ลองใหม่
|
|
</button>
|
|
}
|
|
>
|
|
{error}
|
|
</MessageBar>
|
|
) : null}
|
|
|
|
{!loading && !error && versions.length === 0 ? (
|
|
<p className="text-sm text-ink-soft">ยังไม่มี version ในระบบ</p>
|
|
) : null}
|
|
|
|
{versions.length > 0 ? (
|
|
<ol className="relative">
|
|
{/* ราง timeline — วาดครั้งเดียวระดับรายการ (เวอร์ชันเดียวไม่ต้องมีเส้น) */}
|
|
{versions.length > 1 ? (
|
|
<span className="absolute bottom-2 left-[3.5px] top-2 w-px bg-line" aria-hidden />
|
|
) : null}
|
|
{versions.map((version, index) => {
|
|
const isLatest = index === 0;
|
|
return (
|
|
<li className="relative pb-3 pl-4 last:pb-0" key={version.versionId}>
|
|
<span
|
|
className={`absolute left-0 top-[5px] size-2 rounded-full ${
|
|
isLatest ? "bg-ink" : "border border-ink-faint bg-canvas"
|
|
}`}
|
|
aria-hidden
|
|
/>
|
|
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
<span className="font-display text-sm font-semibold text-ink">v{version.versionNumber}</span>
|
|
{isLatest ? (
|
|
<span className="rounded-full bg-ink/[0.06] px-1.5 py-px text-[11px] text-ink-soft">ล่าสุด</span>
|
|
) : null}
|
|
<span className="ml-auto flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
className={BTN_ACTION}
|
|
disabled={anyDownloading}
|
|
aria-busy={downloadingVersionId === version.versionId}
|
|
onClick={() => onDownload(version)}
|
|
>
|
|
{/* คง label เดิม + สลับเป็น spinner — label ยาว "กำลังดาวน์โหลด…" ทำแถว wrap กระโดด */}
|
|
{downloadingVersionId === version.versionId ? (
|
|
<LoaderCircle className="size-3.5 animate-spin" aria-hidden />
|
|
) : (
|
|
<Download className="size-3.5" aria-hidden />
|
|
)}
|
|
ดาวน์โหลด
|
|
</button>
|
|
{version.hasPdf ? (
|
|
<button
|
|
type="button"
|
|
className={BTN_ACTION}
|
|
disabled={anyDownloading}
|
|
aria-busy={downloadingPdfVersionId === version.versionId}
|
|
onClick={() => onDownloadPdf(version)}
|
|
>
|
|
{downloadingPdfVersionId === version.versionId ? (
|
|
<LoaderCircle className="size-3.5 animate-spin" aria-hidden />
|
|
) : (
|
|
<FileText className="size-3.5" aria-hidden />
|
|
)}
|
|
PDF
|
|
</button>
|
|
) : null}
|
|
</span>
|
|
</div>
|
|
{version.message ? (
|
|
<p className="mt-0.5 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs text-ink">
|
|
{version.message}
|
|
</p>
|
|
) : null}
|
|
<p className="mt-0.5 text-xs text-ink-soft">
|
|
{formatTimestamp(version.createdAt)} · โดย {version.createdByUserId}
|
|
</p>
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default VersionHistoryPanel;
|