diff --git a/src/app/_components/tasks-table.tsx b/src/app/_components/tasks-table.tsx index d7a0542a8..458d54747 100644 --- a/src/app/_components/tasks-table.tsx +++ b/src/app/_components/tasks-table.tsx @@ -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"; @@ -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< [ @@ -33,11 +41,18 @@ interface TasksTableProps { Awaited>, ] >; + 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, @@ -45,27 +60,51 @@ export function TasksTable({ promises }: TasksTableProps) { estimatedHoursRange, ] = React.use(promises); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [rowAction, setRowAction] = React.useState | 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, @@ -73,6 +112,19 @@ export function TasksTable({ promises }: TasksTableProps) { 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 ( <> } > {enableAdvancedFilter ? ( - + {filterFlag === "advancedFilters" ? ( ) : ( - + )} diff --git a/src/app/_lib/queries.ts b/src/app/_lib/queries.ts index 5f7b512ef..26aac9ecc 100644 --- a/src/app/_lib/queries.ts +++ b/src/app/_lib/queries.ts @@ -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) diff --git a/src/app/_lib/validations.ts b/src/app/_lib/validations.ts index ccd74dac3..eb4705361 100644 --- a/src/app/_lib/validations.ts +++ b/src/app/_lib/validations.ts @@ -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({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 728f6e363..0c8a21283 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,11 +14,24 @@ import { getTasks, } from "./_lib/queries"; import { searchParamsCache } from "./_lib/validations"; +import type { Task } from "@/db/schema"; interface IndexPageProps { searchParams: Promise; } +// 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); @@ -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(), @@ -41,22 +56,26 @@ export default async function IndexPage(props: IndexPageProps) { 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 /> } > - + {/* Pass promises, availableColumns, and search */} + diff --git a/src/components/data-table-advanced-toolbar.tsx b/src/components/data-table-advanced-toolbar.tsx index 0a5cadf8c..ef07fb70a 100644 --- a/src/components/data-table-advanced-toolbar.tsx +++ b/src/components/data-table-advanced-toolbar.tsx @@ -2,6 +2,7 @@ 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"; @@ -9,12 +10,18 @@ import { cn } from "@/lib/utils"; interface DataTableAdvancedToolbarProps extends React.ComponentProps<"div"> { table: Table; + availableColumns: { id: keyof Task; label: string }[]; + selectedColumns: string[]; + onColumnsChange: (newColumns: string[]) => void; } export function DataTableAdvancedToolbar({ table, children, className, + availableColumns, + selectedColumns, + onColumnsChange, ...props }: DataTableAdvancedToolbarProps) { return ( @@ -29,7 +36,11 @@ export function DataTableAdvancedToolbar({ >
{children}
- +
); diff --git a/src/components/data-table-toolbar.tsx b/src/components/data-table-toolbar.tsx index 4af583025..c7846f3af 100644 --- a/src/components/data-table-toolbar.tsx +++ b/src/components/data-table-toolbar.tsx @@ -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 extends React.ComponentProps<"div"> { table: Table; + availableColumns: { id: keyof Task; label: string }[]; + selectedColumns: string[]; + onColumnsChange: (newColumns: string[]) => void; } export function DataTableToolbar({ table, children, className, + availableColumns, + selectedColumns, + onColumnsChange, ...props }: DataTableToolbarProps) { const isFiltered = table.getState().columnFilters.length > 0; @@ -62,11 +69,16 @@ export function DataTableToolbar({
{children} - +
); } + interface DataTableToolbarFilterProps { column: Column; } diff --git a/src/components/data-table-view-options.tsx b/src/components/data-table-view-options.tsx index 01399e569..aa1ce079e 100644 --- a/src/components/data-table-view-options.tsx +++ b/src/components/data-table-view-options.tsx @@ -1,6 +1,7 @@ "use client"; import type { Table } from "@tanstack/react-table"; +import type { Task } from "@/db/schema"; // Assuming Task type is accessible import { Check, ChevronsUpDown, Settings2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -20,23 +21,30 @@ import { import { cn } from "@/lib/utils"; import * as React from "react"; -interface DataTableViewOptionsProps { - table: Table; +// Define the structure for available columns prop matching TasksTable +interface AvailableColumn { + id: keyof Task; // Use keyof Task for better type safety + label: string; } -export function DataTableViewOptions({ - table, -}: DataTableViewOptionsProps) { - const columns = React.useMemo( - () => - table - .getAllColumns() - .filter( - (column) => - typeof column.accessorFn !== "undefined" && column.getCanHide(), - ), - [table], - ); +// Update props interface +interface DataTableViewOptionsProps { + availableColumns: AvailableColumn[]; + selectedColumns: string[]; // Array of selected column IDs + onColumnsChange: (newColumns: string[]) => void; // Callback function +} + +export function DataTableViewOptions({ + availableColumns, + selectedColumns, + onColumnsChange, +}: DataTableViewOptionsProps) { + const handleSelect = (columnId: string) => { + const newSelectedColumns = selectedColumns.includes(columnId) + ? selectedColumns.filter((id) => id !== columnId) // Deselect + : [...selectedColumns, columnId]; // Select + onColumnsChange(newSelectedColumns); + }; return ( @@ -48,9 +56,9 @@ export function DataTableViewOptions({ size="sm" className="ml-auto hidden h-8 lg:flex" > - + View - + @@ -59,24 +67,24 @@ export function DataTableViewOptions({ No columns found. - {columns.map((column) => ( - - column.toggleVisibility(!column.getIsVisible()) - } - > - - {column.columnDef.meta?.label ?? column.id} - - - - ))} + {/* Map over availableColumns instead of table.getAllColumns */} + {Array.isArray(availableColumns) && availableColumns.map((column) => { + const isSelected = selectedColumns.includes(column.id); + return ( + handleSelect(column.id)} // Use the new handler + > + {column.label} + + + ); + })}