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 - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: "🔼", - desc: "🔽", - }[header.column.getIsSorted() as string] ?? - null} - {header.column.getCanFilter() && ( - - )} -