Skip to content

Commit

Permalink
feat: DataTable filter (#4)
Browse files Browse the repository at this point in the history
* fix: Loading DataTable

* fix: Reorder columns, account > location > zone

* feat: Use Command to filter
  • Loading branch information
pettermachado authored Oct 2, 2024
1 parent dd591e0 commit 0e47b82
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 88 deletions.
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

0 comments on commit 0e47b82

Please sign in to comment.