mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 01:06:39 +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 accessibleSiteIds = accessibleSites.map((site) => site.siteId);
|
||||||
const baseQuery = querySites(orgId, accessibleSiteIds, query);
|
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
|
const countQuery = db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(conditions);
|
||||||
and(
|
|
||||||
inArray(sites.siteId, accessibleSiteIds),
|
|
||||||
eq(sites.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const sitesList = await baseQuery
|
const sitesList = await baseQuery
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
|
|||||||
@@ -81,10 +81,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
|||||||
<SitesTable
|
<SitesTable
|
||||||
sites={siteRows}
|
sites={siteRows}
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
|
rowCount={pagination.total}
|
||||||
pagination={{
|
pagination={{
|
||||||
pageCount: Math.ceil(
|
|
||||||
pagination.total / pagination.pageSize
|
|
||||||
),
|
|
||||||
pageIndex: pagination.page - 1,
|
pageIndex: pagination.page - 1,
|
||||||
pageSize: pagination.pageSize
|
pageSize: pagination.pageSize
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|||||||
|
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import {
|
|
||||||
DataTable,
|
|
||||||
ExtendedColumnDef,
|
|
||||||
type DataTablePaginationState
|
|
||||||
} from "@app/components/ui/data-table";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -33,6 +28,10 @@ import Link from "next/link";
|
|||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
import {
|
||||||
|
ManualDataTable,
|
||||||
|
type ExtendedColumnDef
|
||||||
|
} from "./ui/manual-data-table";
|
||||||
|
|
||||||
export type SiteRow = {
|
export type SiteRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -53,14 +52,16 @@ export type SiteRow = {
|
|||||||
|
|
||||||
type SitesTableProps = {
|
type SitesTableProps = {
|
||||||
sites: SiteRow[];
|
sites: SiteRow[];
|
||||||
pagination: DataTablePaginationState;
|
pagination: PaginationState;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
rowCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SitesTable({
|
export default function SitesTable({
|
||||||
sites,
|
sites,
|
||||||
orgId,
|
orgId,
|
||||||
pagination
|
pagination,
|
||||||
|
rowCount
|
||||||
}: SitesTableProps) {
|
}: SitesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -427,14 +428,18 @@ export default function SitesTable({
|
|||||||
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
// const = useDebouncedCallback()
|
|
||||||
|
|
||||||
const handleSearchChange = useDebouncedCallback((query: string) => {
|
const handleSearchChange = useDebouncedCallback((query: string) => {
|
||||||
const sp = new URLSearchParams(searchParams);
|
const sp = new URLSearchParams(searchParams);
|
||||||
sp.set("query", query);
|
sp.set("query", query);
|
||||||
|
sp.delete("page");
|
||||||
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
startTransition(() => router.push(`${pathname}?${sp.toString()}`));
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
pagination,
|
||||||
|
rowCount
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedSite && (
|
{selectedSite && (
|
||||||
@@ -459,13 +464,11 @@ export default function SitesTable({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DataTable
|
<ManualDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={sites}
|
rows={sites}
|
||||||
persistPageSize="sites-table"
|
tableId="sites-table"
|
||||||
title={t("sites")}
|
|
||||||
searchPlaceholder={t("searchSitesProgress")}
|
searchPlaceholder={t("searchSitesProgress")}
|
||||||
manualFiltering
|
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPaginationChange={handlePaginationChange}
|
onPaginationChange={handlePaginationChange}
|
||||||
onAdd={() => router.push(`/${orgId}/settings/sites/create`)}
|
onAdd={() => router.push(`/${orgId}/settings/sites/create`)}
|
||||||
@@ -474,10 +477,7 @@ export default function SitesTable({
|
|||||||
addButtonText={t("siteAdd")}
|
addButtonText={t("siteAdd")}
|
||||||
onRefresh={() => startTransition(refreshData)}
|
onRefresh={() => startTransition(refreshData)}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
defaultSort={{
|
rowCount={rowCount}
|
||||||
id: "name",
|
|
||||||
desc: false
|
|
||||||
}}
|
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
niceId: false,
|
niceId: false,
|
||||||
nice: false,
|
nice: false,
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||||
import { formatFingerprintInfo, formatPlatform } from "@app/lib/formatDeviceFingerprint";
|
import {
|
||||||
|
formatFingerprintInfo,
|
||||||
|
formatPlatform
|
||||||
|
} from "@app/lib/formatDeviceFingerprint";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
@@ -188,9 +191,13 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
try {
|
try {
|
||||||
// Fetch approvalId for this client using clientId query parameter
|
// Fetch approvalId for this client using clientId query parameter
|
||||||
const approvalsRes = await api.get<{
|
const approvalsRes = await api.get<{
|
||||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
data: {
|
||||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
approvals: Array<{ approvalId: number; clientId: number }>;
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`
|
||||||
|
);
|
||||||
|
|
||||||
const approval = approvalsRes.data.data.approvals[0];
|
const approval = approvalsRes.data.data.approvals[0];
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
@@ -202,9 +209,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.put(`/org/${clientRow.orgId}/approvals/${approval.approvalId}`, {
|
await api.put(
|
||||||
decision: "approved"
|
`/org/${clientRow.orgId}/approvals/${approval.approvalId}`,
|
||||||
});
|
{
|
||||||
|
decision: "approved"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("accessApprovalUpdated"),
|
title: t("accessApprovalUpdated"),
|
||||||
@@ -230,9 +240,13 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
try {
|
try {
|
||||||
// Fetch approvalId for this client using clientId query parameter
|
// Fetch approvalId for this client using clientId query parameter
|
||||||
const approvalsRes = await api.get<{
|
const approvalsRes = await api.get<{
|
||||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
data: {
|
||||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
approvals: Array<{ approvalId: number; clientId: number }>;
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`
|
||||||
|
);
|
||||||
|
|
||||||
const approval = approvalsRes.data.data.approvals[0];
|
const approval = approvalsRes.data.data.approvals[0];
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
@@ -244,9 +258,12 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.put(`/org/${clientRow.orgId}/approvals/${approval.approvalId}`, {
|
await api.put(
|
||||||
decision: "denied"
|
`/org/${clientRow.orgId}/approvals/${approval.approvalId}`,
|
||||||
});
|
{
|
||||||
|
decision: "denied"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("accessApprovalUpdated"),
|
title: t("accessApprovalUpdated"),
|
||||||
@@ -398,7 +415,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "online",
|
accessorKey: "online",
|
||||||
friendlyName: t("connectivity"),
|
friendlyName: t("online"),
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -548,20 +565,25 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{clientRow.approvalState === "pending" && build !== "oss" && (
|
{clientRow.approvalState === "pending" &&
|
||||||
<>
|
build !== "oss" && (
|
||||||
<DropdownMenuItem
|
<>
|
||||||
onClick={() => approveDevice(clientRow)}
|
<DropdownMenuItem
|
||||||
>
|
onClick={() =>
|
||||||
<span>{t("approve")}</span>
|
approveDevice(clientRow)
|
||||||
</DropdownMenuItem>
|
}
|
||||||
<DropdownMenuItem
|
>
|
||||||
onClick={() => denyDevice(clientRow)}
|
<span>{t("approve")}</span>
|
||||||
>
|
</DropdownMenuItem>
|
||||||
<span>{t("deny")}</span>
|
<DropdownMenuItem
|
||||||
</DropdownMenuItem>
|
onClick={() =>
|
||||||
</>
|
denyDevice(clientRow)
|
||||||
)}
|
}
|
||||||
|
>
|
||||||
|
<span>{t("deny")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (clientRow.archived) {
|
if (clientRow.archived) {
|
||||||
@@ -653,7 +675,10 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (build === "oss") {
|
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;
|
return allOptions;
|
||||||
@@ -717,7 +742,11 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
const rowArchived = row.archived;
|
const rowArchived = row.archived;
|
||||||
const rowBlocked = row.blocked;
|
const rowBlocked = row.blocked;
|
||||||
const approvalState = row.approvalState;
|
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)
|
if (selectedValues.includes("active") && isActive)
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -320,6 +320,11 @@ export function DataTable<TData, TValue>({
|
|||||||
return result;
|
return result;
|
||||||
}, [data, tabs, activeTab, filters, activeFilters]);
|
}, [data, tabs, activeTab, filters, activeFilters]);
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
pagination,
|
||||||
|
paginationState
|
||||||
|
});
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredData,
|
data: filteredData,
|
||||||
columns,
|
columns,
|
||||||
@@ -339,13 +344,10 @@ export function DataTable<TData, TValue>({
|
|||||||
}
|
}
|
||||||
: setPagination,
|
: setPagination,
|
||||||
manualFiltering,
|
manualFiltering,
|
||||||
manualPagination: !!paginationState,
|
manualPagination: Boolean(paginationState),
|
||||||
pageCount: paginationState?.pageCount,
|
pageCount: paginationState?.pageCount,
|
||||||
initialState: {
|
initialState: {
|
||||||
pagination: {
|
pagination,
|
||||||
pageSize: pageSize,
|
|
||||||
pageIndex: 0
|
|
||||||
},
|
|
||||||
columnVisibility: initialColumnVisibility
|
columnVisibility: initialColumnVisibility
|
||||||
},
|
},
|
||||||
state: {
|
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