Replace the in-memory versioningService with real calls to the docver group (/api/documents/*) via apiClient with the ticket-issued Bearer; keep the mock only for ?addindemo (authService.isDemoMode). - uploadVersion: multipart POST /api/documents/versions (file, optional pdf when non-empty, clientVersionId, fields JSON, trimmed message, fileName) — identity fields are never sent (userId/userName/templateId in the form = 400 per the 2026-10-22 contract; docId comes from the fid claim) - getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds optional createdByUserName; history panel shows it with userId fallback) - getDownloadLink: GET presigned URL (+ ?format=pdf) - apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin through withBasePath (/docedit/api/...); the env is now only for a split deployment (must include basePath, HTTPS) - documentId/templateId params stay in signatures but are not sent - tests: versioningService.test.ts covers both modes (real sign-in via signInWithTicket, FormData contract assertions, DTO mapping, format query, 401 -> AuthRequiredError, demo mock without network) - docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section), addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket -> token -> upload docx / docx+pdf -> history -> presigned download of both formats). Co-Authored-By: Claude Code <noreply@anthropic.com>
44 KiB
Add-in UI Flow — คู่มือสร้าง UI ในโปรเจกต์ใหม่ (รอ API จริง)
เอกสารนี้สรุป เฉพาะฝั่ง UI/flow ของ Word add-in (/addin) ตัดส่วนที่เป็น Office.js mechanics ออกให้เหลือแต่สิ่งที่หน้าตา/พฤติกรรม UI ต้องมี เพื่อเอาไปสร้าง UI ในโปรเจกต์ใหม่แล้ว mock API รอ backend จริง
ต้นฉบับอ้างอิง (repo เดิมที่ D:\word-addin-poc\addin): src/taskpane/components/App.tsx, ConfirmVersionDialog.tsx, VersionHistoryPanel.tsx, src/services/versionFlow.ts, versioningService.ts, metadataService.ts, autoSaveService.ts, src/models/versionRecord.ts
1. State machine หลัก
Task pane ทั้งหมดเป็น state machine เดียว ไม่มี routing — component เดียว (App) สลับหน้าตาตาม phase:
ตั้งแต่ 2026-10-19 taskpane มี 2 แท็บ (toggle กลุ่มปุ่ม aria-pressed ใต้ header — แสดงเฉพาะ phase untracked/tracking): Versioning คือทุกอย่างในเอกสารนี้ตามเดิม ส่วน เครื่องมือ Template (ดูหัวข้อ 10) คือเครื่องมือจัดการ Content Controls ฝั่ง Office.js ล้วน ไม่ยิง backend จึงใช้ได้ทั้งสอง phase ปุ่ม toggle ทั้งสองปิดระหว่าง flow ที่เขียนเอกสาร — saving || starting (versioning), saveBusy (บันทึก local/ปิดเอกสาร), templateBusy (operation ของแท็บ template รายงานผ่าน onBusyChange — กันสลับกลาง operation แล้ว flow versioning มา stamp/capture ซ้อน หรือกลับกัน; code review 2026-10-20) · สถานะแท็บรีเซ็ตกลับ versioning ตอนเข้า no-access กัน surprise · banner auto-save error (FR-17) ยกไว้เหนือ toggle ให้เห็นทั้งสองแท็บ
ไม่มี login ตั้งแต่ 2026-10-27 (ยกเลิก Keycloak ตามคำขอผู้ใช้) — auth ทางเข้าเดียวคือ ticket ของ e-Court: detect() อ่าน custom properties ticket_data + HmacSha256 ที่ e-Court stamp มากับไฟล์ แล้วเรียก authService.signInWithTicket() — ตรวจ HMAC ฝั่ง client → ตรวจ exp → ดู session ใน localStorage ตาม uid/fid → ขอ access_token ที่ /api/documents/token เก็บตาม fid (flow เต็ม: docs/requirement/addin-workflow.md + แผน docs/PLAN-ADDIN-AUTH.md)
checking ──(ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ / ขอ token ไม่สำเร็จ)──→ no-access
│
├─(ticket ผ่าน + ไม่ใช่ tracked doc)──→ untracked
└─(ticket ผ่าน + tracked doc)────────→ tracking
การเปลี่ยน phase อื่นๆ:
untracked→ (Start Versioning สำเร็จ) →tracking- ทุก phase → (ไม่มี ticket ตอน detect ใหม่ / 401 หลังพยายาม renew จาก ticket แล้ว = ticket หมดอายุไม่มีทางต่ออายุ) →
no-access - ปุ่ม "ลองใหม่" มีจริงแค่ 2 จุด:
bootErrorในcheckingและ history error ในVersionHistoryPanel—untracked/trackingไม่มีปุ่มลองใหม่แยกต่างหาก (เริ่ม flow ใหม่ด้วยการกดปุ่ม flow ซ้ำ)
State ที่ UI ถือนอกเหนือจาก phase:
| State | ความหมาย |
|---|---|
activeTab |
แท็บที่เปิดอยู่ versioning (default) / template — ดูหัวข้อ 10 |
username |
ชื่อจาก uname ของ ticket — แสดงเป็น chip เฉย ๆ ใน header |
documentId |
tracking id ของเอกสาร (มีเฉพาะ tracking) |
documentTemplateId |
template ต้นทางของไฟล์ร่าง (มีเฉพาะ untracked ที่เป็นไฟล์ร่าง — FR-16) |
versions + historyLoading + historyError |
version history ของ documentId |
noAccessNotice |
ข้อความบนหน้า no-access — ต่างกันตามสาเหตุ (ไม่มี ticket ในไฟล์ / HMAC ไม่ตรง = ไฟล์ถูกแก้ / ticket หมดอายุ / ขอ token ไม่สำเร็จ) |
starting, saving, progressMessage |
busy + ข้อความ progress ของ flow Start Versioning / Save & Version |
confirmOpen, startConfirmOpen |
เปิด dialog ยืนยันของ flow ไหน |
flowError, flowWarning, flowSuccess |
MessageBar 3 แบบของ flow หลัก (แสดงเฉพาะ tracking) |
downloadingVersionId, downloadingPdfVersionId |
versionId ที่กำลังโหลด docx/pdf (spinner ที่ปุ่ม) |
closeSupported |
platform นี้ปิดเอกสารจาก add-in ได้ไหม — คุมการแสดงปุ่มปิดเอกสารทุกจุด (FR-18) |
saveBusy, saveStatus, saveError |
สถานะชุดปุ่ม [บันทึก] / [บันทึกและปิดเอกสาร] (แยกจาก flow versioning) |
autoSaveError |
banner เตือนเมื่อ auto-save ล้ม (FR-17) |
hasUncommitted |
เคยเห็นการแก้ไขหลัง version ล่าสุด → banner เตือน (FR-3) |
bootError |
error ของการ detect ตอนเปิด task pane (แสดงเฉพาะ checking — MessageBar error + ปุ่ม "ลองใหม่" รัน detect ใหม่) |
2. UI แต่ละ phase
Header (ทุก phase)
- ชื่อแอป: e-Court Documents Versioning (semibold)
- ฝั่งขวา (เมื่อผ่าน ticket auth แล้วและไม่ใช่
no-access/checking): chip เล็กพื้นcanvasแสดงusernameเฉย ๆ — ไม่ใช่ปุ่ม (ไม่มี login จึงไม่มีออกจากระบบ) · จอแคบตัดด้วย truncate ชื่อเต็มอยู่ในtitle
checking
Spinner "กำลังตรวจสอบเอกสาร…"ตรงกลาง- ถ้า detect พัง (
bootError): MessageBar error + ปุ่มลองใหม่ข้างใน bar (รัน detect ใหม่)
no-access
- ข้อความ: "เอกสารนี้ยังใช้งาน add-in ไม่ได้"
- คำอธิบายจางๆ (
noAccessNoticeตามสาเหตุ):- ไม่มี ticket ในไฟล์: "ไม่พบ ticket ของ e-Court ในไฟล์ — add-in ใช้ได้กับเอกสารที่ส่งมาจาก e-Court เท่านั้น"
- ticket รูปผิด / HMAC ไม่ตรง: "ตรวจสอบ ticket ไม่ผ่าน — ไฟล์ถูกแก้ไขหรือเสียหาย"
- ticket หมดอายุ (ตอนเปิด หรือโดน 401 กลาง flow หลังพยายาม renew): "ticket ของ e-Court หมดอายุ — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง"
- ขอ token ไม่สำเร็จ (server ลง/network): "ขอ token จากระบบไม่สำเร็จ — ตรวจการเชื่อมต่อแล้วเปิดเอกสารอีกครั้ง"
- ไม่มีปุ่มใด ๆ — ไม่มี login ให้กด (ยกเลิก Keycloak 2026-10-27; ขั้นตอนสำหรับเอกสารทั่วไปจะมีในอนาคต) ผู้ใช้ได้ไฟล์ใหม่จาก e-Court แล้วเปิดใหม่เท่านั้น
untracked
เรียงจากบนลงล่าง:
- ข้อความ: "เอกสารนี้ยังไม่ได้ติดตาม version"
- คำอธิบายจางๆ: "กดปุ่มด้านล่างเพื่อเริ่ม track — ระบบจะ stamp metadata ลงไฟล์และอัปโหลดเป็น version 1 (ไฟล์ที่สร้างจากเว็บ portal จะถูกตรวจพบและข้ามขั้นตอนนี้อัตโนมัติ)"
- เฉพาะไฟล์ร่าง (
documentTemplateIdมีค่า): ข้อความจางๆ เพิ่ม — "ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกเอกสารกับ template ต้นทาง และ sync ค่า fields ให้อัตโนมัติ" - ปุ่ม primary
Start Versioning This Document→ ระหว่างรัน:กำลังเริ่มติดตาม…+ disable - ชุดปุ่มบันทึก
SaveControls(ดูหัวข้อ 3) —disabledเมื่อstarting - progress: spinner เล็ก +
progressMessage(เฉพาะตอนstarting) flowError→ MessageBar error
tracking
เรียงจากบนลงล่าง:
- เฉพาะ
hasUncommitted: MessageBar warning — "มีการแก้ไขที่ยังไม่ถูกบันทึกเป็นเวอร์ชันใหม่ — กด Save & Version เพื่อบันทึกเป็น version ใหม่" (FR-3) flowWarning→ MessageBar warning (เช่น PDF ไม่สำเร็จ / version สำเร็จแต่ปิดเอกสารไม่สำเร็จ)flowSuccess→ MessageBar success (เช่น "สร้าง version 3 เรียบร้อย")flowError→ MessageBar error- ปุ่ม primary ขนาดใหญ่
Save & Version— disable เมื่อsaving - progress: spinner เล็ก +
progressMessage(เฉพาะตอนsaving) - ชุดปุ่มบันทึก
SaveControls—disabledเมื่อsaving - บรรทัดจางๆ:
tracking id: {documentId 8 ตัวแรก}… VersionHistoryPanel(ดูหัวข้อ 5)
3. ชุดปุ่มบันทึก SaveControls (FR-17/FR-18)
Component ใช้ร่วมทั้ง untracked และ tracking — อยู่นอก flow versioning:
- ปุ่ม
บันทึก— local save เดี่ยว ไม่แตะ metadata ไม่สร้าง version - ปุ่ม
บันทึกและปิดเอกสาร— เรนเดอร์เฉพาะcloseSupported === true(Word on the web เรียกclose()ไม่ได้ → ซ่อนทุกแบบ) - สถานะตามลำดับ:
saveBusy && status === "saving"→Spinner "กำลังบันทึก…"!saveBusy && status === "saved"→ text จางๆบันทึกแล้ว— ค้างไว้จนกดบันทึกรอบใหม่ (ไม่ reset เมื่อแก้ไขเอกสาร ไม่มี timeout)saveError→ MessageBar errorautoSaveError→ MessageBar warning (default: "บันทึกอัตโนมัติไม่สำเร็จ — ลองกด "บันทึก" ด้วยตนเอง") — ย้ายไป render เหนือ toggle แท็บใน TaskpaneApp (2026-10-20) ให้เห็นทั้งแท็บ Versioning และ Template ไม่มาใน SaveControls แล้ว
- ทั้งสองปุ่ม
disabledเมื่อdisabled(flow versioning กำลังรัน) หรือsaveBusy
พฤติกรรมปุ่ม:
| ปุ่ม | Flow |
|---|---|
บันทึก |
save → สำเร็จตั้ง saved, พังโชว์ saveError |
บันทึกและปิดเอกสาร |
save → สำเร็จเท่านั้นจึงปิดเอกสาร · save พัง = ไม่ปิด + saveError (AC-21) · ปิดสำเร็จแล้ว task pane จะปิดตามเอกสารเอง — ไม่มี toast ท้าย flow |
4. Dialog ยืนยัน ConfirmVersionDialog (FR-2 + FR-13 + FR-18)
Dialog เดียวใช้ 2 flow (ปรับข้อความด้วย props):
| Prop | Save & Version | Start Versioning |
|---|---|---|
| title | "สร้าง version ใหม่?" | "เริ่มติดตามเอกสารนี้?" |
| description | "ระบบจะบันทึกไฟล์ปัจจุบันและอัปโหลดเป็น version ถัดไปของเอกสารนี้" | ปกติ: "ระบบจะ stamp metadata ลงไฟล์และบันทึกไฟล์ปัจจุบันเป็น version 1 ของเอกสารนี้" · ไฟล์ร่าง: "ระบบจะ stamp metadata ลงไฟล์ บันทึกไฟล์ปัจจุบันเป็น version 1 และผูกเอกสารกับ template ต้นทาง (ไฟล์ร่าง)" |
| ปุ่มยืนยันหลัก | "สร้าง version" | "เริ่มติดตาม (version 1)" |
ภายใน dialog:
- ปุ่ม
ยกเลิก(secondary) มีเสมอ - เมื่อมีปุ่มที่สอง ("บันทึกเวอร์ชันและปิดเอกสาร") → ปุ่มนั้นเป็น primary ตัวเดียว และปุ่มยืนยันหลัก ("สร้าง version"/"เริ่มติดตาม") กลายเป็น secondary
- ช่อง "คำอธิบาย version (ไม่บังคับ)" — textarea,
maxLength 2048, 3 แถว, resize vertical, placeholder "เช่น แก้ไขข้อ 3 สัญญาจ้าง" — ค่าว่างได้ ส่งเป็น optional - ปุ่มที่สอง "บันทึกเวอร์ชันและปิดเอกสาร" (primary) — เรนเดอร์เฉพาะเมื่อ
closeSupportedและเฉพาะ flow Save & Version (Start Versioning ไม่มี) - ตอน
busy: ทุกปุ่ม disable + spinnerกำลังดำเนินการ…+ ปิด dialog ไม่ได้ (ปุ่มยกเลิก/คลิกนอก/ESC ไม่ทำงาน) — สถานะนี้เป็น defense ชั่วขณะก่อน re-render เท่านั้น - กดปุ่มยืนยัน (ตัวใดก็ตาม) → dialog ปิดทันที แล้ว progress/ผลลัพธ์ (spinner + progressMessage + flow messages) แสดงที่ pane — ไม่ปล่อย dialog ค้างบังผลลัพธ์ (กันกดยืนยันซ้ำสร้าง version ซ้ำ)
- focus: ตอนเปิดโฟกัสเข้าตัว dialog (container) + Tab ถูกกักไว้ใน dialog + ตอนปิดคืน focus ให้ปุ่ม trigger เดิม
- เคลียร์ช่อง message ทุกครั้งที่ dialog ปิด (component mount ค้างไว้ตลอด — ต้อง reset ตอน
openเปลี่ยนเป็น false ไม่ใช่ตอน unmount)
5. VersionHistoryPanel (FR-4)
- Header: icon history + "Version history" (semibold)
- สถานะ:
loading→Spinner "กำลังโหลด…"·error→ MessageBar error + ปุ่มลองใหม่· ว่าง (ไม่ loading ไม่ error และไม่มี version) → text จางๆ "ยังไม่มี version ในระบบ" - รายการ version เรียง ใหม่ → เก่า แต่ละแถว (hover มีพื้นหลัง):
Badge v{versionNumber}(สี brand)- timestamp รูปแบบ medium date + short time ตาม locale
โดย {createdByUserId}- commit message (ถ้ามี) — text 2 สี,
white-space: pre-wrap,overflow-wrap: anywhere
- ปุ่มขวาแต่ละแถว (เรียงเป็นคอลัมน์):
ดาวน์โหลด(icon download) → ตอนโหลด label คงเดิม สลับ icon เป็น spinner +aria-busy(label ยาว "กำลังดาวน์โหลด…" ทำแถว wrap กระโดด)PDF(icon pdf) — แสดงเฉพาะhasPdf === true→ ตอนโหลด label คงเดิม + spinner +aria-busy- ระหว่างมี version ใดกำลังโหลด → ปุ่มโหลดของทุกแถว (ทั้ง
ดาวน์โหลดและPDF) disable กันกดซ้อน
6. Flow หลักพร้อมข้อความ progress
ข้อความ flow (flowSuccess/flowWarning/flowError) ถูกเคลียร์เป็น null ทุกครั้งที่เริ่ม flow ใหม่ (Start Versioning / Save & Version / ดาวน์โหลด — ไม่ใช่ตอนจบ ไม่มี timeout)
Detect (FR-1) — ตอนเปิด task pane / กดลองใหม่
- อ่าน custom properties ครั้งเดียวทั้งชุด:
ticket_data,HmacSha256,SavedByAddIn,DocumentTrackingId,LastVersionId,LastVersionTimestamp,TemplateId - ticket auth (
signInWithTicket— PLAN-ADDIN-AUTH.md §5): อ่านไฟล์ไม่ได้ / ไม่มี ticket / HMAC ไม่ตรง / ticket หมดอายุ (ตามเงื่อนไข uid/fid ของ session) / ขอ token ไม่สำเร็จ →no-accessแยกข้อความตามสาเหตุ · ผ่าน = มี access_token ของ fid นี้เก็บใน localStoragewordaddin-authแล้วไปต่อ tracked = (SavedByAddIn === GUID ของ add-in):- tracked และมี
DocumentTrackingId→tracking+ โหลด history - ไม่ track หรือ track แต่ id หาย →
untracked(เก็บTemplateIdไว้ใช้ตอน Start Versioning — ไฟล์ร่างอยู่กลุ่มนี้)
- tracked และมี
Start Versioning (FR-1) — จาก untracked
dialog ยืนยัน → ข้อความ progress ตามลำดับ:
กำลังบันทึก metadata ของเอกสาร…กำลังอัปเดต version history ในไฟล์…กำลังบันทึกไฟล์…กำลังอ่านค่า fields จากเอกสาร…กำลังอัปโหลด version 1…กำลังสร้างสำเนา PDF…(best-effort — ล้มได้ไม่พัง flow)กำลังอัปโหลด version…
ขั้น 5 กับ 7 ไม่ใช่การอัปโหลดสองครั้ง — ขั้น 5 เป็น marker เข้าสู่ช่วง upload ระหว่างนั้นขั้น 6 ค่อย capture PDF (best-effort) แล้วขั้น 7 คือ POST จริงครั้งเดียวพร้อมแนบ
uploadCurrentFileของ versionFlow.ts)
สำเร็จ → tracking + flowSuccess = "เริ่มติดตามเอกสารนี้แล้ว — สร้างเป็น version 1" + refresh history · ถ้า hasPdf === false → flowWarning = "บันทึกแล้ว แต่สร้าง PDF ไม่สำเร็จ — version นี้ไม่มีสำเนา PDF (กด Save & Version อีกครั้งเพื่อสร้าง version ใหม่ที่มี PDF)" · พัง → flowError = "เริ่มติดตามเอกสารไม่สำเร็จ"
Save & Version (FR-2) — จาก tracking
dialog ยืนยัน → progress ทั้ง 7 ขั้นลำดับเดียวกัน ต่างจาก Start Versioning แค่ 2 ข้อความ: ขั้น 1 กำลังบันทึก metadata ของ version ใหม่… (เดิม "…metadata ของเอกสาร…") และขั้น 5 กำลังอัปโหลด version ใหม่… (เดิม "…version 1…") — ขั้น 2/3/4/6/7 ใช้ข้อความเดียวกันกับ Start Versioning เป๊ะ
หลัง upload สำเร็จ: ถ้าเลข version จริงจาก backend ต่างจากที่ client เดา (มี client อื่นแทรก) → แก้ XML part ในไฟล์ให้ตรง (best-effort ล้มได้เงียบๆ ไม่พัง flow)
สำเร็จ → flowSuccess = "สร้าง version {N} เรียบร้อย" + refresh history · hasPdf === false → warning เดียวกับด้านบน · กดปุ่มที่สอง (บันทึกเวอร์ชันและปิดเอกสาร) → version สำเร็จแล้วจึงปิดเอกสาร — ปิดสำเร็จ UI จบเลย (task pane ปิดตาม) · ปิดไม่สำเร็จ → แสดงเป็น warning ไม่ใช่ error ("บันทึก version N เรียบร้อย แต่ปิดเอกสารไม่สำเร็จ: … — ปิดเอกสารเองได้เลย") + refresh history ตาม เพราะ version ปลอดภัยบน backend แล้ว · พังก่อน upload → flowError = "สร้าง version ไม่สำเร็จ"
Download version (FR-4/FR-7)
ปุ่มต่อแถวใน history → ขอ download-link → เปิดในเบราว์เซอร์ของเครื่อง (ห้าม blob download ใน webview ของ Word — ถูกทิ้งเงียบๆ) → flowSuccess = "กำลังดาวน์โหลด v{N} ผ่านเบราว์เซอร์ของคุณ — ดูไฟล์ที่โฟลเดอร์ Downloads" หรือ (pdf) "กำลังเปิด PDF ของ v{N} ในเบราว์เซอร์ของคุณ" · พัง → flowError = "ดาวน์โหลด version ไม่สำเร็จ"
Auto-save (FR-17)
- เริ่มตั้งแต่ mount ไม่ผูกกับ phase (detect ยังไม่จบก็รันได้)
- periodic ทุก 30 วิ: ไฟล์ dirty → save + ตั้ง flag
hasUncommitted; สะอาด → ข้าม - save ล้ม →
autoSaveError(warning bar); สำเร็จครั้งถัดไป → เคลียร์ bar - ทุก flow versioning ต้อง suspend auto-save ตอนเริ่มและ resume ตอนจบ (กัน save แทรกกลาง stamp/capture)
- สร้าง version สำเร็จ → เคลียร์
hasUncommitted
7. API contract ที่ UI เรียก (จุด mock รอ API จริง)
Base URL มาจาก config (dev: https://localhost:7232) · ทุก request แนบ Authorization: Bearer {token} · backend serialize camelCase
POST /api/documents/{documentId}/versions — อัปโหลด version
multipart/form-data:
| field | type | หมายเหตุ |
|---|---|---|
file |
Blob (.docx) | ชื่อไฟล์ดึงจาก URL ของ document ถ้าได้ (desktop มักเป็น path จริง — เติม .docx ถ้ายังไม่มี) · ดึงไม่ได้ = ไม่ส่งชื่อ ให้ backend ตั้งเอง |
clientVersionId |
string (GUID) | client generate เองก่อน upload |
pdf |
Blob | optional — แนบเมื่อ capture PDF สำเร็จ |
fields |
string (JSON) | optional — map { [sdtTag]: text } จาก content controls |
message |
string | optional — commit message (trim แล้วว่าง = ไม่ส่ง, ≤2048) |
templateId |
string (GUID) | optional — เฉพาะ Start Versioning ของไฟล์ร่าง (FR-16) |
response: { hasPdf: boolean, versionId: string, versionNumber: number }
GET /api/documents/{documentId}/versions — history
response: VersionRecord[] เรียงใหม่ → เก่า:
interface VersionRecord {
versionId: string;
versionNumber: number;
createdAt: string; // ISO-8601
createdByUserId: string;
contentHash: string; // SHA256 hex
message: string | null; // commit message (FR-13)
hasPdf?: boolean;
fields: FieldsSnapshot | null;
}
// fields เก็บ 2 รูปแบบ: เอกสารผูก template → { [key]: { value, missing } }
// ไม่ผูก template → { [tag]: text } ดิบๆ — UI ต้องทนทั้งคู่
// (UI ปัจจุบันไม่ได้แสดง fields — type + helper readSnapshotField เตรียมไว้ใช้ภายหลัง)
GET /api/documents/{documentId}/versions/{versionId}/download-link[?format=pdf|docx] — ขอ URL อายุสั้น
response: { url: string, expiresAt: string } — UI เปิด url ในเบราว์เซอร์เครื่องผู้ใช้เท่านั้น
Auth — e-Court ticket (ไม่มี login)
ยกเลิก Keycloak OIDC/PKCE แล้ว (2026-10-27) — ไม่มี Office Dialog ไม่มีหน้า sign-in: e-Court stamp custom properties ticket_data (JSON exp/fid/tid/uid/uname) + HmacSha256 (HMAC-SHA256 ของ string ดิบ ด้วย secret ที่ config ได้ — ปัจจุบัน super_secret) ลงไฟล์ตอนสร้าง แล้ว signInWithTicket() ใช้ได้ทันที · flow ตาม docs/requirement/addin-workflow.md (แผนเต็ม docs/PLAN-ADDIN-AUTH.md): ตรวจ HMAC ฝั่ง client (UX gate) → ตรวจ exp → session ใน localStorage wordaddin-auth ตาม uid (user ใหม่ = เคลียร์ session เก่าทั้งก้อน) → ขอ access_token ที่ POST /api/documents/token แบบ body {ticket_data, HmacSha256} (server ตรวจ HMAC+exp อีกชั้นด้วย secret ฝั่ง server) → เก็บ token ตาม fid ("คีย์คือ fid") — กลับมาเปิดเอกสารเดิม = ใช้ token เดิมไม่ต้องขอใหม่ · token หมดอายุกลางใช้งาน (โดน 401) = ลองขอใหม่จาก ticket 1 ครั้ง (ticket ต้องยังไม่หมดอายุ) ยังไม่ผ่าน = AuthRequiredError → หน้า no-access ให้ขอไฟล์ใหม่จาก e-Court · token นี้คือสิ่งที่ apiClient แนบเป็น Authorization: Bearer ทุก request ตอนต่อ backend จริง (กลุ่ม docver บังคับ Bearer — docs/PLAN-DOCVER.md)
หมายเหตุสถานะปัจจุบัน: UI นี้ถูก implement แล้วใน repo นี้ที่ route
/docedit/wordaddin(ดูcomponents/wordaddin/+wordaddin/) — auth เป็น ticket auth จริงครบ flow (wordaddin/services/authService.ts+ticketService.ts+ ทางเข้า ticket ของ/api/documents/token) และ API ทั้งสามข้างบนเป็นของจริงแล้ว (wordaddin/services/versioningService.tsยิงกลุ่ม/api/documents/*ผ่าน Bearer — ปลด mock 2026-10-27; เหลือ mock เฉพาะโหมด?addindemo) · รายละเอียดการรันกับ backend:docs/WORDADDIN-DEV.md· สร้างไฟล์ทดสอบพร้อม ticket:node scripts/make-ticket-docx.mjs
8. กติกา error ที่ UI ต้องรองรับ
AuthRequiredError(ไม่มี token / 401 หลัง renew-retry) — ทุก flow ต้อง catch แล้ว: กลับno-access+ เคลียร์documentId/documentTemplateId/versions+ ตั้งnoAccessNotice= "ticket ของ e-Court หมดอายุหรือไม่ถูกต้อง — โปรดขอไฟล์ใหม่จาก e-Court แล้วเปิดอีกครั้ง" — ไม่ throw ต่อ ไม่โชว์ error ทับ phase- error อื่น → MessageBar ณ จุดที่เกิด (login/history/flow/download แยกกัน) — ห้ามปล่อย unhandled rejection
- แยก error ของ "ตัว version" ออกจาก "ปิดเอกสารไม่สำเร็จ" — อันหลังเป็น warning เพราะข้อมูลปลอดภัยแล้ว
- PDF เป็น best-effort เสมอ — ล้มแค่เตือน ไม่ fail flow
9. ข้อควรระวังที่กระทบหน้าตา UI
closeSupportedเป็น flag เดียวที่คุมปุ่ม "บันทึกและปิดเอกสาร" (SaveControls) และ ปุ่มที่สองใน ConfirmVersionDialog — Word on the web = false- disable กันกดซ้อน: ปุ่ม flow หนึ่งรัน → ปุ่ม flow อื่น + SaveControls disable ด้วย (แต่
saveBusyกับsavingแยกกัน — จะไม่ปิดกันเองระหว่าง local save) - ของเดิมใช้ Fluent UI v9 (
@fluentui/react-components+@fluentui/react-icons) — mapping ไป library อื่น: Button / Dialog / MessageBar / Spinner / Badge / Textarea / Text - UI ทั้งหมดภาษาไทย — ข้อความสำเนาได้จากไฟล์ต้นฉบับที่อ้างด้านบน
- Task pane แคบ (side panel) — layout เป็นคอลัมน์เดียว ปุ่ม/actions ต้องรองรับความกว้างจำกัด
- id ของ Content Control ไม่เสถียรข้าม session/coauthor (docstring ของ Office.js เอง) — เครื่องมือ Template ใช้ id เฉพาะภายในอายุผลสแกนรอบเดียว ห้ามเก็บไปใช้ภายหลัง
- การมองเห็นช่องตามรุ่นของ Word: collection
contentControlsธรรมดาเห็นเฉพาะ rich text CC (docstring ของBody.contentControls) — เครื่องมือ Template จึง feature-detectWordApi 1.5(isSetSupported) แล้วใช้Document.getContentControls()ที่เห็นทุกชนิด (รวม plain text ซึ่งเป็นส่วนใหญ่ของ template ศาล — เคสจริง: T3 มี plain 26 + rich 1 สแกนเห็นเฉพาะdetail1) บน Word ต่ำกว่า 1.5 (เช่น Office 2019 = 1.3 ตาม manifest) มองเห็น/เขียนได้เฉพาะ rich text พร้อม warning แจ้งผู้ใช้ · ห้ามยก MinVersion ของ manifest เพื่อปลดข้อจำกัดนี้ (Office 2019 จะโหลด add-in ไม่ขึ้น — บทเรียน 2026-10-16)
10. แท็บ "เครื่องมือ Template" (2026-10-19)
เครื่องมือช่วยทำ template สำหรับคนออกแบบเอกสาร — TemplateToolsPanel (state ภายในทั้งหมด มีแค่ prop onBusyChange รายงาน busy ให้ TaskpaneApp ปิด toggle แท็บ) + service wordaddin/services/templateService.ts — Office.js ล้วน ไม่ยิง backend (ส่วนใหญ่ WordApi 1.1 · getByIdOrNullObject 1.3 · Document.getContentControls() 1.5 ผ่าน feature-detect — ดู §9 ข้อ 7) ใช้ได้ทั้ง phase untracked/tracking · ไม่ auto-scan ตอนเปิดแท็บ (กดปุ่มเท่านั้น) · สลับแท็บแล้ว panel unmount = ทิ้ง state ทิ้งโดยตั้งใจ (snapshot เก่าเก็บไว้ก็ stale เสียเอง) · ปุ่มทั้ง panel disable ด้วย busy กันกดซ้อน
§1 ตรวจสอบ Content Controls
- ปุ่ม
สแกนช่องในเอกสาร(icon ScanSearch → spinner +aria-busyตอนรัน) → list timeline แบบ VersionHistoryPanel: ลำดับ · tag (break-all+titleแสดง tag/title/type ดิบ · ช่องไม่มี tag แสดง "(ไม่มี tag)" และจุดโหนดเป็นวงกลมขอบเหลือง) · badge ชนิด "ในบรรทัด"/"บล็อก" (heuristics: enum type ลงท้าย "Inline") · ค่าย่อ truncate 1 บรรทัด +titleเต็ม · ปุ่มดูช่อง(icon Crosshair) →cc.select()ให้ Word เลื่อนไปเลือกช่อง - สรุปหัวกลุ่ม: "N ช่อง"
- warning ใต้ปุ่มเมื่อพบปัญหา: "มี N ช่องที่ยังไม่ได้ตั้ง tag" / "tag ซ้ำ: a, b — ตอน render ระบบอ่านเฉพาะช่องแรกของ tag"
- empty 2 แบบ: ยังไม่สแกน ("กดสแกนเพื่อดูรายการช่องในเอกสาร") กับสแกนแล้วว่าง ("ไม่พบ content control ในเอกสาร")
§2 กรอกค่าทดสอบ
- แสดงเฉพาะมีช่องที่ตั้ง tag ≥1 ช่อง (จากผลสแกน): ฟอร์ม label+input ต่อ tag (id
wordaddin-template-value-<index>) ค่าเริ่ม = ค่าปัจจุบันของช่องแรกของ tag นั้น + ปุ่ม primaryเขียนค่าลงเอกสาร(icon PenLine → "กำลังเขียนค่า…" ตอนรัน) - ช่องที่เว้นว่าง = ไม่ถูกแตะ (กันเผลอล้างค่าจริง) · เขียนทุก CC ที่ tag ตรง (tag ซ้ำหลายช่อง = เขียนทุกช่องแบบ mail merge) · อ่าน id+tag สดรอบเดียวแล้วเขียนแยก Word.run ราย tag — tag ที่ Word ปฏิเสธ fail เฉพาะตัว เหลืออื่นยังเขียนได้ (ผลลัพธ์:
appliedCount= จำนวนช่องที่เขียนจริง +missingTagsไม่พบช่อง +failuresพร้อม code/errorLocation จาก OfficeExtension.Error) · UI รวมเป็น warning พร้อมชี้นำ "ถ้าเอกสารถูกล็อค (Review → Restrict Editing) ให้ปลดก่อน" — เคสจริงที่เจอ: error0x800A16C6บนเอกสารที่make-protected.mjsล็อคไว้ (readOnly + enforcement) - ผลลัพธ์: success "เขียนค่าทดสอบลงแล้ว N ช่อง" (N = จำนวนช่องที่เขียนจริง — tag ซ้ำหลายช่องนับทุกช่อง) + warning missingTags ถ้ามี → สแกนใหม่โดยคงค่าฟอร์ม (เฉพาะ tag ที่ยังอยู่ในเอกสาร — tag ที่ถูกลบไปตัดทิ้ง) ให้แก้ต่อ · ปุ่ม disabled เมื่อยังไม่มีช่องที่กรอกค่าเลย · หลังเขียนสำเร็จ
notifyDocumentChanged()ตั้ง debounce auto-save 3 วิ ส่วน banner FR-3 ตั้งเมื่อ periodic tick ถัดไปของ autoSaveService เห็น document dirty (พฤติกรรมเดิมของ service — ไม่ใช่ทันที)
§3 แทรกย่อหน้า / ตัวแบ่ง (2026-10-19)
- 3 ปุ่มแนวตั้ง (icon บน label ล่าง, grid 3 คอลัมน์,
titleอธิบายเต็ม): ย่อหน้า (CornerDownLeft —insertParagraph("", "After")เหมือนกด Enter) · แบ่งบรรทัด (WrapText —insertBreak("Line", "After")เหมือน Shift+Enter จัดที่อยู่/รายการในย่อหน้าเดิม) · แบ่งหน้า (FilePlus — เหมือน Ctrl+Enter เอกสารหลายหน้า:insertBreak("Page", "After")ก่อน ถ้า host ปฏิเสธ E_FAIL 0x80004005 (บางบริบท เช่น cursor ในตาราง — เคสจริงบน T3 2026-10-19) สลับไปinsertOoxmlแบบ w:p สมบูรณ์ครอบ<w:br w:type="page"/>— fragment ต้องเป็น block-level เต็มตัว ใช้ run เดี่ยวก็ยัง E_FAIL) - กดแล้วทำที่ cursor ทันที (ไม่มี success message — เห็นผลในเอกสารเลย) · icon สลับเป็น spinner ตอนรันตัวนั้น · ทุกปุ่ม disable ด้วย
busyร่วมกับเครื่องมืออื่น · fallback error: "แทรกย่อหน้าไม่สำเร็จ" / "แทรกตัวแบ่งบรรทัดไม่สำเร็จ" / "แทรกตัวแบ่งหน้าไม่สำเร็จ"
หมายเหตุ: ไม่มีปุ่มสลับ formatting marks (และไม่มีชุดแทรกอักขระพิเศษ)
- Office.js ไม่มี API สลับแสดง/ซ่อน formatting marks (¶ ท้ายย่อหน้า/จุดช่องว่าง) — เป็น application setting ของ Word ที่ WordApi ไม่เปิดให้ add-in แตะ ผู้ใช้กด Ctrl+Shift+8 หรือปุ่ม ¶ บนแท็บ Home ของ Word เอง (workflow จริงของผู้ใช้: แทรกย่อหน้าเพิ่ม → เปิด marks ตรวจ → ซ่อนกลับ — จึงมีปุ่มแทรกย่อหน้าให้ ส่วน marks ใช้ของ Word)
- เคยมีชุด "แทรกอักขระพิเศษ" (เลขไทย ๐–๙ + สัญลักษณ์) ต่อท้าย panel แล้วถอดออก 2026-10-19 ตามคำขอผู้ใช้ — ที่ต้องการจริงคือ formatting marks ซึ่งทำผ่าน add-in ไม่ได้ (อ่านบันทึกเต็มใน git history รอบนั้น)
- เคยมีเครื่องมือ "สร้างช่องจากข้อความที่เลือก" (dialog ตั้ง tag +
insertContentControlครอบ selection +validateTag+TagInputDialog+ unit test ของ validateTag) แล้วถอดออกทั้งเส้น 2026-10-19 ตามคำขอผู้ใช้ — การสร้างช่องใหม่ทำใน Word ด้วย Developer tab แทน
ข้อความ error กลางของ panel (MessageBar intent error)
- สแกนพัง: fallback "สแกนช่องในเอกสารไม่สำเร็จ" · ดูช่องพัง: "ไม่พบช่องนี้ในเอกสาร (อาจถูกลบไปแล้ว) — กดสแกนใหม่" / fallback "เลือกช่องไม่สำเร็จ" · เขียนค่าพัง: fallback "เขียนค่าลงเอกสารไม่สำเร็จ" · แทรกพัง: fallback "แทรกย่อหน้าไม่สำเร็จ" / "แทรกตัวแบ่งบรรทัดไม่สำเร็จ" / "แทรกตัวแบ่งหน้าไม่สำเร็จ" (แบ่งหน้าที่พังทั้งสองวิธีจะรวมสาเหตุของทั้ง insertBreak และ OOXML fallback ในข้อความเดียว)
พฤติกรรมใน browser ธรรมดา / ?addindemo
ทุกปุ่มที่แตะเอกสาร (สแกน/ดูช่อง/เขียนค่า/ย่อหน้า/แบ่งบรรทัด/แบ่งหน้า) จบที่ error MessageBar ("Word is not defined") — ตามปรัชญา demo ของ repo เหมือนปุ่ม Save & Version ส่วน toggle/สลับแท็บ/ฟอร์มค่าทดสอบ ทดสอบได้ครบใน browser