formatted tables

This commit is contained in:
2024-11-29 00:10:50 -06:00
parent 59f9e06098
commit f553aa2e67
6 changed files with 236 additions and 174 deletions

View File

@@ -70,7 +70,11 @@ export const DataTable = <TData, TValue>({
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} colSpan={header.colSpan}>
<TableHead
key={header.id}
colSpan={header.colSpan}
style={{ width: header.getSize() }}
>
{header.isPlaceholder
? null
: flexRender(
@@ -91,7 +95,10 @@ export const DataTable = <TData, TValue>({
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
<TableCell
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),

View File

@@ -9,7 +9,7 @@ const Table = React.forwardRef<
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
className={cn("w-full table-fixed caption-bottom text-sm", className)}
{...props}
/>
</div>

View File

@@ -1,169 +0,0 @@
import { type ColumnDef } from "@tanstack/react-table";
import { type VARIANT } from "~/lib/data/list-variants";
import { tableStatuses, type STATUS } from "~/lib/data/task-status";
import { tablePriorities, type PRIORITY } from "~/lib/data/task-priority";
import { api } from "~/trpc/react";
import { Checkbox } from "~/app/_components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/app/_components/ui/dropdown-menu";
import { Button } from "~/app/_components/ui/button";
import { MoreHorizontal } from "lucide-react";
import { DataTableColumnHeader } from "~/app/_components/macro/data-table/data-table-column-header";
type Variant = keyof typeof VARIANT;
type Status = keyof typeof STATUS;
type Priority = keyof typeof PRIORITY;
export type Row = {
done: boolean;
id: string;
title: string;
status: Status;
priority: Priority;
};
const columns: ColumnDef<Row>[] = [
{
accessorKey: "done",
header: ({ column }) => (
<DataTableColumnHeader title="done" column={column} />
),
cell: ({ row }) => {
const task = row.original;
// TODO: Throttle
const { mutate } = api.tasks.update.useMutation();
return <Checkbox checked={task.done} className="translate-y-[2px]" />;
},
enableSorting: false,
},
{
accessorKey: "id",
header: ({ column }) => (
<DataTableColumnHeader title="id" column={column} />
),
},
{
accessorKey: "title",
header: ({ column }) => (
<DataTableColumnHeader title="title" column={column} />
),
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader title="status" column={column} />
),
cell: ({ row }) => {
const status = tableStatuses.find(
(status) => status.value === row.getValue("status"),
);
if (!status) {
return null;
}
return (
<div className="flex w-[100px] items-center">
{status.icon && (
<status.icon className="text-muted-foreground mr-2 h-4 w-4" />
)}
<span>{status.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return value.includes(row.getValue(id));
},
},
{
accessorKey: "priority",
header: ({ column }) => (
<DataTableColumnHeader title="priority" column={column} />
),
cell: ({ row }) => {
const priority = tablePriorities.find(
(priority) => priority.value === row.getValue("priority"),
);
if (!priority) {
return null;
}
return (
<div className="flex items-center">
{priority.icon && (
<priority.icon className="text-muted-foreground mr-2 h-4 w-4" />
)}
<span>{priority.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: ({ row }) => {
const task = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-neutral-100 dark:data-[state=open]:bg-neutral-800"
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(task.id)}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View task details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export const getColumns = (
variant: Variant,
showId: boolean,
): ColumnDef<Row>[] => {
const newColumnNames: string[] = [];
if (variant === "checklist" || variant === "project")
newColumnNames.push("done");
if (showId) newColumnNames.push("id");
newColumnNames.push("title");
if (variant === "project") {
newColumnNames.push("priority");
newColumnNames.push("status");
}
newColumnNames.push("actions");
return columns.filter((col) => {
const id = ((col as unknown as { accessorKey?: string }).accessorKey ??
col.id)!;
return newColumnNames.includes(id);
});
};

View File

@@ -6,9 +6,9 @@ import { type STATUS } from "~/lib/data/task-status";
import { type PRIORITY } from "~/lib/data/task-priority";
import { api } from "~/trpc/react";
import {
getColumns,
type Row,
} from "~/app/list/[id]/_components/table/columns";
useColumns,
} from "~/app/list/[id]/_components/table/use-columns";
type Variant = keyof typeof VARIANT;
type Status = keyof typeof STATUS;
@@ -16,9 +16,11 @@ type Priority = keyof typeof PRIORITY;
export const TaskTable = ({ listId }: { listId: number }) => {
const [list] = api.list.get.useSuspenseQuery({ listId });
const getColumns = useColumns(listId);
const data: Row[] =
list?.tasks.map((ls) => ({
taskId: ls.id,
done: ls.isChecked,
id: ls.visibleId ?? "",
title: ls.title,

View File

@@ -0,0 +1,221 @@
"use client";
import { DataTable } from "~/app/_components/macro/data-table/data-table";
import { type ColumnDef } from "@tanstack/react-table";
import { type VARIANT } from "~/lib/data/list-variants";
import { tableStatuses, type STATUS } from "~/lib/data/task-status";
import { tablePriorities, type PRIORITY } from "~/lib/data/task-priority";
import { api } from "~/trpc/react";
import { Checkbox } from "~/app/_components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/app/_components/ui/dropdown-menu";
import { Button } from "~/app/_components/ui/button";
import { MoreHorizontal } from "lucide-react";
import { DataTableColumnHeader } from "~/app/_components/macro/data-table/data-table-column-header";
type Variant = keyof typeof VARIANT;
type Status = keyof typeof STATUS;
type Priority = keyof typeof PRIORITY;
export type Row = {
taskId: number;
done: boolean;
id: string;
title: string;
status: Status;
priority: Priority;
};
export const useColumns = (listId: number) => {
const utils = api.useUtils();
const { mutate } = api.tasks.update.useMutation({
onMutate: async (taskUpdate) => {
await utils.list.get.cancel();
const prevData = utils.list.get.getData({ listId });
if (!prevData) return;
const tasks = prevData?.tasks.map((t) => {
if (t.id === taskUpdate.taskId) {
const clone = { ...t };
if (taskUpdate.data.isChecked !== undefined)
clone.isChecked = taskUpdate.data.isChecked;
if (taskUpdate.data.title !== undefined)
clone.title = taskUpdate.data.title;
if (taskUpdate.data.status !== undefined)
clone.status = taskUpdate.data.status;
if (taskUpdate.data.priority !== undefined)
clone.priority = taskUpdate.data.priority;
return clone;
}
return t;
});
utils.list.get.setData({ listId }, (old) => {
if (old) old.tasks = tasks;
return old;
});
},
onSettled: () => void utils.list.get.invalidate(),
});
const columns: ColumnDef<Row>[] = [
{
accessorKey: "done",
header: ({ column }) => (
<DataTableColumnHeader title="done" column={column} />
),
cell: ({ row }) => {
const task = row.original;
// TODO: Throttle
const handleToggleCheckbox = (checked: boolean) => {
mutate({ taskId: task.taskId, data: { isChecked: checked } });
};
return (
<Checkbox
checked={task.done}
onCheckedChange={handleToggleCheckbox}
className="translate-y-[2px]"
/>
);
},
enableSorting: false,
size: 0,
},
{
accessorKey: "id",
header: ({ column }) => (
<DataTableColumnHeader title="id" column={column} />
),
size: 0,
},
{
accessorKey: "title",
header: ({ column }) => (
<DataTableColumnHeader title="title" column={column} />
),
},
{
accessorKey: "status",
header: ({ column }) => (
<DataTableColumnHeader title="status" column={column} />
),
cell: ({ row }) => {
const status = tableStatuses.find(
(status) => status.value === row.getValue("status"),
);
if (!status) {
return null;
}
return (
<div className="flex w-[100px] items-center">
{status.icon && (
<status.icon className="text-muted-foreground mr-2 h-4 w-4" />
)}
<span>{status.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return value.includes(row.getValue(id));
},
},
{
accessorKey: "priority",
header: ({ column }) => (
<DataTableColumnHeader title="priority" column={column} />
),
cell: ({ row }) => {
const priority = tablePriorities.find(
(priority) => priority.value === row.getValue("priority"),
);
if (!priority) {
return null;
}
return (
<div className="flex items-center">
{priority.icon && (
<priority.icon className="text-muted-foreground mr-2 h-4 w-4" />
)}
<span>{priority.label}</span>
</div>
);
},
filterFn: (row, id, value) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return value.includes(row.getValue(id));
},
},
{
id: "actions",
cell: ({ row }) => {
const task = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex h-8 w-8 p-0 data-[state=open]:bg-neutral-100 dark:data-[state=open]:bg-neutral-800"
>
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(task.id)}
>
Copy ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View task details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
size: 0,
},
];
const getColumns = (variant: Variant, showId: boolean): ColumnDef<Row>[] => {
const newColumnNames: string[] = [];
if (variant === "checklist" || variant === "project")
newColumnNames.push("done");
if (showId) newColumnNames.push("id");
newColumnNames.push("title");
if (variant === "project") {
newColumnNames.push("priority");
newColumnNames.push("status");
}
newColumnNames.push("actions");
return columns.filter((col) => {
const id = ((col as unknown as { accessorKey?: string }).accessorKey ??
col.id)!;
return newColumnNames.includes(id);
});
};
return getColumns;
};

View File

@@ -24,6 +24,7 @@ export const listRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
const allLists = await ctx.db.query.lists.findMany({
where: eq(lists.userId, ctx.session.user.id),
orderBy: (lists, { desc }) => [desc(lists.id)],
});
return allLists;
}),