# Field Editor — FR-14 / AC-17 / Spec v6 ## Context ฟอร์มสร้างเอกสารบนเว็บ (FR-10) render ทุก field เป็น `` บรรทัดเดียว — field ข้อความยาว (เช่น รายละเอียดคดี, ที่อยู่) กรอกในช่องแคบไม่สะดวก ผู้ใช้ต้องการให้ผู้ดูแลเลือก **editor (รูปแบบช่องกรอก) ต่อ field** ได้ ขอบเขต (ยืนยันกับผู้ใช้แล้ว): **ไม่เพิ่ม dropdown/checkbox** — แค่: - `text` → `textbox` (default) หรือ `textarea` - `number` → `number` (ช่องตัวเลข — ทางเดียว) - `date` → `date` (date picker — ทางเดียว) **ข้อตกลง fixed:** - `Editor` เป็น **presentation metadata แยกจาก `Type`** — validation ของค่า field (generate/sync) ยึด `Type` เท่านั้น ค่า field เป็น string ทั้ง flow (กติกา #5/#6) - คอลัมน์ **nullable ไม่ backfill** — `null` = "default ของ Type" (มิเรอร์ pattern `AddVersionMessage`) ทำให้ path auto-detect (POST create / PUT file) ไม่ต้องแต่ - **Normalize-on-write**: ค่าที่ resolve แล้วเท่ากับ default → เก็บ `null` (DB ไม่มีค่า default ซ้ำซ้อน อนาคตเปลี่ยน default มีผลทุก row) · API คืนค่า resolved เสมอ - UI admin **ซ่อน select เมื่อ type มี editor ทางเดียว** (แสดง `—` muted แทน select หลอก) — backend ยัง validate ค่า `number`/`date` ให้ (round-trip ปลอดภัย) - **Non-goal:** ไม่แตะ add-in (ไม่มี UI แก้ค่า field) · ไม่แตะ `DocumentFieldItem`/หน้า detail (display-only) · ไม่แตะ docx/generate path - ตามกติกา repo: **แก้ spec ก่อน** แล้ว sync plan ## ขั้นที่ 0 — บันทึกแผน + แก้เอกสาร requirement ก่อน 1. บันทึกแผนนี้ที่ `docs/plans/field-editor.md` (ธรรมเนียมเดิม) 2. **[specs/spec-officejs.md](../../specs/spec-officejs.md)**: changelog v6 บรรทัดบนสุด · FR-9 bullet admin config เพิ่ม `editor (FR-14)` · `### FR-14: Field editor` หลัง FR-13 · section 7 `TemplateField` POCO เพิ่ม `public string? Editor { get; set; }` · section 10 เพิ่ม AC-17 (v6) · section 11 เพิ่ม task items `(v6)` ทั้ง backend + web list 3. **[docs/plans/officejs-implementation-plan.md](officejs-implementation-plan.md)**: header ข้อตกลง v5→v6 · `## Phase 10: Field editor — FR-14 (เพิ่มทีหลัง, v6)` + checklist + แถว mapping `| 10 (ทีหลัง, v6) | FR-14 | AC-17 |` + AC-17 ในรายการ AC เพิ่มเติม 4. **[docs/acceptance-review.md](../acceptance-review.md)**: "AC ทั้ง 16 ข้อ" → 17 + section AC-17 (🔶 browser click-through) ## ขั้นที่ 1 — Backend (`backend/WordVersioning.Api`) 1. [Models/TemplateField.cs](../../backend/WordVersioning.Api/Models/TemplateField.cs): `public string? Editor { get; set; }` หลัง `Type` + static `EditorsByType` (`text → [textbox, textarea]`, `number → [number]`, `date → [date]`) + `DefaultEditor(type)` (unknown type → `"textbox"`) 2. [Data/AppDbContext.cs](../../backend/WordVersioning.Api/Data/AppDbContext.cs): ใน block TemplateField เพิ่ม `entity.Property(x => x.Editor).HasMaxLength(16);` (มิเรอร์ Type) 3. Migration: `dotnet ef migrations add AddTemplateFieldEditor` → expect `AddColumn("Editor", "TemplateFields", nullable, maxLength 16)` เดี่ยว ไม่ backfill (pattern เดียวกับ `AddVersionMessage`) 4. [Models/TemplateDtos.cs](../../backend/WordVersioning.Api/Models/TemplateDtos.cs): `TemplateFieldDefinition.Editor` (string? หลัง Type — ไม่ส่ง/ว่าง = default) · `TemplateFieldDto` แทรก `string Editor` หลัง `Type` (คืนค่า resolved เสมอ) 5. [Controllers/TemplatesController.cs](../../backend/WordVersioning.Api/Controllers/TemplatesController.cs): - `Update` field loop หลัง type check: resolve null/whitespace → `DefaultEditor(def.Type)` · ไม่อยู่ใน `EditorsByType[def.Type]` → 400 ข้อความไทย (ครอบทั้งค่าไม่รู้จักและไม่เข้ากับ type) · persist normalize-on-write: `row.Editor = editor == DefaultEditor(def.Type) ? null : editor` - `Create`/`ReplaceFile`: **ไม่แตะ** (Editor null = default ถูกต้องอยู่แล้ว) - `FieldDtos`: `Editor = f.Editor ?? TemplateField.DefaultEditor(f.Type)` (จุด resolve เดียว) 6. **ไม่แตะ** `DocumentsController` (validation ยึด Type), `VersionDtos.cs`, docx path, add-in ## ขั้นที่ 2 — Web (`web`) 1. [types/api.ts](../../web/app/types/api.ts): `export type FieldEditor = 'textbox' | 'textarea' | 'number' | 'date'` · `TemplateFieldDto` เพิ่ม `editor: FieldEditor | string` หลัง `type` (string-tolerant เหมือน `type`) 2. **สร้าง `app/utils/fieldEditor.ts`** (pure util auto-import เหมือน `fieldValidation`): `EDITORS_BY_TYPE` · `defaultEditor(type)` · `editorOptions(type)` (text = 2 ตัว, number/date = 1 ตัว) · `resolveEditor(field)` (valid-for-type → ตัวมันเอง, null/unknown/incompatible → default — degrade อ่อนโยน) · `dateInputValue(value)` (ISO timestamp → `yyyy-MM-dd` 10 หลักแรก, อื่นๆ คงเดิม) 3. [templates/[id]/edit.vue](../../web/app/pages/templates/[id]/edit.vue): payload mapping เพิ่ม `editor: f.editor` · type `` ทั้ง 2 ที่ เพิ่ม `@change="f.editor = defaultEditor(f.type)"` · editor select แสดงเฉพาะ `editorOptions(f.type).length > 1` (mobile card + desktop `/`) ไม่งั้น `—` muted · hint ไทยใต้ subtitle 4. [documents/create/[templateId].vue](../../web/app/pages/documents/create/[templateId].vue): field loop แทน ternary ด้วย `resolveEditor(f)` switch — `textarea` → `` · `number` → input number step any (preserve) · `date` → input date (preserve) · else textbox · prefill `?from=` ของ field editor `date` ผ่าน `dateInputValue` (input[type=date] รับเฉพาะ yyyy-MM-dd — ค่า timestamp เต็มจาก Word sync ทำให้ช่องแสดงว่าง) 5. [templates/new.vue](../../web/app/pages/templates/new.vue): copy ปุ่มลิงก์ `label/type/required` → `label/type/editor/required` ## ขั้นที่ 3 — Tests **Backend — ไฟล์ใหม่ `FieldEditorTests.cs`** (pattern `VersionMessageTests` — `fixture.CreateDb()` + `AsNoTracking` + ใช้ `TemplateTests.CreateDefaultTemplateAsync`): 1. `Update_TextFieldEditorTextarea_PersistsAndEchoed` (response + DB row = "textarea") 2. `Update_OmittedEditor_ResolvesDefaultPerType_StoresNull` (text/number/date → echo textbox/number/date, DB null ทั้งหมด) 3. `Update_EditorEqualToDefault_NormalizedToNull` (ส่ง "textbox" ชัดๆ → echo "textbox", DB null) 4. `Update_UnknownEditor_Returns400` 5. `[Theory] Update_EditorIncompatibleWithType_Returns400` ((number,"textarea"), (text,"date"), (date,"textbox")) 6. `ReplaceFile_AddedTagGetsDefaultEditor` (echo "textbox", DB null) Mirror DTO: `TestDtos.cs` `TemplateFieldItem` เพิ่ม `string Editor` · extend `TemplateTests` — `Create_DetectsAllSdtTags_WithDefaults` assert `Editor == "textbox"` + `Update_SavesFieldDefinitions_AndMetadata` เพิ่ม editor ใน payload **Web — สร้าง `test/fieldEditor.spec.ts`**: `defaultEditor` ทุก type + unknown · `resolveEditor` (explicit/undefined/incompatible/string-tolerant) · `editorOptions` (text=2, number/date=1) · `dateInputValue` (timestamp→date, plain คงเดิม, ค่าอื่นคงเดิม) · `fieldValidation.spec.ts` ไม่แตะ ## Verification ``` # ลำดับ: docs → backend → web cd backend/WordVersioning.Api && dotnet ef migrations add AddTemplateFieldEditor # ตรวจ Up/Down cd backend && dotnet test --filter FullyQualifiedName~FieldEditorTests # ต้องมี Docker (Testcontainers) cd backend && dotnet test cd web && npx vitest run test/fieldEditor.spec.ts && npm test && npm run build ``` Manual E2E (หลัง `docker compose up -d` + backend + `npm run dev`): 1. หน้าแก้ template: field type `text` → เลือก editor `textarea` ได้ · type `number`/`date` → แสดง `—` · เปลี่ยน type จาก text เป็น number → editor กลับเป็น default 2. ฟอร์มสร้างเอกสาร: field textarea → กว้างหลายบรรทัด · number → ช่องตัวเลข · date → date picker (ตามเดิม) 3. Re-generate เอกสารที่ field date เก็บ timestamp เต็ม → ช่อง date ต้องแสดงวันที่ ไม่ใช่ว่างเปล่า ## Risks / gotchas - `TemplateFieldDto` เป็น positional record — แทรก `Editor` หลัง `Type` กระทบจุด construct เดียว (`FieldDtos`) + test mirror (`TestDtos.TemplateFieldItem`) — ตรวจ compile - Validation editor ต้องมาหลัง type check (allow-list keyed ตาม type — ลำดับใน loop เดิมรับประกันอยู่แล้ว) - editor select ใช้ label `editor` lowercase ตาม convention `label`/`type`/`required` ของหน้า — option แสดง raw value เหมือน type select - อย่าลืมอัปเดตตัวเลข test count ใน docs (52/36) หลังรันจริง ## สถานะ: ✅ implement เสร็จ (2026-08-22) ผลรันจริงทั้งชุด: backend `dotnet test` **60/60** (เพิ่ม `FieldEditorTests` 8 กรณี) · web `npm test` **46/46** + build ผ่าน (เพิ่ม `fieldEditor.spec.ts` 10 กรณี) · addin ไม่ถูกแตะ (50/50 คงเดิม) — รายละเอียด checkbox Phase 10 + หมายเหตุจากการทำจริงอยู่ที่ [officejs-implementation-plan.md](officejs-implementation-plan.md) · ผล AC-17 ที่ [acceptance-review.md](../acceptance-review.md) · เหลือยืนยันด้วยมือ: browser click-through ของ admin select + textarea/date rendering ในฟอร์มสร้างเอกสาร