TanStack / query

🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.
https://tanstack.com/query
MIT License
42.23k stars 2.87k forks source link

Inability to Set Object as Filter Value #6582

Closed sadiqhasanrupani closed 10 months ago

sadiqhasanrupani commented 10 months ago

Describe the bug

Bug Issue: Inability to Set Object as Filter Value

Description: The setFilterValue method is being utilized to filter the "leaveType" column based on user input in a React Table component. However, due to the object-type structure of the "leaveType" column, the current implementation fails to handle object values effectively.

Code Analysis: The "leaveType" column contains objects with properties like bgColor, text, and textColor. The setFilterValue method is designed to accept string values, causing issues when attempting to filter object-type data.

Code snippet causing the issue:

<Input
  placeholder="Search for leave types..."
  value={(table.getColumn("leaveType")?.getFilterValue() as string) ?? ""}
  onChange={(event) => table.getColumn("leaveType")?.setFilterValue(event.target.value)}
  className="max-w-sm"
/>

Expected Behavior: The setFilterValue method should handle object-type values appropriately, considering the structure of the "leaveType" column.

Proposed Solution: Adapt the setFilterValue logic to handle object-type values. The filter input should allow users to search based on the "text" property within the "leaveType" objects.

<Input
  placeholder="Search for leave types..."
  value={(table.getColumn("leaveType")?.getFilterValue()?.text as string) ?? ""}
  onChange={(event) => table.getColumn("leaveType")?.setFilterValue({ text: event.target.value })}
  className="max-w-sm"
/>

Additional Information: Ensure that the setFilterValue method is enhanced to handle object-type data for a more seamless user experience in searching and filtering the "leaveType" column.

Your minimal, reproducible example

Typescript, useReactTable(), setFilterValue(), React

Steps to reproduce

Steps to Reproduce:

  1. Navigate to the Leave Type Settings page:
    • Open the application and log in with the provided credentials.
  2. Access the Leave Type Settings Table:
    • Go to the "Leave Type Settings" section/page.
  3. Observe the Search Input for Leave Types:
    • Look for the search input designed for filtering Leave Types.
  4. Attempt to Filter using setFilterValue:
    • Input a search term (e.g., "ca") into the search input.
  5. Observe that the setFilterValue method is used to filter the "leaveType" column:
    • Confirm that the setFilterValue method is utilized for filtering the "leaveType" column.
  6. Verify the Result:
    • Check if the Leave Type column is properly filtered based on the provided search term.

Expected Result:

Actual Result:

Expected behavior

As a user, the expected behavior is that the search functionality for the Leave Type should seamlessly retrieve and display data based on the specific leave type being searched for. Upon entering a search term, the application should accurately filter and present the relevant information related to the specified leave type in the Leave Type column. This ensures an efficient and user-friendly experience, allowing users to quickly locate and access the desired leave type information without any discrepancies or missing data.

How often does this bug happen?

None

Screenshots or Videos

https://github.com/TanStack/query/assets/76765939/f1301d6c-52f1-41f6-8a61-fd187c4665ec

Platform

Tanstack Query adapter

None

TanStack Query version

v8

TypeScript version

5.3.2

Additional context

column.tsx


import { ColumnDef } from "@tanstack/react-table";
import { DotsHorizontalIcon } from "@radix-ui/react-icons";
import { PenSquare, Trash2 } from "lucide-react";

//^ ShadCn Components
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Badge } from "@/components/ui/badge";

export type LeaveTypeSettings = {
  leaveId: number;
  leaveType: { bgColor: string; textColor: string; text: string };
  noOfLeaves: string;
  monthlyLimit: string;
  leavePaidStatus: "paid" | "unpaid";
  department: string[];
  designation: string[];
};

export const columns: ColumnDef<LeaveTypeSettings>[] = [
  {
    id: "select",
    header: ({ table }) => (
      <Checkbox
        checked={
          table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")
        }
        onCheckedChange={(value) => {
          table.toggleAllPageRowsSelected(!!value);
        }}
        aria-label="Select all"
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => {
          row.toggleSelected(!!value);
        }}
        aria-label="Select row"
      />
    ),
    enableSorting: false,
    enableHiding: false,
  },
  {
    accessorKey: "leaveType",
    header: "Leave Type",
    cell: ({ row }) => (
      <Badge
        style={{
          backgroundColor: row.original.leaveType.bgColor,
          color: row.original.leaveType.textColor,
        }}
        className="capitalize cursor-default"
      >
        {row.original.leaveType.text}
      </Badge>
    ),
  },
  {
    accessorKey: "noOfLeaves",
    header: () => {
      return <p>No Of Leaves</p>;
    },
  },
  {
    accessorKey: "monthlyLimit",
    header: () => <div>Monthly Limit</div>,
  },
  {
    accessorKey: "leavePaidStatus",
    header: "Leave Paid Status",
    cell: ({ row }) => {
      const leavePaidStatus = row.original.leavePaidStatus;

      return (
        <Badge
          className={`text-${leavePaidStatus === "paid" ? "white" : "white"} bg-${
            leavePaidStatus === "paid" ? "green" : "red"
          }-500 hover:bg-${leavePaidStatus === "paid" ? "green" : "red"}-400 cursor-default capitalize`}
        >
          {leavePaidStatus}
        </Badge>
      );
    },
  },
  {
    accessorKey: "department",
    header: "Department",
    cell: ({ row }) => {
      const departments = row.original.department;

      return (
        <>
          <div>
            {departments.map((department, index) => {
              let count = index + 1;
              index++;

              return (
                <p key={index} className="flex gap-1">
                  <span>{`${count}.`}</span>
                  <span>{department}</span>
                </p>
              );
            })}
          </div>
        </>
      );
    },
  },
  {
    accessorKey: "designation",
    header: "Designation",
    cell: ({ row }) => {
      const designation = row.original.designation;

      return (
        <>
          <div>
            {designation.map((designation, index) => {
              let count = index + 1;
              index++;

              return (
                <p key={index} className="flex gap-1">
                  <span>{`${count}.`}</span>
                  <span>{designation}</span>
                </p>
              );
            })}
          </div>
        </>
      );
    },
  },
  {
    id: "actions",
    enableHiding: false,
    cell: ({ row }) => {
      const payment = row.original;

      return (
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="ghost" className="h-8 w-8 p-0">
              <span className="sr-only">Open menu</span>
              <DotsHorizontalIcon className="h-4 w-4" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            <DropdownMenuLabel>Actions</DropdownMenuLabel>
            <DropdownMenuItem
              onClick={() => navigator.clipboard.writeText(payment.leaveId.toString())}
            >
              Copy payment ID
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem className="flex gap-2 items-center">
              <span>
                <Trash2 className="w-4 text-red-500" />
              </span>
              <span>Delete</span>
            </DropdownMenuItem>
            <DropdownMenuItem className="flex gap-2 items-center">
              <span>
                <PenSquare className="w-4 text-slate-500" />
              </span>
              <span>Edit</span>
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      );
    },
  },
];

table.tsx:


import { ChevronDownIcon } from "@radix-ui/react-icons";
import {
  ColumnFiltersState,
  SortingState,
  VisibilityState,
  flexRender,
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useReactTable,
} from "@tanstack/react-table";

import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
import { LeaveTypeSettings, columns } from "./column";

const data: LeaveTypeSettings[] = [
  {
    leaveId: 1,
    noOfLeaves: "5",
    monthlyLimit: "2",
    leavePaidStatus: "paid",
    department: ["Marketing", "Sales", "Human Resources", "Public Relations"],
    designation: ["Trainee", "Senior", "Junior", "Team lead"],
    leaveType: {
      bgColor: "green",
      text: "Casual",
      textColor: "white",
    },
  },
  {
    leaveId: 2,
    noOfLeaves: "5",
    monthlyLimit: "2",
    leavePaidStatus: "unpaid",
    department: ["Marketing", "Sales", "Human Resources", "Public Relations"],
    designation: ["Trainee", "Senior", "Junior", "Team lead"],
    leaveType: {
      bgColor: "green",
      text: "Casual",
      textColor: "white",
    },
  },
];

export function LeaveTypeSettingsTable() {
  const [sorting, setSorting] = React.useState<SortingState>([]);
  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
  const [rowSelection, setRowSelection] = React.useState({});

  const table = useReactTable({
    data,
    columns,
    onSortingChange: setSorting,
    onColumnFiltersChange: setColumnFilters,
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    onColumnVisibilityChange: setColumnVisibility,
    onRowSelectionChange: setRowSelection,
    state: {
      sorting,
      columnFilters,
      columnVisibility,
      rowSelection,
    },
  });

  return (
    <div className="w-full">
      <div className="flex items-center py-4">
        <Input
          placeholder="Search for leave types..."
          value={(table.getColumn("leaveType")?.getFilterValue() as string) ?? ""}
          onChange={(event) => table.getColumn("leaveType")?.setFilterValue(event.target.value)}
          className="max-w-sm"
        />

        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <Button variant="outline" className="ml-auto">
              Columns <ChevronDownIcon className="ml-2 h-4 w-4" />
            </Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end">
            {table
              .getAllColumns()
              .filter((column) => column.getCanHide())
              .map((column) => {
                return (
                  <DropdownMenuCheckboxItem
                    key={column.id}
                    className="capitalize"
                    checked={column.getIsVisible()}
                    onCheckedChange={(value) => column.toggleVisibility(!!value)}
                  >
                    {column.id}
                  </DropdownMenuCheckboxItem>
                );
              })}
          </DropdownMenuContent>
        </DropdownMenu>
      </div>
      <div className="rounded-md border">
        <Table>
          <TableHeader>
            {table.getHeaderGroups().map((headerGroup) => (
              <TableRow key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <TableHead key={header.id}>
                      {header.isPlaceholder
                        ? null
                        : flexRender(header.column.columnDef.header, header.getContext())}
                    </TableHead>
                  );
                })}
              </TableRow>
            ))}
          </TableHeader>
          <TableBody>
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) => (
                <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
                  {row.getVisibleCells().map((cell) => (
                    <TableCell key={cell.id}>
                      {flexRender(cell.column.columnDef.cell, cell.getContext())}
                    </TableCell>
                  ))}
                </TableRow>
              ))
            ) : (
              <TableRow>
                <TableCell colSpan={columns.length} className="h-24 text-center">
                  No results.
                </TableCell>
              </TableRow>
            )}
          </TableBody>
        </Table>
      </div>
      <div className="flex items-center justify-end space-x-2 py-4">
        <div className="flex-1 text-sm text-muted-foreground">
          {table.getFilteredSelectedRowModel().rows.length} of{" "}
          {table.getFilteredRowModel().rows.length} row(s) selected.
        </div>
        <div className="space-x-2">
          <Button
            variant="outline"
            size="sm"
            onClick={() => table.previousPage()}
            disabled={!table.getCanPreviousPage()}
          >
            Previous
          </Button>
          <Button
            variant="outline"
            size="sm"
            onClick={() => table.nextPage()}
            disabled={!table.getCanNextPage()}
          >
            Next
          </Button>
        </div>
      </div>
    </div>
  );
}
TkDodo commented 10 months ago

This is the query repo, not table