11import {
22 ColumnDef ,
3+ FilterFn ,
34 flexRender ,
45 getCoreRowModel ,
56 getSortedRowModel ,
@@ -14,13 +15,121 @@ import React from "react"
1415import { ChevronUpIcon , ChevronDownIcon , ChevronUpDownIcon } from "@heroicons/react/24/outline"
1516
1617import 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 / (?< ! n o t \s ) \b c o m p l e t e d \b / . test ( normalized )
48+ }
49+
50+ if ( token === "complete" ) {
51+ return / (?< ! n o t \s ) (?< ! i n ) \b c o m p l e t e \b / . test ( normalized )
52+ }
53+
54+ if ( token === "not completed" ) {
55+ return / \b n o t \s + c o m p l e t e d \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
18125type 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