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>
This commit is contained in:
Suphonchai Phoonsawat 2026-08-20 22:05:49 +07:00
parent b47667f918
commit 7b20f12ede
28 changed files with 928 additions and 141 deletions

View file

@ -185,6 +185,7 @@ onMounted(load)
<p class="font-medium">v{{ v.versionNumber }}</p>
<p class="mt-1 text-xs text-muted">{{ formatDateTime(v.createdAt) }}</p>
<p class="mt-0.5 font-mono text-xs text-muted">{{ v.createdByUserId.slice(0, 8) }}…</p>
<p v-if="v.message" class="mt-1 text-sm break-words">{{ v.message }}</p>
</div>
<div class="flex shrink-0 flex-col items-end gap-2">
<button
@ -241,7 +242,16 @@ onMounted(load)
class="cursor-pointer hover:bg-surface-muted/60"
@click="expandedVersionId = expandedVersionId === v.versionId ? null : v.versionId"
>
<td class="px-4 py-3 font-medium">v{{ v.versionNumber }}</td>
<td class="px-4 py-3 font-medium">
v{{ v.versionNumber }}
<p
v-if="v.message"
class="max-w-md truncate text-xs font-normal text-muted"
:title="v.message"
>
{{ v.message }}
</p>
</td>
<td class="px-4 py-3">{{ formatDateTime(v.createdAt) }}</td>
<td class="px-4 py-3 font-mono text-xs text-muted">{{ v.createdByUserId.slice(0, 8) }}…</td>
<td class="px-4 py-3 text-right whitespace-nowrap">

View file

@ -10,6 +10,8 @@ const templateId = computed(() => String(route.params.templateId))
const detail = ref<TemplateDetail | null>(null)
const values = ref<Record<string, string>>({})
const issues = ref<Record<string, string>>({})
/** คำอธิบาย version แบบ commit message (FR-13) — ทางเลือก · re-generate ไม่ prefill เพราะเป็นเอกสารใหม่ */
const message = ref('')
/** เอกสารต้นทางของ re-generate (จาก ?from=) — ใช้ทั้ง prefill และ banner บอกบริบท */
const fromDoc = ref<{ id: string; fileName: string } | null>(null)
@ -82,7 +84,7 @@ async function submit() {
result.value = await apiJson<GenerateDocumentResponse>('/api/documents/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId: templateId.value, values: values.value }),
body: JSON.stringify({ templateId: templateId.value, values: values.value, message: message.value.trim() || null }),
})
window.scrollTo({ top: 0 })
} catch (err) {
@ -113,6 +115,7 @@ function resetForm() {
downloadError.value = null
issues.value = {}
values.value = { ...blankValues(), ...prefill.value }
message.value = ''
}
onMounted(load)
@ -220,6 +223,18 @@ onMounted(load)
<p v-if="issues[f.key]" class="mt-1 text-xs text-error-strong">{{ issues[f.key] }}</p>
</div>
<div>
<label for="doc-message" class="label">คำอธิบาย version (ไม่บังคับ)</label>
<textarea
id="doc-message"
v-model="message"
class="input"
rows="2"
maxlength="2048"
placeholder="เช่น ฉบับเสนอลงนามครั้งแรก"
/>
</div>
<div v-if="sortedFields.length === 0" class="empty-state">
template นี้ไม่มี field definitions —
<NuxtLink :to="`/templates/${detail.id}/edit`" class="link">ตรวจสอบที่หน้าจัดการ template</NuxtLink>

View file

@ -98,4 +98,6 @@ export interface VersionListItem {
createdByUserId: string
contentHash: string
fields: unknown
/** คำอธิบาย version แบบ commit message ที่ผู้ใช้พิมพ์ตอนสร้าง (FR-13) — null ได้ */
message: string | null
}