Skip to content
Open
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
90 changes: 76 additions & 14 deletions src/app/_components/tasks-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { Task } from "@/db/schema";
import type { DataTableRowAction } from "@/types/data-table";
import * as React from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

import { DataTable } from "@/components/data-table";
import { useDataTable } from "@/hooks/use-data-table";
Expand All @@ -12,18 +13,25 @@ import { DataTableFilterList } from "@/components/data-table-filter-list";
import { DataTableFilterMenu } from "@/components/data-table-filter-menu";
import { DataTableSortList } from "@/components/data-table-sort-list";
import { DataTableToolbar } from "@/components/data-table-toolbar";
import { DataTableViewOptions } from "@/components/data-table-view-options";
import type {
getEstimatedHoursRange,
getTaskPriorityCounts,
getTaskStatusCounts,
getTasks,
} from "../_lib/queries";
import type { GetTasksSchema } from "../_lib/validations";
import { DeleteTasksDialog } from "./delete-tasks-dialog";
import { useFeatureFlags } from "./feature-flags-provider";
import { TasksTableActionBar } from "./tasks-table-action-bar";
import { getTasksTableColumns } from "./tasks-table-columns";
import { UpdateTaskSheet } from "./update-task-sheet";

interface AvailableColumn {
id: keyof Task;
label: string;
}

interface TasksTableProps {
promises: Promise<
[
Expand All @@ -33,54 +41,103 @@ interface TasksTableProps {
Awaited<ReturnType<typeof getEstimatedHoursRange>>,
]
>;
availableColumns: AvailableColumn[];
search: GetTasksSchema;
}

export function TasksTable({ promises }: TasksTableProps) {
export function TasksTable({
promises,
availableColumns,
search,
}: TasksTableProps) {
const { enableAdvancedFilter, filterFlag } = useFeatureFlags();

// Use React.use to resolve the promises
const [
{ data, pageCount },
statusCounts,
priorityCounts,
estimatedHoursRange,
] = React.use(promises);

const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();

const [rowAction, setRowAction] =
React.useState<DataTableRowAction<Task> | null>(null);

const columns = React.useMemo(
() =>
getTasksTableColumns({
statusCounts,
priorityCounts,
estimatedHoursRange,
setRowAction,
}),
[statusCounts, priorityCounts, estimatedHoursRange],
);
// Filter the columns based on the search params
const columns = React.useMemo(() => {
const allPossibleColumns = getTasksTableColumns({
statusCounts,
priorityCounts,
estimatedHoursRange: estimatedHoursRange,
setRowAction,
});

// Ensure 'actions' column is always present if defined
const actionsColumn = allPossibleColumns.find((col) => col.id === "actions");

// Filter based on search.columns, always include 'actions'
const filteredColumns = allPossibleColumns.filter(
(col) => col.id === "actions" || search.columns.includes(col.id as string),
);

// Make sure actions column is included even if not in search.columns (though it shouldn't be)
if (actionsColumn && !filteredColumns.some((col) => col.id === "actions")) {
filteredColumns.push(actionsColumn);
}

return filteredColumns;
}, [
statusCounts,
priorityCounts,
estimatedHoursRange,
search.columns, // Add search.columns to dependency array
]);

const { table, shallow, debounceMs, throttleMs } = useDataTable({
data,
data: data as any,
columns,
pageCount,
enableAdvancedFilter,
initialState: {
sorting: [{ id: "createdAt", desc: true }],
columnVisibility: {},
columnPinning: { right: ["actions"] },
},
getRowId: (originalRow) => originalRow.id,
shallow: false,
clearOnDefault: true,
});

const handleColumnsChange = React.useCallback(
(newColumns: string[]) => {
const current = new URLSearchParams(searchParams?.toString());
if (newColumns.length === 0) {
current.delete("columns");
} else {
current.set("columns", newColumns.join(","));
}
router.replace(`${pathname}?${current.toString()}`);
},
[pathname, router, searchParams],
);

return (
<>
<DataTable
table={table}
actionBar={<TasksTableActionBar table={table} />}
>
{enableAdvancedFilter ? (
<DataTableAdvancedToolbar table={table}>
<DataTableAdvancedToolbar
table={table}
availableColumns={availableColumns}
selectedColumns={search.columns}
onColumnsChange={handleColumnsChange}
>
<DataTableSortList table={table} align="start" />
{filterFlag === "advancedFilters" ? (
<DataTableFilterList
Expand All @@ -100,7 +157,12 @@ export function TasksTable({ promises }: TasksTableProps) {
)}
</DataTableAdvancedToolbar>
) : (
<DataTableToolbar table={table}>
<DataTableToolbar
table={table}
availableColumns={availableColumns}
selectedColumns={search.columns}
onColumnsChange={handleColumnsChange}
>
<DataTableSortList table={table} align="end" />
</DataTableToolbar>
)}
Expand Down
21 changes: 20 additions & 1 deletion src/app/_lib/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,27 @@ export async function getTasks(input: GetTasksSchema) {
: [asc(tasks.createdAt)];

const { data, total } = await db.transaction(async (tx) => {
// Prepare the selection object based on input.columns
const selection = (
input.columns.length > 0
? input.columns.reduce(
(acc, col) => {
if (col in tasks) {
acc[col as keyof typeof tasks] =
tasks[col as keyof typeof tasks];
}
return acc;
},
{} as Record<
string,
(typeof tasks)[keyof typeof tasks]
>,
)
: tasks // Select all columns (tasks table) if input.columns is empty
) as any; // Cast to any as strict typing is problematic

const data = await tx
.select()
.select(selection) // Use the prepared selection object
.from(tasks)
.limit(input.perPage)
.offset(offset)
Expand Down
10 changes: 10 additions & 0 deletions src/app/_lib/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export const searchParamsCache = createSearchParamsCache({
// advanced filter
filters: getFiltersStateParser().withDefault([]),
joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
// define columns to retrieve from the database
columns: parseAsArrayOf(z.string()).withDefault([
"code",
"label",
"title",
"status",
"priority",
"createdAt",
"estimatedHours",
]),
});

export const createTaskSchema = z.object({
Expand Down
41 changes: 30 additions & 11 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,24 @@ import {
getTasks,
} from "./_lib/queries";
import { searchParamsCache } from "./_lib/validations";
import type { Task } from "@/db/schema";

interface IndexPageProps {
searchParams: Promise<SearchParams>;
}

// Define all selectable columns with user-friendly labels
const availableColumns: { id: keyof Task; label: string }[] = [
{ id: "code", label: "Code" },
{ id: "title", label: "Title" },
{ id: "status", label: "Status" },
{ id: "priority", label: "Priority" },
{ id: "label", label: "Label" },
{ id: "estimatedHours", label: "Est. Hours" },
{ id: "createdAt", label: "Created At" },
// Add other columns from your Task schema as needed
];

export default async function IndexPage(props: IndexPageProps) {
const searchParams = await props.searchParams;
const search = searchParamsCache.parse(searchParams);
Expand All @@ -29,6 +42,8 @@ export default async function IndexPage(props: IndexPageProps) {
getTasks({
...search,
filters: validFilters,
// Ensure columns are passed if defined, otherwise default handled by getTasks
columns: search.columns,
}),
getTaskStatusCounts(),
getTaskPriorityCounts(),
Expand All @@ -41,22 +56,26 @@ export default async function IndexPage(props: IndexPageProps) {
<React.Suspense
fallback={
<DataTableSkeleton
columnCount={7}
columnCount={search.columns.length > 0 ? search.columns.length : availableColumns.length} // Adjust skeleton based on selected/available columns
filterCount={2}
cellWidths={[
"10rem",
"30rem",
"10rem",
"10rem",
"6rem",
"6rem",
"6rem",
]}
// Adjust cellWidths based on default/selected columns if needed
// cellWidths={[
// "10rem",
// "30rem",
// "10rem",
// "10rem",
// "6rem",
// ]}
shrinkZero
/>
}
>
<TasksTable promises={promises} />
{/* Pass promises, availableColumns, and search */}
<TasksTable
promises={promises}
availableColumns={availableColumns}
search={search} // Pass the parsed search params
/>
</React.Suspense>
</FeatureFlagsProvider>
</Shell>
Expand Down
13 changes: 12 additions & 1 deletion src/components/data-table-advanced-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@

import type { Table } from "@tanstack/react-table";
import type * as React from "react";
import type { Task } from "@/db/schema";

import { DataTableViewOptions } from "@/components/data-table-view-options";
import { cn } from "@/lib/utils";

interface DataTableAdvancedToolbarProps<TData>
extends React.ComponentProps<"div"> {
table: Table<TData>;
availableColumns: { id: keyof Task; label: string }[];
selectedColumns: string[];
onColumnsChange: (newColumns: string[]) => void;
}

export function DataTableAdvancedToolbar<TData>({
table,
children,
className,
availableColumns,
selectedColumns,
onColumnsChange,
...props
}: DataTableAdvancedToolbarProps<TData>) {
return (
Expand All @@ -29,7 +36,11 @@ export function DataTableAdvancedToolbar<TData>({
>
<div className="flex flex-1 flex-wrap items-center gap-2">{children}</div>
<div className="flex items-center gap-2">
<DataTableViewOptions table={table} />
<DataTableViewOptions
availableColumns={availableColumns}
selectedColumns={selectedColumns}
onColumnsChange={onColumnsChange}
/>
</div>
</div>
);
Expand Down
14 changes: 13 additions & 1 deletion src/components/data-table-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ import { DataTableViewOptions } from "@/components/data-table-view-options";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import type { Task } from "@/db/schema";

interface DataTableToolbarProps<TData> extends React.ComponentProps<"div"> {
table: Table<TData>;
availableColumns: { id: keyof Task; label: string }[];
selectedColumns: string[];
onColumnsChange: (newColumns: string[]) => void;
}

export function DataTableToolbar<TData>({
table,
children,
className,
availableColumns,
selectedColumns,
onColumnsChange,
...props
}: DataTableToolbarProps<TData>) {
const isFiltered = table.getState().columnFilters.length > 0;
Expand Down Expand Up @@ -62,11 +69,16 @@ export function DataTableToolbar<TData>({
</div>
<div className="flex items-center gap-2">
{children}
<DataTableViewOptions table={table} />
<DataTableViewOptions
availableColumns={availableColumns}
selectedColumns={selectedColumns}
onColumnsChange={onColumnsChange}
/>
</div>
</div>
);
}

interface DataTableToolbarFilterProps<TData> {
column: Column<TData>;
}
Expand Down
Loading