feat(comments) : add newest and oldest sorting to admin inbox
This commit is contained in:
@@ -72,6 +72,26 @@ async function click(label: string) {
|
||||
function singleRequests() { return fetchMock.mock.calls.filter(([url]) => String(url) === `/api/comments/${selectedId}`); }
|
||||
|
||||
describe("notification conversations in the admin inbox", () => {
|
||||
it("defaults to newest and keeps oldest across pagination and refresh", async () => {
|
||||
await mount(page(inbox.items, "newest-page"), "");
|
||||
await click("รีเฟรช");
|
||||
expect(new URL(String(fetchMock.mock.calls.at(-1)![0]), "https://guide.sudloh.com").searchParams.get("sort")).toBe("newest");
|
||||
inbox = page([comment("oldest")], "oldest-page");
|
||||
await click("เก่าสุด");
|
||||
expect(rows()).toEqual(["comment-oldest"]);
|
||||
nextPage = page([comment("next-oldest")]);
|
||||
await click("ดูความคิดเห็นเพิ่มเติม");
|
||||
const pagination = new URL(String(fetchMock.mock.calls.at(-1)![0]), "https://guide.sudloh.com").searchParams;
|
||||
expect(pagination.get("sort")).toBe("oldest");
|
||||
expect(pagination.get("cursor")).toBe("oldest-page");
|
||||
await click("รีเฟรช");
|
||||
expect(new URL(String(fetchMock.mock.calls.at(-1)![0]), "https://guide.sudloh.com").searchParams.get("sort")).toBe("oldest");
|
||||
await click("ล่าสุด");
|
||||
const newest = new URL(String(fetchMock.mock.calls.at(-1)![0]), "https://guide.sudloh.com").searchParams;
|
||||
expect(newest.get("sort")).toBe("newest");
|
||||
expect(newest.has("cursor")).toBe(false);
|
||||
});
|
||||
|
||||
it("moves a loaded conversation to the first normal row, expands it, and avoids a second lookup", async () => {
|
||||
await mount(page([...inbox.items, selected]));
|
||||
expect(rows()).toEqual([`comment-${selectedId}`, "comment-newer"]);
|
||||
@@ -107,18 +127,19 @@ describe("notification conversations in the admin inbox", () => {
|
||||
expect(rows()).toEqual([`comment-${selectedId}`, "comment-newer"]);
|
||||
});
|
||||
|
||||
it.each(["visibility", "answered", "target"])("clears the notification selection when the %s filter changes", async (filter) => {
|
||||
it.each(["visibility", "answered", "target", "sort"])("clears the notification selection when the %s filter changes", async (filter) => {
|
||||
await mount();
|
||||
inbox = page([comment("filtered")]);
|
||||
if (filter === "target") {
|
||||
const clear = container.querySelector<HTMLButtonElement>('[data-slot="combobox-clear"]');
|
||||
expect(clear).not.toBeNull();
|
||||
await act(async () => clear!.click());
|
||||
} else await click(filter === "visibility" ? "ซ่อนอยู่" : "ยังไม่ได้ตอบ");
|
||||
} else await click(filter === "visibility" ? "ซ่อนอยู่" : filter === "sort" ? "เก่าสุด" : "ยังไม่ได้ตอบ");
|
||||
expect(rows()).toEqual(["comment-filtered"]);
|
||||
expect(singleRequests()).toHaveLength(1);
|
||||
const query = new URL(String(fetchMock.mock.calls.at(-1)![0]), "https://guide.sudloh.com").searchParams;
|
||||
if (filter === "target") expect(query.has("target")).toBe(false);
|
||||
else if (filter === "sort") expect(query.get("sort")).toBe("oldest");
|
||||
else expect(query.get(filter === "visibility" ? "status" : "unanswered")).toBe(filter === "visibility" ? "hidden" : "true");
|
||||
});
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
|
||||
const [target, setTarget] = useState<TargetOption | null>(targets.find((item) => item.value === initialTarget) ?? null);
|
||||
const [status, setStatus] = useState("all");
|
||||
const [answered, setAnswered] = useState("all");
|
||||
const [sort, setSort] = useState("newest");
|
||||
const [selectedId, setSelectedId] = useState(initialTarget ? initialComment : undefined);
|
||||
const [selectedResult, setSelectedResult] = useState<{ item: CommentItem | null; error: string }>({ item: null, error: "" });
|
||||
const query = new URLSearchParams({ view: "inbox", grouped: "true", status, unanswered: String(answered === "unanswered") });
|
||||
const query = new URLSearchParams({ view: "inbox", grouped: "true", status, sort, unanswered: String(answered === "unanswered") });
|
||||
if (target) query.set("target", target.value);
|
||||
const feed = useCommentFeed(`/api/admin/comments?${query}`, initial);
|
||||
const loadedSelection = feed.page.items.find((item) => item.id === selectedId && item.target === initialTarget && !item.rootId);
|
||||
@@ -58,7 +59,7 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
|
||||
const markRead = useMarkCommentsRead();
|
||||
useEffect(() => { void markRead(target?.value ?? "all"); }, [markRead, target, feed.revision]);
|
||||
return <div className="flex min-w-0 flex-col gap-6 max-md:[&_button]:min-h-11 max-md:[&_button]:min-w-11">
|
||||
<FieldGroup className="lg:grid lg:grid-cols-[repeat(3,minmax(0,1fr))_auto] lg:items-end">
|
||||
<FieldGroup className="lg:grid lg:grid-cols-2 xl:grid-cols-[repeat(4,minmax(0,1fr))_auto] lg:items-end">
|
||||
<Field><FieldLabel>{"ไกด์หรือรอบ Stygian"}</FieldLabel>
|
||||
<FuzzyCombobox items={targets} value={target} onValueChange={(value) => { if (value?.value !== target?.value) setSelectedId(undefined); setTarget(value); }} itemToStringLabel={(item) => item.label} itemToStringValue={(item) => item.value}
|
||||
searchNames={(item) => [item.label]} autoHighlight>
|
||||
@@ -72,6 +73,9 @@ export function AdminCommentInbox({ initial, targets, initialTarget, initialComm
|
||||
<Field><FieldLabel>{"การตอบกลับจากผู้ดูแล"}</FieldLabel><ToggleGroup className="max-w-full flex-wrap" value={[answered]} onValueChange={(values) => { if (values[0] && values[0] !== answered) { setSelectedId(undefined); setAnswered(values[0]); } }} variant="outline" aria-label={"กรองการตอบกลับจากผู้ดูแล"}>
|
||||
<ToggleGroupItem value="all">{"ทุกความคิดเห็น"}</ToggleGroupItem><ToggleGroupItem value="unanswered">{"ยังไม่ได้ตอบ"}</ToggleGroupItem>
|
||||
</ToggleGroup></Field>
|
||||
<Field><FieldLabel>{"เรียงตาม"}</FieldLabel><ToggleGroup className="max-w-full flex-wrap" value={[sort]} onValueChange={(values) => { if (values[0] && values[0] !== sort) { setSelectedId(undefined); setSort(values[0]); } }} variant="outline" aria-label={"เรียงความคิดเห็น"}>
|
||||
<ToggleGroupItem value="newest">{"ล่าสุด"}</ToggleGroupItem><ToggleGroupItem value="oldest">{"เก่าสุด"}</ToggleGroupItem>
|
||||
</ToggleGroup></Field>
|
||||
<Button variant="outline" className="self-end" disabled={feed.busy} onClick={() => void feed.refresh({ silent: false, forceRevision: true })}><RefreshCwIcon data-icon="inline-start" />{"รีเฟรช"}</Button>
|
||||
</FieldGroup>
|
||||
{feed.error && <Alert variant="destructive"><AlertDescription>{feed.error}</AlertDescription></Alert>}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AdminCommentRowsSkeleton({ count = 3 }: { count?: number }) {
|
||||
export function AdminCommentInboxSkeleton() {
|
||||
return <div className="flex min-w-0 flex-col gap-6">
|
||||
<div className="flex flex-col gap-2"><Skeleton className="h-9 w-48 max-w-full" /><Skeleton className="h-5 w-72 max-w-full" /></div>
|
||||
<div className="flex flex-col gap-6 lg:grid lg:grid-cols-[repeat(3,minmax(0,1fr))_auto] lg:items-end">{Array.from({ length: 3 }, (_, index) =>
|
||||
<div className="flex flex-col gap-6 lg:grid lg:grid-cols-2 xl:grid-cols-[repeat(4,minmax(0,1fr))_auto] lg:items-end">{Array.from({ length: 4 }, (_, index) =>
|
||||
<div key={index} className="flex min-w-0 flex-col gap-2"><Skeleton className="h-4 w-36 max-w-full" /><Skeleton className="h-11 w-full md:h-8" /></div>)}
|
||||
<Skeleton className="h-11 w-24 self-end md:h-8" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user