eCourt-Doc-Edit/docs/plans/version-message.md
Suphonchai Phoonsawat 7b20f12ede 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>
2026-08-20 22:05:49 +07:00

97 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 จริง