Skip to content

Commit ead27d4

Browse files
Mason table wide search (#114)
* fixes to stuff * Changes to party list component * Changed functionality to be in seperate table List component * Finished global staff seach functionality * Minor map changes * Minor modifications to Page.tsx * Added global search feature with TanStack * Attempted to create custom filter logic * fix: parties table global filter * revert whitespace --------- Co-authored-by: Nick A <[email protected]>
1 parent 2d0d974 commit ead27d4

File tree

3 files changed

+154
-32
lines changed

3 files changed

+154
-32
lines changed

frontend/src/components/PartyTable.tsx

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ import {
88
import { Party } from "@/types/api/party";
99
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
1010
import { ColumnDef } from "@tanstack/react-table";
11-
import { isWithinInterval, startOfDay } from "date-fns";
11+
import { format, isWithinInterval, startOfDay } from "date-fns";
1212
import { useState } from "react";
1313
import { DateRange } from "react-day-picker";
1414
import ContactInfoChipDetails from "./ContactInfoChipDetails";
1515
import { GenericInfoChip } from "./GenericInfoChip";
1616
import LocationInfoChipDetails from "./LocationInfoChipDetails";
1717
import PartyTableCreateEditForm from "./PartyTableCreateEdit";
18+
import { useSidebar } from "./SidebarContext";
1819
import StudentInfoChipDetails from "./StudentInfoChipDetails";
1920
import { TableTemplate } from "./TableTemplate";
20-
import { useSidebar } from "./SidebarContext";
2121

2222
const partyService = new PartyService();
2323

@@ -245,14 +245,15 @@ export const PartyTable = () => {
245245
};
246246
const columns: ColumnDef<Party>[] = [
247247
{
248-
accessorKey: "location",
248+
id: "location",
249+
accessorFn: (row) => row.location.formattedAddress,
249250
header: "Address",
250251
enableColumnFilter: true,
251252
meta: {
252253
filterType: "text",
253254
},
254255
cell: ({ row }) => {
255-
const location = row.getValue<Party["location"]>("location");
256+
const location = row.original.location;
256257
if (!location) {
257258
return "—";
258259
}
@@ -267,11 +268,8 @@ export const PartyTable = () => {
267268
);
268269
},
269270

270-
filterFn: (row, columnId, filterValue) => {
271-
const location = row.getValue(columnId) as {
272-
streetNumber: string | null;
273-
streetName: string | null;
274-
};
271+
filterFn: (row, _columnId, filterValue) => {
272+
const location = row.original.location;
275273
const addressString = `${location.streetNumber || ""} ${
276274
location.streetName || ""
277275
}`
@@ -281,23 +279,24 @@ export const PartyTable = () => {
281279
},
282280
},
283281
{
284-
accessorKey: "datetime",
282+
id: "datetime",
283+
accessorFn: (row) => format(row.datetime, "MM-dd-yyyy"),
285284
header: "Date",
286285
enableColumnFilter: true,
287286
meta: {
288287
filterType: "dateRange",
289288
},
290289
cell: ({ row }) => {
291-
const datetime = row.getValue("datetime") as Date;
290+
const datetime = row.original.datetime;
292291
const date = new Date(datetime);
293292
return date.toLocaleDateString();
294293
},
295294

296-
filterFn: (row, columnId, filterValue) => {
295+
filterFn: (row, _columnId, filterValue) => {
297296
if (!filterValue) return true;
298297

299298
const dateRange = filterValue as DateRange;
300-
const datetime = row.getValue(columnId) as Date;
299+
const datetime = row.original.datetime;
301300
const date = startOfDay(new Date(datetime));
302301

303302
// If only 'from' date is selected
@@ -317,25 +316,26 @@ export const PartyTable = () => {
317316
},
318317
},
319318
{
320-
accessorKey: "time",
319+
id: "time",
320+
accessorFn: (row) => format(row.datetime, "HH:mm"),
321321
header: "Time",
322322
enableColumnFilter: true,
323323
meta: {
324324
filterType: "time",
325325
},
326326
cell: ({ row }) => {
327-
const datetime = row.getValue("datetime") as Date;
327+
const datetime = row.original.datetime;
328328
const date = new Date(datetime);
329329
return date.toLocaleTimeString([], {
330330
hour: "2-digit",
331331
minute: "2-digit",
332332
});
333333
},
334334

335-
filterFn: (row, columnId, filterValue) => {
335+
filterFn: (row, _columnId, filterValue) => {
336336
if (!filterValue) return true;
337337

338-
const datetime = row.original.datetime as Date;
338+
const datetime = row.original.datetime;
339339
const date = new Date(datetime);
340340

341341
// Get hours and minutes from the time input (e.g., "14:30")
@@ -350,14 +350,16 @@ export const PartyTable = () => {
350350
},
351351
},
352352
{
353-
accessorKey: "contactOne",
353+
id: "contactOne",
354+
accessorFn: (row) =>
355+
`${row.contactOne.firstName} ${row.contactOne.lastName}`,
354356
header: "Contact One",
355357
enableColumnFilter: true,
356358
meta: {
357359
filterType: "text",
358360
},
359361
cell: ({ row }) => {
360-
const contact = row.getValue<Party["contactOne"]>("contactOne");
362+
const contact = row.original.contactOne;
361363
return contact ? (
362364
<GenericInfoChip
363365
chipKey={`party-${row.original.id}-contact-one`}
@@ -371,25 +373,24 @@ export const PartyTable = () => {
371373
);
372374
},
373375

374-
filterFn: (row, columnId, filterValue) => {
375-
const contact = row.getValue(columnId) as {
376-
firstName: string;
377-
lastName: string;
378-
};
376+
filterFn: (row, _columnId, filterValue) => {
377+
const contact = row.original.contactOne;
379378
const fullName =
380379
`${contact.firstName} ${contact.lastName}`.toLowerCase();
381380
return fullName.includes(String(filterValue).toLowerCase());
382381
},
383382
},
384383
{
385-
accessorKey: "contactTwo",
384+
id: "contactTwo",
385+
accessorFn: (row) =>
386+
`${row.contactTwo.firstName} ${row.contactTwo.lastName}`,
386387
header: "Contact Two",
387388
enableColumnFilter: true,
388389
meta: {
389390
filterType: "text",
390391
},
391392
cell: ({ row }) => {
392-
const contact = row.getValue<Party["contactTwo"]>("contactTwo");
393+
const contact = row.original.contactTwo;
393394
const partyId = row.original.id;
394395
if (!contact) return "—";
395396
return (
@@ -403,11 +404,8 @@ export const PartyTable = () => {
403404
);
404405
},
405406

406-
filterFn: (row, columnId, filterValue) => {
407-
const contact = row.getValue(columnId) as {
408-
firstName: string;
409-
lastName: string;
410-
};
407+
filterFn: (row, _columnId, filterValue) => {
408+
const contact = row.original.contactTwo;
411409
const fullName =
412410
`${contact.firstName} ${contact.lastName}`.toLowerCase();
413411
return fullName.includes(String(filterValue).toLowerCase());
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"use client";
2+
3+
import { Location } from "@/types/api/location";
4+
import { Party } from "@/types/api/party";
5+
import { Student } from "@/types/api/student";
6+
import { useState } from "react";
7+
8+
interface TableListProps {
9+
parties: Party[];
10+
students: Student[];
11+
locations: Location[];
12+
setFilteredParties: (val: Party[]) => void;
13+
setFilteredStudents: (val: Student[]) => void;
14+
setFilteredLocations: (val: Location[]) => void;
15+
}
16+
17+
export default function TableList({
18+
parties,
19+
students,
20+
locations,
21+
setFilteredParties,
22+
setFilteredStudents,
23+
setFilteredLocations,
24+
}: TableListProps) {
25+
const [search, setSearch] = useState("");
26+
27+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
28+
const value = e.target.value;
29+
setSearch(value.toLowerCase());
30+
31+
setFilteredParties(
32+
parties.filter((p) =>
33+
p.location.formattedAddress.toLowerCase().includes(value)
34+
)
35+
);
36+
setFilteredStudents(
37+
students.filter(
38+
(s) =>
39+
s.firstName.toLowerCase().includes(value) ||
40+
s.lastName.toLowerCase().includes(value) ||
41+
s.email.toLowerCase().includes(value)
42+
)
43+
);
44+
setFilteredLocations(
45+
locations.filter((l) => l.formattedAddress.toLowerCase().includes(value))
46+
);
47+
};
48+
49+
return (
50+
<div className="mb-4">
51+
<input
52+
type="text"
53+
value={search}
54+
onChange={handleSearch}
55+
placeholder="Search all tables..."
56+
className="w-full p-2 border rounded"
57+
/>
58+
</div>
59+
);
60+
}

frontend/src/components/TableTemplate.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

33
import { Button } from "@/components/ui/button";
4+
45
import {
56
Table,
67
TableBody,
@@ -27,6 +28,7 @@ import {
2728
getPaginationRowModel,
2829
getSortedRowModel,
2930
PaginationState,
31+
Row,
3032
SortingState,
3133
useReactTable,
3234
} from "@tanstack/react-table";
@@ -70,7 +72,7 @@ export type TableProps<T> = {
7072
initialSort?: SortingState;
7173
};
7274

73-
export function TableTemplate<T>({
75+
export function TableTemplate<T extends object>({
7476
data,
7577
columns,
7678
resourceName = "Item",
@@ -96,6 +98,7 @@ export function TableTemplate<T>({
9698
} | null>(null);
9799
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
98100
const [itemToDelete, setItemToDelete] = useState<T | null>(null);
101+
const [globalFilter, setGlobalFilter] = useState<string>("");
99102

100103
const handleDeleteClick = (row: T) => {
101104
setItemToDelete(row);
@@ -153,17 +156,68 @@ export function TableTemplate<T>({
153156
]
154157
: columns;
155158

159+
function flattenValues<T extends object>(obj: T): string {
160+
const result: string[] = [];
161+
162+
const walk = (val: unknown): void => {
163+
if (val == null) return;
164+
165+
if (
166+
typeof val === "string" ||
167+
typeof val === "number" ||
168+
typeof val === "boolean"
169+
) {
170+
result.push(String(val));
171+
return;
172+
}
173+
174+
if (val instanceof Date) {
175+
result.push(val.toISOString());
176+
return;
177+
}
178+
179+
if (Array.isArray(val)) {
180+
val.forEach((child) => walk(child));
181+
return;
182+
}
183+
184+
if (typeof val === "object") {
185+
Object.values(val).forEach((child) => walk(child));
186+
return;
187+
}
188+
};
189+
190+
walk(obj);
191+
return result.join(" ").toLowerCase();
192+
}
193+
194+
const customFilterFn = <T extends object>(
195+
row: Row<T>,
196+
_columnId: string,
197+
filterValue: string
198+
): boolean => {
199+
if (!filterValue) return true;
200+
201+
const flattened = flattenValues(row.original);
202+
const matches = flattened.includes(filterValue.toLowerCase());
203+
204+
return matches;
205+
};
206+
156207
const table = useReactTable({
157208
data,
158209
columns: columnsWithActions,
159210
state: {
160211
sorting,
161212
columnFilters,
162213
pagination,
214+
globalFilter,
163215
},
216+
globalFilterFn: customFilterFn,
164217
onSortingChange: setSorting,
165218
onColumnFiltersChange: setColumnFilters,
166219
getCoreRowModel: getCoreRowModel(),
220+
onGlobalFilterChange: setGlobalFilter,
167221
getPaginationRowModel: getPaginationRowModel(),
168222
onPaginationChange: setPagination,
169223
getSortedRowModel: getSortedRowModel(),
@@ -217,6 +271,16 @@ export function TableTemplate<T>({
217271
{!isLoading && !error && (
218272
<>
219273
<div className="rounded-md border">
274+
<div className="mb-2">
275+
<input
276+
type="text"
277+
value={globalFilter}
278+
onChange={(e) => setGlobalFilter(e.target.value)}
279+
placeholder="Search all columns..."
280+
className="w-full p-2 border rounded"
281+
/>
282+
</div>
283+
220284
<Table>
221285
<TableCaption>{tableDetails}</TableCaption>
222286
<TableHeader>

0 commit comments

Comments
 (0)