groups+summary: redesign หน้า /groups/[typeId] + หน้าสรุปเป็น explorer 2 คอลัมน์ (ตัวกรองซ้าย + list เดียว) — หน้าสรุปใช้มิติแรก "หน้า" ตามแบบหน้ากลุ่มที่ใช้ "ไฟล์" — (1) หน้า /groups/[typeId]: ถอด navbar (เหลือ back link — AppNav เหลือใช้ที่หน้า / อย่างเดียว) + ถอดหมายเหตุ "แสดงเฉพาะเอกสารที่เคยเปิดในระบบนี้", query ทั้งหมดฝั่ง server ส่ง GroupFileSummary[]+GroupListItem[] ให้ GroupExplorer กรองใน memory — ตัวกรอง tree menu 2 ระดับสไตล์ Quasar docs 4 กลุ่ม (ไฟล์/ชนิดรายการ/ชนิดของ Annotation/ผู้บันทึก — ชนิดรายการเป็น checkbox คู่ ไม่เลือก = แสดงทั้งหมด, ติ๊กชนิด → เลือก Annotation อัตโนมัติ, เลิกเลือก Annotation → ล้างชนิดค้าง) + GroupList แถว passive จุดคลิกเดียวคือปุ่ม "View" (2) แยกแผงตัวกรองเป็น component กลาง components/explorer/FacetFilterPanel.tsx — มิติแรกส่งเข้าทาง prop firstGroup (groups = ไฟล์ / สรุป = หน้า พร้อม optionsScroll ให้ scroll ภายในกลุ่ม max-h-64 กันหลายสิบหน้าดันกลุ่มอื่นออกจอ) + TYPE_SHORT/TYPE_ORDER รวมจาก 2 ที่ที่เคย define ซ้ำมาอยู่ utils/annotation-display.ts จุดเดียว (3) หน้าสรุป /annotate/[id]/summary: server ส่ง ExplorerItem[] (หน้า → bookmark ก่อน annotation → ในหน้าเดียวกัน bookmark เรียง yOffset ตาม N5/annotation ตาม createdAt) ให้ SummaryExplorer + SummaryList การ์ดรายหน้าหัวการ์ด sticky (เลขหน้า + บรรทัดสรุปหน้าคำนวณจากรายการที่เห็นจริงหลังกรอง + ปุ่มเปิดหน้านี้) + แถว passive ปุ่ม View deep link ?ann=/?page= เหมือนหน้ากลุ่ม — ถอดรายการตามหน้าจุดไข่ปลา + การ์ด folio + CollapsibleBlock ออก (anchor #page-N ของ section คงไว้ให้ลิงก์เก่ายังใช้ได้) (4) ตอนพิมพ์แสดงฉบับเต็มเสมอ — ตัวกรองที่ตั้งค้างต้องไม่ตัดรายการออกเงียบ ๆ บนกระดาษ (ตอนกรองอยู่ render ชุดบนจอ print:hidden + ชุดเต็ม hidden print:block, prop anchorIds กัน duplicate id) + กฎ print ใหม่ใน globals.css (section>ul>li break-inside:avoid-page + .page-head position:static) (5) ผ่าน code review 2026-09-24 (tsc/build/vitest 144 tests): คืน N5 yOffset ให้ bookmark ต่อหน้าที่ sort createdAt ทับไปโดยไม่ตั้งใจ + จดรับความเสี่ยง localeCompare "th" รันทั้ง SSR/hydration ไว้ที่ comment พร้อมทางแก้ + docs sync ครบ (CLAUDE.md โครงสร้างไฟล์ + PLAN-FEAT ถอดหมายเหตุ + PLAN-DOCSET อ้าง FacetFilterPanel) (คำขอผู้ใช้ 2026-09-24)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
c5c6b65bd2
commit
df502088b7
14 changed files with 1522 additions and 616 deletions
186
components/groups/GroupExplorer.tsx
Normal file
186
components/groups/GroupExplorer.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { ChevronDown, FileImage, FileText, Folder } from "lucide-react";
|
||||
|
||||
import {
|
||||
FacetFilterPanel,
|
||||
createDefaultFacets,
|
||||
type ExplorerItem,
|
||||
type FacetFilterState,
|
||||
} from "@/components/explorer/FacetFilterPanel";
|
||||
import type { AnnotationType } from "@/db/schema";
|
||||
|
||||
import { GroupList } from "./GroupList";
|
||||
|
||||
// N9 หน้ากลุ่ม (redesign 2026-09-24 ตามคำขอผู้ใช้): explorer 2 คอลัมน์ — ซ้ายเป็น
|
||||
// ตัวกรอง (ไฟล์/ชนิดรายการ/ชนิด annotation/ผู้บันทึก) ขวาเป็น list เดียวรวม
|
||||
// bookmark+annotation ทั้งกลุ่มไม่แยกไฟล์ — ข้อมูลทั้งหมด query ฝั่ง server ส่งมา
|
||||
// ครั้งเดียวแล้วกรองใน memory (read-only snapshot เหมือนเดิม ไม่มี SSE)
|
||||
// แผงตัวกรองเป็น component กลาง `components/explorer/FacetFilterPanel` (2026-09-24
|
||||
// — เดิม FilterControls เป็นของหน้านี้) ใช้ร่วมกับหน้าสรุปที่มิติแรกเป็น "หน้า"
|
||||
|
||||
/** ไฟล์ในกลุ่มแบบย่อ — เอาเฉพาะ field ที่ filter/list ใช้ (ไม่ส่ง row เต็มข้าม wire) */
|
||||
export type GroupFileSummary = {
|
||||
id: string;
|
||||
title: string;
|
||||
isImage: boolean;
|
||||
};
|
||||
|
||||
/** รายการเดียวรวมทั้งกลุ่ม — server เรียงตามลำดับอ่านเอกสารให้แล้ว (ไฟล์ตาม
|
||||
* createdAt → หน้า → หน้าเดียวกัน bookmark ก่อน annotation → createdAt) */
|
||||
export type GroupListItem = ExplorerItem;
|
||||
|
||||
export function GroupExplorer({
|
||||
files,
|
||||
items,
|
||||
}: {
|
||||
files: GroupFileSummary[];
|
||||
items: GroupListItem[];
|
||||
}) {
|
||||
const [filters, setFilters] = useState<FacetFilterState>(createDefaultFacets);
|
||||
|
||||
// facet count เป็น static จากข้อมูลทั้งกลุ่ม (ไม่หดตามตัวกรองอื่น — กันเข้าใจ
|
||||
// ผิดว่าตัวเลือกหายไปตอนไล่กรอง)
|
||||
const counts = useMemo(() => {
|
||||
let bm = 0;
|
||||
let ann = 0;
|
||||
const file: Record<string, number> = {};
|
||||
const type: Partial<Record<AnnotationType, number>> = {};
|
||||
const user: Record<string, number> = {};
|
||||
for (const it of items) {
|
||||
file[it.dto.fileId] = (file[it.dto.fileId] ?? 0) + 1;
|
||||
user[it.dto.userId] = (user[it.dto.userId] ?? 0) + 1;
|
||||
if (it.kind === "bm") bm++;
|
||||
else {
|
||||
ann++;
|
||||
type[it.dto.type] = (type[it.dto.type] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
return { total: items.length, bm, ann, file, type, user };
|
||||
}, [items]);
|
||||
|
||||
// รายชื่อผู้บันทึกที่มีรายการจริง เรียงชื่อไทย (คนละไฟล์รวมเป็นคนเดียว)
|
||||
const users = useMemo(() => {
|
||||
const labelById = new Map<string, string>();
|
||||
for (const it of items) {
|
||||
if (!labelById.has(it.dto.userId)) {
|
||||
labelById.set(it.dto.userId, it.dto.userName || "ไม่ทราบชื่อ");
|
||||
}
|
||||
}
|
||||
const out: { userId: string; label: string }[] = [];
|
||||
labelById.forEach((label, userId) => out.push({ userId, label }));
|
||||
return out.sort((a, b) => a.label.localeCompare(b.label, "th"));
|
||||
}, [items]);
|
||||
|
||||
// มิติแรกของหน้ากลุ่ม = ไฟล์ (count รายไฟล์ฝังใน options ของ firstGroup)
|
||||
const firstGroup = {
|
||||
label: "ไฟล์",
|
||||
parentIcon: <Folder aria-hidden className="h-4 w-4 shrink-0 text-ink-soft" />,
|
||||
allLabel: "ทุกไฟล์",
|
||||
options: files.map((f) => ({
|
||||
key: f.id,
|
||||
label: f.title,
|
||||
count: counts.file[f.id] ?? 0,
|
||||
icon: f.isImage ? (
|
||||
<FileImage aria-hidden className="h-4 w-4 shrink-0 text-ink-soft" />
|
||||
) : (
|
||||
<FileText aria-hidden className="h-4 w-4 shrink-0 text-ink-soft" />
|
||||
),
|
||||
})),
|
||||
};
|
||||
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
items.filter(
|
||||
(it) =>
|
||||
(!filters.first || it.dto.fileId === filters.first) &&
|
||||
(filters.kinds.size === 0 || filters.kinds.has(it.kind)) &&
|
||||
(it.kind === "ann"
|
||||
? filters.types.size === 0 || filters.types.has(it.dto.type)
|
||||
: true) &&
|
||||
(!filters.userId || it.dto.userId === filters.userId),
|
||||
),
|
||||
[items, filters],
|
||||
);
|
||||
|
||||
const selectedFile = filters.first
|
||||
? files.find((f) => f.id === filters.first) ?? null
|
||||
: null;
|
||||
|
||||
// จำนวนตัวกรองที่ ≠ default — โชว์ที่ summary มือถือ (กัน "กรองเก่ากรองเงียบ")
|
||||
// และเป็นเงื่อนไขปุ่ม "ล้างตัวกรอง"
|
||||
const activeCount =
|
||||
(filters.first !== null ? 1 : 0) +
|
||||
(filters.kinds.size > 0 ? 1 : 0) +
|
||||
(filters.types.size > 0 ? 1 : 0) +
|
||||
(filters.userId !== null ? 1 : 0);
|
||||
|
||||
const resetFilters = () => setFilters(createDefaultFacets());
|
||||
|
||||
return (
|
||||
<div className="mt-6 lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:items-start lg:gap-8">
|
||||
{/* มือถือ (< lg): ตัวกรองหุบ/กางเป็น <details> แบบเดียวกับหน้าสรุป —
|
||||
จอ lg ถูก display:none เอาออกจาก grid — ใช้ named group `group/filters`
|
||||
ไม่ใช่ `group` เปล่า: `group-open:` ของ parent tree ข้างใน match ทุก
|
||||
ancestor ที่มี .group[open] ใช้ชื่อซ้ำแล้ว chevron ของทุกกลุ่มหมุนตลอด
|
||||
ตอนกางแผงนี้ (responsive review 2026-09-24) */}
|
||||
<details className="group/filters rounded-xl border border-line bg-white lg:hidden">
|
||||
<summary className="focus-ring flex cursor-pointer list-none items-center justify-between rounded-md px-4 py-3 text-sm font-semibold text-ink transition hover:text-primary [&::-webkit-details-marker]:hidden">
|
||||
ตัวกรอง{activeCount > 0 ? ` (${activeCount})` : ""}
|
||||
<ChevronDown
|
||||
aria-hidden
|
||||
className="h-4 w-4 shrink-0 text-ink-faint transition-transform group-open/filters:rotate-180 motion-reduce:transition-none"
|
||||
/>
|
||||
</summary>
|
||||
{/* cap 70dvh + scroll ภายใน: กลุ่ม "ไฟล์"/"ผู้บันทึก" เยอะ ๆ ต้องไม่ดัน
|
||||
"พบ N รายการ"/list ลงไกลจนมองไม่เห็น (responsive review 2026-09-24) */}
|
||||
<div className="max-h-[70dvh] overscroll-contain overflow-y-auto border-t border-line/70 px-2.5 pb-3 pt-1">
|
||||
<FacetFilterPanel
|
||||
state={filters}
|
||||
onChange={setFilters}
|
||||
firstGroup={firstGroup}
|
||||
users={users}
|
||||
counts={counts}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* desktop: แผงตัวกรอง sticky — หน้าไม่มี navbar จึง top-6 พอ ไฟล์เยอะ scroll ภายในแผงเอง
|
||||
(p-3 กระชับตามความ dense ของ tree — design 2026-09-24) — h2 "ตัวกรอง (N)"
|
||||
ให้ parity กับ summary มือถือ, scroll-pt/pb กัน focus ring โดนขอบ scroll
|
||||
ตัด, max-h เป็น dvh ตาม pattern h-dvh ของ viewer (responsive review 2026-09-24) */}
|
||||
<aside
|
||||
aria-label="ตัวกรองรายการ"
|
||||
className="hidden rounded-xl border border-line bg-white p-3 lg:sticky lg:top-6 lg:block lg:max-h-[calc(100dvh-3rem)] lg:scroll-pb-1 lg:scroll-pt-1 lg:overflow-y-auto"
|
||||
>
|
||||
<h2 className="px-1.5 text-sm font-semibold text-ink">
|
||||
ตัวกรอง{activeCount > 0 ? ` (${activeCount})` : ""}
|
||||
</h2>
|
||||
<div className="mt-3">
|
||||
<FacetFilterPanel
|
||||
state={filters}
|
||||
onChange={setFilters}
|
||||
firstGroup={firstGroup}
|
||||
users={users}
|
||||
counts={counts}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="mt-5 lg:mt-0">
|
||||
<GroupList
|
||||
items={filtered}
|
||||
files={files}
|
||||
totalCount={counts.total}
|
||||
selectedFile={selectedFile}
|
||||
selectedFileTotal={
|
||||
selectedFile ? counts.file[selectedFile.id] ?? 0 : null
|
||||
}
|
||||
activeCount={activeCount}
|
||||
onReset={resetFilters}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
293
components/groups/GroupList.tsx
Normal file
293
components/groups/GroupList.tsx
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Bookmark,
|
||||
ClipboardList,
|
||||
FileImage,
|
||||
FileText,
|
||||
ListFilter,
|
||||
StickyNote,
|
||||
} from "lucide-react";
|
||||
|
||||
import { AnnotationTypeIcon } from "@/components/AnnotationTypeIcon";
|
||||
import {
|
||||
annotationNote,
|
||||
annotationPreviewText,
|
||||
annotationTypeLabel,
|
||||
} from "@/utils/annotation-display";
|
||||
import { formatThaiDate } from "@/utils/date-format";
|
||||
|
||||
import type { GroupFileSummary, GroupListItem } from "./GroupExplorer";
|
||||
|
||||
// คอลัมน์ content ของหน้ากลุ่ม (design 2026-09-24) — list เดียวรวมทั้งกลุ่ม
|
||||
// ไม่แยกไฟล์ (ตัวกรองฝั่งซ้ายทำหน้าที่แยกให้แล้ว) แต่ละแถวบอกไฟล์ที่มาที่
|
||||
// ต้น meta line — แถวเป็น passive surface จุดคลิกเดียวคือปุ่ม "View" ท้ายแถว
|
||||
// (deep link ?ann= / ?page= เดียวกับหน้าสรุป — คำขอผู้ใช้ 2026-09-24 ไม่กดทั้งแถว
|
||||
// ทำให้พ้นข้อจำกัด interactive ซ้อน <a> เดิม และกฎ coarse ของ listing-page ที่
|
||||
// display:inline-flex ชนะ utility flex ก็ไม่กระทบแถวอีกต่อไป — เหลือแค่ปุ่ม)
|
||||
|
||||
/** จุดคั่น "·" ของ meta line */
|
||||
function Sep() {
|
||||
return <span aria-hidden className="text-ink-faint">·</span>;
|
||||
}
|
||||
|
||||
/** meta line: ไฟล์ที่มา (truncate เฉพาะ span ของตัวเอง — ไม่งั้นชื่อยาวกินบรรทัด
|
||||
* จนตัด หน้า/ผู้บันทึก/วันที่ ทิ้ง) → หน้า → บริบทกลาง → ผู้บันทึก → วันที่
|
||||
* flex-wrap ให้วันที่เต็มรูปแบบไทยตกบรรทัดใหม่บนมือถืออย่างสง่า — บริบทกลาง
|
||||
* ("ใน: …" ของ sub bookmark — ชื่อ parent มาจากข้อความที่คลุม ยาวไม่มีขอบเขต)
|
||||
* กับ "โดย …" ต้อง min-w-0+truncate ด้วย: whitespace-nowrap เปล่า ๆ หดไม่ได้
|
||||
* ล้นการ์ดแล้วโดน overflow-hidden ตัดดื้อ ๆ ไม่มี ellipsis (review 2026-09-24) */
|
||||
function RowMeta({
|
||||
file,
|
||||
pageIndex,
|
||||
middle,
|
||||
user,
|
||||
date,
|
||||
}: {
|
||||
file: GroupFileSummary;
|
||||
pageIndex: number;
|
||||
/** ชื่อชนิดของ annotation หรือ "ใน: …" ของ sub bookmark */
|
||||
middle?: string;
|
||||
user: string;
|
||||
date: string;
|
||||
}) {
|
||||
return (
|
||||
<span className="mt-0.5 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs text-ink-soft">
|
||||
<span className="inline-flex min-w-0 max-w-[12rem] items-center gap-1 sm:max-w-[16rem]">
|
||||
{file.isImage ? (
|
||||
<FileImage aria-hidden className="h-3 w-3 shrink-0" />
|
||||
) : (
|
||||
<FileText aria-hidden className="h-3 w-3 shrink-0" />
|
||||
)}
|
||||
<span className="truncate" title={file.title}>
|
||||
{file.title}
|
||||
</span>
|
||||
</span>
|
||||
<Sep />
|
||||
<span className="whitespace-nowrap">หน้า {pageIndex + 1}</span>
|
||||
{middle && (
|
||||
<>
|
||||
<Sep />
|
||||
<span className="min-w-0 max-w-full truncate" title={middle}>
|
||||
{middle}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<Sep />
|
||||
<span className="min-w-0 max-w-full truncate">โดย {user}</span>
|
||||
<Sep />
|
||||
{/* formatThaiDate แปลงตาม timezone เครื่องที่ render — SSR/server กับ
|
||||
hydration/ผู้ใช้อาจต่างกัน ใส่ suppressHydrationWarning ตาม convention
|
||||
(เดียวกับ AnnotationSidebar/PDFViewer) */}
|
||||
<span className="whitespace-nowrap" suppressHydrationWarning>
|
||||
{date}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** โน้ตคำอธิบาย (N1) / โน้ตย่อยของ bookmark (N2) — 2 บรรทัดพอ */
|
||||
function RowNote({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="mt-0.5 flex items-start gap-1 text-xs leading-relaxed text-ink-soft">
|
||||
<StickyNote aria-hidden className="mt-0.5 h-3 w-3 shrink-0 text-warning-ink" />
|
||||
<span className="line-clamp-2 whitespace-pre-wrap break-words">{text}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** ปุ่ม secondary "เปิดเอกสารนี้" — ทางเข้าไฟล์ที่ยังไม่มีรายการใด ๆ
|
||||
* (หน้าเดิมมีปุ่ม Annotate ทุกการ์ด — เก็บไว้ตามคำตัดสินใจผู้ใช้ 2026-09-24) */
|
||||
function OpenFileButton({ file }: { file: GroupFileSummary }) {
|
||||
return (
|
||||
<Link
|
||||
href={`/annotate/${file.id}`}
|
||||
className="focus-ring inline-flex items-center gap-1.5 rounded-md border border-line bg-white px-3 py-1.5 text-sm font-medium text-ink transition hover:-translate-y-px hover:border-ink-faint"
|
||||
>
|
||||
{file.isImage ? (
|
||||
<FileImage aria-hidden className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<FileText aria-hidden className="h-3.5 w-3.5" />
|
||||
)}
|
||||
เปิดเอกสารนี้
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function GroupList({
|
||||
items,
|
||||
files,
|
||||
totalCount,
|
||||
selectedFile,
|
||||
selectedFileTotal,
|
||||
activeCount,
|
||||
onReset,
|
||||
}: {
|
||||
/** ผ่านตัวกรองแล้ว (เรียงลำดับจาก server คงเดิม) */
|
||||
items: GroupListItem[];
|
||||
files: GroupFileSummary[];
|
||||
totalCount: number;
|
||||
selectedFile: GroupFileSummary | null;
|
||||
/** จำนวนรายการทั้งหมดของไฟล์ที่เลือก (ก่อนกรอง) — null = ไม่ได้เลือกไฟล์เดี่ยว */
|
||||
selectedFileTotal: number | null;
|
||||
activeCount: number;
|
||||
onReset: () => void;
|
||||
}) {
|
||||
const fileById = useMemo(
|
||||
() => new Map(files.map((f) => [f.id, f] as const)),
|
||||
[files],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* จำนวนผลลัพธ์อยู่ต้นคอลัมน์ content (ที่เห็นผลของตัวกรอง) ไม่ใช่ในแผง */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5 px-1 pb-2.5">
|
||||
<p className="text-sm text-ink-soft">
|
||||
พบ{" "}
|
||||
<span className="font-semibold tabular-nums text-ink">
|
||||
{items.length}
|
||||
</span>{" "}
|
||||
รายการ
|
||||
</p>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{selectedFile && <OpenFileButton file={selectedFile} />}
|
||||
{activeCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="focus-ring rounded-md px-1.5 text-sm text-primary transition hover:underline"
|
||||
>
|
||||
ล้างตัวกรอง
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{items.length > 0 ? (
|
||||
<div className="overflow-hidden rounded-xl border border-line bg-white">
|
||||
<ul className="divide-y divide-line/70">
|
||||
{items.map((item) => {
|
||||
// items สร้างจาก files ชุดเดียวกันตอน build ที่ server — หาได้เสมอ
|
||||
const file = fileById.get(item.dto.fileId)!;
|
||||
// ปลายทาง + คำบอกตำแหน่งต่อชนิดรายการ (title tooltip + aria-label
|
||||
// ของปุ่ม View — แถวเดียวกันหลายสิบปุ่มชื่อเหมือนกัน ต้องรู้จักแยก)
|
||||
const isBm = item.kind === "bm";
|
||||
const where = isBm
|
||||
? `ไปที่หน้า ${item.pageIndex + 1}`
|
||||
: `ดู${annotationTypeLabel(item.dto)}ที่หน้า ${item.pageIndex + 1}`;
|
||||
const href = isBm
|
||||
? `/annotate/${item.dto.fileId}?page=${item.pageIndex + 1}`
|
||||
: `/annotate/${item.dto.fileId}?ann=${item.dto.id}`;
|
||||
return (
|
||||
// แถวเป็น passive surface — จุดคลิกเดียวคือปุ่ม "View" ท้ายแถว
|
||||
// (คำขอผู้ใช้ 2026-09-24: ไม่ต้องกดทั้งแถว) จึงไม่มี hover bg ที่แถว
|
||||
<li
|
||||
key={item.dto.id}
|
||||
className="flex items-start gap-2.5 px-3 py-3 sm:px-4"
|
||||
>
|
||||
{isBm ? (
|
||||
<>
|
||||
<Bookmark
|
||||
aria-hidden
|
||||
className="mt-1 h-4 w-4 shrink-0 text-warning-ink"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm font-medium leading-snug text-ink">
|
||||
{item.dto.title}
|
||||
</span>
|
||||
<RowMeta
|
||||
file={file}
|
||||
pageIndex={item.pageIndex}
|
||||
middle={
|
||||
item.parentTitle
|
||||
? `ใน: ${item.parentTitle}`
|
||||
: undefined
|
||||
}
|
||||
user={item.dto.userName || "ไม่ทราบชื่อ"}
|
||||
date={formatThaiDate(item.dto.createdAt, true)}
|
||||
/>
|
||||
{item.dto.note && <RowNote text={item.dto.note} />}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AnnotationTypeIcon
|
||||
type={item.dto.type}
|
||||
data={item.dto.data}
|
||||
className="mt-1 h-4 w-4 shrink-0 text-ink-soft"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block whitespace-pre-wrap break-words text-sm leading-snug text-ink">
|
||||
{annotationPreviewText(item.dto)}
|
||||
{item.dto.type === "COMMENT" && item.dto.resolved && (
|
||||
<span className="ml-1.5 whitespace-nowrap rounded border border-success/40 bg-success/10 px-1.5 py-0.5 text-xs font-medium text-success-ink">
|
||||
จัดการแล้ว
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<RowMeta
|
||||
file={file}
|
||||
pageIndex={item.pageIndex}
|
||||
middle={annotationTypeLabel(item.dto)}
|
||||
user={item.dto.userName}
|
||||
date={formatThaiDate(item.dto.createdAt, true)}
|
||||
/>
|
||||
{annotationNote(item.dto) && (
|
||||
<RowNote text={annotationNote(item.dto)} />
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href={href}
|
||||
title={`${where} ใน "${file.title}"`}
|
||||
aria-label={`View — ${where} ใน "${file.title}"`}
|
||||
className="focus-ring mt-0.5 inline-flex h-8 shrink-0 items-center rounded-md border border-line bg-white px-3 text-sm font-medium text-ink transition hover:-translate-y-px hover:border-primary hover:text-primary"
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : totalCount === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-line bg-white/70 p-10 text-center">
|
||||
<ClipboardList aria-hidden className="mx-auto h-8 w-8 text-ink-faint" />
|
||||
<p className="mt-3 text-sm font-medium text-ink-soft">
|
||||
ยังไม่มี Annotation หรือ Bookmark ในกลุ่มนี้
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-ink-soft">
|
||||
เปิดเอกสารในกลุ่มเพื่อมาร์กข้อความ วาดเส้น ใส่คอมเมนต์ หรือคั่นหน้า
|
||||
</p>
|
||||
</div>
|
||||
) : selectedFile && selectedFileTotal === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-line bg-white/70 p-10 text-center">
|
||||
<ClipboardList aria-hidden className="mx-auto h-8 w-8 text-ink-faint" />
|
||||
<p className="mt-3 text-sm font-medium text-ink-soft">
|
||||
ไฟล์นี้ยังไม่มี Annotation หรือ Bookmark
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<OpenFileButton file={selectedFile} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl border border-dashed border-line bg-white/70 p-10 text-center">
|
||||
<ListFilter aria-hidden className="mx-auto h-8 w-8 text-ink-faint" />
|
||||
<p className="mt-3 text-sm font-medium text-ink-soft">
|
||||
ไม่มีรายการตรงตามตัวกรอง
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="focus-ring mt-4 inline-flex rounded-md border border-line bg-white px-4 py-2 text-sm font-medium text-ink transition hover:-translate-y-px hover:border-ink-faint"
|
||||
>
|
||||
ล้างตัวกรองทั้งหมด
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue