eCourt-Doc-Edit/docs/plans/version-message.md

98 lines
12 KiB
Markdown
Raw Normal View History

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