Skip to content

Commit 5743efc

Browse files
authored
Merge pull request #493 from STAPLE-verse/432-table-updates
432 table updates
2 parents c33d5ac + b5fd07a commit 5743efc

24 files changed

Lines changed: 949 additions & 111 deletions

src/core/components/Filter.tsx

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Column } from "@tanstack/react-table"
44
function Filter({ column }: { column: Column<any, unknown> }) {
55
const { filterVariant } = column.columnDef.meta ?? {}
66
const isHtml = column.columnDef.meta?.isHtml || false
7+
const selectOptions = column.columnDef.meta?.selectOptions as
8+
| { label: string; value: string }[]
9+
| undefined
710
const columnFilterValue = column.getFilterValue()
811
const facetedUniqueValues = column.getFacetedUniqueValues()
912

@@ -64,16 +67,22 @@ function Filter({ column }: { column: Column<any, unknown> }) {
6467
className={sharedInputStyles}
6568
>
6669
<option value="">All</option>
67-
{sortedUniqueValues.map((value, index) => (
68-
// dynamically generated select options from faceted values feature
69-
<option
70-
value={value}
71-
key={getUniqueKey(value, index)}
72-
dangerouslySetInnerHTML={isHtml ? { __html: value } : undefined}
73-
>
74-
{!isHtml ? value : undefined}
75-
</option>
76-
))}
70+
{selectOptions
71+
? selectOptions.map((option, index) => (
72+
<option value={option.value} key={getUniqueKey(option.value, index)}>
73+
{option.label}
74+
</option>
75+
))
76+
: sortedUniqueValues.map((value, index) => (
77+
// dynamically generated select options from faceted values feature
78+
<option
79+
value={value}
80+
key={getUniqueKey(value, index)}
81+
dangerouslySetInnerHTML={isHtml ? { __html: value } : undefined}
82+
>
83+
{!isHtml ? value : undefined}
84+
</option>
85+
))}
7786
</select>
7887
) : filterVariant === "multiselect" ? (
7988
<div className="dropdown">

src/core/components/GetWidgetDisplay.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export function GetTableDisplay({ data, columns, type }) {
1414
return (
1515
<Table
1616
columns={columns}
17+
enableGlobalSearch={false}
1718
data={data}
1819
classNames={{
1920
thead: "text-base text-base-content",
@@ -85,6 +86,7 @@ export function GetProjectSummaryDisplay({ project, projectManagers }) {
8586
<Table
8687
columns={projectManagersColumns}
8788
data={projectManagers}
89+
enableGlobalSearch={false}
8890
classNames={{
8991
thead: "text-base",
9092
tbody: "text-base",

src/core/components/Table.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ColumnDef,
3+
FilterFn,
34
flexRender,
45
getCoreRowModel,
56
getSortedRowModel,
@@ -14,13 +15,121 @@ import React from "react"
1415
import { ChevronUpIcon, ChevronDownIcon, ChevronUpDownIcon } from "@heroicons/react/24/outline"
1516

1617
import Filter from "src/core/components/Filter"
18+
import { buildSearchableString } from "src/core/utils/tableFilters"
19+
import TooltipWrapper from "./TooltipWrapper"
20+
21+
const specialSearchTokens = new Set([
22+
"read",
23+
"unread",
24+
"completed",
25+
"complete",
26+
"not completed",
27+
"approved",
28+
"not approved",
29+
"pending",
30+
])
31+
32+
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
33+
const containsWholeWord = (text: string, word: string) => {
34+
const escapedWord = escapeRegExp(word)
35+
const regex = new RegExp(`\\b${escapedWord}\\b`)
36+
return regex.test(text)
37+
}
38+
39+
const matchesSpecialTokenInText = (text: string, token: string) => {
40+
if (!text) {
41+
return false
42+
}
43+
44+
const normalized = text.toLowerCase()
45+
46+
if (token === "completed") {
47+
return /(?<!not\s)\bcompleted\b/.test(normalized)
48+
}
49+
50+
if (token === "complete") {
51+
return /(?<!not\s)(?<!in)\bcomplete\b/.test(normalized)
52+
}
53+
54+
if (token === "not completed") {
55+
return /\bnot\s+completed\b/.test(normalized)
56+
}
57+
58+
return containsWholeWord(normalized, token)
59+
}
60+
61+
const matchesBooleanToken = (token: string, value: boolean | null, keyPath: string): boolean => {
62+
const normalizedKey = keyPath.toLowerCase()
63+
const isReadKey = normalizedKey.includes("read")
64+
const isCompletionKey = normalizedKey.includes("status") || normalizedKey.includes("complete")
65+
const isApprovalKey = normalizedKey.includes("approve")
66+
67+
if (token === "read") {
68+
return isReadKey && value === true
69+
}
70+
71+
if (token === "unread") {
72+
return isReadKey && value === false
73+
}
74+
75+
if (token === "completed" || token === "complete") {
76+
return isCompletionKey && value === true
77+
}
78+
79+
if (token === "not completed") {
80+
return isCompletionKey && value === false
81+
}
82+
83+
if (token === "approved") {
84+
return isApprovalKey && value === true
85+
}
86+
87+
if (token === "not approved") {
88+
return isApprovalKey && value === false
89+
}
90+
91+
if (token === "pending") {
92+
return isApprovalKey && (value === null || value === undefined)
93+
}
94+
95+
return false
96+
}
97+
98+
const matchesSpecialToken = (data: unknown, token: string, keyPath = ""): boolean => {
99+
if (data === null || data === undefined) {
100+
return matchesBooleanToken(token, data as null, keyPath)
101+
}
102+
103+
if (typeof data === "boolean") {
104+
return matchesBooleanToken(token, data, keyPath)
105+
}
106+
107+
if (Array.isArray(data)) {
108+
return data.some((item) => matchesSpecialToken(item, token, keyPath))
109+
}
110+
111+
if (data instanceof Date) {
112+
return false
113+
}
114+
115+
if (typeof data === "object") {
116+
return Object.entries(data as Record<string, unknown>).some(([key, value]) => {
117+
const nextPath = keyPath ? `${keyPath}.${key}` : key
118+
return matchesSpecialToken(value, token, nextPath)
119+
})
120+
}
121+
122+
return false
123+
}
17124

18125
type TableProps<TData> = {
19126
columns: ColumnDef<TData, any>[]
20127
data: TData[]
21128
filters?: {} //pass object with the type of filter for a given colunm based on colunm id
22129
enableSorting?: boolean
23130
enableFilters?: boolean
131+
enableGlobalSearch?: boolean
132+
globalSearchPlaceholder?: string
24133
addPagination?: boolean
25134
classNames?: {
26135
table?: string
@@ -33,6 +142,31 @@ type TableProps<TData> = {
33142
pageInfo?: string
34143
goToPageInput?: string
35144
pageSizeSelect?: string
145+
searchContainer?: string
146+
searchInput?: string
147+
}
148+
}
149+
150+
const defaultGlobalFilterFn: FilterFn<any> = (row, _columnId, filterValue) => {
151+
const searchValue = String(filterValue ?? "")
152+
.toLowerCase()
153+
.trim()
154+
155+
if (!searchValue) {
156+
return true
157+
}
158+
159+
try {
160+
const rowValue = buildSearchableString(row.original ?? {})
161+
if (specialSearchTokens.has(searchValue)) {
162+
if (matchesSpecialToken(row.original, searchValue)) {
163+
return true
164+
}
165+
return matchesSpecialTokenInText(rowValue, searchValue)
166+
}
167+
return rowValue.includes(searchValue)
168+
} catch (error) {
169+
return false
36170
}
37171
}
38172

@@ -42,9 +176,12 @@ const Table = <TData,>({
42176
classNames,
43177
enableSorting = true,
44178
enableFilters = true,
179+
enableGlobalSearch = true,
180+
globalSearchPlaceholder = "Search...",
45181
addPagination = false,
46182
}: TableProps<TData>) => {
47183
const [sorting, setSorting] = React.useState([])
184+
const [globalFilter, setGlobalFilter] = React.useState("")
48185

49186
const table = useReactTable({
50187
data,
@@ -59,21 +196,58 @@ const Table = <TData,>({
59196
getFacetedMinMaxValues: getFacetedMinMaxValues(),
60197
state: {
61198
sorting: sorting,
199+
globalFilter: globalFilter,
62200
},
63201
initialState: {
64202
pagination: {
65203
pageSize: 5,
66204
},
67205
},
68206
onSortingChange: setSorting,
207+
onGlobalFilterChange: setGlobalFilter,
208+
globalFilterFn: defaultGlobalFilterFn,
69209
autoResetPageIndex: false,
70210
})
71211

72212
const currentPage = table.getState().pagination.pageIndex + 1
73213
const pageCount = table.getPageCount()
214+
const pageIndex = table.getState().pagination.pageIndex
215+
216+
const globalSearchTooltipId = React.useId()
217+
218+
React.useEffect(() => {
219+
if (!addPagination) {
220+
return
221+
}
222+
223+
if (pageCount > 0 && pageIndex >= pageCount) {
224+
table.setPageIndex(0)
225+
}
226+
}, [addPagination, pageCount, pageIndex, table])
74227

75228
return (
76229
<>
230+
{enableGlobalSearch && (
231+
<div className={`mb-2 mt-2 mr-2 flex justify-end ${classNames?.searchContainer || ""}`}>
232+
<input
233+
type="text"
234+
value={globalFilter ?? ""}
235+
onChange={(event) => setGlobalFilter(event.target.value)}
236+
placeholder={globalSearchPlaceholder}
237+
aria-label="Search table data"
238+
data-tooltip-id={globalSearchTooltipId}
239+
data-tooltip-content="Searches all data in table (including comments, log dates, and more)."
240+
className={`input input-primary input-bordered border-2 bg-base-300 rounded input-sm w-full max-w-xs focus:outline-secondary ${
241+
classNames?.searchInput || ""
242+
}`}
243+
/>
244+
<TooltipWrapper
245+
id={globalSearchTooltipId}
246+
content="Global search scans all table data, including hidden columns and filters."
247+
className="z-[1099] ourtooltips"
248+
/>
249+
</div>
250+
)}
77251
<table className={classNames?.table || "table"}>
78252
<thead className={classNames?.thead || "text-xl text-base-content"}>
79253
{table.getHeaderGroups().map((headerGroup) => (

0 commit comments

Comments
 (0)