Phase 9: version message (FR-13)

- ใส่คำอธิบายแบบ commit message ได้ทุก flow ที่สร้าง version (Save & Version / Start Versioning / generate จากเว็บ) + แสดงใน version history ทุกหน้า
- backend: DocumentVersion.Message (varchar 2048) + migration AddVersionMessage + BadRequest ตรวจความยาวก่อนแตะ MinIO (กัน 500 + orphan object)
- add-in: ConfirmVersionDialog เพิ่ม textarea (Start Versioning ใช้ dialog ด้วย) + แสดง message ใน VersionHistoryPanel
- web: ฟอร์มสร้างเอกสาร + หน้า detail แสดง message (desktop truncate + tooltip, mobile เต็มบรรทัด)
- sync เอกสารทั้งหมด: spec v5 (FR-13/AC-16) + plan Phase 9 + acceptance-review 1–16 + README + CLAUDE.md + web/README
- tests: backend 52 / addin 50 / web 36 — เขียวทั้งชุด

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-08-20 22:05:49 +07:00
parent b47667f918
commit 7b20f12ede
28 changed files with 928 additions and 141 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–8 เสร็จ** (backend + add-in + web portal + tests ครบชุด + ไล่ AC ครบ — รายละเอียดผล AC ทุกข้อที่ [`docs/acceptance-review.md`](../acceptance-review.md)) · เหลือยืนยันด้วยมือเฉพาะบน platform ที่ทำแทนไม่ได้: Word Online/Windows, click-through บน browser จริง, silent refresh จริง
> สถานะ: **Phase 0–9 เสร็จ** (backend + add-in + web portal + tests ครบชุด + ไล่ AC ครบ — รายละเอียดผล AC ทุกข้อที่ [`docs/acceptance-review.md`](../acceptance-review.md)) · เหลือยืนยันด้วยมือเฉพาะบน platform ที่ทำแทนไม่ได้: Word Online/Windows, click-through บน browser จริง, silent refresh จริง
> วันที่แผน: 2026-08-18 (ฉบับแก้ไข v2: เพิ่ม FR-9 ถึง FR-11 — ระบบ template, สร้างเอกสาร v1 ผ่านเว็บ และ field sync)
---
@ -23,7 +23,7 @@
- หน้าจอสร้างเอกสารครั้งแรก = **เว็บแยก (standalone)** — กรอกนอก Word → ดาวน์โหลด .docx ที่ backend stamp metadata + สร้าง v1 ไว้แล้ว → เปิดใน Word แล้ว add-in รู้จักทันที
- Template = **หลาย template + หน้าจัดการเต็มรูปแบบ** (CRUD + นิยาม field ต่อ template)
- Field sync = **เฉพาะ fields จาก template** (ผูกกับ content controls) ไม่เก็บ full text
- **FR-9 ถึง FR-12 เป็น requirement ที่เพิ่มภายหลัง spec เดิม** — spec ปัจจุบันเป็นฉบับ v4 (ครอบ FR-1–12 + AC-1–15: v2 เพิ่ม template/portal/field sync, v3 เพิ่มข้อจำกัดแพลตฟอร์มจากการทดสอบจริง, v4 เพิ่มรายการเอกสารต่อ template + detail + re-generate) ให้ถือ spec เป็น source of truth ของ requirements
- **FR-9 ถึง FR-13 เป็น requirement ที่เพิ่มภายหลัง spec เดิม** — spec ปัจจุบันเป็นฉบับ v5 (ครอบ FR-1–13 + AC-1–16: v2 เพิ่ม template/portal/field sync, v3 เพิ่มข้อจำกัดแพลตฟอร์มจากการทดสอบจริง, v4 เพิ่มรายการเอกสารต่อ template + detail + re-generate, v5 เพิ่ม commit message ต่อ version) ให้ถือ spec เป็น source of truth ของ requirements
---
@ -501,6 +501,28 @@ POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
---
## Phase 9: Version message — FR-13 (เพิ่มทีหลัง, v5)
**เป้า:** ใส่ "คำอธิบาย version" แบบ git commit message ได้ทุก flow ที่สร้าง version (Save & Version, Start Versioning, สร้างจากเว็บ) + แสดงใน version history ทุกหน้า — แผนเต็มที่ [`docs/plans/version-message.md`](version-message.md)
- [x] Backend: `DocumentVersion.Message` (string?, varchar 2048) + `AppDbContext` config + migration `AddVersionMessage`
- [x] Backend: `UploadVersionRequest.Message` (multipart) + `GenerateDocumentRequest.Message` (JSON) + `VersionListItem.Message` + `NormalizeMessage` + BadRequest ตรวจความยาว 2048 ทั้ง upload/generate (กัน 500 + orphan MinIO object)
- [x] Add-in: `ConfirmVersionDialog` generalize (`title?/description?/confirmLabel?` + `onConfirm(message)`) + textarea ทางเลือก + เคลียร์ state ตาม `open` · Start Versioning ใช้ dialog ด้วย (ตัวใหม่ใน App.tsx)
- [x] Add-in: `versionFlow` (`saveAndVersion`/`startVersioning`/`uploadCurrentFile`) + `versioningService.uploadVersion` ส่ง `message` · `VersionRecord.message` + แสดงใน `VersionHistoryPanel`
- [x] Web: ฟอร์ม generate เพิ่ม textarea ทางเลือก + payload `message` · `VersionListItem.message` + แสดงใน history (desktop ใต้ v{N} truncate + tooltip, mobile เต็มบรรทัด)
- [x] Tests: `VersionMessageTests` (backend) + `versionFlow.test.ts` เพิ่ม 3 กรณี (แนบ/ว่างไม่แนบ/startVersioning)
หมายเหตุจากการทำจริง (2026-08-20):
- เพิ่ม param เป็น optional positional วางก่อน callback (callback-last) — test เดิมที่เรียก `saveAndVersion(id, 1)` / `startVersioning()` คอมไพล์ผ่านโดยไม่ต้องแก้
- `ConfirmVersionDialog` ถูก mount ค้างไว้นอก phase branch ของ App — เคลียร์ textarea ด้วย `useEffect` ตาม `open` ไม่ใช่ตอน unmount
- เลือกตรวจความยาว 2048 เป็น BadRequest เอง (ต่างจาก `Template.Description` ที่ปล่อยให้ DB reject) เพราะ Generate จะเกิด 500 **หลัง** MinIO upload แล้ว = orphan object ใน bucket
**DoD:** `dotnet test` + addin `npm test` + web `npm test`/`npm run build` เขียวทั้งชุด
> ✅ ตรวจผ่านจริง (2026-08-20): `dotnet test` **52/52** · addin `npm test` **50/50** · web `npm test` **36/36** + build ผ่าน
---
## 4. ตาราง mapping: Phase ↔ FR ↔ Acceptance Criteria
| Phase | FR ที่ implement | Acceptance criteria ที่ตรวจได้ |
@ -515,6 +537,7 @@ POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
| 6.4 (ทีหลัง, v4) | FR-12 | AC-15 |
| 7 | — | คุ้มกัน regression ทุก AC (รวม AC-13, 14) |
| 8 | — | ทบทวน AC-1 ถึง AC-14 ทั้งหมด |
| 9 (ทีหลัง, v5) | FR-13 | AC-16 |
**AC เพิ่มเติม (นอก spec เดิม):**
- **AC-11:** upload template .docx ที่มี content controls → ระบบ detect fields อัตโนมัติ + บันทึก field definitions ได้
@ -522,6 +545,7 @@ POST /api/documents/generate { templateId, values: { <fieldKey>: <value> } }
- **AC-13:** แก้ค่าใน content control ที่ Word แล้วกด Save & Version → field value ใน DB เปลี่ยนตาม + snapshot ต่อ version ถูกเก็บ
- **AC-14:** ลบ content control ออกจาก Word แล้วกด Save & Version → flow ยังสำเร็จ ค่าเดิมของ field นั้นคงอยู่ (mark missing) ไม่ error
- **AC-15:** (v4) หน้ารายการเอกสารของ template + หน้า detail (ค่าทุก field + version history ขยายเห็นค่าต่อ version + ดาวน์โหลด) + re-generate ได้เอกสารใหม่ v1 โดยเอกสารต้นทางไม่มี version ใหม่ — รายละเอียดตาม FR-12
- **AC-16:** (v5) พิมพ์คำอธิบายตอนสร้าง version ได้ทุก flow (Save & Version, Start Versioning, สร้างจากเว็บ) → ข้อความถูกเก็บกับ version นั้นใน DB และแสดงใน version history ทุกหน้า · เว้นว่างได้ — รายละเอียดตาม FR-13
---
@ -554,14 +578,14 @@ docker compose up -d
cd backend/WordVersioning.Api
dotnet run # รัน + swagger ที่ http://localhost:5xxx
dotnet ef migrations add <Name>
cd .. && dotnet test # integration tests (42 ตัว) — ต้องมี Docker รันอยู่ (Testcontainers ยก infra เอง ~1 นาที)
cd .. && dotnet test # integration tests (52 ตัว) — ต้องมี Docker รันอยู่ (Testcontainers ยก infra เอง ~1 นาที)
# add-in (https://localhost:3000)
cd addin
npm install
npm run build # webpack build
npm start # dev server — sideload ผ่าน manifest นี้
npm test # jest (47 ตัว)
npm test # jest (50 ตัว)
npx office-addin-dev-certs install # ครั้งแรกครั้งเดียว เพื่อให้ https localhost ใช้ได้
# web portal (http://localhost:5173 — ห้ามใช้ 3000 เพราะชน add-in)

View file

@ -0,0 +1,97 @@
# Version Message (commit message) — FR-13 / AC-16 / Spec v5
## Context
ผู้ใช้ต้องการให้ใส่ "ข้อความอธิบาย" ตอนสร้าง version ใหม่ได้ เหมือน git commit message — เก็บกับแต่ละ version และแสดงใน version history ทุกหน้า ขอบเขต (ยืนยันกับผู้ใช้แล้ว): **ทุก flow ที่สร้าง version** = add-in Save & Version + add-in Start Versioning (เพิ่ม dialog ใหม่) + ฟอร์ม generate บน web
**ข้อตกลง fixed:**
- Field ชื่อ `message` — nullable string, max 2048, normalize `IsNullOrWhiteSpace → null` + `.Trim()` (นโยบายเดียวกับ `Template.Description` เป๊ะ)
- Optional ทุกที่ ไม่บังคับกรอก
- **Non-goal: ไม่ฝังลง docx** (custom properties / Custom XML part) — backend คือ source of truth (กติกา #5) และเลี่ยง parse-compat กับไฟล์เก่า (`XmlVersionEntry` ไม่แตะ)
- ตามกติกา repo: **แก้ spec ก่อน** แล้ว sync plan
## ขั้นที่ 0 — บันทึกแผน + แก้เอกสาร requirement ก่อน
1. บันทึกแผนนี้ที่ `docs/plans/version-message.md` (ธรรมเนียมเดิม)
2. **[specs/spec-officejs.md](specs/spec-officejs.md)**: เพิ่ม changelog v5 บรรทัดบนสุด · `### FR-13: คำอธิบายต่อ version — commit message (เพิ่มเติม v5)` ต่อท้าย section 6 (หลัง FR-12) ตาม format เดิม — ระบุ field `message` + ทั้ง 2 endpoint + นโยบาย null/trim + แสดงผลทุก history + Start Versioning ได้ dialog · section 7 `DocumentVersion` POCO เพิ่ม `public string? Message { get; set; }` · section 10 เพิ่ม AC-16 (v5) · section 11 เพิ่ม task items `(v5)` ทั้ง 3 list
3. **[docs/plans/officejs-implementation-plan.md](docs/plans/officejs-implementation-plan.md)**: เพิ่ม `## Phase 9: Version message — FR-13 (เพิ่มทีหลัง, v5)` + checklist + แถวตาราง mapping `| 9 (v5) | FR-13 | AC-16 |`
4. **[docs/acceptance-review.md](docs/acceptance-review.md)**: 1–15 → 1–16 + section AC-16 (🔶 ส่วน dialog ใน Word ต้องยืนยันมือ)
## ขั้นที่ 1 — Backend (`backend/WordVersioning.Api`)
1. [Models/DocumentVersion.cs](backend/WordVersioning.Api/Models/DocumentVersion.cs): `public string? Message { get; set; }` (+ doc comment FR-13)
2. [Data/AppDbContext.cs](backend/WordVersioning.Api/Data/AppDbContext.cs): ใน block DocumentVersion เพิ่ม `entity.Property(x => x.Message).HasMaxLength(2048);` (มิเรอร์ Template.Description)
3. Migration: `dotnet ef migrations add AddVersionMessage` → expect `AddColumn<string>("Message", "DocumentVersions", nullable, maxLength 2048)` (tests รัน MigrateAsync อยู่แล้ว = migration ถูก exercise)
4. [Models/VersionDtos.cs](backend/WordVersioning.Api/Models/VersionDtos.cs): `UploadVersionRequest.Message` (string? หลัง Fields) · `GenerateDocumentRequest.Message` (string? หลัง Values) · `VersionListItem` เพิ่ม `string? Message` ท้าย record
5. [Controllers/DocumentsController.cs](backend/WordVersioning.Api/Controllers/DocumentsController.cs):
- เพิ่ม `MaxVersionMessageLength = 2048` + `NormalizeMessage()` helper (static private — มี pattern `ParseSnapshot`/`IsUniqueViolation` อยู่แล้ว)
- **ตัดสินใจ: ใส่ BadRequest check ความยาว** (ต่างจาก Template.Description ที่ไม่มี) เพราะ: ถ้าไม่เช็ค = 500 จาก DB และใน Generate จะเกิด 500 **หลัง** MinIO upload แล้ว = orphan object ใน bucket โดยเปล่า + UploadVersion เป็น action ที่ validate เองด้วยข้อความไทยอยู่แล้ว
- UploadVersion: normalize + check หลัง validation ของ fields → `Message = message` ใน `new DocumentVersion` ใน retry loop (ทุกรอบได้ค่าเดียวกัน)
- Generate: normalize + check **ก่อน** ทำ MinIO/template IO → `Message = message` ใน v1 initializer
- GetVersions: เพิ่ม `v.Message` ใน projection + `, r.Message` ตอน construct `VersionListItem`
- Rollback path ไม่ต้องแตะเพิ่ม (MinIO fail → Remove ทั้ง row อยู่แล้ว / Generate detach ก่อน SaveChanges)
## ขั้นที่ 2 — Add-in (`addin`) — Fluent UI v9
1. [ConfirmVersionDialog.tsx](addin/src/taskpane/components/ConfirmVersionDialog.tsx) — generalize ด้วย optional props (ไม่สร้าง component ใหม่): `title?`, `description?`, `confirmLabel?` + **`onConfirm: (message: string) => void`**; เพิ่ม `Field`+`Textarea` (label "คำอธิบาย version (ไม่บังคับ)", placeholder "เช่น แก้ไขข้อ 3 สัญญาจ้าง", maxLength 2048, rows 3, resize vertical, disabled={busy}); local state `message` + **`useEffect` เคลียร์เมื่อ `!open`** (FC ถูก mount ค้างไว้ — ห้ามพึ่ง remount)
2. [versionFlow.ts](addin/src/services/versionFlow.ts) — optional positional param วางก่อน callback (callback-last): `uploadCurrentFile(documentId, clientVersionId, fields, message?)` · `startVersioning(message?, onProgress?)` · `saveAndVersion(documentId, latestVersionNumber, message?, onProgress?)` — test เดิม 5 ตัวเรียก 2 args / 0 args คอมไพล์ผ่านไม่ต้องแก้
3. [versioningService.ts](addin/src/services/versioningService.ts) `uploadVersion` — เพิ่ม `message?` ท้าย signature + `const trimmed = message?.trim(); if (trimmed) form.append("message", trimmed)` (มิเรอร์ guard ของ fields)
4. [versionRecord.ts](addin/src/models/versionRecord.ts) — `VersionRecord` เพิ่ม `message: string | null`
5. [VersionHistoryPanel.tsx](addin/src/taskpane/components/VersionHistoryPanel.tsx) — แสดง `version.message` ใต้บรรทัด "โดย …" (แบบเต็ม, `pre-wrap`, muted เล็ก) — เพิ่ม style `messageRow`
6. [App.tsx](addin/src/taskpane/components/App.tsx) — `handleSaveAndVersion(message)` / `handleStartVersioning(message)` ส่งต่อ; state `startConfirmOpen`; ปุ่ม Start Versioning เปลี่ยนเป็นเปิด dialog; render `ConfirmVersionDialog` ตัวที่สอง (title "เริ่มติดตามเอกสารนี้?", confirmLabel "เริ่มติดตาม (version 1)")
## ขั้นที่ 3 — Web (`web`)
1. [documents/create/[templateId].vue](web/app/pages/documents/create/[templateId].vue) — `const message = ref('')`; textarea ทางเลือก (`label`/`input` class, rows 2, maxlength 2048) หลัง field loop; payload เพิ่ม `message: message.value.trim() || null`; `resetForm` เคลียร์; **re-generate ไม่ prefill message** (เอกสารใหม่ได้ข้อความใหม่ — ใส่ comment กำกับ)
2. [types/api.ts](web/app/types/api.ts) — `VersionListItem` เพิ่ม `message: string | null`
3. [documents/[documentId].vue](web/app/pages/documents/[documentId].vue) — **ไม่เพิ่มคอลัมน์ใหม่** (colspan="4" ใน expanded row + คอลัมน์ action จะบีบ): desktop แสดงใต้ `v{N}` เป็น `truncate text-xs text-muted` + `:title` tooltip; mobile card แสดงเต็ม `break-words`
## ขั้นที่ 4 — Tests
**Backend — ไฟล์ใหม่ `VersionMessageTests.cs`** (ตาม pattern `UploadVersionTests` — `VersionForm`, `fixture.CreateDb()`, `AsNoTracking`):
1. `Upload_WithMessage_PersistsRow_AndEchoesInHistory`
2. `[Theory] Upload_BlankOrMissingMessage_StoredAsNull` (null / "" / " ")
3. `Upload_TrimsSurroundingWhitespace`
4. `Upload_OversizeMessage_Returns400_NoSideEffect` (2049 ตัว, ไม่มี row หลงเหลือ)
5. `Upload_MessageExactly2048_Succeeds` (boundary)
6. `SecondUpload_KeepsPerVersionMessages` (v1=A, v2=B → history [B,A])
7. `Generate_WithMessage_CreatesV1WithMessage`
8. `Generate_OversizeMessage_Returns400` (เช็คว่า 400 เกิดก่อน MinIO)
แก้ helper: `TestDtos.cs` — `VersionItem` เพิ่ม `string? Message`, `VersionForm(..., string? message = null)`; `TestFlow.GenerateDocumentAsync(..., string? message = null)`
**Add-in — `versionFlow.test.ts`**: เพิ่ม 3 test — `form.get("message")` หลัง `saveAndVersion(id, 1, "…")`, whitespace → null, `startVersioning("…")` แนบ v1 (test เดิมไม่ต้องแก้)
**Web**: type-only — ไม่มี component test ตาม convention (เช็คด้วย build)
## Verification
```
# ลำดับ: docs → backend → add-in → web
cd backend/WordVersioning.Api && dotnet ef migrations add AddVersionMessage
cd backend && dotnet test --filter VersionMessageTests # ต้องมี Docker (Testcontainers)
cd backend && dotnet test # ทั้งชุด
cd addin && npm run build && npm test
cd web && npm run build && npm test
```
Manual E2E (หลัง `docker compose up -d` + backend + `npm run dev`/`npm start`):
1. Web generate พร้อม message → หน้า detail แสดงใต้ v1 (desktop + mobile card); ไม่กรอก → ไม่แสดง
2. Re-generate จากเอกสารที่มี message → textarea ว่าง
3. Add-in: untracked → Start Versioning เปิด dialog → พิมพ์ message → v1 + panel แสดง
4. Add-in: Save & Version พร้อม message → history refresh แสดง
5. เปิด dialog → พิมพ์ → ยกเลิก → เปิดใหม่ → textarea ต้องว่าง
6. Word sideload caveat: ห้าม `cp` ทับ manifest ใน wef (symlink), kill dev server เก่าก่อน `npm start`
## Risks / gotchas
- **Param เพิ่มท้าย + callback-last** = test เดิมคอมไพล์ผ่าน (verify แล้วว่าเรียก 2/0 args)
- **เคลียร์ dialog state ด้วย useEffect ตาม `open`** — อย่าพึ่ง unmount
- Postgres varchar นับเป็น characters (ไทย 3 bytes/char ไม่มีปัญหา); JS maxLength กับ C# `.Length` นับ UTF-16 code units เหมือนกัน
- `GenerateDocumentRequest.Message` เป็น plain string — JSON ส่ง type ผิดได้ 400 อัตโนมัติจาก [ApiController] (body อังกฤษ — ยอมรับได้)
- AC-16 ส่วน dialog/textarea ใน Word webview ต้องยืนยันมือ (🔶 ใน acceptance-review)
- อย่าลืมอัปเดตตัวเลข test count ใน docs (47/42/36) หลังรันจริง
## สถานะ: ✅ implement เสร็จ (2026-08-20)
ผลรันจริงทั้งชุด: backend `dotnet test` **52/52** · addin `npm test` **50/50** + build + lint + manifest validate ผ่าน · web `npm test` **36/36** + build ผ่าน — รายละเอียด checkbox Phase 9 + หมายเหตุจากการทำจริงอยู่ที่ [officejs-implementation-plan.md](officejs-implementation-plan.md#phase-9-version-message--fr-13-เพิ่มทีหลัง-v5) · ผล AC-16 ที่ [acceptance-review.md](../acceptance-review.md) · เหลือยืนยันด้วยมือเฉพาะ dialog/textarea ใน Word จริง

View file

@ -186,3 +186,9 @@ Desktop เป็นตารางปกติ (`hidden md:block table-card`)
- Dark ครีม→เทาอุ่น: saturation เกิน ~15% จะเลี่ยน — ค่าที่เลือก desaturate แล้ว; shadow มองไม่เห็นบนพื้นดำ (border token เป็นตัวแบ่ง elevation ใน dark)
- ตารางครีมที่ text-sm อาจแน่น — fallback บรรทัดเดียว: `table-card` → `bg-surface`
- ลำดับสำคัญ: tokens/config → component classes → .vue edits
## หมายเหตุจากการทำจริง (2026-08-20)
- ✅ **implement ครบทั้ง 12 ไฟล์ + verification ผ่านทั้งหมด** — web `npm test` 36/36 เขียวทั้งก่อน/หลัง · `npm run build` ผ่าน · smoke test production server ยืนยัน fonts link, `lang="th"`, sticky header + token classes, FOUC script ครบ · audit grep palette เดิม (`slate/sky/red/emerald/amber-*`) = **0 เหลือ** · `dark:` variants (เดิม 209 จุด) = **0 เหลือ**
- 🐛 **บั๊กแฝงที่เจอระหว่างทำ (สำคัญ):** `@nuxtjs/tailwindcss` resolve default cssPath แบบ relative ต่อ rootDir → บน Nuxt 4 (srcDir = `app/`) หา `app/assets/css/tailwind.css` ไม่เจอและ **fallback ไปไฟล์ default ของ tailwindcss package เงียบๆ** (build ผ่าน แต่ token/component classes หายทั้งหมด) — ก่อนหน้านี้ไม่มีใครเจอเพราะไฟล์เดิมมีเนื้อหา 3 directives เหมือน fallback เป๊ะ · แก้ด้วย `tailwindcss: { cssPath: '~/assets/css/tailwind.css' }` ใน nuxt.config.ts (บันทึกเป็นกติกา #14 ใน CLAUDE.md แล้ว)
- smoke test เครื่องมือ: `PORT=5174 node .output/server/index.mjs` + curl ตรวจ HTML — ใช้ซ้ำได้เมื่อแก้ shell/global