formatted tables
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
221
src/app/list/[id]/_components/table/use-columns.tsx
Normal file
221
src/app/list/[id]/_components/table/use-columns.tsx
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user