goobs-frontend 0.9.23 → 0.9.25

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goobs-frontend",
3
- "version": "0.9.23",
3
+ "version": "0.9.25",
4
4
  "type": "module",
5
5
  "description": "A comprehensive React-based libary that extends the functionality of Material-UI",
6
6
  "license": "MIT",
@@ -37,10 +37,10 @@
37
37
  "otplib": "^12",
38
38
  "react-datepicker": "^8",
39
39
  "react-qr-code": "^2",
40
- "slate": "^0.112",
41
- "slate-dom": "^0.112",
42
- "slate-history": "^0.110",
43
- "slate-react": "^0.112",
40
+ "slate": "^0.114",
41
+ "slate-dom": "^0.114",
42
+ "slate-history": "^0.113",
43
+ "slate-react": "^0.114",
44
44
  "storybook": "^8",
45
45
  "zod": "^3",
46
46
  "zod-formik-adapter": "^1"
@@ -38,8 +38,7 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
38
38
  // If we're mobile, just render a single dropdown + "select all" checkbox
39
39
  if (isMobile) {
40
40
  const mobileOptions = allColumns.map(col => ({
41
- value: col.field,
42
- label: col.headerName ?? col.field,
41
+ value: col.headerName ?? col.field,
43
42
  }))
44
43
 
45
44
  // Find the currently-selected column as an object
@@ -47,7 +46,19 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
47
46
  mobileOptions.find(opt => opt.value === selectedOverflowField) || null
48
47
 
49
48
  const handleMobileChange = (value: { value: string } | null) => {
50
- setSelectedOverflowField(value?.value || '')
49
+ if (value && value.value) {
50
+ // Try to find a column with matching headerName first
51
+ const matchingColumn = allColumns.find(
52
+ col => col.headerName === value.value || col.field === value.value
53
+ )
54
+
55
+ // If found, use its field property, otherwise use the value directly
56
+ setSelectedOverflowField(
57
+ matchingColumn ? matchingColumn.field : value.value
58
+ )
59
+ } else {
60
+ setSelectedOverflowField('')
61
+ }
51
62
  }
52
63
 
53
64
  return (
@@ -107,7 +118,20 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
107
118
  // Desktop logic
108
119
  // ---------------------------
109
120
  const handleOverflowChange = (value: { value: string } | null) => {
110
- setSelectedOverflowField(value?.value || '')
121
+ // If using headerName for value in dropdown, we need to find the corresponding field
122
+ if (value && value.value) {
123
+ // Try to find a column with matching headerName first
124
+ const matchingColumn = overflowDesktopColumns.find(
125
+ col => col.headerName === value.value || col.field === value.value
126
+ )
127
+
128
+ // If found, use its field property, otherwise use the value directly
129
+ setSelectedOverflowField(
130
+ matchingColumn ? matchingColumn.field : value.value
131
+ )
132
+ } else {
133
+ setSelectedOverflowField('')
134
+ }
111
135
  }
112
136
 
113
137
  return (
@@ -150,8 +174,7 @@ const ColumnHeaderRow: React.FC<ColumnHeaderRowProps> = ({
150
174
  <SearchableDropdown
151
175
  label="More Columns"
152
176
  options={overflowDesktopColumns.map(oc => ({
153
- value: oc.field,
154
- label: oc.headerName ?? oc.field,
177
+ value: oc.headerName ?? oc.field,
155
178
  }))}
156
179
  defaultValue={selectedOverflowField}
157
180
  onChange={handleOverflowChange}
@@ -4,11 +4,38 @@ import DatePicker from 'react-datepicker'
4
4
  import 'react-datepicker/dist/react-datepicker.css'
5
5
  import CalendarTodayIcon from '@mui/icons-material/CalendarToday'
6
6
  import TextField, { TextFieldProps } from '../../Field/Text'
7
+ import { Box } from '@mui/material'
8
+
9
+ /**
10
+ * DateRange interface for range mode
11
+ */
12
+ export interface DateRange {
13
+ start: Date | null
14
+ end: Date | null
15
+ }
7
16
 
8
17
  export interface DateFieldProps
9
18
  extends Omit<TextFieldProps, 'onChange' | 'value' | 'endAdornment'> {
10
- onChange?: (date: Date | null) => void
11
- value?: Date | null
19
+ /**
20
+ * Callback when date changes
21
+ */
22
+ onChange?: (date: Date | null | DateRange) => void
23
+ /**
24
+ * Current date value
25
+ */
26
+ value?: Date | null | DateRange
27
+ /**
28
+ * Whether to show date range picker instead of single date
29
+ */
30
+ isRange?: boolean
31
+ /**
32
+ * Start date label (for range mode)
33
+ */
34
+ startLabel?: string
35
+ /**
36
+ * End date label (for range mode)
37
+ */
38
+ endLabel?: string
12
39
  }
13
40
 
14
41
  interface CustomInputProps {
@@ -35,6 +62,9 @@ const DateField: React.FC<DateFieldProps> = ({
35
62
  onChange,
36
63
  label = 'Select Date',
37
64
  value,
65
+ isRange = false,
66
+ startLabel = 'Start Date',
67
+ endLabel = 'End Date',
38
68
  ...rest
39
69
  }) => {
40
70
  const formatDate = (date: Date | null) => {
@@ -49,22 +79,45 @@ const DateField: React.FC<DateFieldProps> = ({
49
79
  return ''
50
80
  }
51
81
 
52
- const [selectedDate, setSelectedDate] = useState<Date>(value || new Date())
82
+ // Initialize state based on whether in range mode or single date mode
83
+ const [selectedDate, setSelectedDate] = useState<Date | null>(
84
+ isRange ? null : (value as Date | null) || new Date()
85
+ )
86
+ const [dateRange, setDateRange] = useState<DateRange>(
87
+ isRange
88
+ ? (value as DateRange) || { start: new Date(), end: new Date() }
89
+ : { start: new Date(), end: new Date() }
90
+ )
53
91
  const [isOpen, setIsOpen] = useState(false)
54
- const [inputValue, setInputValue] = useState(formatDate(selectedDate))
92
+ const [isStartDateOpen, setIsStartDateOpen] = useState(false)
93
+ const [isEndDateOpen, setIsEndDateOpen] = useState(false)
94
+ const [inputValue, setInputValue] = useState(
95
+ isRange ? '' : formatDate(selectedDate)
96
+ )
97
+ const [startDateInputValue, setStartDateInputValue] = useState(
98
+ formatDate(dateRange.start)
99
+ )
100
+ const [endDateInputValue, setEndDateInputValue] = useState(
101
+ formatDate(dateRange.end)
102
+ )
55
103
 
104
+ // Single date mode handlers
56
105
  const handleChange = (date: Date | null) => {
57
- if (date) {
58
- setSelectedDate(date)
59
- setInputValue(formatDate(date))
60
- setIsOpen(false)
61
- if (onChange) {
62
- onChange(date)
106
+ if (!isRange) {
107
+ if (date) {
108
+ setSelectedDate(date)
109
+ setInputValue(formatDate(date))
110
+ setIsOpen(false)
111
+ if (onChange) {
112
+ onChange(date)
113
+ }
63
114
  }
64
115
  }
65
116
  }
66
117
 
67
118
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
119
+ if (isRange) return // Only for single date mode
120
+
68
121
  const input = e.target
69
122
  const newValue = e.target.value
70
123
  const selectionStart = input.selectionStart || 0
@@ -103,6 +156,117 @@ const DateField: React.FC<DateFieldProps> = ({
103
156
  }, 0)
104
157
  }
105
158
 
159
+ // Range mode handlers
160
+ const handleStartDateChange = (date: Date | null) => {
161
+ if (isRange && date) {
162
+ const newRange = { ...dateRange, start: date }
163
+ setDateRange(newRange)
164
+ setStartDateInputValue(formatDate(date))
165
+ setIsStartDateOpen(false)
166
+ if (onChange) {
167
+ onChange(newRange)
168
+ }
169
+ }
170
+ }
171
+
172
+ const handleEndDateChange = (date: Date | null) => {
173
+ if (isRange && date) {
174
+ const newRange = { ...dateRange, end: date }
175
+ setDateRange(newRange)
176
+ setEndDateInputValue(formatDate(date))
177
+ setIsEndDateOpen(false)
178
+ if (onChange) {
179
+ onChange(newRange)
180
+ }
181
+ }
182
+ }
183
+
184
+ const handleStartDateInputChange = (
185
+ e: React.ChangeEvent<HTMLInputElement>
186
+ ) => {
187
+ if (!isRange) return
188
+
189
+ const input = e.target
190
+ const newValue = e.target.value
191
+ const selectionStart = input.selectionStart || 0
192
+
193
+ setStartDateInputValue(newValue)
194
+
195
+ const parts = newValue.split('/')
196
+ if (parts.length === 3) {
197
+ const month = parseInt(parts[0], 10)
198
+ const day = parseInt(parts[1], 10)
199
+ const year = parseInt(parts[2], 10)
200
+
201
+ if (!isNaN(month) && !isNaN(day) && !isNaN(year)) {
202
+ const newDate = new Date(year, month - 1, day)
203
+ if (
204
+ newDate.getMonth() === month - 1 &&
205
+ newDate.getDate() === day &&
206
+ newDate.getFullYear() === year
207
+ ) {
208
+ const newRange = { ...dateRange, start: newDate }
209
+ setDateRange(newRange)
210
+ if (onChange) {
211
+ onChange(newRange)
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ setTimeout(() => {
218
+ if (selectionStart <= 2) {
219
+ input.setSelectionRange(selectionStart, selectionStart)
220
+ } else if (selectionStart <= 5) {
221
+ input.setSelectionRange(selectionStart, selectionStart)
222
+ } else {
223
+ input.setSelectionRange(selectionStart, selectionStart)
224
+ }
225
+ }, 0)
226
+ }
227
+
228
+ const handleEndDateInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
229
+ if (!isRange) return
230
+
231
+ const input = e.target
232
+ const newValue = e.target.value
233
+ const selectionStart = input.selectionStart || 0
234
+
235
+ setEndDateInputValue(newValue)
236
+
237
+ const parts = newValue.split('/')
238
+ if (parts.length === 3) {
239
+ const month = parseInt(parts[0], 10)
240
+ const day = parseInt(parts[1], 10)
241
+ const year = parseInt(parts[2], 10)
242
+
243
+ if (!isNaN(month) && !isNaN(day) && !isNaN(year)) {
244
+ const newDate = new Date(year, month - 1, day)
245
+ if (
246
+ newDate.getMonth() === month - 1 &&
247
+ newDate.getDate() === day &&
248
+ newDate.getFullYear() === year
249
+ ) {
250
+ const newRange = { ...dateRange, end: newDate }
251
+ setDateRange(newRange)
252
+ if (onChange) {
253
+ onChange(newRange)
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ setTimeout(() => {
260
+ if (selectionStart <= 2) {
261
+ input.setSelectionRange(selectionStart, selectionStart)
262
+ } else if (selectionStart <= 5) {
263
+ input.setSelectionRange(selectionStart, selectionStart)
264
+ } else {
265
+ input.setSelectionRange(selectionStart, selectionStart)
266
+ }
267
+ }, 0)
268
+ }
269
+
106
270
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
107
271
  const input = e.currentTarget
108
272
  const selectionStart = input.selectionStart || 0
@@ -118,7 +282,120 @@ const DateField: React.FC<DateFieldProps> = ({
118
282
 
119
283
  if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
120
284
  e.preventDefault()
121
- const newDate = new Date(selectedDate)
285
+
286
+ if (!isRange) {
287
+ // Single date mode
288
+ const newDate = new Date(selectedDate || new Date())
289
+ const increment = e.key === 'ArrowUp' ? 1 : -1
290
+
291
+ switch (selectedPart) {
292
+ case 'month':
293
+ newDate.setMonth(newDate.getMonth() + increment)
294
+ break
295
+ case 'day':
296
+ newDate.setDate(newDate.getDate() + increment)
297
+ break
298
+ case 'year':
299
+ newDate.setFullYear(newDate.getFullYear() + increment)
300
+ break
301
+ }
302
+
303
+ setSelectedDate(newDate)
304
+ setInputValue(formatDate(newDate))
305
+ if (onChange) {
306
+ onChange(newDate)
307
+ }
308
+ }
309
+
310
+ setTimeout(() => {
311
+ switch (selectedPart) {
312
+ case 'month':
313
+ input.setSelectionRange(0, 2)
314
+ break
315
+ case 'day':
316
+ input.setSelectionRange(3, 5)
317
+ break
318
+ case 'year':
319
+ input.setSelectionRange(6, 10)
320
+ break
321
+ }
322
+ }, 0)
323
+ }
324
+ }
325
+
326
+ const handleStartDateKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
327
+ if (!isRange) return
328
+
329
+ const input = e.currentTarget
330
+ const selectionStart = input.selectionStart || 0
331
+
332
+ let selectedPart: 'month' | 'day' | 'year'
333
+ if (selectionStart <= 2) {
334
+ selectedPart = 'month'
335
+ } else if (selectionStart <= 5) {
336
+ selectedPart = 'day'
337
+ } else {
338
+ selectedPart = 'year'
339
+ }
340
+
341
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
342
+ e.preventDefault()
343
+ const newDate = new Date(dateRange.start || new Date())
344
+ const increment = e.key === 'ArrowUp' ? 1 : -1
345
+
346
+ switch (selectedPart) {
347
+ case 'month':
348
+ newDate.setMonth(newDate.getMonth() + increment)
349
+ break
350
+ case 'day':
351
+ newDate.setDate(newDate.getDate() + increment)
352
+ break
353
+ case 'year':
354
+ newDate.setFullYear(newDate.getFullYear() + increment)
355
+ break
356
+ }
357
+
358
+ const newRange = { ...dateRange, start: newDate }
359
+ setDateRange(newRange)
360
+ setStartDateInputValue(formatDate(newDate))
361
+ if (onChange) {
362
+ onChange(newRange)
363
+ }
364
+
365
+ setTimeout(() => {
366
+ switch (selectedPart) {
367
+ case 'month':
368
+ input.setSelectionRange(0, 2)
369
+ break
370
+ case 'day':
371
+ input.setSelectionRange(3, 5)
372
+ break
373
+ case 'year':
374
+ input.setSelectionRange(6, 10)
375
+ break
376
+ }
377
+ }, 0)
378
+ }
379
+ }
380
+
381
+ const handleEndDateKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
382
+ if (!isRange) return
383
+
384
+ const input = e.currentTarget
385
+ const selectionStart = input.selectionStart || 0
386
+
387
+ let selectedPart: 'month' | 'day' | 'year'
388
+ if (selectionStart <= 2) {
389
+ selectedPart = 'month'
390
+ } else if (selectionStart <= 5) {
391
+ selectedPart = 'day'
392
+ } else {
393
+ selectedPart = 'year'
394
+ }
395
+
396
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
397
+ e.preventDefault()
398
+ const newDate = new Date(dateRange.end || new Date())
122
399
  const increment = e.key === 'ArrowUp' ? 1 : -1
123
400
 
124
401
  switch (selectedPart) {
@@ -133,10 +410,11 @@ const DateField: React.FC<DateFieldProps> = ({
133
410
  break
134
411
  }
135
412
 
136
- setSelectedDate(newDate)
137
- setInputValue(formatDate(newDate))
413
+ const newRange = { ...dateRange, end: newDate }
414
+ setDateRange(newRange)
415
+ setEndDateInputValue(formatDate(newDate))
138
416
  if (onChange) {
139
- onChange(newDate)
417
+ onChange(newRange)
140
418
  }
141
419
 
142
420
  setTimeout(() => {
@@ -170,7 +448,23 @@ const DateField: React.FC<DateFieldProps> = ({
170
448
 
171
449
  const handleIconClick = (e: React.MouseEvent) => {
172
450
  e.stopPropagation()
173
- setIsOpen(true)
451
+ if (!isRange) {
452
+ setIsOpen(true)
453
+ }
454
+ }
455
+
456
+ const handleStartIconClick = (e: React.MouseEvent) => {
457
+ e.stopPropagation()
458
+ if (isRange) {
459
+ setIsStartDateOpen(true)
460
+ }
461
+ }
462
+
463
+ const handleEndIconClick = (e: React.MouseEvent) => {
464
+ e.stopPropagation()
465
+ if (isRange) {
466
+ setIsEndDateOpen(true)
467
+ }
174
468
  }
175
469
 
176
470
  const calendarIcon = (
@@ -187,6 +481,94 @@ const DateField: React.FC<DateFieldProps> = ({
187
481
  />
188
482
  )
189
483
 
484
+ const startCalendarIcon = (
485
+ <CalendarTodayIcon
486
+ onClick={handleStartIconClick}
487
+ sx={{
488
+ cursor: 'pointer',
489
+ '&:hover': {
490
+ opacity: 0.8,
491
+ },
492
+ fontSize: '20px',
493
+ color: 'black',
494
+ }}
495
+ />
496
+ )
497
+
498
+ const endCalendarIcon = (
499
+ <CalendarTodayIcon
500
+ onClick={handleEndIconClick}
501
+ sx={{
502
+ cursor: 'pointer',
503
+ '&:hover': {
504
+ opacity: 0.8,
505
+ },
506
+ fontSize: '20px',
507
+ color: 'black',
508
+ }}
509
+ />
510
+ )
511
+
512
+ if (isRange) {
513
+ return (
514
+ <Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
515
+ <Box sx={{ flex: 1 }}>
516
+ <TextField
517
+ label={startLabel}
518
+ value={startDateInputValue}
519
+ onChange={handleStartDateInputChange}
520
+ endAdornment={startCalendarIcon}
521
+ slotProps={{
522
+ input: {
523
+ readOnly: false,
524
+ style: { cursor: 'text', height: '40px' },
525
+ onKeyDown: handleStartDateKeyDown,
526
+ onClick: handleClick,
527
+ },
528
+ }}
529
+ {...rest}
530
+ />
531
+ <DatePicker
532
+ selected={dateRange.start ?? undefined}
533
+ onChange={handleStartDateChange}
534
+ dateFormat="MM/dd/yyyy"
535
+ customInput={<CustomInput />}
536
+ open={isStartDateOpen}
537
+ onClickOutside={() => setIsStartDateOpen(false)}
538
+ shouldCloseOnSelect
539
+ />
540
+ </Box>
541
+ <Box sx={{ flex: 1 }}>
542
+ <TextField
543
+ label={endLabel}
544
+ value={endDateInputValue}
545
+ onChange={handleEndDateInputChange}
546
+ endAdornment={endCalendarIcon}
547
+ slotProps={{
548
+ input: {
549
+ readOnly: false,
550
+ style: { cursor: 'text', height: '40px' },
551
+ onKeyDown: handleEndDateKeyDown,
552
+ onClick: handleClick,
553
+ },
554
+ }}
555
+ {...rest}
556
+ />
557
+ <DatePicker
558
+ selected={dateRange.end ?? undefined}
559
+ onChange={handleEndDateChange}
560
+ dateFormat="MM/dd/yyyy"
561
+ customInput={<CustomInput />}
562
+ open={isEndDateOpen}
563
+ onClickOutside={() => setIsEndDateOpen(false)}
564
+ shouldCloseOnSelect
565
+ minDate={dateRange.start ?? undefined}
566
+ />
567
+ </Box>
568
+ </Box>
569
+ )
570
+ }
571
+
190
572
  return (
191
573
  <>
192
574
  <TextField
@@ -205,7 +587,7 @@ const DateField: React.FC<DateFieldProps> = ({
205
587
  {...rest}
206
588
  />
207
589
  <DatePicker
208
- selected={selectedDate}
590
+ selected={selectedDate ?? undefined}
209
591
  onChange={handleChange}
210
592
  dateFormat="MM/dd/yyyy"
211
593
  customInput={<CustomInput />}