mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 10:56:38 +00:00
add archive to org clients and add unarchive
This commit is contained in:
@@ -33,7 +33,7 @@ import { Button } from "@app/components/ui/button";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||
import { Plus, Search, RefreshCw, Columns } from "lucide-react";
|
||||
import { Plus, Search, RefreshCw, Columns, Filter } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -140,6 +140,22 @@ type TabFilter = {
|
||||
filterFn: (row: any) => boolean;
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ExtendedColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
@@ -156,6 +172,8 @@ type DataTableProps<TData, TValue> = {
|
||||
};
|
||||
tabs?: TabFilter[];
|
||||
defaultTab?: string;
|
||||
filters?: DataTableFilter[];
|
||||
filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter)
|
||||
persistPageSize?: boolean | string;
|
||||
defaultPageSize?: number;
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
@@ -178,6 +196,8 @@ export function DataTable<TData, TValue>({
|
||||
defaultSort,
|
||||
tabs,
|
||||
defaultTab,
|
||||
filters,
|
||||
filterDisplayMode = "label",
|
||||
persistPageSize = false,
|
||||
defaultPageSize = 20,
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
@@ -235,6 +255,15 @@ export function DataTable<TData, TValue>({
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
defaultTab || tabs?.[0]?.id || ""
|
||||
);
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
||||
// Track initial values to avoid storing defaults on first render
|
||||
const initialPageSize = useRef(pageSize);
|
||||
@@ -242,19 +271,32 @@ export function DataTable<TData, TValue>({
|
||||
const hasUserChangedPageSize = useRef(false);
|
||||
const hasUserChangedColumnVisibility = useRef(false);
|
||||
|
||||
// Apply tab filter to data
|
||||
// Apply tab and custom filters to data
|
||||
const filteredData = useMemo(() => {
|
||||
if (!tabs || activeTab === "") {
|
||||
return data;
|
||||
let result = data;
|
||||
|
||||
// Apply tab filter
|
||||
if (tabs && activeTab !== "") {
|
||||
const activeTabFilter = tabs.find((tab) => tab.id === activeTab);
|
||||
if (activeTabFilter) {
|
||||
result = result.filter(activeTabFilter.filterFn);
|
||||
}
|
||||
}
|
||||
|
||||
const activeTabFilter = tabs.find((tab) => tab.id === activeTab);
|
||||
if (!activeTabFilter) {
|
||||
return data;
|
||||
// Apply custom filters
|
||||
if (filters && filters.length > 0) {
|
||||
filters.forEach((filter) => {
|
||||
const selectedValues = activeFilters[filter.id] || [];
|
||||
if (selectedValues.length > 0) {
|
||||
result = result.filter((row) =>
|
||||
filter.filterFn(row, selectedValues)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data.filter(activeTabFilter.filterFn);
|
||||
}, [data, tabs, activeTab]);
|
||||
return result;
|
||||
}, [data, tabs, activeTab, filters, activeFilters]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
@@ -318,6 +360,64 @@ export function DataTable<TData, TValue>({
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
};
|
||||
|
||||
const handleFilterChange = (
|
||||
filterId: string,
|
||||
optionValue: string | number | boolean,
|
||||
checked: boolean
|
||||
) => {
|
||||
setActiveFilters((prev) => {
|
||||
const currentValues = prev[filterId] || [];
|
||||
const filter = filters?.find((f) => f.id === filterId);
|
||||
|
||||
if (!filter) return prev;
|
||||
|
||||
let newValues: (string | number | boolean)[];
|
||||
|
||||
if (filter.multiSelect) {
|
||||
// Multi-select: add or remove the value
|
||||
if (checked) {
|
||||
newValues = [...currentValues, optionValue];
|
||||
} else {
|
||||
newValues = currentValues.filter((v) => v !== optionValue);
|
||||
}
|
||||
} else {
|
||||
// Single-select: replace the value
|
||||
newValues = checked ? [optionValue] : [];
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[filterId]: newValues
|
||||
};
|
||||
});
|
||||
// Reset to first page when changing filters
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
};
|
||||
|
||||
// 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 ");
|
||||
};
|
||||
|
||||
// Enhanced pagination component that updates our local state
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
hasUserChangedPageSize.current = true;
|
||||
@@ -387,6 +487,63 @@ export function DataTable<TData, TValue>({
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
{tabs && tabs.length > 0 && (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
|
||||
Reference in New Issue
Block a user