From 2c5fd0ea76d16743420e8cc6c380042227b837e5 Mon Sep 17 00:00:00 2001 From: Nick A Date: Thu, 3 Apr 2025 21:37:08 -0400 Subject: [PATCH 1/6] sorting and filter functionality --- .../FilterBox/{index.tsx => FilterBox.tsx} | 1 - compass/components/Table/ResourceTable.tsx | 3 + compass/components/Table/Table.tsx | 61 +++++++++++++++---- compass/components/Table/TableAction.tsx | 2 +- 4 files changed, 54 insertions(+), 13 deletions(-) rename compass/components/FilterBox/{index.tsx => FilterBox.tsx} (99%) diff --git a/compass/components/FilterBox/index.tsx b/compass/components/FilterBox/FilterBox.tsx similarity index 99% rename from compass/components/FilterBox/index.tsx rename to compass/components/FilterBox/FilterBox.tsx index be0e6fa..6e6fe6b 100644 --- a/compass/components/FilterBox/index.tsx +++ b/compass/components/FilterBox/FilterBox.tsx @@ -1,4 +1,3 @@ -// FilterBox.tsx import { useState } from "react"; import { ChevronDownIcon } from "@heroicons/react/24/solid"; import { ContainsDropdown } from "./ContainsDropdown"; diff --git a/compass/components/Table/ResourceTable.tsx b/compass/components/Table/ResourceTable.tsx index a1e6391..590b264 100644 --- a/compass/components/Table/ResourceTable.tsx +++ b/compass/components/Table/ResourceTable.tsx @@ -118,6 +118,9 @@ export default function ResourceTable({ ), + meta: { + filterVariant: "select", + } }), columnHelper.accessor("summary", { header: () => ( diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 538e546..95b0047 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -5,17 +5,13 @@ import { getCoreRowModel, flexRender, createColumnHelper, + getFilteredRowModel, + ColumnFiltersState, + getSortedRowModel, + SortingState, } from "@tanstack/react-table"; -import { - ChangeEvent, - useState, - useEffect, - Key, - Dispatch, - SetStateAction, -} from "react"; +import { ChangeEvent, useState, Dispatch, SetStateAction } from "react"; import { TableAction } from "./TableAction"; -import { PlusIcon } from "@heroicons/react/24/solid"; import { rankItem } from "@tanstack/match-sorter-utils"; import { RowOptionMenu } from "./RowOptionMenu"; import DataPoint from "@/utils/models/DataPoint"; @@ -81,7 +77,8 @@ export default function Table({ createEndpoint, isAdmin = false, }: TableProps) { - console.log(data); + const [filters, setFilters] = useState([]); + const [sorting, setSorting] = useState([]); const columnHelper = createColumnHelper(); @@ -166,10 +163,16 @@ export default function Table({ }, state: { globalFilter: query, + columnFilters: filters, + sorting, }, onGlobalFilterChange: setQuery, globalFilterFn: fuzzyFilter, getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setFilters, + onSortingChange: setSorting, }); return ( @@ -185,12 +188,24 @@ export default function Table({ {header.isPlaceholder ? null @@ -198,6 +213,14 @@ export default function Table({ header.column.columnDef.header, header.getContext() )} + {{ + asc: "🔼", + desc: "🔽", + }[header.column.getIsSorted() as string] ?? + null} + {header.column.getCanFilter() && ( + + )} ))} @@ -266,3 +289,19 @@ export default function Table({ ); } + +function Filter({ column }: { column: any }) { + return ( +
+ { + column.setFilterValue(e.target.value); + }} + placeholder="Search..." + className="border border-gray-300 rounded p-1" + /> +
+ ); +} diff --git a/compass/components/Table/TableAction.tsx b/compass/components/Table/TableAction.tsx index c8cecc9..ff9a77f 100644 --- a/compass/components/Table/TableAction.tsx +++ b/compass/components/Table/TableAction.tsx @@ -1,7 +1,7 @@ // TableAction.tsx import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react"; -import { FilterBox } from "../FilterBox"; +import { FilterBox } from "../FilterBox/FilterBox"; type TableActionProps = { query: string; From f84231e64c3e543fa34e12ef42b826c2ee9facf8 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 13 Apr 2025 14:01:14 -0400 Subject: [PATCH 2/6] Added filter and sort functionality and dropdowns on each column --- compass/components/Table/ColumnHeader.tsx | 188 ++++++++++++++++++ compass/components/Table/Table.tsx | 71 +------ .../{TableAction.tsx => TableSearch.tsx} | 25 +-- 3 files changed, 200 insertions(+), 84 deletions(-) create mode 100644 compass/components/Table/ColumnHeader.tsx rename compass/components/Table/{TableAction.tsx => TableSearch.tsx} (64%) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx new file mode 100644 index 0000000..55fabc5 --- /dev/null +++ b/compass/components/Table/ColumnHeader.tsx @@ -0,0 +1,188 @@ +import { flexRender, Header } from "@tanstack/react-table"; +import { useState, useEffect, useRef } from "react"; +import { + CheckIcon, + ArrowUpIcon, + ArrowDownIcon, + FunnelIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; + +function DropdownCheckIcon({ className }: { className?: string }) { + return ( + + ); +} + +/** + * Component for rendering the header of a table column, + * as well as the dropdown menu for sorting and filtering. + */ +export function ColumnHeader({ header }: { header: Header }) { + const { column } = header; + + const [dropdownType, setDropdownType] = useState<"menu" | "filter" | null>( + null + ); + const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>( + null + ); + + const isFiltered = + column.getFilterValue() !== undefined && + column.getFilterValue() !== null && + column.getFilterValue() !== ""; + + const headerRef = useRef(null); + const menuRef = useRef(null); + const filterRef = useRef(null); + + // Close the dropdown menu/filter input when clicking outside of it + useEffect(() => { + const handleOutsideClick = (e: MouseEvent) => { + const target = e.target as Node; + const clickOutsideMenu = + menuRef.current && !menuRef.current.contains(target); + const clickOutsideFilter = + filterRef.current && !filterRef.current.contains(target); + + if (clickOutsideMenu || clickOutsideFilter) { + setDropdownType(null); + } + }; + document.addEventListener("click", handleOutsideClick); + return () => { + document.removeEventListener("click", handleOutsideClick); + }; + }, [dropdownType]); + + // Set the sort direction based on the current state + useEffect(() => { + switch (sortDirection) { + case "asc": + column.toggleSorting(false); + break; + case "desc": + column.toggleSorting(true); + break; + default: + column.clearSorting(); + } + }, [sortDirection, column]); + + return ( + +
+ {header.isPlaceholder ? null : ( +
+ setDropdownType((prev) => + prev === null ? "menu" : null + ) + } + > +
+ {flexRender( + column.columnDef.header, + header.getContext() + )} + {/* Choose the icon based on sort direction */} + {{ + asc: , + desc: ( + + ), + }[column.getIsSorted() as string] ?? null} +
+
+ )} +
+
+ {/* Dropdown menu to add sorting or filter */} + {column.getCanFilter() && dropdownType === "menu" && ( +
+ + + +
+ )} + {/* Dropdown menu to add a filter value */} + {column.getCanFilter() && dropdownType === "filter" && ( +
+
+ Contains + { + column.setFilterValue(e.target.value); + }} + placeholder="Filter..." + className="border border-gray-300 rounded p-1" + /> +
+
+ )} +
+ + ); +} diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 95b0047..d5e48d1 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -11,12 +11,13 @@ import { SortingState, } from "@tanstack/react-table"; import { ChangeEvent, useState, Dispatch, SetStateAction } from "react"; -import { TableAction } from "./TableAction"; -import { rankItem } from "@tanstack/match-sorter-utils"; -import { RowOptionMenu } from "./RowOptionMenu"; +import { TableSearch } from "@/components/Table/TableSearch"; +import { RowOptionMenu } from "@/components/Table/RowOptionMenu"; +import { ColumnHeader } from "@/components/Table/ColumnHeader"; +import CreateDrawer from "@/components/Drawer/CreateDrawer"; +import { Details } from "@/components/Drawer/Drawer"; import DataPoint from "@/utils/models/DataPoint"; -import CreateDrawer from "../Drawer/CreateDrawer"; -import { Details } from "../Drawer/Drawer"; +import { rankItem } from "@tanstack/match-sorter-utils"; type TableProps = { data: T[]; @@ -150,10 +151,6 @@ export default function Table({ setQuery(String(target.value)); }; - // TODO: Filtering - - // TODO: Sorting - // Define Tanstack table const table = useReactTable({ columns, @@ -178,50 +175,14 @@ export default function Table({ return (
- +
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, i) => ( - + ))} ))} @@ -289,19 +250,3 @@ export default function Table({ ); } - -function Filter({ column }: { column: any }) { - return ( -
- { - column.setFilterValue(e.target.value); - }} - placeholder="Search..." - className="border border-gray-300 rounded p-1" - /> -
- ); -} diff --git a/compass/components/Table/TableAction.tsx b/compass/components/Table/TableSearch.tsx similarity index 64% rename from compass/components/Table/TableAction.tsx rename to compass/components/Table/TableSearch.tsx index ff9a77f..3364b66 100644 --- a/compass/components/Table/TableAction.tsx +++ b/compass/components/Table/TableSearch.tsx @@ -1,20 +1,17 @@ -// TableAction.tsx import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import { ChangeEventHandler, FunctionComponent, useRef, useState } from "react"; -import { FilterBox } from "../FilterBox/FilterBox"; -type TableActionProps = { +type TableSearchProps = { query: string; handleChange: ChangeEventHandler; }; -export const TableAction: FunctionComponent = ({ +export const TableSearch: FunctionComponent = ({ query, handleChange, }) => { const searchInput = useRef(null); const [searchActive, setSearchActive] = useState(false); - const [showFilterBox, setShowFilterBox] = useState(false); const activateSearch = () => { setSearchActive(true); @@ -25,29 +22,15 @@ export const TableAction: FunctionComponent = ({ searchInput.current.addEventListener("focusout", () => { if (searchInput.current?.value.trim() === "") { searchInput.current.value = ""; - deactivateSearch(); + setSearchActive(false); } }); }; - const deactivateSearch = () => setSearchActive(false); - - const toggleFilterBox = () => setShowFilterBox((prev) => !prev); - return (
- Filter - - {showFilterBox && } - - Sort - - From 2e0f434a6c0fd047ef76a806c7c494a7da951fac Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 13 Apr 2025 14:42:57 -0400 Subject: [PATCH 3/6] added line between sorting and filtering in dropdown --- compass/components/Table/ColumnHeader.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx index 55fabc5..4948a4b 100644 --- a/compass/components/Table/ColumnHeader.tsx +++ b/compass/components/Table/ColumnHeader.tsx @@ -107,7 +107,7 @@ export function ColumnHeader({ header }: { header: Header }) { {column.getCanFilter() && dropdownType === "menu" && (
+
), - meta: { - filterVariant: "select", - } }), columnHelper.accessor("summary", { header: () => ( From ee4bea60ac2db0d203c3445a645f903785f3c7ff Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 13 Apr 2025 15:15:48 -0400 Subject: [PATCH 6/6] removed another another unnecessary change --- compass/components/FilterBox/{FilterBox.tsx => index.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename compass/components/FilterBox/{FilterBox.tsx => index.tsx} (100%) diff --git a/compass/components/FilterBox/FilterBox.tsx b/compass/components/FilterBox/index.tsx similarity index 100% rename from compass/components/FilterBox/FilterBox.tsx rename to compass/components/FilterBox/index.tsx
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: "🔼", - desc: "🔽", - }[header.column.getIsSorted() as string] ?? - null} - {header.column.getCanFilter() && ( - - )} -