Phase 10: field editor (FR-14)
เพิ่ม editor ต่อ template field — ผู้ดูแลเลือกรูปแบบช่องกรอกในฟอร์มสร้างเอกสารได้: text → textbox/textarea, number/date → control ตาม type (presentation metadata ล้วน — validation ยังยึด Type, ค่า field เป็น string ทั้ง flow, ไม่แตะ generate/sync/add-in) - Backend: TemplateField.Editor (nullable, null = default ของ type) + migration AddTemplateFieldEditor + validate ตาม type ใน PUT templates (ค่าไม่รู้จัก/ไม่เข้ากับ type → 400, normalize-on-write, API คืน resolved เสมอ) - Web: util fieldEditor.ts + editor select หน้าแก้ template (desktop+mobile, ซ่อนเมื่อ type มีทางเดียว) + ฟอร์มสร้างเอกสาร render ตาม editor + แก้ prefill re-generate ของ field date จาก ISO timestamp เต็ม (input[type=date] รับเฉพาะ yyyy-MM-dd เดิมแสดงว่าง) - Docs: spec v6 (FR-14/AC-17) + plan Phase 10 + sub-plan field-editor.md + acceptance-review AC-17 + README/CLAUDE.md - Tests: backend 60/60 (FieldEditorTests ใหม่ 8 กรณี) · web 46/46 + build ผ่าน Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7b20f12ede
commit
9bb4a48c00
23 changed files with 865 additions and 45 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
/** FR-9 UI — แก้ไข template: metadata + field definitions (label/type/required/ลำดับ) + แทนที่ไฟล์ */
|
||||
/** FR-9 UI — แก้ไข template: metadata + field definitions (label/type/editor/required/ลำดับ) + แทนที่ไฟล์ */
|
||||
import type { FieldType, ReplaceTemplateFileResponse, TemplateDetail } from '~/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
|
|
@ -36,7 +36,12 @@ function applyDetail(d: TemplateDetail) {
|
|||
name.value = d.name
|
||||
description.value = d.description ?? ''
|
||||
isActive.value = d.isActive
|
||||
fields.value = d.fields.map((f) => ({ ...f }))
|
||||
fields.value = d.fields.map((f) => ({ ...f, editor: resolveEditor(f) }))
|
||||
}
|
||||
|
||||
/** เปลี่ยน type → reset editor เป็น default ของ type ใหม่ (FR-14) — อ่านค่าจาก select ตรงๆ ไม่พึ่งลำดับ v-model) */
|
||||
function onTypeChange(f: (typeof fields.value)[number], e: Event) {
|
||||
f.editor = defaultEditor((e.target as HTMLSelectElement).value)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
|
|
@ -76,6 +81,7 @@ async function save() {
|
|||
key: f.key,
|
||||
label: f.label.trim(),
|
||||
type: f.type,
|
||||
editor: f.editor,
|
||||
isRequired: f.isRequired,
|
||||
sortOrder: f.sortOrder,
|
||||
})),
|
||||
|
|
@ -185,6 +191,9 @@ onMounted(load)
|
|||
<p class="mt-1 text-xs text-muted">
|
||||
<span class="font-mono">key</span> มาจาก content control tag ในไฟล์ — เพิ่ม/ลบ key ต้องแทนที่ไฟล์ด้านล่าง
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
<span class="font-mono">editor</span> = รูปแบบช่องกรอกในฟอร์มสร้างเอกสาร (เลือกได้เฉพาะ type text)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- มือถือ: card ต่อ field (v-model ชุดเดียวกับตาราง) -->
|
||||
|
|
@ -198,7 +207,13 @@ onMounted(load)
|
|||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label :for="`m-type-${f.key}`" class="label">type</label>
|
||||
<select :id="`m-type-${f.key}`" v-model="f.type" :aria-label="`type ของ ${f.key}`" class="input">
|
||||
<select
|
||||
:id="`m-type-${f.key}`"
|
||||
v-model="f.type"
|
||||
:aria-label="`type ของ ${f.key}`"
|
||||
class="input"
|
||||
@change="onTypeChange(f, $event)"
|
||||
>
|
||||
<option v-for="t in FIELD_TYPES" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
@ -213,6 +228,12 @@ onMounted(load)
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="editorOptions(f.type).length > 1">
|
||||
<label :for="`m-editor-${f.key}`" class="label">editor</label>
|
||||
<select :id="`m-editor-${f.key}`" v-model="f.editor" :aria-label="`editor ของ ${f.key}`" class="input">
|
||||
<option v-for="e in editorOptions(f.type)" :key="e" :value="e">{{ e }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input v-model="f.isRequired" type="checkbox" :aria-label="`required ของ ${f.key}`" class="h-4 w-4 accent-primary" />
|
||||
required
|
||||
|
|
@ -231,6 +252,7 @@ onMounted(load)
|
|||
<th class="px-4 py-3">key</th>
|
||||
<th class="px-4 py-3">label</th>
|
||||
<th class="px-4 py-3">type</th>
|
||||
<th class="px-4 py-3">editor</th>
|
||||
<th class="px-4 py-3 text-center">required</th>
|
||||
<th class="px-4 py-3">ลำดับ</th>
|
||||
</tr>
|
||||
|
|
@ -247,10 +269,26 @@ onMounted(load)
|
|||
/>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<select v-model="f.type" :aria-label="`type ของ ${f.key}`" class="input px-2 py-1">
|
||||
<select
|
||||
v-model="f.type"
|
||||
:aria-label="`type ของ ${f.key}`"
|
||||
class="input px-2 py-1"
|
||||
@change="onTypeChange(f, $event)"
|
||||
>
|
||||
<option v-for="t in FIELD_TYPES" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="px-4 py-2">
|
||||
<select
|
||||
v-if="editorOptions(f.type).length > 1"
|
||||
v-model="f.editor"
|
||||
:aria-label="`editor ของ ${f.key}`"
|
||||
class="input px-2 py-1"
|
||||
>
|
||||
<option v-for="e in editorOptions(f.type)" :key="e" :value="e">{{ e }}</option>
|
||||
</select>
|
||||
<span v-else class="text-muted">—</span>
|
||||
</td>
|
||||
<td class="px-4 py-2 text-center">
|
||||
<input
|
||||
v-model="f.isRequired"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue