Skip to content

Commit

Permalink
Generic table components (#15)
Browse files Browse the repository at this point in the history
* Add react-table dependency

* Add shadcn Tasks components

* Expose PopoverClose

* Add adapted filters for future tags
  • Loading branch information
abefernan authored Oct 15, 2024
1 parent 041becf commit 24bbcd3
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 2 deletions.
70 changes: 70 additions & 0 deletions components/data-table/data-table-column-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import {
ArrowDownIcon,
ArrowUpIcon,
CaretSortIcon,
EyeNoneIcon,
} from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";

interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title: string;
}

export function DataTableColumnHeader<TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) {
if (!column.getCanSort()) {
return <div className={cn(className)}>{title}</div>;
}

return (
<div className={cn("flex items-center space-x-2", className)}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="-ml-3 h-8 data-[state=open]:bg-accent"
>
<span>{title}</span>
{column.getIsSorted() === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : column.getIsSorted() === "asc" ? (
<ArrowUpIcon className="ml-2 h-4 w-4" />
) : (
<CaretSortIcon className="ml-2 h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Hide
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
129 changes: 129 additions & 0 deletions components/data-table/data-table-kv-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Pencil2Icon } from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { useState } from "react";
import { Input } from "../ui/input";
import { Label } from "../ui/label";

interface DataTableKvButtonProps<TData, TValue> {
column?: Column<TData, TValue>;
filteredKey: string;
}

export function DataTableKvButton<TData, TValue>({
column,
filteredKey,
}: DataTableKvButtonProps<TData, TValue>) {
const [open, setOpen] = useState(false);
const [filteredValue, setFilteredValue] = useState(
(column?.getFilterValue() as Map<string, string>).get(filteredKey) || "",
);

return (
<Popover
open={open}
onOpenChange={(open) => {
setOpen(open);
setFilteredValue(
(column?.getFilterValue() as Map<string, string>).get(filteredKey) ||
"",
);
}}
>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8">
{filteredKey}
<Pencil2Icon className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80" forceMount>
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Filter {filteredKey}</h4>
</div>
<form
className="space-y-2"
onSubmit={(e) => {
e.preventDefault();
}}
>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Key</Label>
<Input
id="key"
className="col-span-2 h-8"
defaultValue={filteredKey}
disabled
aria-disabled
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Value</Label>
<Input
id="value"
className="col-span-2 h-8"
value={filteredValue}
onChange={({ target }) => {
setFilteredValue(target.value);
}}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<Button
type="button"
variant="destructive"
onClick={() => {
column?.setFilterValue(
(kvs: Map<string, string> | undefined) => {
if (kvs) {
const newKvs = kvs;
newKvs.delete(filteredKey);
return newKvs.size > 0 ? newKvs : undefined;
}
},
);
setOpen(false);
}}
>
Remove
</Button>
<PopoverClose asChild>
<Button type="button" variant="secondary">
Cancel
</Button>
</PopoverClose>
<Button
type="submit"
disabled={!filteredValue}
onClick={() => {
column?.setFilterValue(
(kvs: Map<string, string> | undefined) => {
if (kvs) {
return kvs.set(filteredKey, filteredValue);
} else {
return new Map([[filteredKey, filteredValue]]);
}
},
);
setOpen(false);
}}
>
Filter
</Button>
</div>
</form>
</div>
</PopoverContent>
</Popover>
);
}
126 changes: 126 additions & 0 deletions components/data-table/data-table-kv-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Separator } from "@/components/ui/separator";
import { PlusCircledIcon } from "@radix-ui/react-icons";
import { Column } from "@tanstack/react-table";
import { useState } from "react";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { DataTableKvButton } from "./data-table-kv-button";

interface DataTableKvFilterProps<TData, TValue> {
column?: Column<TData, TValue>;
title?: string;
}

export function DataTableKvFilter<TData, TValue>({
column,
title,
}: DataTableKvFilterProps<TData, TValue>) {
const [open, setOpen] = useState(false);
const [kvKey, setKvKey] = useState("");
const [kvValue, setKvValue] = useState("");

const filteredKeys = Array.from(
(column?.getFilterValue() as Map<string, string>)?.keys() ?? new Map(),
).sort();

return (
<Popover open={open} onOpenChange={setOpen}>
<div className="flex flex-wrap gap-2">
{filteredKeys.map((filteredKey) => (
<DataTableKvButton
key={filteredKey}
column={column}
filteredKey={filteredKey}
/>
))}
<Separator orientation="vertical" className="mx-2 h-8" />
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-8 border-dashed">
<PlusCircledIcon className="mr-2 h-4 w-4" />
{title}
</Button>
</PopoverTrigger>
</div>
<PopoverContent className="w-80">
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">
New {column?.id} filter
</h4>
</div>
<form
className="space-y-2"
onSubmit={(e) => {
e.preventDefault();
}}
>
<div className="grid gap-2">
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="width">Key</Label>
<Input
id="key"
className="col-span-2 h-8"
value={kvKey}
onChange={({ target }) => {
setKvKey(target.value);
}}
/>
</div>
<div className="grid grid-cols-3 items-center gap-4">
<Label htmlFor="maxWidth">Value</Label>
<Input
id="value"
className="col-span-2 h-8"
value={kvValue}
onChange={({ target }) => {
setKvValue(target.value);
}}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<PopoverClose asChild>
<Button
type="button"
variant="secondary"
className="col-start-2"
>
Cancel
</Button>
</PopoverClose>
<Button
type="submit"
disabled={!kvKey || !kvValue}
onClick={() => {
column?.setFilterValue(
(kvs: Map<string, string> | undefined) => {
if (kvs) {
return kvs.set(kvKey, kvValue);
} else {
return new Map([[kvKey, kvValue]]);
}
},
);
setOpen(false);
setKvKey("");
setKvValue("");
}}
>
Filter
</Button>
</div>
</form>
</div>
</PopoverContent>
</Popover>
);
}
Loading

0 comments on commit 24bbcd3

Please sign in to comment.