Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: DataTable filter #4

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 123 additions & 61 deletions app/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ import {
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import {
BookmarkFilledIcon,
BookmarkIcon,
CaretDownIcon,
CaretUpIcon,
CheckIcon,
ChevronLeftIcon,
ChevronRightIcon,
MixerHorizontalIcon,
} from "@radix-ui/react-icons";
import {
Popover,
Expand All @@ -38,14 +40,16 @@ import {
} from "~/components/ui/popover";
import { Label } from "~/components/ui/label";
import { PopoverClose } from "@radix-ui/react-popover";
import { Skeleton } from "./ui/skeleton";
import { Command, CommandInput, CommandItem } from "./ui/command";
import { CommandEmpty, CommandList } from "cmdk";
import { cn } from "~/lib/utils";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "./ui/tooltip";

type BulkAction<TData> = {
key: Key | null | undefined;
Expand All @@ -58,13 +62,17 @@ interface DataTableProps<TData, TValue> {
data: TData[];
bulkActions?: BulkAction<TData>[];
pagination?: boolean;
loading?: boolean;
loadingRows?: number;
}

export const DataTable = function DataTable<TData, TValue>({
columns,
data,
bulkActions,
pagination,
loading = false,
loadingRows = 0,
}: DataTableProps<TData, TValue>) {
const id = useId();
const [rowSelection, setRowSelection] = useState({});
Expand Down Expand Up @@ -97,7 +105,7 @@ export const DataTable = function DataTable<TData, TValue>({
key={action.key}
size="sm"
variant="ghost"
disabled={selectedRows.length === 0}
disabled={selectedRows.length === 0 || loading}
onClick={async () => {
await action.action(
selectedRows.map((row) => row.original),
Expand All @@ -117,6 +125,7 @@ export const DataTable = function DataTable<TData, TValue>({
<Button
size="sm"
variant="ghost"
disabled={loading}
onClick={() => table.setColumnFilters([])}
>
Clear filters
Expand All @@ -133,7 +142,7 @@ export const DataTable = function DataTable<TData, TValue>({
const cx = header.column.getCanSort()
? "cursor-pointer select-none"
: undefined;
const onClick = header.column.getCanSort()
const handleSort = header.column.getCanSort()
? header.column.getToggleSortingHandler()
: undefined;
let sortIcon = null;
Expand All @@ -149,18 +158,22 @@ export const DataTable = function DataTable<TData, TValue>({
<TableHead key={header.id}>
{header.isPlaceholder ? null : (
<div className={"group flex items-center " + cx}>
<div className="flex items-center" onClick={onClick}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
<div className="flex items-center">
<div onClick={handleSort}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
{header.column.getCanFilter() && (
<ColumnFilter column={header.column} id={id} />
)}
{header.column.getCanSort() && (
<div className="w-6">{sortIcon}</div>
<div className="w-6" onClick={handleSort}>
{sortIcon}
</div>
)}
</div>
{header.column.getCanFilter() && (
<ColumnFilter column={header.column} id={id} />
)}
</div>
)}
</TableHead>
Expand All @@ -170,7 +183,7 @@ export const DataTable = function DataTable<TData, TValue>({
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
{table.getRowModel().rows?.length && !loading ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
Expand All @@ -186,13 +199,18 @@ export const DataTable = function DataTable<TData, TValue>({
))}
</TableRow>
))
) : loading && loadingRows > 0 ? (
<LoadingRows
n={loadingRows}
colSpan={table.getVisibleFlatColumns().length}
/>
) : (
<TableRow>
<TableCell
colSpan={columns.length}
colSpan={table.getVisibleFlatColumns().length}
className="h-24 text-center"
>
No results.
{loading ? "Loading ..." : "No results"}
</TableCell>
</TableRow>
)}
Expand All @@ -214,15 +232,15 @@ export const DataTable = function DataTable<TData, TValue>({
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
disabled={!table.getCanPreviousPage() || loading}
>
<ChevronLeftIcon />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
disabled={!table.getCanNextPage() || loading}
>
<ChevronRightIcon />
</Button>
Expand All @@ -233,6 +251,19 @@ export const DataTable = function DataTable<TData, TValue>({
);
};

function LoadingRows({ n, colSpan }: { n: number; colSpan: number }) {
const rows: React.ReactNode[] = [];
const cell = (
<TableCell colSpan={colSpan}>
<Skeleton>&nbsp;</Skeleton>
</TableCell>
);
for (let i = 0; i < n; i++) {
rows.push(<TableRow key={"loadingRow-" + i}>{cell}</TableRow>);
}
return rows;
}

function ColumnFilter<TData>(props: { id: string; column: Column<TData> }) {
const { id, column } = props;
const { filter } = (props.column.columnDef.meta ?? {}) as { filter?: string };
Expand All @@ -246,55 +277,86 @@ function ColumnFilter<TData>(props: { id: string; column: Column<TData> }) {
: [],
[column.getFacetedUniqueValues(), filter],
);
const currentFilterValue = column.getFilterValue();
return (
<Popover>
<PopoverTrigger asChild>
<MixerHorizontalIcon
className={
(column.getFilterValue() ? "opacity-100 " : "") +
"opacity-0 group-hover:opacity-100 ml-1"
}
/>
<PopoverTrigger>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
{currentFilterValue ? (
<BookmarkFilledIcon
className={
(currentFilterValue ? "opacity-100 " : "") +
"opacity-0 group-hover:opacity-100 ml-1"
}
/>
) : (
<BookmarkIcon
className={"opacity-0 group-hover:opacity-100 ml-1"}
/>
)}
</TooltipTrigger>
<TooltipContent>
{currentFilterValue
? "This column is filtered"
: "Click to filter"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</PopoverTrigger>
<PopoverContent>
<Label
htmlFor={"filter-input-" + column.id + id}
className="mb-2 block"
>
Filter
</Label>

<PopoverContent className="p-0">
{filter === "select" && (
<Select
value={column.getFilterValue()?.toString()}
onValueChange={column.setFilterValue}
>
<SelectTrigger id={"filter-input-" + column.id + id}>
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<div>
<Command>
<CommandInput placeholder="Filter" />
<CommandEmpty>No filter</CommandEmpty>
<CommandList>
{sortedUniqueValues.map((v) => {
return (
<SelectItem key={v} value={v}>
{v}
</SelectItem>
<CommandItem
key={v}
onSelect={() =>
column.setFilterValue(
currentFilterValue !== v ? v : null,
)
}
>
<div className="w-full flex items-center justify-between">
{v}
<CheckIcon
className={cn(
v === currentFilterValue
? "opacity-100"
: "opacity-0",
)}
/>
</div>
</CommandItem>
);
})}
</SelectGroup>
</SelectContent>
</Select>
</CommandList>
</Command>
</div>
)}
{filter !== "select" && (
<Input
id={"filter-input-" + column.id + id}
value={(column.getFilterValue() ?? "") as string}
onChange={(e) => {
column.setFilterValue(e.target.value);
}}
/>
<div className="p-2">
<Label
htmlFor={"filter-input-" + column.id + id}
className="mb-2 block"
>
Filter
</Label>
<Input
id={"filter-input-" + column.id + id}
value={(column.getFilterValue() ?? "") as string}
onChange={(e) => {
column.setFilterValue(e.target.value);
}}
/>
</div>
)}
<div className="mt-2">
<div className="p-2">
<PopoverClose asChild>
<Button size="sm" variant="outline">
Close
Expand Down
38 changes: 19 additions & 19 deletions app/components/columns/zone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ export const columns: ColumnDef<ZoneRow>[] = [
enableSorting: false,
enableHiding: false,
},
{
id: "account",
header: "Account",
accessorFn: ({ zone }) => zone.account.businessName,
cell: (props) => {
return <AccountLink data={props.row.original.zone.account} />;
},
meta: {
filter: "select",
},
},
{
id: "location",
header: "Location",
accessorFn: ({ zone }) => zone.location.name,
meta: {
filter: "select",
},
},
{
id: "zone",
header: "Zone",
Expand All @@ -54,25 +73,6 @@ export const columns: ColumnDef<ZoneRow>[] = [
);
},
},
{
id: "location",
header: "Location",
accessorFn: ({ zone }) => zone.location.name,
meta: {
filter: "select",
},
},
{
id: "account",
header: "Account",
accessorFn: ({ zone }) => zone.account.businessName,
cell: (props) => {
return <AccountLink data={props.row.original.zone.account} />;
},
meta: {
filter: "select",
},
},
{
accessorKey: "onAction",
header: "",
Expand Down
Loading