feat(hk): persist named table views for staff accounts
This commit is contained in:
1 parent
74223984dc
commit
a647b5451f
7 files changed
+480
No files matched your search
@@ -16,6 +16,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useRef, useState, useTransition } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { TableSavedViews } from "@/components/admin/table-saved-views";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -327,6 +328,29 @@ export function DataTable<T extends { id: number | string }>({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TableSavedViews
|
||||
path={pathname}
|
||||
query={searchParams.toString()}
|
||||
hidden={[...hiddenColumns]}
|
||||
onApply={(view) => {
|
||||
setHiddenColumns(
|
||||
() =>
|
||||
new Set(
|
||||
view.state.hidden.filter((key) =>
|
||||
columns.some((column) => column.key === key),
|
||||
),
|
||||
),
|
||||
);
|
||||
const params = new URLSearchParams(view.state.params);
|
||||
params.set("page", "1");
|
||||
startTransition(() =>
|
||||
router.replace(`${pathname}?${params.toString()}`, {
|
||||
scroll: false,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Column Filters */}
|
||||
{filterColumns.length > 0 && (
|
||||
<Popover>
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"use client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useId, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
deleteTableView,
|
||||
listTableViews,
|
||||
saveTableView,
|
||||
} from "@/actions/admin-table-views";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
captureTableView,
|
||||
type NamedTableView,
|
||||
} from "@/lib/admin/table-view-state";
|
||||
|
||||
export function TableSavedViews({
|
||||
path,
|
||||
query,
|
||||
hidden,
|
||||
onApply,
|
||||
}: {
|
||||
path: string;
|
||||
query: string;
|
||||
hidden: string[];
|
||||
onApply: (view: NamedTableView) => void;
|
||||
}) {
|
||||
const inputId = useId();
|
||||
const t = useTranslations("pages.admin.table.views");
|
||||
const [views, setViews] = useState<NamedTableView[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
async function load() {
|
||||
setBusy(true);
|
||||
setError(false);
|
||||
setLoaded(false);
|
||||
setViews([]);
|
||||
try {
|
||||
const result = await listTableViews({ path });
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
setViews((result.data as { views: NamedTableView[] }).views);
|
||||
setLoaded(true);
|
||||
} catch {
|
||||
setError(true);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (busy || !name.trim()) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await saveTableView({
|
||||
path,
|
||||
name: name.trim(),
|
||||
state: captureTableView(query, hidden),
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
toast.success(t("saved"));
|
||||
setName("");
|
||||
await load();
|
||||
} catch {
|
||||
toast.error(t("error"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function remove(view: NamedTableView) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const result = await deleteTableView({ path, name: view.name });
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
await load();
|
||||
} catch {
|
||||
toast.error(t("error"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
onOpenChange={(open) => {
|
||||
if (open) void load();
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
{t("title")}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-80 max-w-[calc(100vw-2rem)] space-y-3"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">{t("hint")}</p>
|
||||
{error && (
|
||||
<p role="alert" className="text-sm">
|
||||
{t("error")}{" "}
|
||||
<Button variant="link" size="sm" onClick={() => void load()}>
|
||||
{t("retry")}
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
{busy && (
|
||||
<p role="status" className="text-sm">
|
||||
{t("loading")}
|
||||
</p>
|
||||
)}
|
||||
{loaded && views.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground">{t("empty")}</p>
|
||||
)}
|
||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||
{views.map((view) => (
|
||||
<div key={view.name} className="flex gap-2 items-center">
|
||||
<Button
|
||||
disabled={busy}
|
||||
variant="secondary"
|
||||
className="flex-1 min-w-0 justify-start"
|
||||
onClick={() => onApply(view)}
|
||||
>
|
||||
<span className="truncate">{view.name}</span>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busy}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label={t("deleteNamed", { name: view.name })}
|
||||
onClick={() => void remove(view)}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<label htmlFor={inputId} className="block text-sm">
|
||||
{t("name")}
|
||||
<Input
|
||||
id={inputId}
|
||||
value={name}
|
||||
maxLength={60}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
disabled={busy || !loaded || !name.trim()}
|
||||
onClick={() => void save()}
|
||||
className="w-full"
|
||||
>
|
||||
{views.some(
|
||||
(view) =>
|
||||
view.name.toLocaleLowerCase() === name.trim().toLocaleLowerCase(),
|
||||
)
|
||||
? t("replace")
|
||||
: t("save")}
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user