1
- import { FC , ReactNode , forwardRef , useEffect , useRef , useState } from "react"
1
+ import { ReactNode , forwardRef , useEffect , useRef , useState } from "react"
2
2
import Box from "@mui/material/Box"
3
3
import TextField from "@mui/material/TextField"
4
4
import KeyboardArrowDown from "@mui/icons-material/KeyboardArrowDown"
@@ -7,7 +7,6 @@ import Menu from "@mui/material/Menu"
7
7
import MenuItem from "@mui/material/MenuItem"
8
8
import SearchOutlined from "@mui/icons-material/SearchOutlined"
9
9
import InputAdornment from "@mui/material/InputAdornment"
10
- import { Palette , PaletteColor } from "@mui/material/styles"
11
10
import IconButton from "@mui/material/IconButton"
12
11
import Tooltip from "@mui/material/Tooltip"
13
12
import CloseOutlined from "@mui/icons-material/CloseOutlined"
@@ -19,13 +18,11 @@ import {
19
18
hasError ,
20
19
isApiValidationError ,
21
20
} from "api/errors"
22
- import { StatusAutocomplete } from "./autocompletes "
23
- import { StatusOption , BaseOption } from "./options"
21
+ import { useFilterMenu } from "./menu "
22
+ import { BaseOption } from "./options"
24
23
import debounce from "just-debounce-it"
25
24
26
- export type FilterValues = {
27
- status ?: string
28
- }
25
+ type FilterValues = Record < string , string | undefined >
29
26
30
27
export const useFilter = ( {
31
28
onUpdate,
@@ -88,7 +85,7 @@ const stringifyFilter = (filterValue: FilterValues): string => {
88
85
let result = ""
89
86
90
87
for ( const key in filterValue ) {
91
- const value = filterValue [ key as keyof FilterValues ]
88
+ const value = filterValue [ key ]
92
89
if ( value ) {
93
90
result += `${ key } :${ value } `
94
91
}
@@ -97,7 +94,7 @@ const stringifyFilter = (filterValue: FilterValues): string => {
97
94
return result . trim ( )
98
95
}
99
96
100
- const FilterSkeleton = ( props : SkeletonProps ) => {
97
+ const BaseSkeleton = ( props : SkeletonProps ) => {
101
98
return (
102
99
< Skeleton
103
100
variant = "rectangular"
@@ -112,94 +109,103 @@ const FilterSkeleton = (props: SkeletonProps) => {
112
109
)
113
110
}
114
111
112
+ export const SearchFieldSkeleton = ( ) => < BaseSkeleton width = "100%" />
113
+ export const MenuSkeleton = ( ) => (
114
+ < BaseSkeleton width = "200px" sx = { { flexShrink : 0 } } />
115
+ )
116
+
115
117
export const Filter = ( {
116
118
filter,
117
- autocomplete ,
119
+ isLoading ,
118
120
error,
121
+ skeleton,
122
+ options,
119
123
} : {
120
124
filter : ReturnType < typeof useFilter >
125
+ skeleton : ReactNode
126
+ isLoading : boolean
121
127
error ?: unknown
122
- autocomplete : {
123
- status : StatusAutocomplete
124
- }
128
+ options ?: ReactNode
125
129
} ) => {
126
130
const shouldDisplayError = hasError ( error ) && isApiValidationError ( error )
127
131
const hasFilterQuery = filter . query !== ""
128
- const isIinitializingFilters = autocomplete . status . isInitializing
129
132
const [ searchQuery , setSearchQuery ] = useState ( filter . query )
130
133
131
134
useEffect ( ( ) => {
132
135
setSearchQuery ( filter . query )
133
136
} , [ filter . query ] )
134
137
135
- if ( isIinitializingFilters ) {
136
- return (
137
- < Box display = "flex" sx = { { gap : 1 , mb : 2 } } >
138
- < FilterSkeleton width = "100%" />
139
- < FilterSkeleton width = "200px" sx = { { flexShrink : 0 } } />
140
- </ Box >
141
- )
142
- }
143
-
144
138
return (
145
139
< Box display = "flex" sx = { { gap : 1 , mb : 2 } } >
146
- < TextField
147
- fullWidth
148
- error = { shouldDisplayError }
149
- helperText = {
150
- shouldDisplayError ? getValidationErrorMessage ( error ) : undefined
151
- }
152
- size = "small"
153
- InputProps = { {
154
- name : "query" ,
155
- placeholder : "Search..." ,
156
- value : searchQuery ,
157
- onChange : ( e ) => {
158
- setSearchQuery ( e . target . value )
159
- filter . debounceUpdate ( e . target . value )
160
- } ,
161
- sx : {
162
- borderRadius : "6px" ,
163
- "& input::placeholder" : {
164
- color : ( theme ) => theme . palette . text . secondary ,
165
- } ,
166
- } ,
167
- startAdornment : (
168
- < InputAdornment position = "start" >
169
- < SearchOutlined
170
- sx = { {
171
- fontSize : 14 ,
140
+ { isLoading ? (
141
+ skeleton
142
+ ) : (
143
+ < >
144
+ < TextField
145
+ fullWidth
146
+ error = { shouldDisplayError }
147
+ helperText = {
148
+ shouldDisplayError ? getValidationErrorMessage ( error ) : undefined
149
+ }
150
+ size = "small"
151
+ InputProps = { {
152
+ name : "query" ,
153
+ placeholder : "Search..." ,
154
+ value : searchQuery ,
155
+ onChange : ( e ) => {
156
+ setSearchQuery ( e . target . value )
157
+ filter . debounceUpdate ( e . target . value )
158
+ } ,
159
+ sx : {
160
+ borderRadius : "6px" ,
161
+ "& input::placeholder" : {
172
162
color : ( theme ) => theme . palette . text . secondary ,
173
- } }
174
- />
175
- </ InputAdornment >
176
- ) ,
177
- endAdornment : hasFilterQuery && (
178
- < InputAdornment position = "end" >
179
- < Tooltip title = "Clear filter" >
180
- < IconButton
181
- size = "small"
182
- onClick = { ( ) => {
183
- filter . update ( "" )
184
- } }
185
- >
186
- < CloseOutlined sx = { { fontSize : 14 } } />
187
- </ IconButton >
188
- </ Tooltip >
189
- </ InputAdornment >
190
- ) ,
191
- } }
192
- />
163
+ } ,
164
+ } ,
165
+ startAdornment : (
166
+ < InputAdornment position = "start" >
167
+ < SearchOutlined
168
+ sx = { {
169
+ fontSize : 14 ,
170
+ color : ( theme ) => theme . palette . text . secondary ,
171
+ } }
172
+ />
173
+ </ InputAdornment >
174
+ ) ,
175
+ endAdornment : hasFilterQuery && (
176
+ < InputAdornment position = "end" >
177
+ < Tooltip title = "Clear filter" >
178
+ < IconButton
179
+ size = "small"
180
+ onClick = { ( ) => {
181
+ filter . update ( "" )
182
+ } }
183
+ >
184
+ < CloseOutlined sx = { { fontSize : 14 } } />
185
+ </ IconButton >
186
+ </ Tooltip >
187
+ </ InputAdornment >
188
+ ) ,
189
+ } }
190
+ />
193
191
194
- < StatusFilter autocomplete = { autocomplete . status } />
192
+ { options }
193
+ </ >
194
+ ) }
195
195
</ Box >
196
196
)
197
197
}
198
198
199
- const StatusFilter = ( {
200
- autocomplete,
199
+ export const FilterMenu = < TOption extends BaseOption > ( {
200
+ id,
201
+ menu,
202
+ label,
203
+ children,
201
204
} : {
202
- autocomplete : StatusAutocomplete
205
+ menu : ReturnType < typeof useFilterMenu < TOption > >
206
+ label : ReactNode
207
+ id : string
208
+ children : ( values : { option : TOption ; isSelected : boolean } ) => ReactNode
203
209
} ) => {
204
210
const buttonRef = useRef < HTMLButtonElement > ( null )
205
211
const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
@@ -215,14 +221,10 @@ const StatusFilter = ({
215
221
onClick = { ( ) => setIsMenuOpen ( true ) }
216
222
sx = { { width : 200 } }
217
223
>
218
- { autocomplete . selectedOption ? (
219
- < StatusOptionItem option = { autocomplete . selectedOption } />
220
- ) : (
221
- "All statuses"
222
- ) }
224
+ { label }
223
225
</ MenuButton >
224
226
< Menu
225
- id = "status-filter-menu"
227
+ id = { id }
226
228
anchorEl = { buttonRef . current }
227
229
open = { isMenuOpen }
228
230
onClose = { handleClose }
@@ -235,63 +237,33 @@ const StatusFilter = ({
235
237
exit : 0 ,
236
238
} }
237
239
>
238
- { autocomplete . searchOptions ?. map ( ( option ) => (
240
+ { menu . searchOptions ?. map ( ( option ) => (
239
241
< MenuItem
240
242
key = { option . label }
241
- selected = { option . value === autocomplete . selectedOption ?. value }
243
+ selected = { option . value === menu . selectedOption ?. value }
242
244
onClick = { ( ) => {
243
- autocomplete . selectOption ( option )
245
+ menu . selectOption ( option )
244
246
handleClose ( )
245
247
} }
246
248
>
247
- < StatusOptionItem
248
- option = { option }
249
- isSelected = { option . value === autocomplete . selectedOption ?. value }
250
- />
249
+ { children ( {
250
+ option,
251
+ isSelected : option . value === menu . selectedOption ?. value ,
252
+ } ) }
251
253
</ MenuItem >
252
254
) ) }
253
255
</ Menu >
254
256
</ div >
255
257
)
256
258
}
257
259
258
- const StatusOptionItem = ( {
259
- option,
260
- isSelected,
261
- } : {
262
- option : StatusOption
263
- isSelected ?: boolean
264
- } ) => {
265
- return (
266
- < OptionItem
267
- option = { option }
268
- left = { < StatusIndicator option = { option } /> }
269
- isSelected = { isSelected }
270
- />
271
- )
272
- }
273
-
274
- const StatusIndicator : FC < { option : StatusOption } > = ( { option } ) => {
275
- return (
276
- < Box
277
- height = { 8 }
278
- width = { 8 }
279
- borderRadius = { 9999 }
280
- sx = { {
281
- backgroundColor : ( theme ) =>
282
- ( theme . palette [ option . color as keyof Palette ] as PaletteColor ) . light ,
283
- } }
284
- />
285
- )
286
- }
287
-
288
260
type OptionItemProps = {
289
261
option : BaseOption
290
262
left ?: ReactNode
291
263
isSelected ?: boolean
292
264
}
293
265
294
- const OptionItem = ( { option, left, isSelected } : OptionItemProps ) => {
266
+ export const OptionItem = ( { option, left, isSelected } : OptionItemProps ) => {
295
267
return (
296
268
< Box
297
269
display = "flex"
0 commit comments