bookmark: ดักความยาวชื่อ/โน้ตที่ช่องกรอก + F19 สรุปสิ่งที่เปลี่ยนใน dialog ประวัติ + formatThaiDate — (1) bookmark limits: utils/bookmark-limits.ts ใหม่เป็น source of truth เดียว (ชื่อ 120/โน้ต 200 ตัวอักษร) server action import ร่วมแทน const ท้องถิ่น + ใส่ maxLength ที่ popup บันทึก bookmark (PDFViewer) และฟอร์มแก้ไข ✏ ใน sidebar ดักทั้งพิมพ์/paste ตั้งแต่ในช่อง ไม่รอ error ตอนกดบันทึก + แก้ off-by-one ของ normalizeBookmarkTitle (เดิม slice 120 แล้วต่อ … = 121 ตัว ทำให้คลุมข้อความยาวแล้วกด B บันทึกเลยโดน limit ของ server ตีกลับทั้งที่ไม่ได้พิมพ์เอง — ตอนนี้ slice 119 + … = 120 พอดีตามสเปก §7 N2 ที่เขียนไว้ ≤ 120 อยู่แล้ว) + PLAN-FEAT.md §7 N2 บันทึกเพิ่ม — (2) งานคู่ขนาดจากอีก session (แชร์ไฟล์ PDFViewer/sidebar เลยรวม commit): dialog ประวัติ F19 แสดงสรุปสิ่งที่เปลี่ยนต่อรอบผ่าน describeAnnotationChange ใหม่ใน utils/annotation-display.ts (เทียบ snapshot ก่อนแก้ v.data กับ state ถัดไป — snapshot รอบใหม่กว่า หรือค่าปัจจุบันจาก realtime lookup สด historyCurrent — สรุปเป็นหน่วยเดียวกับตัวปรับ UI: ความหนา ×10 ความทึบ % fontSize px ชื่อสี preset ย้าย/ปรับขนาดจาก bbox ผลต่างเกิน GEOM_EPS_PCT 0.5% ข้อความ/โน้ต/label) + dialog ขยาย w-96 max-h-[60vh] แถวเปลี่ยน items-start + utils/date-format.ts ใหม่ formatThaiDate วันที่ไทยเต็ม "18 กันยายน พ.ศ. 2569" (+เวลา) แทน toLocaleString("th-TH") ทุกจุดแสดงวันที่ใน UI (หน้ารายการไฟล์ sidebar ประวัติ/รายการ timestamps ใน PDFViewer) กัน server/client locale ไม่ตรงกัน + tests ใหม่/ขยาย (date-format + describeAnnotationChange) — CLAUDE.md sync โครงสร้าง utils — verify: tsc strict ผ่าน + vitest 139/139 ผ่าน
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 43s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 3s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-09-17 17:02:31 +07:00
parent 8ba911a119
commit e0c78d68fe
11 changed files with 469 additions and 38 deletions

View file

@ -51,8 +51,17 @@ import type {
RedactionData,
ShapeData,
} from "@/db/schema";
import { annotationNote, annotationPreviewText } from "@/utils/annotation-display";
import {
annotationNote,
annotationPreviewText,
describeAnnotationChange,
} from "@/utils/annotation-display";
import { withBasePath } from "@/utils/base-path";
import {
MAX_BOOKMARK_NOTE_CHARS,
MAX_BOOKMARK_TITLE_CHARS,
} from "@/utils/bookmark-limits";
import { formatThaiDate } from "@/utils/date-format";
/** UX audit 2026-09-16: focus ring ตอน keyboard focus — ค่าเดียวกับ FOCUS_RING_CLS
* ของ PDFViewer.tsx (ห้าม import ข้ามไฟล์: PDFViewer → sidebar → PDFViewer เป็น
@ -455,6 +464,12 @@ export default function AnnotationSidebar({
return () => document.removeEventListener("keydown", onKey);
}, [historyFor]);
// state ปัจจุบันของรายการที่เปิดประวัติอยู่ — lookup สดจาก realtime ก่อน
// (SSE update หลังเปิด dialog ก็ยังเทียบกับค่าล่าสุดได้) โดย fallback เป็น
// snapshot ตอนเปิด (รายการจากผลค้นหา F14 ไม่อยู่ใน prop annotations)
const historyCurrent =
historyFor && (annotations.find((a) => a.id === historyFor.id) ?? historyFor);
// F10/A5: แยกกลุ่มเฉพาะ COMMENT — "ค้างดำเนินการ" (รวม annotation ชนิดอื่น)
// กับ "จัดการแล้ว" (comment ที่ถูก resolve)
const pendingItems = annotations.filter((a) => a.type !== "COMMENT" || !a.resolved);
@ -773,6 +788,7 @@ export default function AnnotationSidebar({
<div className={`rounded py-1 ${pad}`}>
<input
autoFocus
maxLength={MAX_BOOKMARK_TITLE_CHARS}
value={eb.title}
onChange={(e) => setEditBookmark({ ...eb, title: e.target.value })}
onKeyDown={(e) => {
@ -785,6 +801,7 @@ export default function AnnotationSidebar({
/>
<textarea
rows={2}
maxLength={MAX_BOOKMARK_NOTE_CHARS}
value={eb.note}
onChange={(e) => setEditBookmark({ ...eb, note: e.target.value })}
onKeyDown={(e) => {
@ -1516,7 +1533,7 @@ export default function AnnotationSidebar({
role="dialog"
aria-modal="true"
aria-label="ประวัติการเปลี่ยนแปลง"
className="w-80 rounded-lg border border-slate-200 bg-white p-4 shadow-xl"
className="w-96 max-w-full rounded-lg border border-slate-200 bg-white p-4 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<p className="text-sm font-semibold">ประวัติการเปลี่ยนแปลง</p>
@ -1537,27 +1554,46 @@ export default function AnnotationSidebar({
</p>
)}
{versions !== null && versions.length > 0 && (
<ul className="mt-3 max-h-56 divide-y divide-slate-100 overflow-y-auto">
{versions.map((v) => (
<li key={v.id} className="flex items-center gap-2 py-2">
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium">
{v.kind === "delete" ? "ถูกลบ" : "ถูกแก้ไข"}
<ul className="mt-3 max-h-[60vh] divide-y divide-slate-100 overflow-y-auto">
{versions.map((v, i) => {
// สิ่งที่เปลี่ยนของรอบนี้ = snapshot ก่อนแก้ (v.data) เทียบ state
// ถัดไป: snapshot ของรอบแก้ที่ใหม่กว่าถัดไป (i-1) หรือค่าปัจจุบัน
// ของรายการ (version ใหม่สุด) — แถว "ถูกลบ" เป็น snapshot สุดท้าย
// ก่อนลบ ไม่มี state ต่อจากนั้น จึงไม่ diff
const changes =
v.kind === "update" && historyCurrent
? describeAnnotationChange(
v.type,
v.data,
i === 0 ? historyCurrent.data : versions[i - 1].data,
)
: [];
return (
<li key={v.id} className="flex items-start gap-2 py-2">
<span className="min-w-0 flex-1">
<span className="block text-xs font-medium">
{v.kind === "delete" ? "ถูกลบ" : "ถูกแก้ไข"}
</span>
<span className="block text-[11px] text-slate-500" suppressHydrationWarning>
{formatThaiDate(v.createdAt, true)} · โดย {v.userName}
</span>
{changes.map((c) => (
<span key={c} className="mt-0.5 block text-[11px] text-slate-600">
{c}
</span>
))}
</span>
<span className="block text-[11px] text-slate-500" suppressHydrationWarning>
{new Date(v.createdAt).toLocaleString("th-TH")}
</span>
</span>
<button
type="button"
disabled={restoringId !== null}
className={`shrink-0 rounded border border-slate-300 px-2 py-1 text-[11px] text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
onClick={() => void handleRestore(v.id)}
>
{restoringId === v.id ? "กำลังกู้คืน…" : "กู้คืน"}
</button>
</li>
))}
<button
type="button"
disabled={restoringId !== null}
className={`mt-0.5 shrink-0 rounded border border-slate-300 px-2 py-1 text-[11px] text-slate-600 hover:bg-slate-100 disabled:opacity-40 ${FOCUS_RING_CLS}`}
onClick={() => void handleRestore(v.id)}
>
{restoringId === v.id ? "กำลังกู้คืน…" : "กู้คืน"}
</button>
</li>
);
})}
</ul>
)}
<div className="mt-3 flex justify-end text-xs">