mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 09:56:36 +00:00
🚧 wip: pagination and search work
This commit is contained in:
@@ -222,15 +222,24 @@ export async function listSites(
|
||||
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||
const baseQuery = querySites(orgId, accessibleSiteIds, query);
|
||||
|
||||
let conditions = and(
|
||||
inArray(sites.siteId, accessibleSiteIds),
|
||||
eq(sites.orgId, orgId)
|
||||
);
|
||||
if (query) {
|
||||
conditions = and(
|
||||
conditions,
|
||||
or(
|
||||
ilike(sites.name, "%" + query + "%"),
|
||||
ilike(sites.niceId, "%" + query + "%")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const countQuery = db
|
||||
.select({ count: count() })
|
||||
.from(sites)
|
||||
.where(
|
||||
and(
|
||||
inArray(sites.siteId, accessibleSiteIds),
|
||||
eq(sites.orgId, orgId)
|
||||
)
|
||||
);
|
||||
.where(conditions);
|
||||
|
||||
const sitesList = await baseQuery
|
||||
.limit(pageSize)
|
||||
|
||||
@@ -81,10 +81,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
<SitesTable
|
||||
sites={siteRows}
|
||||
orgId={params.orgId}
|
||||
rowCount={pagination.total}
|
||||
pagination={{
|
||||
pageCount: Math.ceil(
|
||||
pagination.total / pagination.pageSize
|
||||
),
|
||||
pageIndex: pagination.page - 1,
|
||||
pageSize: pagination.pageSize
|
||||
}}
|
||||
|
||||
@@ -4,11 +4,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
DataTable,
|
||||
ExtendedColumnDef,
|
||||
type DataTablePaginationState
|
||||
} from "@app/components/ui/data-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -33,6 +28,10 @@ import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import {
|
||||
ManualDataTable,
|
||||
type ExtendedColumnDef
|
||||
} from "./ui/manual-data-table";
|
||||
|
||||
export type SiteRow = {
|
||||
id: number;
|
||||
@@ -53,14 +52,16 @@ export type SiteRow = {
|
||||
|
||||
type SitesTableProps = {
|
||||
sites: SiteRow[];
|
||||
pagination: DataTablePaginationState;
|
||||
pagination: PaginationState;
|
||||
orgId: string;
|
||||
rowCount: number;
|
||||
};
|
||||
|
||||
export default function SitesTable({
|
||||
sites,
|
||||
orgId,
|
||||
pagination
|
||||
pagination,
|
||||
rowCount
|
||||
}: SitesTableProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -427,14 +428,18 @@ export default function SitesTable({
|
||||
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
||||
};
|
||||
|
||||
// const = useDebouncedCallback()
|
||||
|
||||
const handleSearchChange = useDebouncedCallback((query: string) => {
|
||||
const sp = new URLSearchParams(searchParams);
|
||||
sp.set("query", query);
|
||||
sp.delete("page");
|
||||
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
||||
}, 300);
|
||||
|
||||
console.log({
|
||||
pagination,
|
||||
rowCount
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedSite && (
|
||||
@@ -459,13 +464,11 @@ export default function SitesTable({
|
||||
/>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
<ManualDataTable
|
||||
columns={columns}
|
||||
data={sites}
|
||||
persistPageSize="sites-table"
|
||||
title={t("sites")}
|
||||
rows={sites}
|
||||
tableId="sites-table"
|
||||
searchPlaceholder={t("searchSitesProgress")}
|
||||
manualFiltering
|
||||
pagination={pagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
onAdd={() => router.push(`/${orgId}/settings/sites/create`)}
|
||||
@@ -474,10 +477,7 @@ export default function SitesTable({
|
||||
addButtonText={t("siteAdd")}
|
||||
onRefresh={() => startTransition(refreshData)}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
rowCount={rowCount}
|
||||
columnVisibility={{
|
||||
niceId: false,
|
||||
nice: false,
|
||||
|
||||
@@ -13,7 +13,10 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { formatFingerprintInfo, formatPlatform } from "@app/lib/formatDeviceFingerprint";
|
||||
import {
|
||||
formatFingerprintInfo,
|
||||
formatPlatform
|
||||
} from "@app/lib/formatDeviceFingerprint";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
@@ -188,9 +191,13 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
try {
|
||||
// Fetch approvalId for this client using clientId query parameter
|
||||
const approvalsRes = await api.get<{
|
||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||
|
||||
data: {
|
||||
approvals: Array<{ approvalId: number; clientId: number }>;
|
||||
};
|
||||
}>(
|
||||
`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`
|
||||
);
|
||||
|
||||
const approval = approvalsRes.data.data.approvals[0];
|
||||
|
||||
if (!approval) {
|
||||
@@ -202,9 +209,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.put(`/org/${clientRow.orgId}/approvals/${approval.approvalId}`, {
|
||||
decision: "approved"
|
||||
});
|
||||
await api.put(
|
||||
`/org/${clientRow.orgId}/approvals/${approval.approvalId}`,
|
||||
{
|
||||
decision: "approved"
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("accessApprovalUpdated"),
|
||||
@@ -230,9 +240,13 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
try {
|
||||
// Fetch approvalId for this client using clientId query parameter
|
||||
const approvalsRes = await api.get<{
|
||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||
|
||||
data: {
|
||||
approvals: Array<{ approvalId: number; clientId: number }>;
|
||||
};
|
||||
}>(
|
||||
`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`
|
||||
);
|
||||
|
||||
const approval = approvalsRes.data.data.approvals[0];
|
||||
|
||||
if (!approval) {
|
||||
@@ -244,9 +258,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.put(`/org/${clientRow.orgId}/approvals/${approval.approvalId}`, {
|
||||
decision: "denied"
|
||||
});
|
||||
await api.put(
|
||||
`/org/${clientRow.orgId}/approvals/${approval.approvalId}`,
|
||||
{
|
||||
decision: "denied"
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("accessApprovalUpdated"),
|
||||
@@ -398,7 +415,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
friendlyName: t("connectivity"),
|
||||
friendlyName: t("online"),
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@@ -548,20 +565,25 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{clientRow.approvalState === "pending" && build !== "oss" && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => approveDevice(clientRow)}
|
||||
>
|
||||
<span>{t("approve")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => denyDevice(clientRow)}
|
||||
>
|
||||
<span>{t("deny")}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{clientRow.approvalState === "pending" &&
|
||||
build !== "oss" && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
approveDevice(clientRow)
|
||||
}
|
||||
>
|
||||
<span>{t("approve")}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
denyDevice(clientRow)
|
||||
}
|
||||
>
|
||||
<span>{t("deny")}</span>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (clientRow.archived) {
|
||||
@@ -653,7 +675,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
];
|
||||
|
||||
if (build === "oss") {
|
||||
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
|
||||
return allOptions.filter(
|
||||
(option) =>
|
||||
option.value !== "pending" && option.value !== "denied"
|
||||
);
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
@@ -717,7 +742,11 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
const rowArchived = row.archived;
|
||||
const rowBlocked = row.blocked;
|
||||
const approvalState = row.approvalState;
|
||||
const isActive = !rowArchived && !rowBlocked && approvalState !== "pending" && approvalState !== "denied";
|
||||
const isActive =
|
||||
!rowArchived &&
|
||||
!rowBlocked &&
|
||||
approvalState !== "pending" &&
|
||||
approvalState !== "denied";
|
||||
|
||||
if (selectedValues.includes("active") && isActive)
|
||||
return true;
|
||||
|
||||
@@ -320,6 +320,11 @@ export function DataTable<TData, TValue>({
|
||||
return result;
|
||||
}, [data, tabs, activeTab, filters, activeFilters]);
|
||||
|
||||
console.log({
|
||||
pagination,
|
||||
paginationState
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
columns,
|
||||
@@ -339,13 +344,10 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
: setPagination,
|
||||
manualFiltering,
|
||||
manualPagination: !!paginationState,
|
||||
manualPagination: Boolean(paginationState),
|
||||
pageCount: paginationState?.pageCount,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: pageSize,
|
||||
pageIndex: 0
|
||||
},
|
||||
pagination,
|
||||
columnVisibility: initialColumnVisibility
|
||||
},
|
||||
state: {
|
||||
|
||||
567
src/components/ui/manual-data-table.tsx
Normal file
567
src/components/ui/manual-data-table.tsx
Normal file
@@ -0,0 +1,567 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
PaginationState,
|
||||
useReactTable
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from "@/components/ui/table";
|
||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@app/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useStoredColumnVisibility } from "@app/hooks/useStoredColumnVisibility";
|
||||
|
||||
import { Columns, Filter, Plus, RefreshCw, Search } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
||||
export type ExtendedColumnDef<TData, TValue = unknown> = ColumnDef<
|
||||
TData,
|
||||
TValue
|
||||
> & {
|
||||
friendlyName?: string;
|
||||
};
|
||||
|
||||
type FilterOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string | number | boolean;
|
||||
};
|
||||
|
||||
type DataTableFilter = {
|
||||
id: string;
|
||||
label: string;
|
||||
options: FilterOption[];
|
||||
multiSelect?: boolean;
|
||||
filterFn: (
|
||||
row: any,
|
||||
selectedValues: (string | number | boolean)[]
|
||||
) => boolean;
|
||||
defaultValues?: (string | number | boolean)[];
|
||||
displayMode?: "label" | "calculated"; // How to display the filter button text
|
||||
};
|
||||
|
||||
export type DataTablePaginationUpdateFn = (newPage: PaginationState) => void;
|
||||
|
||||
type ManualDataTableProps<TData, TValue> = {
|
||||
columns: ExtendedColumnDef<TData, TValue>[];
|
||||
rows: TData[];
|
||||
tableId: string;
|
||||
addButtonText?: string;
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
filters?: DataTableFilter[];
|
||||
filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter)
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
enableColumnVisibility?: boolean;
|
||||
onSearch: (input: string) => void;
|
||||
searchQuery?: string;
|
||||
onPaginationChange: DataTablePaginationUpdateFn;
|
||||
stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column
|
||||
stickyRightColumn?: string; // Column ID or accessorKey for right sticky column (typically "actions")
|
||||
rowCount: number;
|
||||
pagination: PaginationState;
|
||||
};
|
||||
|
||||
export function ManualDataTable<TData, TValue>({
|
||||
columns,
|
||||
rows,
|
||||
addButtonText,
|
||||
onAdd,
|
||||
onRefresh,
|
||||
isRefreshing,
|
||||
searchPlaceholder = "Search...",
|
||||
filters,
|
||||
filterDisplayMode = "label",
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
enableColumnVisibility = false,
|
||||
tableId,
|
||||
pagination,
|
||||
stickyLeftColumn,
|
||||
onSearch,
|
||||
searchQuery,
|
||||
onPaginationChange,
|
||||
stickyRightColumn,
|
||||
rowCount
|
||||
}: ManualDataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useStoredColumnVisibility(
|
||||
tableId,
|
||||
defaultColumnVisibility
|
||||
);
|
||||
|
||||
// TODO: filters
|
||||
const [activeFilters, setActiveFilters] = useState<
|
||||
Record<string, (string | number | boolean)[]>
|
||||
>(() => {
|
||||
const initial: Record<string, (string | number | boolean)[]> = {};
|
||||
filters?.forEach((filter) => {
|
||||
initial[filter.id] = filter.defaultValues || [];
|
||||
});
|
||||
return initial;
|
||||
});
|
||||
|
||||
console.log({
|
||||
pagination
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
// getFilteredRowModel: getFilteredRowModel(),
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: (state) => {
|
||||
const newState =
|
||||
typeof state === "function" ? state(pagination) : state;
|
||||
onPaginationChange(newState);
|
||||
},
|
||||
manualFiltering: true,
|
||||
manualPagination: true,
|
||||
// pageCount: pagination.pageCount,
|
||||
rowCount,
|
||||
state: {
|
||||
columnFilters,
|
||||
columnVisibility,
|
||||
pagination
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate display text for a filter based on selected values
|
||||
const getFilterDisplayText = (filter: DataTableFilter): string => {
|
||||
const selectedValues = activeFilters[filter.id] || [];
|
||||
|
||||
if (selectedValues.length === 0) {
|
||||
return filter.label;
|
||||
}
|
||||
|
||||
const selectedOptions = filter.options.filter((option) =>
|
||||
selectedValues.includes(option.value)
|
||||
);
|
||||
|
||||
if (selectedOptions.length === 0) {
|
||||
return filter.label;
|
||||
}
|
||||
|
||||
if (selectedOptions.length === 1) {
|
||||
return selectedOptions[0].label;
|
||||
}
|
||||
|
||||
// Multiple selections: always join with "and"
|
||||
return selectedOptions.map((opt) => opt.label).join(" and ");
|
||||
};
|
||||
|
||||
// Helper function to check if a column should be sticky
|
||||
const isStickyColumn = (
|
||||
columnId: string | undefined,
|
||||
accessorKey: string | undefined,
|
||||
position: "left" | "right"
|
||||
): boolean => {
|
||||
if (position === "left" && stickyLeftColumn) {
|
||||
return (
|
||||
columnId === stickyLeftColumn ||
|
||||
accessorKey === stickyLeftColumn
|
||||
);
|
||||
}
|
||||
if (position === "right" && stickyRightColumn) {
|
||||
return (
|
||||
columnId === stickyRightColumn ||
|
||||
accessorKey === stickyRightColumn
|
||||
);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get sticky column classes
|
||||
const getStickyClasses = (
|
||||
columnId: string | undefined,
|
||||
accessorKey: string | undefined
|
||||
): string => {
|
||||
if (isStickyColumn(columnId, accessorKey, "left")) {
|
||||
return "md:sticky md:left-0 z-10 bg-card [mask-image:linear-gradient(to_left,transparent_0%,black_20px)]";
|
||||
}
|
||||
if (isStickyColumn(columnId, accessorKey, "right")) {
|
||||
return "sticky right-0 z-10 w-auto min-w-fit bg-card [mask-image:linear-gradient(to_right,transparent_0%,black_20px)]";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-12xl">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
|
||||
<div className="flex flex-row space-y-3 w-full sm:mr-2 gap-2">
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) =>
|
||||
onSearch(e.currentTarget.value)
|
||||
}
|
||||
className="w-full pl-8"
|
||||
/>
|
||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
</div>
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
{filters.map((filter) => {
|
||||
const selectedValues =
|
||||
activeFilters[filter.id] || [];
|
||||
const hasActiveFilters =
|
||||
selectedValues.length > 0;
|
||||
const displayMode =
|
||||
filter.displayMode || filterDisplayMode;
|
||||
const displayText =
|
||||
displayMode === "calculated"
|
||||
? getFilterDisplayText(filter)
|
||||
: filter.label;
|
||||
|
||||
return (
|
||||
<DropdownMenu key={filter.id}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size="sm"
|
||||
className="h-9"
|
||||
>
|
||||
<Filter className="h-4 w-4 mr-2" />
|
||||
{displayText}
|
||||
{displayMode === "label" &&
|
||||
hasActiveFilters && (
|
||||
<span className="ml-2 bg-muted text-foreground rounded-full px-2 py-0.5 text-xs">
|
||||
{
|
||||
selectedValues.length
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-48"
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{filter.label}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{filter.options.map(
|
||||
(option) => {
|
||||
const isChecked =
|
||||
selectedValues.includes(
|
||||
option.value
|
||||
);
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={option.id}
|
||||
checked={
|
||||
isChecked
|
||||
}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) => {
|
||||
// handleFilterChange(
|
||||
// filter.id,
|
||||
// option.value,
|
||||
// checked
|
||||
// )
|
||||
}}
|
||||
onSelect={(e) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:justify-end">
|
||||
{onRefresh && (
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span className="hidden sm:inline">
|
||||
{t("refresh")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{onAdd && addButtonText && (
|
||||
<div>
|
||||
<Button onClick={onAdd}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const columnId = header.column.id;
|
||||
const accessorKey = (
|
||||
header.column.columnDef as any
|
||||
).accessorKey as string | undefined;
|
||||
const stickyClasses =
|
||||
getStickyClasses(
|
||||
columnId,
|
||||
accessorKey
|
||||
);
|
||||
const isRightSticky =
|
||||
isStickyColumn(
|
||||
columnId,
|
||||
accessorKey,
|
||||
"right"
|
||||
);
|
||||
const hasHideableColumns =
|
||||
enableColumnVisibility &&
|
||||
table
|
||||
.getAllColumns()
|
||||
.some((col) =>
|
||||
col.getCanHide()
|
||||
);
|
||||
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={`whitespace-nowrap ${stickyClasses}`}
|
||||
>
|
||||
{header.isPlaceholder ? null : isRightSticky &&
|
||||
hasHideableColumns ? (
|
||||
<div className="flex flex-col items-end pr-3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 mb-1"
|
||||
>
|
||||
<Columns className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
{t(
|
||||
"columns"
|
||||
) ||
|
||||
"Columns"}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-48"
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{t(
|
||||
"toggleColumns"
|
||||
) ||
|
||||
"Toggle columns"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(
|
||||
column
|
||||
) =>
|
||||
column.getCanHide()
|
||||
)
|
||||
.map(
|
||||
(
|
||||
column
|
||||
) => {
|
||||
const columnDef =
|
||||
column.columnDef as any;
|
||||
const friendlyName =
|
||||
columnDef.friendlyName;
|
||||
const displayName =
|
||||
friendlyName ||
|
||||
(typeof columnDef.header ===
|
||||
"string"
|
||||
? columnDef.header
|
||||
: column.id);
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={
|
||||
column.id
|
||||
}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(
|
||||
value
|
||||
) =>
|
||||
column.toggleVisibility(
|
||||
!!value
|
||||
)
|
||||
}
|
||||
onSelect={(
|
||||
e
|
||||
) =>
|
||||
e.preventDefault()
|
||||
}
|
||||
>
|
||||
{
|
||||
displayName
|
||||
}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="h-0 opacity-0 pointer-events-none overflow-hidden">
|
||||
{flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={
|
||||
row.getIsSelected() &&
|
||||
"selected"
|
||||
}
|
||||
>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const columnId =
|
||||
cell.column.id;
|
||||
const accessorKey = (
|
||||
cell.column
|
||||
.columnDef as any
|
||||
).accessorKey as
|
||||
| string
|
||||
| undefined;
|
||||
const stickyClasses =
|
||||
getStickyClasses(
|
||||
columnId,
|
||||
accessorKey
|
||||
);
|
||||
const isRightSticky =
|
||||
isStickyColumn(
|
||||
columnId,
|
||||
accessorKey,
|
||||
"right"
|
||||
);
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={`whitespace-nowrap ${stickyClasses} ${isRightSticky ? "text-right" : ""}`}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{rowCount > 0 && (
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
totalCount={rowCount}
|
||||
onPageSizeChange={(pageSize) =>
|
||||
onPaginationChange({
|
||||
...pagination,
|
||||
pageSize
|
||||
})
|
||||
}
|
||||
onPageChange={(pageIndex) => {
|
||||
onPaginationChange({
|
||||
...pagination,
|
||||
pageIndex
|
||||
});
|
||||
}}
|
||||
isServerPagination
|
||||
pageSize={pagination.pageSize}
|
||||
pageIndex={pagination.pageIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user