feat(wordaddin): port Word add-in taskpane from poc
พอร์ต 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>
This commit is contained in:
parent
7a146d48be
commit
c401f4deb7
37 changed files with 11216 additions and 17 deletions
149
components/wordaddin/VersionHistoryPanel.tsx
Normal file
149
components/wordaddin/VersionHistoryPanel.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue