เลือก annotation บนหน้า PDF → sidebar ตามไปเน้นรายการนั้น + สร้างเสร็จเลือกให้ทันที + กัน duplicate ชั่วขณะตอนสร้าง: (1) ส่ง selectedId ให้ AnnotationSidebar — ไฮไลต์แถว (ทั้งลิสต์หลักและผลค้นหา) + scrollIntoView block:nearest + สลับมาแท็บ Annotations ให้เองถ้ากำลังอยู่ Bookmarks (rAF รอแถว render หลังสลับแท็บค่อยเลื่อน) (2) AUTO_SELECT_TYPES — สร้าง DRAW/SHAPE/TEXTBOX/STAMP/HIGHLIGHT/REDACTION เสร็จ = เลือกตัวใหม่ทันทีด้วย saved.id (กรอบ selection + sidebar ตามไปเลย; COMMENT ยกเว้นเพราะ composer/pin ทำหน้าที่นั้นอยู่แล้ว) — แก้ปัญหา "วาดเสร็จคลิกที่เส้น tool ยังเป็นวาด sidebar นิ่ง" (3) กันอาการ row โผล่แล้วหาย (0→2→1): เดิม optimistic row กับ row จาก server คนละ id ทำให้ SSE event มาก่อน action เสร็จ dedupe ไม่ได้ — ตอนนี้ client ส่ง id ของ optimistic row ให้ createAnnotation ใช้ซ้ำ (validate UUID, ไม่ส่ง = สุ่มเองตามเดิมสำหรับ redo path) ทุกชั้น dedupe ด้วย id เดียวกันตั้งแต่แรก — sync PLAN.md §4 + rules/realtime.md ให้ตรงพฤติกรรมใหม่

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
DESKTOP-1R2VSQH\Lenovo ThinkPad E490 2026-09-14 11:35:05 +07:00
parent 4934b9d0b3
commit 6ddc35d6d5
5 changed files with 67 additions and 12 deletions

View file

@ -366,6 +366,17 @@ const TOOLS: { key: Tool; label: string; Icon: LucideIcon }[] = [
{ key: "stamp", label: "ตรายาง (stamp)", Icon: Stamp },
];
/** ชนิดที่สร้างเสร็จแล้วเลือกรายการใหม่ให้อัตโนมัติ (กรอบ selection + sidebar ไฮไลต์)
* ทุกชนิดยกเว้น COMMENT — เปิด composer พิมพ์ก่อนบันทึก และคลิก pin = เลือกอยู่แล้ว */
const AUTO_SELECT_TYPES = new Set<AnnotationType>([
"DRAW",
"SHAPE",
"TEXTBOX",
"STAMP",
"HIGHLIGHT",
"REDACTION",
]);
/** cursor รูปยางลบของ tool erase — SVG inline สไตล์ lucide (path เดียวกับ icon บน
* แถบ tool) hotspot ที่ปลายยางมุมล่างซ้าย = จุดที่ใช้ลบจริง */
const ERASER_CURSOR = `url("data:image/svg+xml,${encodeURIComponent(
@ -1550,6 +1561,7 @@ export default function PDFViewer({
applyAnnotation({ kind: "add", row: temp });
try {
const saved = await createAnnotation({
id: temp.id, // ใช้ id เดียวกับ optimistic row — กัน duplicate ชั่วขณะ (row โผล่แล้วหาย)
fileId: file.id,
pageIndex,
userId: user.userId,
@ -1566,6 +1578,12 @@ export default function PDFViewer({
setAnnotations((prev) =>
prev.some((a) => a.id === saved.id) ? prev : [...prev, saved],
);
// สร้าง "วัตถุ" เสร็จ = เลือกตัวใหม่ทันที (ใช้ id จาก server — id ชั่วคราวของ
// optimistic row ถูกถอดตอน transition จบ เลือก id นั้นไฮไลต์จะหายเอง)
if (AUTO_SELECT_TYPES.has(type)) {
setSelectedId(saved.id);
setPreviewData(null);
}
// F2: undo ของ create = ลบ id ปัจจุบัน; redo = สร้างใหม่ (id ใหม่) แล้วส่งต่อให้ undo รอบถัดไป
let currentId = saved.id;
pushHistory({
@ -2400,6 +2418,7 @@ export default function PDFViewer({
onUpdateBookmark={(id, patch) => void handleUpdateBookmark(id, patch)}
onGoToBookmark={goToBookmark}
onGoToAnnotation={goToAnnotation}
selectedId={selectedId}
/>
</aside>

View file

@ -1,6 +1,6 @@
"use client";
import { useRef, useState, type FormEvent } from "react";
import { useEffect, useRef, useState, type FormEvent } from "react";
import { createPortal } from "react-dom";
import {
ArrowRight,
@ -132,6 +132,7 @@ export default function AnnotationSidebar({
onUpdateBookmark,
onGoToBookmark,
onGoToAnnotation,
selectedId,
}: {
annotations: AnnotationDTO[];
bookmarks: BookmarkDTO[];
@ -157,9 +158,26 @@ export default function AnnotationSidebar({
) => void;
onGoToBookmark: (b: BookmarkDTO) => void;
onGoToAnnotation: (a: AnnotationDTO) => void;
/** id ของ annotation ที่ถูกเลือกอยู่บนหน้า PDF (null = ไม่มี) — แถวในแผงนี้ไฮไลต์ตาม */
selectedId: string | null;
}) {
const [tab, setTab] = useState<"bookmarks" | "annotations">("bookmarks");
// คลิกเลือก annotation บนหน้าเอกสาร = สลับมาแท็บ annotations (ถ้ายังอยู่ bookmarks)
// แล้วเลื่อนแถวนั้นเข้ามุมมอง — แถว render หลัง setTab จึงค่อยเลื่อนใน rAF ถัดไป
// (dep รวม tab ตาม lint — ผลข้างเคียง: เปิดแท็บนี้ตอนยังมีรายการถูกเลือก = เลื่อนไปแถวนั้นเช่นกัน)
const listRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!selectedId) return;
if (tab !== "annotations") setTab("annotations");
const raf = requestAnimationFrame(() => {
listRef.current
?.querySelector(`[data-ann-id="${CSS.escape(selectedId)}"]`)
?.scrollIntoView({ block: "nearest" });
});
return () => cancelAnimationFrame(raf);
}, [selectedId, tab]);
// ─── F14/D1: ค้นหาคำในหมายเหตุทั้งไฟล์ (highlight text / comment message) ────
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<AnnotationDTO[] | null>(null);
@ -488,9 +506,12 @@ export default function AnnotationSidebar({
return (
<li
key={a.id}
className={`group rounded px-2 py-1.5 hover:bg-slate-100 ${
a.resolved ? "opacity-60" : ""
}`}
data-ann-id={a.id}
className={`group rounded px-2 py-1.5 ${
a.id === selectedId
? "bg-primary/10 ring-1 ring-inset ring-primary/30"
: "hover:bg-slate-100"
} ${a.resolved ? "opacity-60" : ""}`}
>
<div className="flex items-center gap-2">
<button
@ -677,7 +698,11 @@ export default function AnnotationSidebar({
<li key={a.id}>
<button
type="button"
className="flex w-full items-start gap-2 rounded px-1 py-1 text-left hover:bg-slate-100"
className={`flex w-full items-start gap-2 rounded px-1 py-1 text-left ${
a.id === selectedId
? "bg-primary/10 ring-1 ring-inset ring-primary/30"
: "hover:bg-slate-100"
}`}
onClick={() => onGoToAnnotation(a)}
>
<TypeIcon type={a.type} data={a.data} />
@ -717,7 +742,7 @@ export default function AnnotationSidebar({
))}
</div>
<div className="flex-1 overflow-y-auto p-2">
<div ref={listRef} className="flex-1 overflow-y-auto p-2">
{tab === "bookmarks" ? (
bookmarks.length === 0 ? (
<p className="p-4 text-center text-xs text-slate-400">