eCourt-note/docs/addin-ui-flow.md
Your Name c401f4deb7 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>
2026-10-16 12:07:25 +07:00

27 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:

checking ──(ไม่มี token)──────────────→ signed-out
   │
   ├─(มี token + ไม่ใช่ tracked doc)──→ untracked
   └─(มี token + tracked doc)────────→ tracking

การเปลี่ยน phase อื่นๆ:

  • signed-out → (sign in สำเร็จ) → checking → ตัดสินใหม่ตามไฟล์ปัจจุบัน
  • untracked → (Start Versioning สำเร็จ) → tracking
  • ทุก phase → (401 ที่ refresh token ไม่ช่วย / กดออกจากระบบ) → signed-out
  • ปุ่ม "ลองใหม่" มีจริงแค่ 2 จุด: bootError ใน checking และ history error ใน VersionHistoryPanel — untracked/tracking ไม่มีปุ่มลองใหม่แยกต่างหาก (เริ่ม flow ใหม่ด้วยการกดปุ่ม flow ซ้ำ) · กลไกเดียวที่วิ่งกลับมา checking คือ sign-in สำเร็จ

State ที่ UI ถือนอกเหนือจาก phase:

State ความหมาย
username ชื่อผู้ใช้จาก token (แสดงใน header)
documentId tracking id ของเอกสาร (มีเฉพาะ tracking)
documentTemplateId template ต้นทางของไฟล์ร่าง (มีเฉพาะ untracked ที่เป็นไฟล์ร่าง — FR-16)
versions + historyLoading + historyError version history ของ documentId
signingIn, loginError สถานะหน้า sign-in
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)
  • ฝั่งขวา (เมื่อ login แล้วและไม่ใช่ checking/signed-out): ปุ่มเล็ก icon + ออกจากระบบ — username อยู่ใน title/aria-label (จอแคบ ไม่กินพื้นที่แถบหัว) — กดแล้ว sign out + กลับ signed-out

checking

  • Spinner "กำลังตรวจสอบเอกสาร…" ตรงกลาง
  • ถ้า detect พัง (bootError): MessageBar error + ปุ่ม ลองใหม่ ข้างใน bar (รัน detect ใหม่)

signed-out

  • ข้อความ: "เข้าสู่ระบบเพื่อเริ่มใช้งาน versioning"
  • คำอธิบายจางๆ: "ใช้บัญชีของ realm word-versioning — หน้าต่าง login จะเปิดแยกต่างหาก"
  • ปุ่ม primary เข้าสู่ระบบ → ระหว่างรันเปลี่ยนเป็น กำลังเข้าสู่ระบบ… + disable
  • ถ้า login พัง: MessageBar warning (loginError) — เช่น "เซสชันหมดอายุ — กรุณา login ใหม่" ตอนโดน 401 กลาง flow

ใน project จริง login เปิดหน้าต่างแยกผ่าน Office Dialog API (ห้าม redirect iframe ไป Keycloak ตรงๆ) — ถ้าโปรเจกต์ใหม่ยังไม่มี auth ให้ mock login() คืน { ok: true } ไปก่อน

untracked

เรียงจากบนลงล่าง:

  1. ข้อความ: "เอกสารนี้ยังไม่ได้ติดตาม version"
  2. คำอธิบายจางๆ: "กดปุ่มด้านล่างเพื่อเริ่ม track — ระบบจะ stamp metadata ลงไฟล์และอัปโหลดเป็น version 1 (ไฟล์ที่สร้างจากเว็บ portal จะถูกตรวจพบและข้ามขั้นตอนนี้อัตโนมัติ)"
  3. เฉพาะไฟล์ร่าง (documentTemplateId มีค่า): ข้อความจางๆ เพิ่ม — "ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกเอกสารกับ template ต้นทาง และ sync ค่า fields ให้อัตโนมัติ"
  4. ปุ่ม primary Start Versioning This Document → ระหว่างรัน: กำลังเริ่มติดตาม… + disable
  5. ชุดปุ่มบันทึก SaveControls (ดูหัวข้อ 3) — disabled เมื่อ starting
  6. progress: spinner เล็ก + progressMessage (เฉพาะตอน starting)
  7. flowError → MessageBar error

tracking

เรียงจากบนลงล่าง:

  1. เฉพาะ hasUncommitted: MessageBar warning — "มีการแก้ไขที่ยังไม่ถูกบันทึกเป็นเวอร์ชันใหม่ — กด Save & Version เพื่อบันทึกเป็น version ใหม่" (FR-3)
  2. flowWarning → MessageBar warning (เช่น PDF ไม่สำเร็จ / version สำเร็จแต่ปิดเอกสารไม่สำเร็จ)
  3. flowSuccess → MessageBar success (เช่น "สร้าง version 3 เรียบร้อย")
  4. flowError → MessageBar error
  5. ปุ่ม primary ขนาดใหญ่ Save & Version — disable เมื่อ saving
  6. progress: spinner เล็ก + progressMessage (เฉพาะตอน saving)
  7. ชุดปุ่มบันทึก SaveControls — disabled เมื่อ saving
  8. บรรทัดจางๆ: tracking id: {documentId 8 ตัวแรก}…
  9. 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 error
    • autoSaveError → MessageBar warning (default: "บันทึกอัตโนมัติไม่สำเร็จ — ลองกด "บันทึก" ด้วยตนเอง")
  • ทั้งสองปุ่ม 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 / หลัง login / กดลองใหม่

  1. ตรวจ token — ไม่มี → signed-out
  2. อ่าน custom properties ครั้งเดียวทั้งชุด: SavedByAddIn, DocumentTrackingId, LastVersionId, LastVersionTimestamp, TemplateId
  3. tracked = (SavedByAddIn === GUID ของ add-in):
    • tracked และมี DocumentTrackingId → tracking + โหลด history
    • ไม่ track หรือ track แต่ id หาย → untracked (เก็บ TemplateId ไว้ใช้ตอน Start Versioning — ไฟล์ร่างอยู่กลุ่มนี้)

Start Versioning (FR-1) — จาก untracked

dialog ยืนยัน → ข้อความ progress ตามลำดับ:

  1. กำลังบันทึก metadata ของเอกสาร…
  2. กำลังอัปเดต version history ในไฟล์…
  3. กำลังบันทึกไฟล์…
  4. กำลังอ่านค่า fields จากเอกสาร…
  5. กำลังอัปโหลด version 1…
  6. กำลังสร้างสำเนา PDF… (best-effort — ล้มได้ไม่พัง flow)
  7. กำลังอัปโหลด version…

ขั้น 5 กับ 7 ไม่ใช่การอัปโหลดสองครั้ง — ขั้น 5 เป็น marker เข้าสู่ช่วง upload ระหว่างนั้นขั้น 6 ค่อย capture PDF (best-effort) แล้วขั้น 7 คือ POST จริงครั้งเดียวพร้อมแนบ pdf ใน multipart เดียวกัน (ตาม 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 ไม่รอ sign-in
  • 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 เตรียมไว้ใช้ภายหลัง)

response: { url: string, expiresAt: string } — UI เปิด url ในเบราว์เซอร์เครื่องผู้ใช้เท่านั้น

Auth — OIDC PKCE ผ่าน Office Dialog API

displayDialogAsync เปิดหน้า login แยก → messageParent ส่งผลกลับ task pane · token เก็บ ใน memory เท่านั้น (ห้าม localStorage) · refresh token auto-refresh เงียบๆ จน 401 → สถานะ AuthRequiredError · mock ช่วงรอ: login() คืน { ok: true }, getAccessToken() คืน token ปลอม, getUsername() คืนชื่อ test

หมายเหตุสถานะปัจจุบัน (2026-10-14): UI นี้ถูก implement แล้วใน repo นี้ที่ route /docedit/wordaddin (ดู components/wordaddin/ + wordaddin/) — ทั้ง auth (แทนด้วย mock wordaddin/services/authService.ts) และ API ทั้งสามข้างบน (แทนด้วย in-memory mock wordaddin/services/versioningService.ts) · วิธีสลับกลับของจริง: docs/WORDADDIN-DEV.md


8. กติกา error ที่ UI ต้องรองรับ

  1. AuthRequiredError (401 ที่ refresh ไม่ช่วย) — ทุก flow ต้อง catch แล้ว: กลับ signed-out + เคลียร์ documentId/documentTemplateId/versions + ตั้ง loginError = "เซสชันหมดอายุ — กรุณา login ใหม่" — ไม่ throw ต่อ ไม่โชว์ error ทับ phase
  2. error อื่น → MessageBar ณ จุดที่เกิด (login/history/flow/download แยกกัน) — ห้ามปล่อย unhandled rejection
  3. แยก error ของ "ตัว version" ออกจาก "ปิดเอกสารไม่สำเร็จ" — อันหลังเป็น warning เพราะข้อมูลปลอดภัยแล้ว
  4. PDF เป็น best-effort เสมอ — ล้มแค่เตือน ไม่ fail flow

9. ข้อควรระวังที่กระทบหน้าตา UI

  1. closeSupported เป็น flag เดียวที่คุมปุ่ม "บันทึกและปิดเอกสาร" (SaveControls) และ ปุ่มที่สองใน ConfirmVersionDialog — Word on the web = false
  2. disable กันกดซ้อน: ปุ่ม flow หนึ่งรัน → ปุ่ม flow อื่น + SaveControls disable ด้วย (แต่ saveBusy กับ saving แยกกัน — จะไม่ปิดกันเองระหว่าง local save)
  3. ของเดิมใช้ Fluent UI v9 (@fluentui/react-components + @fluentui/react-icons) — mapping ไป library อื่น: Button / Dialog / MessageBar / Spinner / Badge / Textarea / Text
  4. UI ทั้งหมดภาษาไทย — ข้อความสำเนาได้จากไฟล์ต้นฉบับที่อ้างด้านบน
  5. Task pane แคบ (side panel) — layout เป็นคอลัมน์เดียว ปุ่ม/actions ต้องรองรับความกว้างจำกัด