From 474f7c03c0d66b4359c09fb8aada76352d8d1afc Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Thu, 22 Aug 2024 15:34:42 +0100 Subject: [PATCH 01/15] wip: enable filtering date columns --- app/events/EventTable.tsx | 4 +- app/students/StudentTable.tsx | 3 +- components/DateTimePicker.tsx | 3 + components/TanstackTable.tsx | 119 ++++++++++++++++++++++------------ 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/app/events/EventTable.tsx b/app/events/EventTable.tsx index ac3d1298..56ded15a 100644 --- a/app/events/EventTable.tsx +++ b/app/events/EventTable.tsx @@ -2,7 +2,7 @@ import { DeleteEvent } from "@/components/DeleteEvent" import Link from "@/components/Link" -import TanstackTable from "@/components/TanstackTable" +import TanstackTable, { dateFilterFn } from "@/components/TanstackTable" import { EditEvent } from "@/components/UpsertEvent" import styles from "./eventTable.module.scss" @@ -73,7 +73,7 @@ const EventTable = ({ header: "Start Date", sortingFn: "datetime", id: "dateStart", - enableColumnFilter: false, + filterFn: dateFilterFn, }, shortDescription: { cell: info => info.getValue(), diff --git a/app/students/StudentTable.tsx b/app/students/StudentTable.tsx index cd5c1bc1..3df034ab 100644 --- a/app/students/StudentTable.tsx +++ b/app/students/StudentTable.tsx @@ -1,7 +1,7 @@ "use client" import Link from "@/components/Link" -import TanstackTable from "@/components/TanstackTable" +import TanstackTable, { dateFilterFn } from "@/components/TanstackTable" import UserAvatar from "@/components/UserAvatar" import { StudentProfile, User } from "@prisma/client" @@ -46,6 +46,7 @@ const StudentTable = ({ sortingFn: "datetime", id: "graduationDate", enableColumnFilter: true, + filterFn: dateFilterFn, }, course: { cell: info => info.getValue(), diff --git a/components/DateTimePicker.tsx b/components/DateTimePicker.tsx index 1c71b4bc..1d1b8829 100644 --- a/components/DateTimePicker.tsx +++ b/components/DateTimePicker.tsx @@ -13,11 +13,13 @@ const DateTimePicker = ({ placeholder, required = false, defaultDate, + onChange, }: { name: string placeholder: string required?: boolean defaultDate?: Date | null + onChange?: (e: React.ChangeEvent) => void }) => { return ( ) } diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 7dd84f31..69aff0f7 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -1,6 +1,7 @@ "use client" import Chip from "./Chip" +import DateTimePicker from "./DateTimePicker" import { Dropdown } from "./Dropdown" import styles from "./tanstack-table.module.scss" @@ -19,6 +20,7 @@ import { Box, Button, Flex, Grid, IconButton, Table, Text } from "@radix-ui/them import { ColumnDef, ColumnFiltersState, + Row, SortDirection, SortingState, VisibilityState, @@ -29,6 +31,7 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table" +import { isAfter } from "date-fns" import React, { useEffect, useMemo, useState } from "react" import { Pagination } from "react-headless-pagination" import { useMediaQuery } from "react-responsive" @@ -43,6 +46,9 @@ interface TanstackTableProps { invisibleColumns?: VisibilityState } +export const dateFilterFn = (row: Row, id: string, filterValue: any[]): boolean => + isAfter(row.getValue(id), filterValue[0]) && (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { case false: @@ -95,6 +101,10 @@ export default function TanstackTable({ setIsClient(true) }, []) + const dateFilterColumns = useMemo(() => { + return columns.filter(column => column.sortingFn === "datetime").map(column => column.id) + }, [columns]) + const filterableColumns = useMemo( () => table @@ -107,8 +117,9 @@ export default function TanstackTable({ const [searchQuery, setSearchQuery] = useState("") const [currentFilteredColumn, setCurrentFilteredColumn] = useState(filterableColumns[0]?.id ?? "") - const [prevFilters, setPrevFilters] = useState([]) + const [dateStart, setDateStart] = useState("") + const [dateEnd, setDateEnd] = useState("") if (!isClient) { return ( @@ -134,52 +145,76 @@ export default function TanstackTable({ setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) } + const updateChips = (value: any) => { + const newFilters = [ + ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value }, // Add the new filter for this column + ] + + // Live-update filter chips which are currently displayed (i.e. in prevFilters) + if (prevFilters.find(f => f.id === currentFilteredColumn)) { + if (!value) { + // Delete the chip if the search query is now empty + setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) + } else { + // Update the chip with the new search query + setPrevFilters(newFilters) + } + } + + // Update the actual filters that are applied to the table + setColumnFilters(newFilters) + } + return ( {enableSearch && ( - - { - if (e.key === "Enter") { - addFilter() - } - }} - onChange={e => { - setSearchQuery(e.target.value) - - const newFilters = [ - ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value: e.target.value }, // Add the new filter for this column - ] - - // Live-update filter chips which are currently displayed (i.e. in prevFilters) - if (prevFilters.find(f => f.id === currentFilteredColumn)) { - if (e.target.value === "") { - // Delete the chip if the search query is now empty - setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) - } else { - // Update the chip with the new search query - setPrevFilters(newFilters) + + {!dateFilterColumns.includes(currentFilteredColumn) ? ( + { + if (e.key === "Enter") { + addFilter() } - } - - // Update the actual filters that are applied to the table - setColumnFilters(newFilters) - }} - value={searchQuery} - > - - - - - - - + }} + onChange={e => { + setSearchQuery(e.target.value) + updateChips(e.target.value) + }} + value={searchQuery} + > + + + + + + + + ) : ( + + { + setDateStart(e.target.value) + updateChips([e.target.value, dateEnd]) + }} + /> + { + setDateEnd(e.target.value) + updateChips([dateStart, e.target.value]) + }} + /> + + )} ({ item: col.columnDef.header!.toString(), From c06a3b935c879ce0b4f240da507b5bcf8ce00b68 Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Thu, 22 Aug 2024 15:38:22 +0100 Subject: [PATCH 02/15] style: change filter chips to show column header instead of id --- components/TanstackTable.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 69aff0f7..0fd0e471 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -231,7 +231,12 @@ export default function TanstackTable({ {prevFilters.map(({ id, value }, index) => ( - deleteFilter(index)} /> + def.id === id)?.header} includes "${value}"`} + deletable + onDelete={() => deleteFilter(index)} + /> ))} From 70c2c85c075e1d25e37e4edc2260abff2ef4e4b8 Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Thu, 22 Aug 2024 15:44:15 +0100 Subject: [PATCH 03/15] feat: disable add filter button if search queries not filled in properly --- components/TanstackTable.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 0fd0e471..98852cde 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -47,7 +47,8 @@ interface TanstackTableProps { } export const dateFilterFn = (row: Row, id: string, filterValue: any[]): boolean => - isAfter(row.getValue(id), filterValue[0]) && (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + (!filterValue[0] || isAfter(row.getValue(id), filterValue[0])) && + (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { @@ -227,7 +228,12 @@ export default function TanstackTable({ title: "Filter by column", }} /> - + {prevFilters.map(({ id, value }, index) => ( From 1befb4fc746bcee52500f8b85033942e8882fba0 Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Thu, 22 Aug 2024 16:31:12 +0100 Subject: [PATCH 04/15] feat: allow date ranges as filter Co-authored-by: Alexander Biraben-Renard Co-authored-by: IlliaDerevianko Co-authored-by: nick-bolas --- components/DateTimePicker.tsx | 8 ++++++-- components/TanstackTable.tsx | 31 ++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/components/DateTimePicker.tsx b/components/DateTimePicker.tsx index 1d1b8829..6c6a3a33 100644 --- a/components/DateTimePicker.tsx +++ b/components/DateTimePicker.tsx @@ -14,21 +14,25 @@ const DateTimePicker = ({ required = false, defaultDate, onChange, + showTime = true, }: { name: string placeholder: string required?: boolean defaultDate?: Date | null onChange?: (e: React.ChangeEvent) => void + showTime?: boolean }) => { return ( ) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 98852cde..f0642966 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -47,8 +47,8 @@ interface TanstackTableProps { } export const dateFilterFn = (row: Row, id: string, filterValue: any[]): boolean => - (!filterValue[0] || isAfter(row.getValue(id), filterValue[0])) && - (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + (!filterValue[0] || isAfter(row.getValue(id), filterValue[0] + "T00:00")) && + (!filterValue[1] || isAfter(filterValue[1] + "T23:59", row.getValue(id))) const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { @@ -134,11 +134,18 @@ export default function TanstackTable({ const FooterWrapper = isLowWidth ? Flex : Grid const addFilter = () => { - setPrevFilters([ - ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value: searchQuery }, - ]) - setSearchQuery("") + if (dateFilterColumns.includes(currentFilteredColumn)) { + setPrevFilters([ + ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value: [dateStart, dateEnd] }, + ]) + } else { + setPrevFilters([ + ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value: searchQuery }, + ]) + setSearchQuery("") + } } const deleteFilter = (index: number) => { @@ -199,6 +206,7 @@ export default function TanstackTable({ ) : ( { @@ -207,6 +215,7 @@ export default function TanstackTable({ }} /> { @@ -229,7 +238,7 @@ export default function TanstackTable({ }} /> @@ -292,7 +299,7 @@ export default function TanstackTable({ : `${columns.find(def => def.id === id)?.header} includes "${value}"` } deletable - onDelete={() => deleteFilter(index)} + onDelete={() => deleteChip(index)} /> ))} From 151c6b5e9e25b9eeaee8759d64a62c7228954f31 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi Date: Fri, 23 Aug 2024 11:32:13 +0100 Subject: [PATCH 12/15] fix: make reset button clear filter as well --- components/TanstackTable.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 157cd3f8..bb6878ce 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -197,6 +197,15 @@ export default function TanstackTable({ setColumnFilters(newFilters) } + /** Clears the current filter if it is not set as a chip */ + const clearCurrentFilter = () => { + if (!prevFilters.find(f => f.id === currentFilteredColumn)) { + setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) + } + + setSearchQuery("") + } + return ( {enableSearch && ( @@ -221,7 +230,7 @@ export default function TanstackTable({ - @@ -267,13 +276,9 @@ export default function TanstackTable({ }))} defaultValue={filterableColumns[0].id ?? ""} onValueChange={newFilterCol => { - // If not in prevFilters, remove from current filters - if (!prevFilters.find(f => f.id === currentFilteredColumn)) { - setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) - } + clearCurrentFilter() setCurrentFilteredColumn(newFilterCol) - setSearchQuery("") setDateStart("") setDateEnd("") }} From 333bbf44a890c168c01bfc1ccc3f91c24f1e9265 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi Date: Fri, 23 Aug 2024 11:35:51 +0100 Subject: [PATCH 13/15] refactor: useCallback, useCallbacks everywhere --- components/TanstackTable.tsx | 74 +++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index bb6878ce..bf495cee 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -32,7 +32,7 @@ import { useReactTable, } from "@tanstack/react-table" import { format, isAfter } from "date-fns" -import React, { useEffect, useMemo, useState } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { Pagination } from "react-headless-pagination" import { useMediaQuery } from "react-responsive" @@ -136,19 +136,10 @@ export default function TanstackTable({ const [dateStart, setDateStart] = useState("") const [dateEnd, setDateEnd] = useState("") - if (!isClient) { - return ( - - Loading... - - - ) - } - const FooterWrapper = isLowWidth ? Flex : Grid /** Creates a chip to display a filter that the user has created as they were typing into the search box & they have since pressed Add Filter */ - const addChip = () => { + const addChip = useCallback(() => { if (dateFilterColumns.includes(currentFilteredColumn)) { setPrevFilters([ ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column @@ -163,47 +154,62 @@ export default function TanstackTable({ ]) setSearchQuery("") } - } + }, [currentFilteredColumn, dateEnd, dateFilterColumns, dateStart, prevFilters, searchQuery]) /** Unset a filter and delete its chip that is displayed to the user */ - const deleteChip = (index: number) => { - setPrevFilters(prevFilters.filter((_, i) => i !== index)) - setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) - } + const deleteChip = useCallback( + (index: number) => { + setPrevFilters(prevFilters.filter((_, i) => i !== index)) + setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) + }, + [columnFilters, prevFilters], + ) /** * Update the chips, and then set the filters on the table. * * If a filter already exists & is shown as a chip, update the chip instead of setting a new filter. */ - const updateFilters = (value: any) => { - const newFilters = [ - ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value }, // Add the new filter for this column - ] + const updateFilters = useCallback( + (value: any) => { + const newFilters = [ + ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value }, // Add the new filter for this column + ] - // Live-update filter chips which are currently displayed (i.e. in prevFilters) - if (prevFilters.find(f => f.id === currentFilteredColumn)) { - if (!value || (Array.isArray(value) && value.every(v => !v))) { - // Delete the chip if the search query is now empty - setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) - } else { - // Update the chip with the new search query - setPrevFilters(newFilters) + // Live-update filter chips which are currently displayed (i.e. in prevFilters) + if (prevFilters.find(f => f.id === currentFilteredColumn)) { + if (!value || (Array.isArray(value) && value.every(v => !v))) { + // Delete the chip if the search query is now empty + setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) + } else { + // Update the chip with the new search query + setPrevFilters(newFilters) + } } - } - // Update the actual filters that are applied to the table - setColumnFilters(newFilters) - } + // Update the actual filters that are applied to the table + setColumnFilters(newFilters) + }, + [columnFilters, currentFilteredColumn, prevFilters], + ) /** Clears the current filter if it is not set as a chip */ - const clearCurrentFilter = () => { + const clearCurrentFilter = useCallback(() => { if (!prevFilters.find(f => f.id === currentFilteredColumn)) { setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) } setSearchQuery("") + }, [columnFilters, currentFilteredColumn, prevFilters]) + + if (!isClient) { + return ( + + Loading... + + + ) } return ( From 48337b486b321204a233b09f58462b0526c4ec0d Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Fri, 23 Aug 2024 14:51:10 +0100 Subject: [PATCH 14/15] chore: change deadline date format --- app/opportunities/OpportunityTable.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/opportunities/OpportunityTable.tsx b/app/opportunities/OpportunityTable.tsx index aba90b26..15f419e5 100644 --- a/app/opportunities/OpportunityTable.tsx +++ b/app/opportunities/OpportunityTable.tsx @@ -11,7 +11,7 @@ import { getCompanyLink } from "../companies/getCompanyLink" import type { CompanyProfile, Opportunity } from "@prisma/client" import { Flex } from "@radix-ui/themes" import { ColumnDef, DisplayColumnDef, createColumnHelper } from "@tanstack/react-table" -import { formatDistanceToNowStrict } from "date-fns" +import { format, formatDistanceToNowStrict } from "date-fns" import Image from "next/image" import { useMemo } from "react" @@ -90,11 +90,12 @@ const OpportunityTable = ({ filterFn: dateFilterFn, }, deadline: { - cell: info => ( - - ), + cell: info => , + // cell: info => ( + // + // ), header: "Application Deadline", sortingFn: "datetime", id: "deadline", From 3de01734c59bcd8fd9e0479f0c8a5ed2bb93784a Mon Sep 17 00:00:00 2001 From: Matthew Alex Date: Fri, 23 Aug 2024 14:58:49 +0100 Subject: [PATCH 15/15] docs: improve documentation for dateFilterFn --- app/opportunities/OpportunityTable.tsx | 5 ----- components/TanstackTable.tsx | 10 +++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/opportunities/OpportunityTable.tsx b/app/opportunities/OpportunityTable.tsx index 15f419e5..29408b1b 100644 --- a/app/opportunities/OpportunityTable.tsx +++ b/app/opportunities/OpportunityTable.tsx @@ -91,11 +91,6 @@ const OpportunityTable = ({ }, deadline: { cell: info => , - // cell: info => ( - // - // ), header: "Application Deadline", sortingFn: "datetime", id: "deadline", diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index bf495cee..afb74260 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -60,7 +60,15 @@ const formatDateRange = (dates: [string, string]): string => { return "" } -export const dateFilterFn = (row: Row, id: string, filterValue: any[]): boolean => +/** + * Filter function for columns with type date. Keeps rows which are after the start date (if set) and before the end date (if set). + * @template T The type of the row + * @param row The row to filter + * @param id The column id to filter + * @param filterValue The filter value to be applied of the form [start, end] + * @returns Whether the row should be displayed + */ +export const dateFilterFn = (row: Row, id: string, filterValue: [string, string]): boolean => (!filterValue[0] || isAfter(row.getValue(id), filterValue[0] + "T00:00")) && (!filterValue[1] || isAfter(filterValue[1] + "T23:59", row.getValue(id)))