ไฟล์ร่าง (draft) — generate docx จาก template แบบ stateless ไม่สร้าง
Document/version; stamp เฉพาะ custom property TemplateId เพื่อให้ add-in
เห็นเป็น untracked; Start Versioning ส่ง form field templateId ให้ backend
ผูก template ตอน auto-create — เอกสารเริ่มที่ v1 (ไม่มี v0) + field sync เต็มระบบ
- Backend: POST /api/templates/{id}/generate-draft (200 คืนไฟล์) +
DocxGenerationService.GenerateDraft + แยก TemplateFieldValidation ใช้ร่วมกับ
generate + UploadVersion รับ templateId (non-GUID/unknown → 400,
deactivated → ผูกได้, document เดิม → เฉย) + CORS expose Content-Disposition
- Add-in: readTrackedProperties อ่าน TemplateId + ส่ง templateId เฉพาะ
startVersioning + hint ไฟล์ร่างใน task pane
- Web: ปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม + downloadFile รับ init (POST → blob)
- Docs: spec v8 (FR-16, AC-19) + Phase 12 + CONTEXT.md + ADR 0002 +
acceptance-review AC-19
- Tests: backend 82 / add-in 66 / web 59 + build ครบ; QA reviewer PASS
(2 rounds); ค้าง manual บน Word จริง
Co-Authored-By: Claude Code <noreply@anthropic.com>
15 KiB
แผนงาน: ไฟล์ร่าง (draft) — generate docx จาก template โดยยังไม่สร้าง version (FR-16 / spec v8 / Phase 12)
สถานะ: implement แล้ว (2026-09-10) — ติดตามผลที่
officejs-implementation-plan.mdPhase 12 และacceptance-review.mdAC-19 (ค้าง manual บน Word จริง) แผนนี้เก็บไว้เป็นหลักฐานการตัดสินใจของ grill session — รายละเอียด decision เพิ่มที่docs/adr/0002-draft-stateless.md
Context
ปัจจุบันการสร้างเอกสารจาก template ผ่านเว็บ (POST /api/documents/generate, FR-10) สร้าง Document + field values + v0 ฉบับตั้งต้น ทันที แม้ผู้ใช้จะยังไม่เคยเปิดไฟล์ใน Word
Flow ใหม่: ฟอร์มเว็บกรอกค่า → กด "สร้างไฟล์ร่าง" → API คืน docx → เปิดใน Word → add-in เห็น untracked → กด Start Versioning → เกิด Document (ผูก template) + v1 + field sync
การทบทวน version numbering (ไม่ต้องแก้โค้ด): UploadVersion คำนวณ (max ?? 0) + 1 ที่ backend/WordVersioning.Api/Controllers/DocumentsController.cs:140-144 — Document ใหม่ไม่มี version → max = null → v1 อัตโนมัติ มี test เดิมยืนยันแล้ว · เพิ่ม test ยืนยันผ่าน TemplateLinkingTests (ด้านล่าง)
Decisions (จาก grill session — final)
| # | หัวข้อ | การตัดสินใจ |
|---|---|---|
| 1 | การเก็บ draft | Stateless — POST คืน docx stream ตรงๆ ไม่สร้าง Document/version ไม่แตะ MinIO · เว็บ browser-download · ไม่มีปุ่ม "เปิดใน Word" (ms-word:ofe ต้องการ URL ซึ่ง draft ไม่มี) |
| 2 | Stamping | Stamp custom property TemplateId เท่านั้น — ไม่มี SavedByAddIn/DocumentTrackingId (กันสถานะ "tracking + history 404" ซึ่ง add-in จัดการไม่ได้ในปัจจุบัน) |
| 3 | กลไกผูก template ตอน Start Versioning | ขยาย POST /api/documents/{documentId}/versions เพิ่ม form field ทางเลือก templateId — ใช้เฉพาะตอน auto-create Document; document มีอยู่แล้ว → เฉย (validate ทุกครั้งที่ส่งมา) |
| 4 | Validation | upload: ไม่ใช่ GUID → 400 · ไม่พบ template → 400 · deactivated → ผูกได้ · draft endpoint เอง: unknown → 404, deactivated → 400 ตามนโยบาย FR-10 (deactivated สร้างไฟล์ใหม่ไม่ได้ แต่ draft เก่าที่สร้างตอน active ผูกต่อได้) |
| 5 | Web UI | คง flow v0 + เพิ่มปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม · re-generate (FR-12) ใช้ v0 flow เดิม |
| 6 | ศัพท์ | "ไฟล์ร่าง (draft)" ทั้งระบบ (UI/spec/CONTEXT.md) |
Endpoint design
POST /api/templates/{templateId:guid}/generate-draft (ใน TemplatesController — draft เป็น stateless function ของ template; controller นี้มี DI ครบและมี precedent sub-resource {id}/documents แล้ว)
- Request:
GenerateDraftRequest { [Required] JsonElement Values }(ในModels/TemplateDtos.cs) — ไม่มีmessage(ไม่มี version จึงไม่มี commit message) - Response: 200
FileContentResult—File(bytes, docxContentType, BuildGeneratedFileName(template.Name, now))(ไม่ใช่ 201 — ไม่มีอะไรถูกสร้าง) - Status: unknown template → 404 ·
!IsActive→ 400 · values ผิด format/required/type → 400 (ข้อความเดิมของ Generate) · template file หายใน MinIO/corrupt → 502
Implementation
1. Backend
Services/AddInMetadata.cs— เพิ่มTemplateIdPropertyName = "TemplateId"Services/DocxGenerationService.cs— แยก SDT-replace loop ของGenerateเป็นprivate static ReplaceSdtValues(...)ใช้ร่วม · เพิ่มbyte[] GenerateDraft(Stream templateStream, IReadOnlyDictionary<string,string> values, Guid templateId)(replace เหมือนเดิม + stamp เฉพาะTemplateId) · ย้ายBuildGeneratedFileNameจาก DocumentsController มาเป็นpublic staticที่นี่Services/TemplateFieldValidation.cs(ไฟล์ใหม่) — ยก required/type validation block จากGenerate(~lines 260-285) มาเป็นstatic string? Validate(IEnumerable<TemplateField>, IReadOnlyDictionary<string,string?>)(ข้อความ error ไทยต้องเหมือนเดิมเป๊ะ —GenerateTestsassert ไว้)Controllers/DocumentsController.cs:Generateเปลี่ยนไปใช้ validator + filename ที่แยกออกมาแล้วUploadVersion: early validationrequest.TemplateId(ไม่ใช่ GUID → 400) → หลัง doc lookup ถ้ามี templateId ให้db.Templates.AnyAsync(ไม่พบ → 400; deactivated ผ่าน) → ใส่TemplateId = templateIdToLinkใน auto-create initializer (~lines 105-111) ก่อน branchdoc.TemplateId is not null(~line 120) ไม่งั้น field sync จะหลุดเป็น raw-map path
Models/VersionDtos.cs—UploadVersionRequestเพิ่มstring? TemplateIdControllers/TemplatesController.cs— action ใหม่GenerateDraft(Guid id, [FromBody] GenerateDraftRequest, CancellationToken): load template (404/400) → load field defs →FieldMapJson.TryNormalize→TemplateFieldValidation.Validate→ load template stream (502) →GenerateDraft(...)→ log →File(...)Program.cs— CORS เพิ่ม.WithExposedHeaders("Content-Disposition")(จำเป็น — draft download อ่านชื่อไฟล์ไทยจาก header นี้ผ่าน fetch; ปัจจุบันไม่ได้ expose ชื่อจะหายเงียบๆ — integration tests จับไม่ได้เพราะ same-origin)
2. Add-in
addin/src/services/metadataService.ts—TEMPLATE_ID_PROPERTY+templateId?: stringในTrackedDocumentProperties+ อ่านเป็น property ที่ 5 ในreadTrackedProperties()(Word.run เดิม)addin/src/services/versioningService.ts—uploadVersion(..., pdf?, templateId?)(trailing param, caller เดิมไม่กระทบ) append form field เมื่อมีค่าaddin/src/services/versionFlow.ts—startVersioning(message?, templateId?, onProgress?)(แทรกก่อน callback — TS จะจับ call site เก่าทั้งหมดให้แก้) ·saveAndVersionไม่ส่ง templateIdaddin/src/taskpane/components/App.tsx— statedocumentTemplateId· restructuredetect()เป็นreadTrackedProperties()ครั้งเดียวแล้ว derive tracked เอง (ถ้าคงเรียกisTrackedDocument()ก่อน ไฟล์ร่าง untracked จะไม่ได้อ่าน TemplateId เลย) · เคลียร์ state ตอน sign-out/detect ใหม่ · untracked phase แสดง hint เมื่อเป็นไฟล์ร่าง: "ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกกับ template ต้นทางและ sync ค่า fields ให้อัตโนมัติ" ·handleStartVersioningส่งdocumentTemplateId ?? undefined
3. Web
web/app/composables/useApi.ts—downloadFile(path, fallbackName, init: RequestInit = {})ส่ง init ต่อให้request(รองรับ POST+JSON→blob; ปัจจุบัน GET-only และไม่มี caller — non-breaking)web/app/pages/documents/create/[templateId].vue— statedraftSubmitting/draftNotice·submitDraft(): validate ด้วยvalidateFieldValuesเดิม →downloadFile('/api/templates/{id}/generate-draft', fallback, { method:'POST', body: JSON.stringify({ values }) })→ notice แนะนำเปิดใน Word แล้วกด Start Versioning · ฟอร์มอยู่ต่อ (ไม่มี result card, ไม่มีปุ่มเปิดใน Word) · ปุ่มรอง "สร้างไฟล์ร่าง" (type="button", disabled gating เดียวกับปุ่มหลักรวม!detail.isActive) · helper text ใต้ช่องคำอธิบาย "(ใช้กับการสร้างเอกสาร version 0 เท่านั้น — ไฟล์ร่างไม่มี version)" ·resetForm()เคลียร์draftNotice
4. Docs (repo rule: spec ก่อน แล้ว sync plan)
specs/spec-officejs.md— v8 header note ·### FR-16: ไฟล์ร่าง (draft) — สร้าง .docx จาก template โดยไม่สร้าง Document (เพิ่มเติม v8)· section 8 metadata table เพิ่มแถวTemplateId· AC-19 · section เพิ่มเติม v8docs/plans/officejs-implementation-plan.md— append## Phase 12: ไฟล์ร่าง (draft) — FR-16 (เพิ่มทีหลัง, v8)ท้ายไฟล์ (รูปแบบเดียวกับ Phase 11: ตาราง mapping + checklist + DoD)CONTEXT.md— glossary เพิ่ม:- **ไฟล์ร่าง (draft)**: ไฟล์ .docx ที่สร้างจาก template แบบ stateless — ยังไม่มี Document/version ในระบบ; ระบุ template ต้นทางผ่าน custom property TemplateId และเริ่ม track จริงตอนกด Start Versioning ใน Word (version 1)docs/adr/0002-draft-stateless.md(ไฟล์ใหม่ — format ตาม ADR 0001): Decision (stateless endpoint, stamp TemplateId ตัวเดียว, ผูก template ทีหลังผ่าน form field) + Alternatives rejected (สร้าง Document สถานะร่าง + lifecycle/cleanup / stamp tracked props เต็มชุด = สถานะ "tracking + 404" พัง / ปุ่ม "เปิดใน Word" สำหรับ draft) + Consequences (draft หาย = ไม่มีของค้างในระบบ, v1 ไม่มี v0, field sync เริ่ม v1, ต้อง expose Content-Disposition)docs/acceptance-review.md— แถว AC-19 ·CLAUDE.md— sync สถานะ (FR-1–16, AC 1–19, Phase 0–12) + ย่อหน้าสถาปัตยกรรมสั้นๆ หลัง section FR-15
Tests
Backend (integration — Testcontainers, [Collection(IntegrationTestCollection.Name)]):
DraftTests.cs(ใหม่): happy path 200 + ค่า SDT ครบ (ใช้TestDocx.ReadSdtText) +ReadCustomProperty("TemplateId")ตรง +SavedByAddIn/DocumentTrackingIdเป็น null + DB ไม่เกิด Document/version ใดๆ · missing-required 400 · wrong-type 400 · unknown template 404 · deactivated 400 · extra keys ignoredTemplateLinkingTests.cs(ใหม่): upload ใหม่ + templateId → 201 VersionNumber=1,doc.TemplateIdชัด,AddInSignature="",DocumentFieldValueครบ (SourceVersionId/UpdatedByUserId ถูก), snapshot non-null · upload ครั้งสอง (ไม่ส่ง) → v2 · ไม่ใช่ GUID → 400 · unknown GUID → 400 · deactivated → ผูกได้ · document เดิม + templateId → เฉย (TemplateId คงเดิม) · document เดิม + templateId เสีย → 400TestDtos.cs—VersionFormเพิ่ม optionaltemplateIdต่อท้าย (caller เดิมไม่กระทบ) ·AuthTests.cs— เพิ่ม[InlineData("POST", "api/templates/{0}/generate-draft")]
Add-in (jest): metadataService อ่าน templateId ได้ (ปรับ expectation toEqual ของ readTrackedProperties เดิม) · versionFlow ส่ง templateId เฉพาะ startVersioning ไม่ใช่ saveAndVersion · versioningService append form field
Web (vitest): web/test/useApi.spec.ts (ใหม่ — pattern ตาม useVersionOpen.spec.ts): POST init ส่งถึง fetch + Bearer · 401 retry ครั้งเดียว · ชื่อไฟล์ filename* UTF-8 ไทย ไปอยู่ที่ anchor download · !ok → ApiError พร้อมข้อความจาก backend
Verification (DoD)
cd backend && dotnet test # เขียวทั้งชุด (รวม suite เดิมที่ไม่ถูกแตะ)
cd addin && npm test
cd web && npm test && npm run build
Manual บน Word for Mac: สร้างไฟล์ร่างจากเว็บ → ได้ .docx ชื่อไทยถูกต้อง → เปิดใน Word → task pane แสดง untracked + hint ไฟล์ร่าง → กด Start Versioning → ได้ v1 + หน้า document detail บนเว็บแสดง template และค่า fields · Save & Version ถัดไป → v2
Risks
- Content-Disposition ไม่ถูก CORS-expose — จับไม่ได้ด้วย integration tests (same-origin) ครอบด้วย useApi.spec + manual check
- ลำดับแทรกใน UploadVersion สำคัญ (TemplateId ต้อง set ก่อน field-sync branch) — TemplateLinkingTests คุมไว้
startVersioningเปลี่ยน positional signature — TS จับ call site ทั้งหมด แก้ให้ครบใน commit เดียวกัน- ไม่มี DB migration ใดๆ (ไม่แตะ schema —
UploadVersionRequest.TemplateIdเป็น form field, คอลัมน์Document.TemplateIdมีอยู่แล้ว)