implement phase 5 และ fix bugs

This commit is contained in:
Suphonchai Phoonsawat 2026-08-19 23:06:57 +07:00
parent f4b1c3e14b
commit 1bb850c6b8
40 changed files with 1507 additions and 429 deletions

View file

@ -1,7 +1,7 @@
# Implementation Plan: Word Versioning Add-in (Office.js + .NET 8)
> แผนงานสร้างระบบจาก spec: [`specs/spec-officejs.md`](../../specs/spec-officejs.md)
> สถานะ: Phase 0–4 เสร็จ (scaffold + infra + backend ครบ + add-in services layer) — ต่อไป: phase 5 (add-in UI) หรือ phase 6 (web) ทำขนานกันได้
> สถานะ: Phase 0–5 เสร็จ (backend ครบ + add-in ครบทั้ง services และ UI — เหลือตรวจ sideload บน Word จริง) — ต่อไป: phase 6 (web portal)
> วันที่แผน: 2026-08-18 (ฉบับแก้ไข v2: เพิ่ม FR-9 ถึง FR-11 — ระบบ template, สร้างเอกสาร v1 ผ่านเว็บ และ field sync)
---
@ -51,24 +51,32 @@
/word-addin
/addin # Phase 0, 4, 5
manifest.xml
eslint.config.mjs # office-addin-lint ใช้ไฟล์นี้แทน config ของมัน (ปิด rule ที่ crash)
jest.config.js
/src
/taskpane
taskpane.html
taskpane.tsx
auth-callback.html
index.tsx # Office.onReady → FluentProvider + App
auth-callback.html/.ts # redirect target ของ Keycloak (รันใน Office Dialog)
/components
App.tsx # state machine: checking → signed-out → untracked → tracking
VersionHistoryPanel.tsx
ConfirmVersionDialog.tsx
/services
authService.ts # PKCE + Dialog API + token in-memory + silent refresh
metadataService.ts # custom properties + custom XML part + content controls
versioningService.ts
fileService.ts
authService.ts
apiClient.ts
fileService.ts # getFileAsync → slices → Blob (normalize slice.data ทุกรูปแบบ)
apiClient.ts # fetch + Bearer + retry-on-401 ครั้งเดียว
versioningService.ts # uploadVersion / getVersions / download + download-link
versionFlow.ts # orchestration ของ Start Versioning / Save & Version (7 ขั้น)
errors.ts
/models
versionRecord.ts
/test # wordMock / httpMock / setup (jsdom polyfills)
package.json
webpack.config.js
webpack.config.js # DefinePlugin: KEYCLOAK_AUTHORITY / API_BASE_URL จาก env
/scripts
dev-token.py # เดิน PKCE จริงได้ token ทดสอบ backend (admin-cli ไม่มี aud)
/backend # Phase 0, 2, 3
/WordVersioning.Api
Program.cs
@ -344,13 +352,13 @@ POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
**เป้า:** UI ครบทุก flow — ตรง spec ส่วน 11 (frontend ข้อ 4)
- [ ] `taskpane.tsx` state machine หลัก: `checking → signed-out → untracked → tracking`
- [ ] **Sign-in gate (FR-1):** `Office.onReady()` → เช็ค token ก่อน — ไม่มี/หมดอายุ → หน้า "Sign in" (ปุ่มเรียก authService); มี → เช็ค property `SavedByAddIn` เทียบ `ADDIN_SIGNATURE_GUID`
- [x] `taskpane.tsx` state machine หลัก: `checking → signed-out → untracked → tracking`
- [x] **Sign-in gate (FR-1):** `Office.onReady()` → เช็ค token ก่อน — ไม่มี/หมดอายุ → หน้า "Sign in" (ปุ่มเรียก authService); มี → เช็ค property `SavedByAddIn` เทียบ `ADDIN_SIGNATURE_GUID`
- ตรง → อ่าน `DocumentTrackingId` → `getVersions()` → เข้าสถานะ `tracking` (**ก็คือ flow ของไฟล์ที่สร้างจากเว็บ — เปิดปุ๊บเห็น v1 ทันที ไม่ต้อง Start Versioning**)
- ไม่ตรง → ปุ่ม **"Start Versioning This Document"**
- [ ] **Start Versioning:** stamp metadata รอบแรก (`SavedByAddIn`, `DocumentTrackingId` ใหม่) → upload version แรก → สลับเป็น `tracking`
- [ ] `ConfirmVersionDialog.tsx` — ยืนยัน "สร้าง version ใหม่ของเอกสารนี้หรือไม่?"
- [ ] **Save & Version flow (FR-2) ตามลำดับ spec แบบเคร่งครัด:**
- [x] **Start Versioning:** stamp metadata รอบแรก (`SavedByAddIn`, `DocumentTrackingId` ใหม่) → upload version แรก → สลับเป็น `tracking`
- [x] `ConfirmVersionDialog.tsx` — ยืนยัน "สร้าง version ใหม่ของเอกสารนี้หรือไม่?"
- [x] **Save & Version flow (FR-2) ตามลำดับ spec แบบเคร่งครัด:**
1. เปิด confirm dialog
2. Stamp properties: `SavedByAddIn`, `DocumentTrackingId`, `LastVersionId`, `LastVersionTimestamp`
3. อัปเดต XML part (delete-then-add, fail ได้แบบ silent)
@ -359,13 +367,29 @@ POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
6. `POST /api/documents/{documentId}/versions` — แนบ `fields` ด้วย
7. แสดงผล + refresh history
- ทุก step มี loading state + error message + **ห้ามปล่อย unhandled rejection**
- [ ] `VersionHistoryPanel.tsx` (FR-4) — list version number/timestamp/ผู้สร้าง + ปุ่ม "Download this version" (`GET .../download`)
- [ ] **Soft reminder (FR-3):** polling/debounce ทุก ~30 วินาทีเช็ค `context.document.saved` — `false` ต่อเนื่องเกิน threshold → banner "มีการแก้ไขที่ยังไม่ได้ version" (ไม่ block ใคร)
- [ ] จัดการ token หมดอายุกลาง session: 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in อย่างเนียน (AC-9)
- [ ] Fluent UI components ให้หน้าตาเข้ากับ Office
- [x] `VersionHistoryPanel.tsx` (FR-4) — list version number/timestamp/ผู้สร้าง + ปุ่ม "Download this version" (`GET .../download`)
- [x] **Soft reminder (FR-3):** polling/debounce ทุก ~30 วินาทีเช็ค `context.document.saved` — `false` ต่อเนื่องเกิน threshold → banner "มีการแก้ไขที่ยังไม่ได้ version" (ไม่ block ใคร)
- [x] จัดการ token หมดอายุกลาง session: 401 → silent refresh → ยังไม่ได้ → กลับหน้า sign-in อย่างเนียน (AC-9)
- [x] Fluent UI components ให้หน้าตาเข้ากับ Office
หมายเหตุจากการทำจริง (2026-08-19):
- **state machine อยู่ที่ `src/taskpane/components/App.tsx`** (yo office ให้โครง index.tsx + App.tsx — ไม่มี taskpane.tsx ตามชื่อในแผน) และ orchestration ของ Start Versioning / Save & Version แยกเป็น `services/versionFlow.ts` เพื่อ unit test ได้ (mock ทั้ง Word/Office/fetch) — `npm test` ขยายเป็น 45 ตัว
- **`LastVersionId` ในไฟล์ = `clientVersionId`** (GUID ที่ client generate) ไม่ใช่ versionId ของ backend — เพราะต้อง stamp ลง properties *ก่อน* `document.save()` จึงจะไปอยู่ในไฟล์ที่อัปโหลด (backend versionId ได้มาหลังไฟล์ถูก capture ไปแล้ว) · ส่วน entry ใน XML part เดาเลข version จาก history ล่าสุด+1 ก่อน upload ถ้า backend ตอบเลขต่าง (มี client อื่นแทรก) จะเขียน XML part แก้ให้ตรงแบบ best-effort
- **เปิด `strict` ใน tsconfig ของ addin** — พบว่า tsconfig ของ scaffold ปิด strictNullChecks ซึ่งทำให้ boolean-literal discriminated union (`{ok:true} | {ok:false; error}`) **narrow ไม่ได้** (compile error) — เปิดแล้วมี error เพิ่มแค่ 4 จุดใน test (แก้แล้ว)
- ชื่อไฟล์ตอน upload ได้จาก `Office.context.document.url` (desktop มักมี path จริง) fallback `{documentId}.docx` (Word Online อาจไม่มี URL)
- FR-3: poll ทุก 30 วิ **เฉพาะ phase `tracking`** อ่าน `context.document.saved` — fail รอบใดข้ามรอบนั้น (banner เป็น soft reminder เท่านั้น)
- ดาวน์โหลด version: fetch เป็น blob (ต้องแนบ Bearer) → `URL.createObjectURL` + anchor `download` — ชื่อไฟล์ `document_v{N}.docx`
- ลบ scaffold demo ออก (Header/HeroList/TextInsertion/taskpane.ts) — task pane เป็นหน้าจริงของระบบแล้ว
- ⚠️ **DoD ส่วน sideload ต้องตรวจด้วยมือใน Word จริง** (Claude รัน Word ไม่ได้) — ขั้นตอน: รัน infra + backend + `npm start` แล้ว sideload manifest.xml บน Word Desktop/Online แล้วไล่ checklist AC-1, 2, 4–8, 13 (ส่วนที่ automate ได้คือ build/test/lint/validate ซึ่งผ่านทั้งหมดแล้ว)
- 🩹 **แก้จาก sideload test จริงรอบแรก:** กด Sign in แล้วโดน "The HTTP protocol is not supported. Use HTTPS instead" — **Office Dialog API ปฏิเสธ http:// โดยสมบูรณ์** ทั้งที่ Keycloak dev เดิมรัน http://localhost:8080 → แก้โดยให้ Keycloak เสิร์ฟ **HTTPS ที่ 8443** ด้วย cert ของ `office-addin-dev-certs` (mount `${HOME}/.office-addin-dev-certs` เข้า container + `KC_HTTPS_CERTIFICATE_FILE/KEY_FILE`) · 8080 เหลือไว้เฉพาะภายใน network ของ compose (ให้ service `api` ใช้) · ค่าที่ตามไปเปลี่ยน: backend `Authentication:Authority` → `https://localhost:8443/realms/word-versioning`, add-in webpack default `KEYCLOAK_AUTHORITY` → เดียวกัน, README/.env.example/test constants ทั้งหมด · ตรวจจริงครบ: discovery ผ่าน (issuer = https…8443), PKCE flow จริงได้ token ที่มี `aud` ถูกต้อง, backend ตอบ 200 · **ผลพลอยได้:** เจอว่าตัวอย่าง curl admin-cli เดิมเรียก backend ไม่ผ่าน (token ไม่มี `aud`) → เพิ่ม `scripts/dev-token.py` (เดิน PKCE จริง ได้ token ใช้ทดสอบ backend/Swagger) และแก้ README ให้ใช้แทน
- 🩹 **แก้จาก sideload test จริงรอบสี่:** กดดาวน์โหลด version แล้ว**ไฟล์ไม่ไปไหนเลย** (ค้น ~/Downloads + Word container + Spotlight ไม่เจอ) — **WKWebView ของ Word ทิ้ง blob download (`a[download]` + object URL) แบบเงียบๆ** → เปลี่ยนวิธีเป็น "ขอ presigned URL อายุสั้นแล้วเปิดในเบราว์เซอร์ของเครื่อง": backend เพิ่ม `GET .../download-link` (MinIO presigned GET · อายุ config `MinIO:PresignedExpirySeconds` default 120s clamp 30–600 — spec ส่วน 9 อนุญาต presigned URL อายุสั้น) · add-in ใช้ `Office.context.ui.openBrowserWindow` (manifest เพิ่ม `OpenBrowserWindowApi 1.1`) โดยมี blob เดิมเป็น fallback บน host ที่ไม่มี API นี้ · จุดเจาะจง: **AWSSDK.S3 v4 บังคับ https ใน presigned URL เสมอ** (พิสูจน์ด้วยโปรเจกต์ทดสอบแยก — UseHttp ไม่ช่วย) แต่ SigV4 เซ็นค่า host header ไม่รวม scheme จึงสลับ https→http หลังเซ็นตาม config `UseSSL` ได้ · ตรวจจริง: upload → ขอ link → curl presigned URL ได้ไบต์ตรงเป๊ะ ✓ · อื่นๆ รอบนี้: **ลบ `manifest.json` ทิ้ง** (unified manifest ฝั่ง Outlook จาก scaffold — ไม่ได้ใช้ แก้แล้วไม่มีผลกับ Word) และเปลี่ยนชื่อเป็น **e-Court Documents Versioning** ทั้งใน manifest.xml (DisplayName/group/ปุ่ม) และหัว task pane · ⚠️ **อย่า `cp` ทับ manifest ใน wef folder** — office-addin-debugging สร้าง symlink ไว้ การ cp ทับทำให้กลายเป็นไฟล์ธรรมดา แล้ว `npm start` ครั้งถัดไปจะ EEXIST — ให้แก้ที่ `addin/manifest.xml` ต้นทางแล้ว restart Word
- 🩹 **แก้จาก sideload test จริงรอบสาม:** เข้า state tracking ได้แล้ว แต่ทุก API call โดน "Load failed" — **WKWebView ของ Word บน Mac บล็อก fetch จากหน้า https ไป http แบบเงียบๆ (mixed content/ATS)**: task pane เป็น `https://localhost:3000` ส่วน API เดิมเป็น `http://localhost:5075` (login ผ่านเพราะ Keycloak เป็น https แล้ว · CORS ตรวจแล้วสมบูรณ์ ไม่ใช่ต้นเหตุ) → แก้ด้วยวิธีเดียวกับ Keycloak: **backend เปิด endpoint `https://localhost:7232` ด้วย dev cert ชุดเดียวกัน** — `Program.cs` เพิ่ม `ConfigureKestrel` (เปิดเฉพาะ Development + เจอ cert, config `DevHttps:*` ใน appsettings.Development.json, Listen ทั้ง 5075/7232 แทน URL จาก launchSettings) · จุดเจาะจง: key ของ office-addin-dev-certs เป็น **PKCS#1** ซึ่ง `UseHttps(cert, key)` overload ไม่รับ → โหลดเองด้วย `RSA.ImportFromPem` + `CopyWithPrivateKey` + export PFX in-memory (macOS ต้องทำแบบนี้ SslStream ถึงใช้ key ได้) · add-in `API_BASE_URL` default → `https://localhost:7232` · ตรวจจริง: รัน temp instance แล้ว curl ผ่าน https ด้วย dev-token.py ได้ 200 · **สรุปกติกา dev: ทุก origin ที่หน้า https จะเรียก (Keycloak 8443, backend 7232) ต้องเป็น https ด้วย cert จาก office-addin-dev-certs ทั้งหมด**
- 🩹 **แก้จาก sideload test จริงรอบสอง:** login ผ่านแล้ว กด Start Versioning โดน "รูปแบบข้อมูลของ slice ไม่รองรับ (ต้องเป็น byte array)" — **host แต่ละตัวส่ง `slice.data` กลับคนละรูป** (เอกสารระบุลอยๆ ว่า "byte array"): Word Win = ArrayBuffer/view แต่ **Word Mac ส่งมาเป็นอย่างอื่น** (base64 string / number[]) → `sliceDataToBytes()` ใน fileService รองรับครบทั้ง 4 รูปแบบ + เติม test ครบทุกรูป (47 ตัว) · อีกเรื่องที่เจอ: restart `npm start` ไม่ได้แทน dev-server ตัวเก่า (port 3000 ถูกถือครอง → ตัวเก่ายัง serve bundle เก่า) — ต้อง kill ตัวเก่าก่อนเสมอ · และ webpack **ไม่ restart เองเมื่อ webpack.config.js เปลี่ยน** (DefinePlugin ค่าเก่าติดไปกับ bundle เก่า) — แก้ config แล้วต้อง restart dev-server ทุกครั้ง
**DoD:** sideload บน Word Desktop (Mac ที่มีอยู่ + Windows ถ้ามี) และ Word Online → ครบ flow: login ผ่าน dialog, start versioning, save & version, เห็น history, download ไฟล์ย้อนหลังเปิดได้ปกติ (AC-1, 2, 4, 5, 6, 7, 8 ผ่าน) + แก้ค่าใน content control แล้ว Save & Version → DB field values เปลี่ยนตาม (AC-13) + เปิดไฟล์ที่ generate จากเว็บ (Phase 6 เสร็จ) → เห็นเป็น tracked + v1 ทันที (AC-12)
> 🟡 implement + automated checks ผ่านหมด (2026-08-19): `npm run build` compiled successfully, `npm test` 47/47 (versionFlow + fileService ครบทุกรูปแบบ slice), `npm run lint` + `npm run validate` ผ่าน · **sideload ทดสอบจริงบน Word for Mac แล้วบางส่วน**: login ผ่าน dialog ✓ · ตรวจจับ + เข้า state tracking ✓ · Start Versioning / Save & Version ✓ — เจอปัญหาตกมุม 4 รอบและแก้หมดแล้ว (ดู 🩹 ด้านล่าง) · **ยังเหลือ**: ยืนยันการดาวน์โหลดผ่าน presigned link หลังแก้รอบล่าสุด, ไล่ AC เต็มชุด (โดยเฉพาะ AC-9/10/13) และทดสอบบน Word Online/Windows
---
## Phase 6: Web Portal — FR-9 (UI), FR-10 (UI) *(ใหม่)*