From 2c5fd0ea76d16743420e8cc6c380042227b837e5 Mon Sep 17 00:00:00 2001 From: Nick A Date: Thu, 3 Apr 2025 21:37:08 -0400 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 06/11] 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 From 3750fcf66f8c5d614d9f0540c0dadfeaa1636edb Mon Sep 17 00:00:00 2001 From: Nick A Date: Fri, 18 Apr 2025 15:54:06 -0400 Subject: [PATCH 07/11] small code quality changes --- compass/components/Table/ColumnHeader.tsx | 39 +++++++++++------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx index 4948a4b..5d8e307 100644 --- a/compass/components/Table/ColumnHeader.tsx +++ b/compass/components/Table/ColumnHeader.tsx @@ -29,9 +29,7 @@ export function ColumnHeader({ header }: { header: Header }) { ); const isFiltered = - column.getFilterValue() !== undefined && - column.getFilterValue() !== null && - column.getFilterValue() !== ""; + column.getFilterValue() != null && column.getFilterValue() !== ""; const headerRef = useRef(null); const menuRef = useRef(null); @@ -92,12 +90,16 @@ export function ColumnHeader({ header }: { header: Header }) { header.getContext() )} {/* Choose the icon based on sort direction */} - {{ - asc: , - desc: ( - - ), - }[column.getIsSorted() as string] ?? null} + { + { + asc: ( + + ), + desc: ( + + ), + }[column.getIsSorted() as "asc" | "desc"] + }
)} @@ -137,26 +139,23 @@ export function ColumnHeader({ header }: { header: Header }) {
@@ -177,7 +176,7 @@ export function ColumnHeader({ header }: { header: Header }) { onChange={(e) => { column.setFilterValue(e.target.value); }} - placeholder="Filter..." + placeholder="Type a value…" className="border border-gray-300 rounded p-1" /> From c21d12b40850a9ce297b63ab0820ca32e7583d03 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 13:04:39 -0400 Subject: [PATCH 08/11] added tags filter --- compass/components/Table/ColumnHeader.tsx | 50 +++++++++++++++-------- compass/components/Table/ServiceTable.tsx | 21 +++++++++- compass/components/Table/Table.tsx | 27 +++++++++--- compass/components/Table/UserTable.tsx | 14 ++++++- compass/components/TagsInput/Index.tsx | 46 +++++++++++++++++---- compass/package-lock.json | 16 +++++++- compass/package.json | 1 + 7 files changed, 139 insertions(+), 36 deletions(-) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx index 5d8e307..8520297 100644 --- a/compass/components/Table/ColumnHeader.tsx +++ b/compass/components/Table/ColumnHeader.tsx @@ -7,6 +7,15 @@ import { FunnelIcon, XMarkIcon, } from "@heroicons/react/24/outline"; +import { Details } from "../Drawer/Drawer"; +import FilterDropdown, { FilterFn } from "./FilterDropdown"; +import DataPoint from "@/utils/models/DataPoint"; + +interface ColumnHeaderProps { + header: Header; + details: Details | undefined; + setFilterFn?: (field: string, filterFn: FilterFn) => void; +} function DropdownCheckIcon({ className }: { className?: string }) { return ( @@ -18,7 +27,11 @@ function DropdownCheckIcon({ className }: { className?: string }) { * 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 }) { +export function ColumnHeader({ + header, + details, + setFilterFn, +}: ColumnHeaderProps) { const { column } = header; const [dropdownType, setDropdownType] = useState<"menu" | "filter" | null>( @@ -68,23 +81,33 @@ export function ColumnHeader({ header }: { header: Header }) { } }, [sortDirection, column]); + if (!details) { + return
; + } + return (
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - {{ - asc: "🔼", - desc: "🔽", - }[header.column.getIsSorted() as string] ?? - null} - {header.column.getCanFilter() && ( - - )} -
{header.isPlaceholder ? null : (
setDropdownType((prev) => prev === null ? "menu" : null ) } > -
+
{flexRender( column.columnDef.header, header.getContext() @@ -166,20 +189,11 @@ export function ColumnHeader({ header }: { header: Header }) { ref={filterRef} className="absolute -top-2 left-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10" > -
- Contains - { - column.setFilterValue(e.target.value); - }} - placeholder="Type a value…" - className="border border-gray-300 rounded p-1" - /> -
+
)}
diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx index 237c3e2..e9d50be 100644 --- a/compass/components/Table/ServiceTable.tsx +++ b/compass/components/Table/ServiceTable.tsx @@ -1,11 +1,10 @@ import { - Bars2Icon, CheckCircleIcon, DocumentTextIcon, ListBulletIcon, UserIcon, } from "@heroicons/react/24/solid"; -import { Dispatch, SetStateAction, useState } from "react"; +import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import Table from "@/components/Table/Table"; import { RowOpenAction } from "@/components/Table/RowOpenAction"; @@ -13,6 +12,7 @@ import Service from "@/utils/models/Service"; import { Details } from "../Drawer/Drawer"; import { Tag } from "../TagsInput/Tag"; import User from "@/utils/models/User"; +import { FilterFn } from "./FilterDropdown"; type ServiceTableProps = { data: Service[]; @@ -31,6 +31,8 @@ export default function ServiceTable({ user, }: ServiceTableProps) { const columnHelper = createColumnHelper(); + const [requirementsFilterFn, setRequirementsFilterFn] = + useState("arrIncludesSome"); const [programPresets, setProgramPresets] = useState([ "DOMESTIC", @@ -151,6 +153,13 @@ export default function ServiceTable({
), + filterFn: (row, columnId, filterValue) => { + const rowValue = row.getValue(columnId); + if (Array.isArray(filterValue)) { + return filterValue.includes(rowValue); + } + return true; + }, }), columnHelper.accessor("requirements", { header: () => ( @@ -170,6 +179,7 @@ export default function ServiceTable({ )}
), + filterFn: requirementsFilterFn, }), columnHelper.accessor("summary", { header: () => ( @@ -188,11 +198,18 @@ export default function ServiceTable({ }), ]; + const setFilterFn = (field: string, filterFn: FilterFn) => { + if (field === "requirements") { + setRequirementsFilterFn(filterFn); + } + }; + return ( = { data: T[]; setData: Dispatch>; columns: ColumnDef[]; + setFilterFn?: (field: string, filterFn: FilterFn) => void; details: Details[]; createEndpoint: string; isAdmin?: boolean; @@ -74,6 +81,7 @@ export default function Table({ data, setData, columns, + setFilterFn, details, createEndpoint, isAdmin = false, @@ -182,7 +190,14 @@ export default function Table({ {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header, i) => ( - + d.key === header.column.id + )} + setFilterFn={setFilterFn} + key={header.id} + /> ))} ))} @@ -199,9 +214,11 @@ export default function Table({ {row.getVisibleCells().map((cell, i) => (
{flexRender( cell.column.columnDef.cell, diff --git a/compass/components/Table/UserTable.tsx b/compass/components/Table/UserTable.tsx index 3754d83..43798ec 100644 --- a/compass/components/Table/UserTable.tsx +++ b/compass/components/Table/UserTable.tsx @@ -10,6 +10,7 @@ import { RowOpenAction } from "@/components/Table/RowOpenAction"; import User from "@/utils/models/User"; import { Details } from "../Drawer/Drawer"; import { Tag } from "../TagsInput/Tag"; +import { FilterFn } from "./FilterDropdown"; type UserTableProps = { data: User[]; @@ -24,6 +25,8 @@ type UserTableProps = { */ export default function UserTable({ data, setData, user }: UserTableProps) { const columnHelper = createColumnHelper(); + const [programFilterFn, setProgramFilterFn] = + useState("arrIncludesSome"); const [rolePresets, setRolePresets] = useState([ "ADMIN", @@ -103,6 +106,7 @@ export default function UserTable({ data, setData, user }: UserTableProps) { ), + filterFn: "arrIncludesSome", }), columnHelper.accessor("email", { header: () => ( @@ -133,14 +137,22 @@ export default function UserTable({ data, setData, user }: UserTableProps) { )} ), + filterFn: programFilterFn, }), ]; + const setFilterFn = (field: string, filterFn: FilterFn) => { + if (field === "program") { + setProgramFilterFn(filterFn); + } + }; + return ( - + >; onTagsChange?: (tags: Set) => void; singleValue?: boolean; + cellSelectedPreset?: boolean; + filterState?: [FilterFn | null, Dispatch>]; } const TagsInput: React.FC = ({ @@ -18,9 +21,11 @@ const TagsInput: React.FC = ({ setPresetOptions, onTagsChange, singleValue = false, + cellSelectedPreset = false, + filterState, }) => { const [inputValue, setInputValue] = useState(""); - const [cellSelected, setCellSelected] = useState(false); + const [cellSelected, setCellSelected] = useState(cellSelectedPreset); // TODO: Add tags to the database and remove the presetValue and lowercasing const [tags, setTags] = useState>(new Set(presetValue)); @@ -31,7 +36,7 @@ const TagsInput: React.FC = ({ const dropdown = useRef(null); const handleClick = () => { - if (!cellSelected) { + if (!cellSelectedPreset) { setCellSelected(true); setTimeout(() => { window.addEventListener("click", handleOutsideClick); @@ -138,19 +143,42 @@ const TagsInput: React.FC = ({ } }; + const FilterSelect = () => { + const [filter, setFilter] = filterState ?? [null, null]; + return ( + filter != null && + setFilter != null && ( + + ) + ); + }; + return (
- {!cellSelected ? ( - + {!cellSelectedPreset ? ( + <> + + + ) : (
-
+
+ =16.8" } }, - "node_modules/@tanstack/table-core": { + "node_modules/@tanstack/react-table/node_modules/@tanstack/table-core": { "version": "8.21.2", "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz", "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==", @@ -668,6 +669,19 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", diff --git a/compass/package.json b/compass/package.json index 9ac0308..cf9d20c 100644 --- a/compass/package.json +++ b/compass/package.json @@ -15,6 +15,7 @@ "@supabase/supabase-js": "^2.42.3", "@tanstack/match-sorter-utils": "^8.15.1", "@tanstack/react-table": "^8.15.0", + "@tanstack/table-core": "^8.21.3", "bufferutil": "^4.0.8", "next": "^13.5.8", "react": "^18", From b83298ed04377bb8a23c5abbf0ea9a0da3b2fe2e Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 13:06:02 -0400 Subject: [PATCH 09/11] added filter dropdown --- compass/components/Table/FilterDropdown.tsx | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 compass/components/Table/FilterDropdown.tsx diff --git a/compass/components/Table/FilterDropdown.tsx b/compass/components/Table/FilterDropdown.tsx new file mode 100644 index 0000000..880a206 --- /dev/null +++ b/compass/components/Table/FilterDropdown.tsx @@ -0,0 +1,84 @@ +import DataPoint from "@/utils/models/DataPoint"; +import { Column } from "@tanstack/react-table"; +import { Details } from "../Drawer/Drawer"; +import { useEffect, useState } from "react"; +import TagsInput from "../TagsInput/Index"; + +export type FilterFn = "arrIncludesSome" | "arrIncludesAll"; + +interface FilterDropdownProps { + details: Details; + column: Column; + setFilterFn?: (field: string, filterFn: FilterFn) => void; +} + +export default function FilterDropdown({ + details, + column, + setFilterFn, +}: FilterDropdownProps) { + const filterState = useState( + details.inputType === "select-multiple" || + details.inputType === "select-one" + ? "arrIncludesSome" + : null + ); + const [filter] = filterState; + const { inputType, presetOptionsValues, presetOptionsSetter } = details; + + useEffect(() => { + if (filter && setFilterFn) { + setFilterFn(details.key, filter); + column.setFilterValue((prev: any) => prev); + } + }, [details.key, filter, setFilterFn, column]); + + switch (inputType) { + case "select-one": + return ( +
+ {})} + presetValue={[]} + onTagsChange={(tags) => { + const tagsArray = Array.from(tags); + column.setFilterValue(tagsArray); + }} + cellSelectedPreset={true} + /> +
+ ); + case "select-multiple": + return ( +
+ {})} + presetValue={[]} + onTagsChange={(tags) => { + const tagsArray = Array.from(tags); + column.setFilterValue(tagsArray); + }} + cellSelectedPreset={true} + filterState={filterState} + /> +
+ ); + default: + return ( +
+ Contains + { + column.setFilterValue(e.target.value); + }} + placeholder="Type a value…" + className="border border-gray-300 rounded p-1" + /> +
+ ); + } +} From 15ad26722581ab2a60cbe3f06ad3142d620b9114 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 15:47:59 -0400 Subject: [PATCH 10/11] main branch integraiton --- compass/components/Table/ColumnHeader.tsx | 4 +++- compass/components/Table/Table.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx index 8520297..f4adcdb 100644 --- a/compass/components/Table/ColumnHeader.tsx +++ b/compass/components/Table/ColumnHeader.tsx @@ -14,6 +14,7 @@ import DataPoint from "@/utils/models/DataPoint"; interface ColumnHeaderProps { header: Header; details: Details | undefined; + hasHorizontalBorders: boolean; setFilterFn?: (field: string, filterFn: FilterFn) => void; } @@ -30,6 +31,7 @@ function DropdownCheckIcon({ className }: { className?: string }) { export function ColumnHeader({ header, details, + hasHorizontalBorders, setFilterFn, }: ColumnHeaderProps) { const { column } = header; @@ -90,7 +92,7 @@ export function ColumnHeader({ scope="col" className={`border-gray-200 border-y font-medium ${ isFiltered ? "bg-purple-50" : "" - }`} + } ${hasHorizontalBorders ? "border-x" : ""}`} ref={headerRef} >
diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 603e582..2ce3dc5 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -251,7 +251,7 @@ export default function Table({ key={cell.id} className={`[&:nth-child(n+${ 2 + offset - })]:border-x pl-2 relative first:text-left first:px-0 last:border-none ${ + })]:border-x px-2 relative first:text-left first:px-0 last:border-none ${ cell.column.getIsFiltered() ? "bg-purple-50" : "" From 81d3a4b7aa7706ec79d82640172fd7e3eee5e403 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 16:27:13 -0400 Subject: [PATCH 11/11] documentation and small fixes --- compass/components/Table/ColumnHeader.tsx | 12 +++++++----- compass/components/Table/FilterDropdown.tsx | 9 ++++++++- compass/components/Table/ServiceTable.tsx | 3 ++- compass/components/Table/Table.tsx | 20 +++++--------------- compass/components/TagsInput/Index.tsx | 4 ++-- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/compass/components/Table/ColumnHeader.tsx b/compass/components/Table/ColumnHeader.tsx index f4adcdb..f955c18 100644 --- a/compass/components/Table/ColumnHeader.tsx +++ b/compass/components/Table/ColumnHeader.tsx @@ -14,8 +14,8 @@ import DataPoint from "@/utils/models/DataPoint"; interface ColumnHeaderProps { header: Header; details: Details | undefined; - hasHorizontalBorders: boolean; setFilterFn?: (field: string, filterFn: FilterFn) => void; + className?: string; } function DropdownCheckIcon({ className }: { className?: string }) { @@ -27,12 +27,16 @@ function DropdownCheckIcon({ className }: { className?: string }) { /** * Component for rendering the header of a table column, * as well as the dropdown menu for sorting and filtering. + * @param props.header The header object from TanStack Table. + * @param props.details The details object containing metadata about the column. + * @param props.setFilterFn Include this state setter if the column has multiple filter options. + * @param props.className Optional additional class names for styling. */ export function ColumnHeader({ header, details, - hasHorizontalBorders, setFilterFn, + className }: ColumnHeaderProps) { const { column } = header; @@ -46,7 +50,6 @@ export function ColumnHeader({ const isFiltered = column.getFilterValue() != null && column.getFilterValue() !== ""; - const headerRef = useRef(null); const menuRef = useRef(null); const filterRef = useRef(null); @@ -92,8 +95,7 @@ export function ColumnHeader({ scope="col" className={`border-gray-200 border-y font-medium ${ isFiltered ? "bg-purple-50" : "" - } ${hasHorizontalBorders ? "border-x" : ""}`} - ref={headerRef} + } ${className ?? ""}`} >
{header.isPlaceholder ? null : ( diff --git a/compass/components/Table/FilterDropdown.tsx b/compass/components/Table/FilterDropdown.tsx index 880a206..ce9d917 100644 --- a/compass/components/Table/FilterDropdown.tsx +++ b/compass/components/Table/FilterDropdown.tsx @@ -12,6 +12,12 @@ interface FilterDropdownProps { setFilterFn?: (field: string, filterFn: FilterFn) => void; } +/** + * Component for rendering a dropdown menu when adding a filter to a column. + * @param props.details The details object containing metadata about the column. + * @param props.column The column object from TanStack Table. + * @param props.setFilterFn Include this state setter if the column has multiple filter options. + */ export default function FilterDropdown({ details, column, @@ -26,10 +32,11 @@ export default function FilterDropdown({ const [filter] = filterState; const { inputType, presetOptionsValues, presetOptionsSetter } = details; + // Update the column filter function when the state changes useEffect(() => { if (filter && setFilterFn) { setFilterFn(details.key, filter); - column.setFilterValue((prev: any) => prev); + column.setFilterValue((prev: any) => prev); // Trigger a re-render based on new filter value } }, [details.key, filter, setFilterFn, column]); diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx index 5ac36d6..1a60575 100644 --- a/compass/components/Table/ServiceTable.tsx +++ b/compass/components/Table/ServiceTable.tsx @@ -4,7 +4,7 @@ import { ListBulletIcon, UserIcon, } from "@heroicons/react/24/solid"; -import { Dispatch, SetStateAction, useMemo, useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import Table from "@/components/Table/Table"; import { RowOpenAction } from "@/components/Table/RowOpenAction"; @@ -153,6 +153,7 @@ export default function ServiceTable({
), + // Filter by if the value is in the tags array filterFn: (row, columnId, filterValue) => { const rowValue = row.getValue(columnId); if (Array.isArray(filterValue)) { diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 2ce3dc5..e0c6ecf 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -72,6 +72,8 @@ const fuzzyFilter = ( * @param props.data Stateful list of data to be held in the table * @param props.setData State setter for the list of data * @param props.columns Column definitions made with Tanstack columnHelper + * @param props.setFilterFn This optional state setter should change the filter funciton of the provided column if possible. + * It should be included if the column has multiple filter options. */ export default function Table({ data, @@ -116,20 +118,6 @@ export default function Table({ const visibilitySort = (a: T, b: T) => a.visible === b.visible ? 0 : a.visible ? -1 : 1; - // // Sort data on load - // useEffect(() => { - // setData((prevData) => prevData.sort(visibilitySort)); - // }, [setData]); - - // // Data manipulation methods - // // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData) - // const deleteData = (dataId: number) => { - // console.log(data); - // setData((currentData) => - // currentData.filter((data) => data.id !== dataId) - // ); - // }; - const hideData = (dataId: number) => { console.log(`Toggling visibility for data with ID: ${dataId}`); setData((currentData) => { @@ -228,8 +216,10 @@ export default function Table({ (d) => d.key === header.column.id )} setFilterFn={setFilterFn} - hasHorizontalBorders={ + className={ offset < i && i < columns.length - 1 + ? "border-x" + : "" } key={header.id} /> diff --git a/compass/components/TagsInput/Index.tsx b/compass/components/TagsInput/Index.tsx index cab091b..697bfcb 100644 --- a/compass/components/TagsInput/Index.tsx +++ b/compass/components/TagsInput/Index.tsx @@ -36,7 +36,7 @@ const TagsInput: React.FC = ({ const dropdown = useRef(null); const handleClick = () => { - if (!cellSelectedPreset) { + if (!cellSelected) { setCellSelected(true); setTimeout(() => { window.addEventListener("click", handleOutsideClick); @@ -164,7 +164,7 @@ const TagsInput: React.FC = ({ return (
- {!cellSelectedPreset ? ( + {!cellSelected ? ( <>