"use client"; import { useCallback, useRef, useState } from "react"; // Undo/Redo (PLAN-FEAT.md §3 F2, D10) — history ฝั่ง client ต่อ session ต่อผู้ใช้ // ไม่เก็บใน DB; push เฉพาะ action ของตัวเองที่สำเร็จเท่านั้น (event ของคนอื่นไม่เกี่ยว) // ตัว entry เก็บ closure ที่เรียก Server Action + sync state เอง — id ของ annotation // ที่ "สร้างใหม่" หลัง undo ต้องส่งต่อให้ redo ได้ จึงให้ entry เป็น object ที่แก้ field ได้ export type HistoryEntry = { undo: () => Promise; redo: () => Promise; }; export function useHistory(options?: { /** เรียกเมื่อ undo/redo ล้มเหลว (เน็ตสะดุด/session หมดอายุ ฯลฯ) — entry ถูกข้าม * ไปแล้วโดย hook ผู้ใช้ควรเห็น toast ของ caller ไม่ใช่เงียบหายไปเฉย ๆ */ onError?: (err: unknown, kind: "undo" | "redo") => void; }) { // source of truth เป็น ref — state ที่เปลี่ยนแค่ไว้ trigger re-render ปุ่ม disabled const undoRef = useRef([]); const redoRef = useRef([]); const [, bump] = useState(0); // เก็บ callback ล่าสุดของ caller ใน ref — callback ที่ส่งเข้ามาเปลี่ยนทุก render ได้ const onErrorRef = useRef(options?.onError); onErrorRef.current = options?.onError; /** บันทึก action ที่สำเร็จ — action ใหม่เคลียร์ redo stack เสมอ */ const push = useCallback((entry: HistoryEntry) => { undoRef.current.push(entry); redoRef.current = []; bump((n) => n + 1); }, []); const undo = useCallback(async () => { const entry = undoRef.current.pop(); if (!entry) return; try { await entry.undo(); redoRef.current.push(entry); } catch (err) { // เช่น undo ของ create แต่ row ถูกลบไปแล้วโดยแท็บอื่น → ข้าม ไม่ crash (§5) console.error("undo ไม่สำเร็จ — ข้ามรายการนี้", err); onErrorRef.current?.(err, "undo"); } bump((n) => n + 1); }, []); const redo = useCallback(async () => { const entry = redoRef.current.pop(); if (!entry) return; try { await entry.redo(); undoRef.current.push(entry); } catch (err) { console.error("redo ไม่สำเร็จ — ข้ามรายการนี้", err); onErrorRef.current?.(err, "redo"); } bump((n) => n + 1); }, []); return { push, undo, redo, canUndo: undoRef.current.length > 0, canRedo: redoRef.current.length > 0, }; }