goobs-frontend 0.9.11 → 0.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/components/ComplexTextEditor/MarkdownEditor/index.tsx +1 -16
- package/src/components/ComplexTextEditor/Toolbars/Complex/index.tsx +0 -3
- package/src/components/ComplexTextEditor/index.tsx +20 -0
- package/src/components/ConfirmationCodeInput/codeinput.stories.tsx +491 -67
- package/src/components/ConfirmationCodeInput/index.tsx +313 -76
- package/src/components/DataGrid/JotaiProvider.tsx +13 -0
- package/src/components/DataGrid/utils/useComputeTableResize.tsx +4 -1
- package/src/components/DataGrid/utils/useInitializeGrid.tsx +12 -4
- package/src/components/DataGrid/utils/useManageColumn.tsx +7 -2
- package/src/components/Field/Dropdown/Searchable/index.tsx +683 -31
- package/src/components/Field/Dropdown/Searchable/searchabledropdown.stories.tsx +164 -0
- package/src/components/ProjectBoard/index.tsx +12 -2
- package/src/components/ProjectBoard/jotai/provider.tsx +23 -0
- package/src/components/QRCode/index.tsx +31 -33
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, SyntheticEvent } from 'react'
|
|
3
|
+
import React, { useState, useEffect, SyntheticEvent, useRef } from 'react'
|
|
4
4
|
import {
|
|
5
5
|
Autocomplete,
|
|
6
6
|
InputLabel,
|
|
7
7
|
OutlinedInput,
|
|
8
8
|
FormHelperText,
|
|
9
9
|
FormControl,
|
|
10
|
+
Box,
|
|
11
|
+
Tabs,
|
|
12
|
+
Tab,
|
|
13
|
+
styled as muiStyled,
|
|
14
|
+
Popper,
|
|
10
15
|
} from '@mui/material'
|
|
11
16
|
import { styled } from '@mui/material/styles'
|
|
12
17
|
import { black, white } from '../../../../styles/palette'
|
|
13
18
|
import Typography from '../../../Typography'
|
|
14
19
|
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
|
|
20
|
+
import HistoryIcon from '@mui/icons-material/History'
|
|
21
|
+
import SearchIcon from '@mui/icons-material/Search'
|
|
22
|
+
import { unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils'
|
|
23
|
+
|
|
24
|
+
// Define a history item type with timestamp
|
|
25
|
+
interface HistoryItem {
|
|
26
|
+
text: string
|
|
27
|
+
timestamp: Date
|
|
28
|
+
// Optional formatted date string for display
|
|
29
|
+
formattedDate?: string
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
export interface DropdownOption {
|
|
17
33
|
value: string
|
|
@@ -44,10 +60,12 @@ export interface SearchableDropdownProps {
|
|
|
44
60
|
placeholder?: string
|
|
45
61
|
disabled?: boolean
|
|
46
62
|
width?: string
|
|
47
|
-
// Added style property to allow additional styling (e.g., marginBottom)
|
|
48
63
|
style?: React.CSSProperties
|
|
49
|
-
// New variant property to determine display style
|
|
50
64
|
variant?: 'simple' | 'complex'
|
|
65
|
+
// Update search history type to support timestamps
|
|
66
|
+
searchHistory?: HistoryItem[] | string[]
|
|
67
|
+
onSearch?: (searchTerm: string, timestamp?: Date) => void
|
|
68
|
+
maxHistoryItems?: number
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
const StyledFormControl = styled(FormControl)<{ width?: string }>(
|
|
@@ -99,6 +117,26 @@ interface StyledAutocompleteProps {
|
|
|
99
117
|
variant?: 'simple' | 'complex'
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
// Styled tab component for the dropdown footer
|
|
121
|
+
const StyledTab = muiStyled(Tab)(() => ({
|
|
122
|
+
minHeight: '36px',
|
|
123
|
+
fontSize: '12px',
|
|
124
|
+
padding: '6px 12px',
|
|
125
|
+
color: black.main,
|
|
126
|
+
'&.Mui-selected': {
|
|
127
|
+
color: black.main,
|
|
128
|
+
fontWeight: 'bold',
|
|
129
|
+
},
|
|
130
|
+
}))
|
|
131
|
+
|
|
132
|
+
const StyledTabs = muiStyled(Tabs)(() => ({
|
|
133
|
+
minHeight: '36px',
|
|
134
|
+
borderTop: `1px solid ${black.light}`,
|
|
135
|
+
'& .MuiTabs-indicator': {
|
|
136
|
+
backgroundColor: black.main,
|
|
137
|
+
},
|
|
138
|
+
}))
|
|
139
|
+
|
|
102
140
|
const StyledAutocomplete = styled(
|
|
103
141
|
Autocomplete<DropdownOption, false, false, true>
|
|
104
142
|
)<StyledAutocompleteProps>(props => {
|
|
@@ -236,10 +274,138 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
236
274
|
width,
|
|
237
275
|
style,
|
|
238
276
|
variant = 'simple', // Default to simple variant
|
|
277
|
+
searchHistory = [], // Default to empty array
|
|
278
|
+
onSearch,
|
|
279
|
+
maxHistoryItems = 5, // Default to showing 5 history items
|
|
239
280
|
}) => {
|
|
240
281
|
const [value, setValue] = useState<DropdownOption | string | null>(null)
|
|
241
282
|
const [inputValue, setInputValue] = useState('')
|
|
242
283
|
const [isFocused, setIsFocused] = useState(false)
|
|
284
|
+
// Update local history state to support timestamps
|
|
285
|
+
const [localHistory, setLocalHistory] = useState<HistoryItem[]>([])
|
|
286
|
+
|
|
287
|
+
// Add state for active tab - 0 for All Options, 1 for History
|
|
288
|
+
const [activeTab, setActiveTab] = useState<number>(0)
|
|
289
|
+
|
|
290
|
+
// Use ref to track if we've done initial history setup to avoid loops
|
|
291
|
+
const initializedRef = React.useRef(false)
|
|
292
|
+
|
|
293
|
+
// Always use controlled open state
|
|
294
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
295
|
+
|
|
296
|
+
// Reference to the tab container
|
|
297
|
+
const tabsRef = useRef<HTMLDivElement | null>(null)
|
|
298
|
+
|
|
299
|
+
// Ref for the input/autocomplete
|
|
300
|
+
const autocompleteRef = useRef<HTMLDivElement>(null)
|
|
301
|
+
|
|
302
|
+
// Use enhanced effect to handle tab clicks without closing dropdown
|
|
303
|
+
useEnhancedEffect(() => {
|
|
304
|
+
// If tabs aren't mounted yet, do nothing
|
|
305
|
+
if (!tabsRef.current) return
|
|
306
|
+
|
|
307
|
+
const tabsElement = tabsRef.current
|
|
308
|
+
|
|
309
|
+
// Prevent any click inside the tabs from bubbling up
|
|
310
|
+
const preventClose = (e: MouseEvent) => {
|
|
311
|
+
e.preventDefault()
|
|
312
|
+
e.stopPropagation()
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add click handler to the tabs element
|
|
316
|
+
tabsElement.addEventListener('mousedown', preventClose, true)
|
|
317
|
+
tabsElement.addEventListener('click', preventClose, true)
|
|
318
|
+
|
|
319
|
+
// Cleanup
|
|
320
|
+
return () => {
|
|
321
|
+
tabsElement.removeEventListener('mousedown', preventClose, true)
|
|
322
|
+
tabsElement.removeEventListener('click', preventClose, true)
|
|
323
|
+
}
|
|
324
|
+
}, [tabsRef.current])
|
|
325
|
+
|
|
326
|
+
// Function to format date for display
|
|
327
|
+
const formatDate = (date: Date): string => {
|
|
328
|
+
const now = new Date()
|
|
329
|
+
const diff = now.getTime() - date.getTime()
|
|
330
|
+
|
|
331
|
+
// If less than a minute ago
|
|
332
|
+
if (diff < 60 * 1000) {
|
|
333
|
+
return 'Just now'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// If less than an hour ago
|
|
337
|
+
if (diff < 60 * 60 * 1000) {
|
|
338
|
+
const minutes = Math.floor(diff / (60 * 1000))
|
|
339
|
+
return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// If less than a day ago
|
|
343
|
+
if (diff < 24 * 60 * 60 * 1000) {
|
|
344
|
+
const hours = Math.floor(diff / (60 * 60 * 1000))
|
|
345
|
+
return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// If less than a week ago
|
|
349
|
+
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
|
350
|
+
const days = Math.floor(diff / (24 * 60 * 60 * 1000))
|
|
351
|
+
return `${days} ${days === 1 ? 'day' : 'days'} ago`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Otherwise, format as date
|
|
355
|
+
return date.toLocaleDateString(undefined, {
|
|
356
|
+
year: 'numeric',
|
|
357
|
+
month: 'short',
|
|
358
|
+
day: 'numeric',
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Initialize history with timestamps if using simple strings
|
|
363
|
+
useEffect(() => {
|
|
364
|
+
if (!initializedRef.current && searchHistory && searchHistory.length > 0) {
|
|
365
|
+
// Check if searchHistory items are strings or already HistoryItems
|
|
366
|
+
if (typeof searchHistory[0] === 'string') {
|
|
367
|
+
const historyWithTimestamps = (searchHistory as string[]).map(
|
|
368
|
+
(text, index) => {
|
|
369
|
+
// Create timestamps with slight variations so older items are truly older
|
|
370
|
+
const timestamp = new Date()
|
|
371
|
+
timestamp.setMinutes(timestamp.getMinutes() - (index + 1))
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
text,
|
|
375
|
+
timestamp,
|
|
376
|
+
formattedDate: formatDate(timestamp),
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
setLocalHistory(historyWithTimestamps)
|
|
381
|
+
} else {
|
|
382
|
+
// Already the right format, just update formatted dates
|
|
383
|
+
const formattedHistory = (searchHistory as HistoryItem[]).map(item => ({
|
|
384
|
+
...item,
|
|
385
|
+
formattedDate: formatDate(item.timestamp),
|
|
386
|
+
}))
|
|
387
|
+
setLocalHistory(formattedHistory)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
initializedRef.current = true
|
|
391
|
+
}
|
|
392
|
+
}, [searchHistory])
|
|
393
|
+
|
|
394
|
+
// Refresh formatted dates every minute for "minutes ago" style timestamps
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
const intervalId = setInterval(() => {
|
|
397
|
+
if (localHistory.length > 0) {
|
|
398
|
+
setLocalHistory(prev =>
|
|
399
|
+
prev.map(item => ({
|
|
400
|
+
...item,
|
|
401
|
+
formattedDate: formatDate(item.timestamp),
|
|
402
|
+
}))
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
}, 60000) // Update every minute
|
|
406
|
+
|
|
407
|
+
return () => clearInterval(intervalId)
|
|
408
|
+
}, [localHistory.length])
|
|
243
409
|
|
|
244
410
|
useEffect(() => {
|
|
245
411
|
const defaultOption = options.find(option => option.value === defaultValue)
|
|
@@ -260,6 +426,26 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
260
426
|
setValue(newValue)
|
|
261
427
|
setInputValue(newValue)
|
|
262
428
|
onChange?.(null)
|
|
429
|
+
|
|
430
|
+
// Add to search history if it's a string input
|
|
431
|
+
if (newValue.trim()) {
|
|
432
|
+
const now = new Date()
|
|
433
|
+
|
|
434
|
+
if (onSearch) {
|
|
435
|
+
onSearch(newValue.trim(), now)
|
|
436
|
+
} else {
|
|
437
|
+
// Only update local history if we're not using onSearch callback
|
|
438
|
+
setLocalHistory(prev => {
|
|
439
|
+
const newItem = {
|
|
440
|
+
text: newValue.trim(),
|
|
441
|
+
timestamp: now,
|
|
442
|
+
formattedDate: formatDate(now),
|
|
443
|
+
}
|
|
444
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
445
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
}
|
|
263
449
|
} else {
|
|
264
450
|
setValue(newValue)
|
|
265
451
|
if (newValue) {
|
|
@@ -268,6 +454,24 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
268
454
|
newValue.value.replace(/_/g, ' ').slice(1)
|
|
269
455
|
setInputValue(displayText)
|
|
270
456
|
onChange?.(newValue)
|
|
457
|
+
|
|
458
|
+
// Add to search history
|
|
459
|
+
const now = new Date()
|
|
460
|
+
|
|
461
|
+
if (onSearch) {
|
|
462
|
+
onSearch(displayText, now)
|
|
463
|
+
} else if (newValue.value.trim()) {
|
|
464
|
+
// Only update local history if we're not using onSearch callback
|
|
465
|
+
setLocalHistory(prev => {
|
|
466
|
+
const newItem = {
|
|
467
|
+
text: displayText,
|
|
468
|
+
timestamp: now,
|
|
469
|
+
formattedDate: formatDate(now),
|
|
470
|
+
}
|
|
471
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
472
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
473
|
+
})
|
|
474
|
+
}
|
|
271
475
|
} else {
|
|
272
476
|
setInputValue('')
|
|
273
477
|
onChange?.(null)
|
|
@@ -275,6 +479,34 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
275
479
|
}
|
|
276
480
|
}
|
|
277
481
|
|
|
482
|
+
// Handler for when the input is submitted (e.g., Enter key)
|
|
483
|
+
const handleInputSubmit = () => {
|
|
484
|
+
if (inputValue.trim()) {
|
|
485
|
+
const now = new Date()
|
|
486
|
+
|
|
487
|
+
if (onSearch) {
|
|
488
|
+
onSearch(inputValue.trim(), now)
|
|
489
|
+
} else {
|
|
490
|
+
// Only update local history if we're not using onSearch callback
|
|
491
|
+
setLocalHistory(prev => {
|
|
492
|
+
const newItem = {
|
|
493
|
+
text: inputValue.trim(),
|
|
494
|
+
timestamp: now,
|
|
495
|
+
formattedDate: formatDate(now),
|
|
496
|
+
}
|
|
497
|
+
const filteredPrev = prev.filter(h => h.text !== newItem.text)
|
|
498
|
+
return [newItem, ...filteredPrev].slice(0, maxHistoryItems)
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
505
|
+
if (event.key === 'Enter') {
|
|
506
|
+
handleInputSubmit()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
278
510
|
const handleFocus = () => {
|
|
279
511
|
setIsFocused(true)
|
|
280
512
|
}
|
|
@@ -283,10 +515,245 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
283
515
|
if (!value && !inputValue) {
|
|
284
516
|
setIsFocused(false)
|
|
285
517
|
}
|
|
518
|
+
// Save search when blurring
|
|
519
|
+
if (inputValue.trim()) {
|
|
520
|
+
handleInputSubmit()
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Function to handle tab change
|
|
525
|
+
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
|
526
|
+
// Set tab value
|
|
527
|
+
setActiveTab(newValue)
|
|
528
|
+
|
|
529
|
+
// Keep the dropdown open when switching tabs
|
|
530
|
+
setIsOpen(true)
|
|
531
|
+
|
|
532
|
+
// Prevent any bubbling that might close the dropdown
|
|
533
|
+
_event.preventDefault()
|
|
534
|
+
_event.stopPropagation()
|
|
286
535
|
}
|
|
287
536
|
|
|
288
537
|
const labelId = `${name}-label`
|
|
289
538
|
|
|
539
|
+
// Use a combined history that prioritizes prop history when available but falls back to local history
|
|
540
|
+
const combinedHistory = React.useMemo(() => {
|
|
541
|
+
if (searchHistory?.length) {
|
|
542
|
+
// Convert string[] to HistoryItem[] if needed
|
|
543
|
+
if (typeof searchHistory[0] === 'string') {
|
|
544
|
+
return (searchHistory as string[]).map((text, index) => {
|
|
545
|
+
// Create timestamps with slight variations
|
|
546
|
+
const timestamp = new Date()
|
|
547
|
+
timestamp.setMinutes(timestamp.getMinutes() - (index + 1))
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
text,
|
|
551
|
+
timestamp,
|
|
552
|
+
formattedDate: formatDate(timestamp),
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Already HistoryItem[] format
|
|
558
|
+
return searchHistory as HistoryItem[]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return localHistory
|
|
562
|
+
}, [searchHistory, localHistory])
|
|
563
|
+
|
|
564
|
+
// Create a combined options array based on active tab and input value
|
|
565
|
+
const getFilteredOptions = React.useCallback(() => {
|
|
566
|
+
const currentInputVal = inputValue.trim()
|
|
567
|
+
|
|
568
|
+
// HISTORY TAB - only apply when variant is complex
|
|
569
|
+
if (activeTab === 1 && variant === 'complex') {
|
|
570
|
+
if (combinedHistory.length === 0) {
|
|
571
|
+
// Show a placeholder message if no history
|
|
572
|
+
return [
|
|
573
|
+
{
|
|
574
|
+
value: 'No search history',
|
|
575
|
+
uniqueKey: 'no-history',
|
|
576
|
+
attribute1: 'Try searching for something first',
|
|
577
|
+
},
|
|
578
|
+
]
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Map history items to dropdown options
|
|
582
|
+
return combinedHistory.map(item => {
|
|
583
|
+
// Check if this history item matches any of the original options
|
|
584
|
+
const matchingOption = options.find(
|
|
585
|
+
opt =>
|
|
586
|
+
opt.value.toLowerCase() === item.text.toLowerCase() ||
|
|
587
|
+
(
|
|
588
|
+
opt.value.replace(/_/g, ' ').charAt(0).toUpperCase() +
|
|
589
|
+
opt.value.replace(/_/g, ' ').slice(1)
|
|
590
|
+
).toLowerCase() === item.text.toLowerCase()
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
if (matchingOption) {
|
|
594
|
+
// If we have a matching original option, use its attributes
|
|
595
|
+
return {
|
|
596
|
+
value: item.text,
|
|
597
|
+
uniqueKey: `history-${item.text}`,
|
|
598
|
+
attribute1: 'Search History',
|
|
599
|
+
attribute2: item.formattedDate || formatDate(item.timestamp),
|
|
600
|
+
attribute3: matchingOption.attribute1,
|
|
601
|
+
attribute4: matchingOption.attribute2,
|
|
602
|
+
attribute5: matchingOption.attribute3,
|
|
603
|
+
attribute6: matchingOption.attribute4,
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
// Otherwise just use the basic history item with timestamp
|
|
607
|
+
return {
|
|
608
|
+
value: item.text,
|
|
609
|
+
uniqueKey: `history-${item.text}`,
|
|
610
|
+
attribute1: 'Search History',
|
|
611
|
+
attribute2: item.formattedDate || formatDate(item.timestamp),
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
})
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ALL OPTIONS TAB (activeTab === 0)
|
|
618
|
+
// If input is empty, return all options
|
|
619
|
+
if (!currentInputVal) {
|
|
620
|
+
return options
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Filter options based on current input
|
|
624
|
+
const filteredOpts = options.filter(opt =>
|
|
625
|
+
opt.value.toLowerCase().includes(currentInputVal.toLowerCase())
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
// If no matches found, add the current input as a custom option
|
|
629
|
+
if (filteredOpts.length === 0) {
|
|
630
|
+
return [
|
|
631
|
+
{
|
|
632
|
+
value: currentInputVal,
|
|
633
|
+
uniqueKey: `current-${currentInputVal}`,
|
|
634
|
+
attribute1: 'Search',
|
|
635
|
+
},
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Check if the input exactly matches any option
|
|
640
|
+
const exactMatch = filteredOpts.some(
|
|
641
|
+
opt => opt.value.toLowerCase() === currentInputVal.toLowerCase()
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
// If no exact match, add the current input as the first option
|
|
645
|
+
if (!exactMatch) {
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
value: currentInputVal,
|
|
649
|
+
uniqueKey: `current-${currentInputVal}`,
|
|
650
|
+
attribute1: 'Search',
|
|
651
|
+
},
|
|
652
|
+
...filteredOpts,
|
|
653
|
+
]
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return filteredOpts
|
|
657
|
+
}, [inputValue, combinedHistory, options, activeTab, variant])
|
|
658
|
+
|
|
659
|
+
// Create the footer component for the dropdown with tabs
|
|
660
|
+
const ListboxFooter = React.forwardRef<HTMLDivElement>((_, ref) => (
|
|
661
|
+
<Box
|
|
662
|
+
ref={(node: HTMLDivElement | null) => {
|
|
663
|
+
// Set both refs
|
|
664
|
+
if (ref) {
|
|
665
|
+
if (typeof ref === 'function') {
|
|
666
|
+
ref(node)
|
|
667
|
+
} else {
|
|
668
|
+
ref.current = node
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
tabsRef.current = node
|
|
672
|
+
}}
|
|
673
|
+
sx={{
|
|
674
|
+
position: 'sticky',
|
|
675
|
+
bottom: 0,
|
|
676
|
+
backgroundColor: white.main,
|
|
677
|
+
zIndex: 2,
|
|
678
|
+
borderTop: `1px solid ${black.light}`,
|
|
679
|
+
}}
|
|
680
|
+
onClick={e => {
|
|
681
|
+
e.preventDefault()
|
|
682
|
+
e.stopPropagation()
|
|
683
|
+
}}
|
|
684
|
+
onMouseDown={e => {
|
|
685
|
+
e.preventDefault()
|
|
686
|
+
e.stopPropagation()
|
|
687
|
+
}}
|
|
688
|
+
>
|
|
689
|
+
<StyledTabs
|
|
690
|
+
value={activeTab}
|
|
691
|
+
onChange={handleTabChange}
|
|
692
|
+
centered
|
|
693
|
+
sx={{
|
|
694
|
+
pointerEvents: 'all',
|
|
695
|
+
}}
|
|
696
|
+
>
|
|
697
|
+
<StyledTab
|
|
698
|
+
icon={<SearchIcon fontSize="small" />}
|
|
699
|
+
label="ALL OPTIONS"
|
|
700
|
+
iconPosition="start"
|
|
701
|
+
sx={{
|
|
702
|
+
pointerEvents: 'all',
|
|
703
|
+
}}
|
|
704
|
+
/>
|
|
705
|
+
<StyledTab
|
|
706
|
+
icon={<HistoryIcon fontSize="small" />}
|
|
707
|
+
label="HISTORY"
|
|
708
|
+
iconPosition="start"
|
|
709
|
+
sx={{
|
|
710
|
+
pointerEvents: 'all',
|
|
711
|
+
}}
|
|
712
|
+
/>
|
|
713
|
+
</StyledTabs>
|
|
714
|
+
</Box>
|
|
715
|
+
))
|
|
716
|
+
|
|
717
|
+
// Add display name to ListboxFooter
|
|
718
|
+
ListboxFooter.displayName = 'ListboxFooter'
|
|
719
|
+
|
|
720
|
+
// Custom Listbox component that adds the footer
|
|
721
|
+
const CustomListbox = React.forwardRef<
|
|
722
|
+
HTMLElement,
|
|
723
|
+
React.HTMLAttributes<HTMLElement>
|
|
724
|
+
>((props, ref) => {
|
|
725
|
+
const { children, ...other } = props
|
|
726
|
+
return (
|
|
727
|
+
<div
|
|
728
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
729
|
+
onClick={e => {
|
|
730
|
+
e.stopPropagation()
|
|
731
|
+
return false
|
|
732
|
+
}}
|
|
733
|
+
style={{ pointerEvents: 'auto' }}
|
|
734
|
+
>
|
|
735
|
+
<ul {...other}>{children}</ul>
|
|
736
|
+
{variant === 'complex' && <ListboxFooter />}
|
|
737
|
+
</div>
|
|
738
|
+
)
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// Add display name to CustomListbox
|
|
742
|
+
CustomListbox.displayName = 'CustomListbox'
|
|
743
|
+
|
|
744
|
+
// Memoize the filtered options to prevent recreation on every render
|
|
745
|
+
const filteredOptions = React.useMemo(
|
|
746
|
+
() => getFilteredOptions(),
|
|
747
|
+
[getFilteredOptions]
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
// Ensure activeTab is always 0 for simple variant
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
if (variant === 'simple' && activeTab !== 0) {
|
|
753
|
+
setActiveTab(0)
|
|
754
|
+
}
|
|
755
|
+
}, [variant, activeTab])
|
|
756
|
+
|
|
290
757
|
return (
|
|
291
758
|
<StyledFormControl
|
|
292
759
|
error={error}
|
|
@@ -308,7 +775,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
308
775
|
</StyledInputLabel>
|
|
309
776
|
<StyledAutocomplete
|
|
310
777
|
id={name}
|
|
311
|
-
options={
|
|
778
|
+
options={filteredOptions}
|
|
312
779
|
freeSolo
|
|
313
780
|
value={value}
|
|
314
781
|
onChange={handleChange}
|
|
@@ -321,6 +788,18 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
321
788
|
onFocus={handleFocus}
|
|
322
789
|
onBlur={handleBlur}
|
|
323
790
|
forcePopupIcon
|
|
791
|
+
open={isOpen}
|
|
792
|
+
onOpen={() => {
|
|
793
|
+
setIsOpen(true)
|
|
794
|
+
}}
|
|
795
|
+
onClose={event => {
|
|
796
|
+
// Don't close when clicking tabs
|
|
797
|
+
if (tabsRef.current?.contains(event.target as Node)) {
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
setIsOpen(false)
|
|
802
|
+
}}
|
|
324
803
|
popupIcon={
|
|
325
804
|
<ArrowDropDownIcon
|
|
326
805
|
sx={{ color: disabled ? 'rgba(0, 0, 0, 0.38)' : black.main }}
|
|
@@ -328,8 +807,69 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
328
807
|
}
|
|
329
808
|
disablePortal={false}
|
|
330
809
|
ListboxProps={{
|
|
331
|
-
style: {
|
|
810
|
+
style: {
|
|
811
|
+
maxHeight: '300px',
|
|
812
|
+
overflowY: 'auto',
|
|
813
|
+
pointerEvents: 'all',
|
|
814
|
+
},
|
|
815
|
+
}}
|
|
816
|
+
ListboxComponent={CustomListbox}
|
|
817
|
+
componentsProps={{
|
|
818
|
+
popper: {
|
|
819
|
+
onClick: e => {
|
|
820
|
+
// Prevent clicks in the popper from closing the dropdown
|
|
821
|
+
e.stopPropagation()
|
|
822
|
+
},
|
|
823
|
+
style: {
|
|
824
|
+
pointerEvents: 'all',
|
|
825
|
+
// Ensure clicks inside the popper don't close it
|
|
826
|
+
inset: '0px auto auto 0px',
|
|
827
|
+
zIndex: 9999,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
paper: {
|
|
831
|
+
onClick: e => {
|
|
832
|
+
// Prevent clicks on the paper from closing the dropdown
|
|
833
|
+
e.stopPropagation()
|
|
834
|
+
},
|
|
835
|
+
style: { pointerEvents: 'all' },
|
|
836
|
+
},
|
|
332
837
|
}}
|
|
838
|
+
ref={autocompleteRef}
|
|
839
|
+
PopperComponent={props => (
|
|
840
|
+
<Popper
|
|
841
|
+
{...props}
|
|
842
|
+
onClick={e => {
|
|
843
|
+
e.stopPropagation()
|
|
844
|
+
}}
|
|
845
|
+
style={{
|
|
846
|
+
...props.style,
|
|
847
|
+
pointerEvents: 'all',
|
|
848
|
+
zIndex: 9999,
|
|
849
|
+
}}
|
|
850
|
+
modifiers={[
|
|
851
|
+
{
|
|
852
|
+
name: 'preventOverflow',
|
|
853
|
+
enabled: true,
|
|
854
|
+
options: {
|
|
855
|
+
altAxis: true,
|
|
856
|
+
altBoundary: true,
|
|
857
|
+
tether: true,
|
|
858
|
+
rootBoundary: 'document',
|
|
859
|
+
padding: 8,
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: 'offset',
|
|
864
|
+
options: {
|
|
865
|
+
offset: [0, 0],
|
|
866
|
+
},
|
|
867
|
+
},
|
|
868
|
+
]}
|
|
869
|
+
>
|
|
870
|
+
{props.children}
|
|
871
|
+
</Popper>
|
|
872
|
+
)}
|
|
333
873
|
disabled={disabled}
|
|
334
874
|
backgroundcolor={backgroundcolor}
|
|
335
875
|
outlinecolor={outlinecolor}
|
|
@@ -337,10 +877,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
337
877
|
inputfontcolor={inputfontcolor}
|
|
338
878
|
placeholdercolor={placeholdercolor}
|
|
339
879
|
variant={variant}
|
|
340
|
-
filterOptions={
|
|
341
|
-
const input = state.inputValue.toLowerCase()
|
|
342
|
-
return opts.filter(o => o.value.toLowerCase().includes(input))
|
|
343
|
-
}}
|
|
880
|
+
filterOptions={opts => opts} // We're handling filtering ourselves
|
|
344
881
|
getOptionLabel={(option: DropdownOption | string) => {
|
|
345
882
|
if (typeof option === 'string') {
|
|
346
883
|
return option
|
|
@@ -373,24 +910,63 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
373
910
|
// Use the uniqueKey prop if available, otherwise fall back to the provided key
|
|
374
911
|
const optionKey = option.uniqueKey || key
|
|
375
912
|
|
|
913
|
+
// Check if this is a history item
|
|
914
|
+
const isHistoryItem = option.uniqueKey?.startsWith('history-')
|
|
915
|
+
const isCurrentInput = option.uniqueKey?.startsWith('current-')
|
|
916
|
+
const isNoHistoryPlaceholder = option.uniqueKey === 'no-history'
|
|
917
|
+
|
|
376
918
|
return (
|
|
377
919
|
<li key={optionKey} {...restLiProps} style={liStyle}>
|
|
378
920
|
{/* Main value - both variants */}
|
|
379
921
|
<Typography
|
|
380
922
|
fontvariant="merriparagraph"
|
|
381
|
-
text={
|
|
923
|
+
text={
|
|
924
|
+
isNoHistoryPlaceholder
|
|
925
|
+
? option.value
|
|
926
|
+
: isCurrentInput
|
|
927
|
+
? `Search: "${option.value}"`
|
|
928
|
+
: isHistoryItem
|
|
929
|
+
? `History: ${option.value.replace(/_/g, ' ')}`
|
|
930
|
+
: option.value.replace(/_/g, ' ')
|
|
931
|
+
}
|
|
382
932
|
fontcolor={black.main}
|
|
383
933
|
sx={{
|
|
384
934
|
fontSize: '14px',
|
|
385
|
-
fontWeight:
|
|
935
|
+
fontWeight: isCurrentInput
|
|
936
|
+
? '500'
|
|
937
|
+
: isHistoryItem
|
|
938
|
+
? '400'
|
|
939
|
+
: variant === 'complex'
|
|
940
|
+
? '500'
|
|
941
|
+
: 'normal',
|
|
942
|
+
fontStyle: isHistoryItem ? 'italic' : 'normal',
|
|
386
943
|
lineHeight: '20px',
|
|
387
944
|
width: '100%',
|
|
388
945
|
textAlign: 'left',
|
|
389
946
|
}}
|
|
390
947
|
/>
|
|
391
948
|
|
|
949
|
+
{/* For history items, show the timestamp */}
|
|
950
|
+
{isHistoryItem && option.attribute2 && (
|
|
951
|
+
<Typography
|
|
952
|
+
fontvariant="merriparagraph"
|
|
953
|
+
text={option.attribute2}
|
|
954
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
955
|
+
sx={{
|
|
956
|
+
fontSize: '11px',
|
|
957
|
+
lineHeight: '14px',
|
|
958
|
+
width: '100%',
|
|
959
|
+
textAlign: 'left',
|
|
960
|
+
fontStyle: 'italic',
|
|
961
|
+
}}
|
|
962
|
+
/>
|
|
963
|
+
)}
|
|
964
|
+
|
|
392
965
|
{/* For simple variant - show attribute1 and attribute2 on one line */}
|
|
393
966
|
{variant === 'simple' &&
|
|
967
|
+
!isHistoryItem &&
|
|
968
|
+
!isCurrentInput &&
|
|
969
|
+
!isNoHistoryPlaceholder &&
|
|
394
970
|
(option.attribute1 || option.attribute2) && (
|
|
395
971
|
<Typography
|
|
396
972
|
fontvariant="merriparagraph"
|
|
@@ -408,26 +984,68 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
408
984
|
)}
|
|
409
985
|
|
|
410
986
|
{/* For complex variant - show attributes on separate lines */}
|
|
411
|
-
{variant === 'complex' &&
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
987
|
+
{variant === 'complex' &&
|
|
988
|
+
!isHistoryItem &&
|
|
989
|
+
!isCurrentInput &&
|
|
990
|
+
!isNoHistoryPlaceholder && (
|
|
991
|
+
<>
|
|
992
|
+
{/* First line of attributes */}
|
|
993
|
+
{(option.attribute1 || option.attribute2) && (
|
|
994
|
+
<Typography
|
|
995
|
+
fontvariant="merriparagraph"
|
|
996
|
+
text={[option.attribute1, option.attribute2]
|
|
997
|
+
.filter(Boolean)
|
|
998
|
+
.join(' | ')}
|
|
999
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1000
|
+
sx={{
|
|
1001
|
+
fontSize: '12px',
|
|
1002
|
+
lineHeight: '16px',
|
|
1003
|
+
width: '100%',
|
|
1004
|
+
textAlign: 'left',
|
|
1005
|
+
}}
|
|
1006
|
+
/>
|
|
1007
|
+
)}
|
|
1008
|
+
|
|
1009
|
+
{/* Second line of attributes */}
|
|
1010
|
+
{(option.attribute3 || option.attribute4) && (
|
|
1011
|
+
<Typography
|
|
1012
|
+
fontvariant="merriparagraph"
|
|
1013
|
+
text={[option.attribute3, option.attribute4]
|
|
1014
|
+
.filter(Boolean)
|
|
1015
|
+
.join(' | ')}
|
|
1016
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1017
|
+
sx={{
|
|
1018
|
+
fontSize: '12px',
|
|
1019
|
+
lineHeight: '16px',
|
|
1020
|
+
width: '100%',
|
|
1021
|
+
textAlign: 'left',
|
|
1022
|
+
}}
|
|
1023
|
+
/>
|
|
1024
|
+
)}
|
|
429
1025
|
|
|
430
|
-
|
|
1026
|
+
{/* Third line of attributes */}
|
|
1027
|
+
{(option.attribute5 || option.attribute6) && (
|
|
1028
|
+
<Typography
|
|
1029
|
+
fontvariant="merriparagraph"
|
|
1030
|
+
text={[option.attribute5, option.attribute6]
|
|
1031
|
+
.filter(Boolean)
|
|
1032
|
+
.join(' | ')}
|
|
1033
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1034
|
+
sx={{
|
|
1035
|
+
fontSize: '12px',
|
|
1036
|
+
lineHeight: '16px',
|
|
1037
|
+
width: '100%',
|
|
1038
|
+
textAlign: 'left',
|
|
1039
|
+
}}
|
|
1040
|
+
/>
|
|
1041
|
+
)}
|
|
1042
|
+
</>
|
|
1043
|
+
)}
|
|
1044
|
+
|
|
1045
|
+
{/* For history items, show additional attributes from original options */}
|
|
1046
|
+
{isHistoryItem && variant === 'complex' && (
|
|
1047
|
+
<>
|
|
1048
|
+
{/* Show attribute3/4 as first additional line for history */}
|
|
431
1049
|
{(option.attribute3 || option.attribute4) && (
|
|
432
1050
|
<Typography
|
|
433
1051
|
fontvariant="merriparagraph"
|
|
@@ -440,11 +1058,12 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
440
1058
|
lineHeight: '16px',
|
|
441
1059
|
width: '100%',
|
|
442
1060
|
textAlign: 'left',
|
|
1061
|
+
fontStyle: 'italic',
|
|
443
1062
|
}}
|
|
444
1063
|
/>
|
|
445
1064
|
)}
|
|
446
1065
|
|
|
447
|
-
{/*
|
|
1066
|
+
{/* Show attribute5/6 as second additional line for history */}
|
|
448
1067
|
{(option.attribute5 || option.attribute6) && (
|
|
449
1068
|
<Typography
|
|
450
1069
|
fontvariant="merriparagraph"
|
|
@@ -457,11 +1076,43 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
457
1076
|
lineHeight: '16px',
|
|
458
1077
|
width: '100%',
|
|
459
1078
|
textAlign: 'left',
|
|
1079
|
+
fontStyle: 'italic',
|
|
460
1080
|
}}
|
|
461
1081
|
/>
|
|
462
1082
|
)}
|
|
463
1083
|
</>
|
|
464
1084
|
)}
|
|
1085
|
+
|
|
1086
|
+
{/* For current input search, show a simpler display */}
|
|
1087
|
+
{isCurrentInput && option.attribute1 && (
|
|
1088
|
+
<Typography
|
|
1089
|
+
fontvariant="merriparagraph"
|
|
1090
|
+
text={option.attribute1}
|
|
1091
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1092
|
+
sx={{
|
|
1093
|
+
fontSize: '12px',
|
|
1094
|
+
lineHeight: '16px',
|
|
1095
|
+
width: '100%',
|
|
1096
|
+
textAlign: 'left',
|
|
1097
|
+
}}
|
|
1098
|
+
/>
|
|
1099
|
+
)}
|
|
1100
|
+
|
|
1101
|
+
{/* For no history placeholder, show the instructions */}
|
|
1102
|
+
{isNoHistoryPlaceholder && option.attribute1 && (
|
|
1103
|
+
<Typography
|
|
1104
|
+
fontvariant="merriparagraph"
|
|
1105
|
+
text={option.attribute1}
|
|
1106
|
+
fontcolor="rgba(0, 0, 0, 0.6)"
|
|
1107
|
+
sx={{
|
|
1108
|
+
fontSize: '12px',
|
|
1109
|
+
lineHeight: '16px',
|
|
1110
|
+
width: '100%',
|
|
1111
|
+
textAlign: 'left',
|
|
1112
|
+
fontStyle: 'italic',
|
|
1113
|
+
}}
|
|
1114
|
+
/>
|
|
1115
|
+
)}
|
|
465
1116
|
</li>
|
|
466
1117
|
)
|
|
467
1118
|
}}
|
|
@@ -471,6 +1122,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
471
1122
|
inputProps={{
|
|
472
1123
|
...params.inputProps,
|
|
473
1124
|
'aria-labelledby': labelId,
|
|
1125
|
+
onKeyDown: handleKeyDown,
|
|
474
1126
|
}}
|
|
475
1127
|
placeholder={placeholder}
|
|
476
1128
|
error={error}
|