goobs-frontend 0.9.12 → 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 +11 -4
- 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
|
@@ -3,11 +3,13 @@ import React, {
|
|
|
3
3
|
ChangeEvent,
|
|
4
4
|
KeyboardEvent,
|
|
5
5
|
useState,
|
|
6
|
+
useEffect,
|
|
6
7
|
useCallback,
|
|
7
8
|
FC,
|
|
8
9
|
} from 'react'
|
|
9
10
|
import { Input, Box } from '@mui/material'
|
|
10
11
|
import { red, green } from '../../styles/palette'
|
|
12
|
+
import CustomButton, { CustomButtonProps } from '../Button'
|
|
11
13
|
|
|
12
14
|
export interface ConfirmationCodeInputsProps {
|
|
13
15
|
identifier?: string
|
|
@@ -18,16 +20,41 @@ export interface ConfirmationCodeInputsProps {
|
|
|
18
20
|
'aria-invalid'?: boolean
|
|
19
21
|
onChange?: (value: string) => void
|
|
20
22
|
value?: string
|
|
23
|
+
|
|
24
|
+
/** Whether a verification code has been sent (to toggle between "Send Code" and "Resend Code") */
|
|
25
|
+
codeSent?: boolean
|
|
26
|
+
|
|
27
|
+
/** Callback function for when the Verify button is clicked */
|
|
28
|
+
onVerify?: () => void | Promise<void>
|
|
29
|
+
|
|
30
|
+
/** Callback function for when the Send/Resend button is clicked */
|
|
31
|
+
onSendResend?: () => void | Promise<void>
|
|
32
|
+
|
|
33
|
+
/** Custom props for the Verify button */
|
|
34
|
+
verifyButtonProps?: Partial<CustomButtonProps>
|
|
35
|
+
|
|
36
|
+
/** Custom props for the Send/Resend button */
|
|
37
|
+
sendResendButtonProps?: Partial<CustomButtonProps>
|
|
38
|
+
|
|
39
|
+
/** Whether to show action buttons (verify and send/resend) */
|
|
40
|
+
showActionButtons?: boolean
|
|
41
|
+
|
|
42
|
+
/** Whether to show the Send/Resend button. If false, only the Verify button will be shown. */
|
|
43
|
+
showSendResendButton?: boolean
|
|
21
44
|
}
|
|
22
45
|
|
|
23
46
|
interface UseCodeConfirmationProps {
|
|
24
47
|
codeLength: number
|
|
25
48
|
onChange?: (value: string) => void
|
|
49
|
+
inputRefs: React.MutableRefObject<(HTMLInputElement | null)[]>
|
|
50
|
+
setInternalValue: React.Dispatch<React.SetStateAction<string>>
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
const useCodeConfirmation = ({
|
|
29
54
|
codeLength,
|
|
30
55
|
onChange,
|
|
56
|
+
inputRefs,
|
|
57
|
+
setInternalValue,
|
|
31
58
|
}: UseCodeConfirmationProps) => {
|
|
32
59
|
const [code, setCode] = useState<Record<string, string>>(
|
|
33
60
|
Object.fromEntries(
|
|
@@ -39,6 +66,27 @@ const useCodeConfirmation = ({
|
|
|
39
66
|
(event: React.ChangeEvent<HTMLInputElement>, index: number) => {
|
|
40
67
|
// Only keep digits
|
|
41
68
|
const value = event.target.value.replace(/\D/g, '')
|
|
69
|
+
|
|
70
|
+
// Handle the case when the input is cleared
|
|
71
|
+
if (value === '') {
|
|
72
|
+
// When clearing, we just update the code
|
|
73
|
+
setCode(prevCode => {
|
|
74
|
+
const newCode = {
|
|
75
|
+
...prevCode,
|
|
76
|
+
[`code${index + 1}`]: value,
|
|
77
|
+
}
|
|
78
|
+
// Combine all code pieces
|
|
79
|
+
const combinedValue = Object.values(newCode).join('')
|
|
80
|
+
|
|
81
|
+
// Update both internal state and external handler
|
|
82
|
+
setInternalValue(combinedValue)
|
|
83
|
+
onChange?.(combinedValue)
|
|
84
|
+
return newCode
|
|
85
|
+
})
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle direct value setting via typing
|
|
42
90
|
if (value.length <= 1) {
|
|
43
91
|
// Only process if it's a single digit or empty
|
|
44
92
|
setCode(prevCode => {
|
|
@@ -48,12 +96,15 @@ const useCodeConfirmation = ({
|
|
|
48
96
|
}
|
|
49
97
|
// Combine all code pieces
|
|
50
98
|
const combinedValue = Object.values(newCode).join('')
|
|
99
|
+
|
|
100
|
+
// Update both internal state and external handler
|
|
101
|
+
setInternalValue(combinedValue)
|
|
51
102
|
onChange?.(combinedValue)
|
|
52
103
|
return newCode
|
|
53
104
|
})
|
|
54
105
|
}
|
|
55
106
|
},
|
|
56
|
-
[onChange]
|
|
107
|
+
[onChange, setInternalValue]
|
|
57
108
|
)
|
|
58
109
|
|
|
59
110
|
const handleKeyDown = useCallback(
|
|
@@ -76,42 +127,41 @@ const useCodeConfirmation = ({
|
|
|
76
127
|
|
|
77
128
|
// If user pressed backspace on an empty input, move cursor to previous
|
|
78
129
|
if (event.key === 'Backspace' && !code[`code${index + 1}`] && index > 0) {
|
|
130
|
+
event.preventDefault()
|
|
79
131
|
setCode(prevCode => {
|
|
80
132
|
const newCode = {
|
|
81
133
|
...prevCode,
|
|
82
134
|
[`code${index}`]: '',
|
|
83
135
|
}
|
|
84
136
|
const combinedValue = Object.values(newCode).join('')
|
|
137
|
+
|
|
138
|
+
// Update both internal state and external handler
|
|
139
|
+
setInternalValue(combinedValue)
|
|
85
140
|
onChange?.(combinedValue)
|
|
86
141
|
return newCode
|
|
87
142
|
})
|
|
88
143
|
|
|
89
144
|
// Move focus to previous input
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
if (inputRefs && inputRefs.current && inputRefs.current[index - 1]) {
|
|
147
|
+
inputRefs.current[index - 1]?.focus()
|
|
148
|
+
}
|
|
149
|
+
}, 0)
|
|
96
150
|
} else if (event.key === 'ArrowLeft' && index > 0) {
|
|
97
151
|
// Move focus left
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (prevInput) {
|
|
102
|
-
prevInput.focus()
|
|
152
|
+
event.preventDefault()
|
|
153
|
+
if (inputRefs && inputRefs.current && inputRefs.current[index - 1]) {
|
|
154
|
+
inputRefs.current[index - 1]?.focus()
|
|
103
155
|
}
|
|
104
156
|
} else if (event.key === 'ArrowRight' && index < codeLength - 1) {
|
|
105
157
|
// Move focus right
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (nextInput) {
|
|
110
|
-
nextInput.focus()
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
if (inputRefs && inputRefs.current && inputRefs.current[index + 1]) {
|
|
160
|
+
inputRefs.current[index + 1]?.focus()
|
|
111
161
|
}
|
|
112
162
|
}
|
|
113
163
|
},
|
|
114
|
-
[code, codeLength, onChange]
|
|
164
|
+
[code, codeLength, onChange, inputRefs, setInternalValue]
|
|
115
165
|
)
|
|
116
166
|
|
|
117
167
|
return {
|
|
@@ -124,33 +174,132 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
124
174
|
codeLength = 6,
|
|
125
175
|
isValid,
|
|
126
176
|
onChange,
|
|
127
|
-
value,
|
|
177
|
+
value = '',
|
|
128
178
|
'aria-label': ariaLabel,
|
|
129
179
|
'aria-required': ariaRequired,
|
|
130
180
|
'aria-invalid': ariaInvalid,
|
|
181
|
+
codeSent = false,
|
|
182
|
+
onVerify,
|
|
183
|
+
onSendResend,
|
|
184
|
+
verifyButtonProps = {},
|
|
185
|
+
sendResendButtonProps = {},
|
|
186
|
+
showActionButtons = false,
|
|
187
|
+
showSendResendButton = true,
|
|
131
188
|
...props
|
|
132
189
|
}) => {
|
|
190
|
+
// Initialize internal state with the value prop
|
|
191
|
+
const [internalValue, setInternalValue] = useState(value)
|
|
192
|
+
|
|
193
|
+
// Create refs for each input
|
|
194
|
+
const inputRefs = React.useRef<(HTMLInputElement | null)[]>([])
|
|
195
|
+
|
|
196
|
+
// Initialize refs array
|
|
197
|
+
React.useEffect(() => {
|
|
198
|
+
inputRefs.current = inputRefs.current.slice(0, codeLength)
|
|
199
|
+
// Fill with nulls if needed
|
|
200
|
+
while (inputRefs.current.length < codeLength) {
|
|
201
|
+
inputRefs.current.push(null)
|
|
202
|
+
}
|
|
203
|
+
}, [codeLength])
|
|
204
|
+
|
|
205
|
+
// Update internal state when value prop changes
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
// Only update if the value actually changed to avoid infinite loops
|
|
208
|
+
if (internalValue !== value) {
|
|
209
|
+
setInternalValue(value)
|
|
210
|
+
}
|
|
211
|
+
}, [value, internalValue])
|
|
212
|
+
|
|
213
|
+
// Custom onChange handler that updates both internal state and calls the external onChange
|
|
214
|
+
const handleChangeWithState = (newValue: string) => {
|
|
215
|
+
// Set internal state only if it's changed
|
|
216
|
+
if (internalValue !== newValue) {
|
|
217
|
+
setInternalValue(newValue)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Call external onChange if provided
|
|
221
|
+
if (onChange && newValue !== value) {
|
|
222
|
+
onChange(newValue)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
133
226
|
const { handleCodeChange, handleKeyDown } = useCodeConfirmation({
|
|
134
227
|
codeLength,
|
|
135
|
-
onChange,
|
|
228
|
+
onChange: handleChangeWithState,
|
|
229
|
+
inputRefs,
|
|
230
|
+
setInternalValue, // Pass setInternalValue to ensure direct updates
|
|
136
231
|
})
|
|
137
232
|
|
|
233
|
+
// Special document-level handler for left arrow
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
const handleGlobalKeyDown = (e: KeyboardEvent): void => {
|
|
236
|
+
if (e.key === 'ArrowLeft') {
|
|
237
|
+
const activeIndex = inputRefs.current.findIndex(
|
|
238
|
+
ref => ref === document.activeElement
|
|
239
|
+
)
|
|
240
|
+
if (activeIndex > 0) {
|
|
241
|
+
e.preventDefault()
|
|
242
|
+
inputRefs.current[activeIndex - 1]?.focus()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
document.addEventListener(
|
|
248
|
+
'keydown',
|
|
249
|
+
handleGlobalKeyDown as unknown as EventListener
|
|
250
|
+
)
|
|
251
|
+
return () => {
|
|
252
|
+
document.removeEventListener(
|
|
253
|
+
'keydown',
|
|
254
|
+
handleGlobalKeyDown as unknown as EventListener
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}, [])
|
|
258
|
+
|
|
259
|
+
// Calculate container width based on number of inputs
|
|
260
|
+
// Each input is 40px + 4px gap (MUI spacing 1) between them
|
|
261
|
+
const inputAreaWidth = codeLength * 40 + (codeLength - 1) * 8 + 44 // Add 44px for the indicator dot and margin
|
|
262
|
+
|
|
263
|
+
// Calculate button container width - ensure minimum width for buttons
|
|
264
|
+
// Each button should be at least 120px for good UX
|
|
265
|
+
const minButtonWidth = 120
|
|
266
|
+
// If only showing the verify button, we need less space
|
|
267
|
+
const buttonContainerWidth = showSendResendButton
|
|
268
|
+
? Math.max(inputAreaWidth, minButtonWidth * 2 + 16) // 16px for gap between buttons
|
|
269
|
+
: Math.max(inputAreaWidth, minButtonWidth)
|
|
270
|
+
|
|
138
271
|
// For manual changes (typing digits), also handle auto-focus next field
|
|
139
272
|
const handleChange = (
|
|
140
273
|
event: ChangeEvent<HTMLInputElement>,
|
|
141
274
|
index: number
|
|
142
275
|
) => {
|
|
143
276
|
const inputValue = event.target.value.replace(/\D/g, '') // Only keep digits
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
277
|
+
|
|
278
|
+
// Handle case when user types multiple digits at once
|
|
279
|
+
if (inputValue.length > 1) {
|
|
280
|
+
// Take only the last character typed
|
|
281
|
+
const lastChar = inputValue.charAt(inputValue.length - 1)
|
|
282
|
+
event.target.value = lastChar
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Apply the change to state
|
|
286
|
+
handleCodeChange(event, index)
|
|
287
|
+
|
|
288
|
+
// Only move focus if there's a non-empty value and not the last input
|
|
289
|
+
if (inputValue && inputValue.length > 0 && index < codeLength - 1) {
|
|
290
|
+
// Focus next input
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
if (inputRefs.current && inputRefs.current[index + 1]) {
|
|
293
|
+
inputRefs.current[index + 1]?.focus()
|
|
152
294
|
}
|
|
153
|
-
}
|
|
295
|
+
}, 0)
|
|
296
|
+
} else if (inputValue === '' && index > 0) {
|
|
297
|
+
// If input is cleared and not the first input, try to focus the previous input
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (inputRefs.current && inputRefs.current[index - 1]) {
|
|
300
|
+
inputRefs.current[index - 1]?.focus()
|
|
301
|
+
}
|
|
302
|
+
}, 0)
|
|
154
303
|
}
|
|
155
304
|
}
|
|
156
305
|
|
|
@@ -158,66 +307,154 @@ const ConfirmationCodeInputs: FC<ConfirmationCodeInputsProps> = ({
|
|
|
158
307
|
event: KeyboardEvent<HTMLInputElement>,
|
|
159
308
|
index: number
|
|
160
309
|
) => {
|
|
310
|
+
// Direct focus handling for left arrow
|
|
311
|
+
if (event.key === 'ArrowLeft' && index > 0) {
|
|
312
|
+
event.preventDefault()
|
|
313
|
+
if (inputRefs.current && inputRefs.current[index - 1]) {
|
|
314
|
+
inputRefs.current[index - 1]?.focus()
|
|
315
|
+
}
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// For other keys, use the normal handler
|
|
161
320
|
handleKeyDown(event, index)
|
|
162
321
|
}
|
|
163
322
|
|
|
164
|
-
// Safely create an array of string digits
|
|
165
|
-
const digits: string[] =
|
|
166
|
-
?
|
|
323
|
+
// Safely create an array of string digits from internal state
|
|
324
|
+
const digits: string[] = internalValue
|
|
325
|
+
? internalValue.split('')
|
|
167
326
|
: Array.from({ length: codeLength }, () => '')
|
|
168
327
|
|
|
328
|
+
// Check if all code fields are filled
|
|
329
|
+
const allFieldsFilled =
|
|
330
|
+
digits.filter(digit => digit !== '' && digit !== undefined).length >=
|
|
331
|
+
codeLength
|
|
332
|
+
|
|
169
333
|
return (
|
|
170
334
|
<Box
|
|
171
335
|
display="flex"
|
|
172
|
-
flexDirection="
|
|
173
|
-
alignItems="
|
|
336
|
+
flexDirection="column"
|
|
337
|
+
alignItems="flex-start"
|
|
174
338
|
role="group"
|
|
175
339
|
aria-label={ariaLabel || 'Confirmation Code'}
|
|
340
|
+
width={`${inputAreaWidth}px`}
|
|
341
|
+
position="relative"
|
|
176
342
|
>
|
|
177
|
-
<Box
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
'
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
343
|
+
<Box
|
|
344
|
+
display="flex"
|
|
345
|
+
justifyContent="space-between"
|
|
346
|
+
width="100%"
|
|
347
|
+
position="relative"
|
|
348
|
+
marginBottom={2}
|
|
349
|
+
>
|
|
350
|
+
<Box display="flex" gap={1}>
|
|
351
|
+
{Array.from({ length: codeLength }, (_, index) => (
|
|
352
|
+
<Input
|
|
353
|
+
key={`code-input-${index}-${digits[index] || ''}`}
|
|
354
|
+
name={`code${index + 1}`}
|
|
355
|
+
value={digits[index] || ''}
|
|
356
|
+
inputRef={(el: HTMLInputElement | null) => {
|
|
357
|
+
inputRefs.current[index] = el
|
|
358
|
+
}}
|
|
359
|
+
inputProps={{
|
|
360
|
+
maxLength: 1,
|
|
361
|
+
pattern: '[0-9]*',
|
|
362
|
+
inputMode: 'numeric',
|
|
363
|
+
'aria-label': `Code Digit ${index + 1}`,
|
|
364
|
+
'aria-required': ariaRequired,
|
|
365
|
+
'aria-invalid': ariaInvalid,
|
|
366
|
+
'data-testid': `code-input-${index + 1}`,
|
|
367
|
+
'data-index': index,
|
|
368
|
+
}}
|
|
369
|
+
sx={{
|
|
370
|
+
border: '1px solid',
|
|
371
|
+
borderColor: 'black',
|
|
372
|
+
borderRadius: 1,
|
|
373
|
+
width: 40,
|
|
374
|
+
height: 50,
|
|
375
|
+
input: {
|
|
376
|
+
textAlign: 'center',
|
|
377
|
+
color: 'black',
|
|
378
|
+
},
|
|
379
|
+
}}
|
|
380
|
+
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
|
381
|
+
handleChange(event, index)
|
|
382
|
+
}
|
|
383
|
+
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) =>
|
|
384
|
+
handleKeyDownWrapper(event, index)
|
|
385
|
+
}
|
|
386
|
+
onFocus={() => {
|
|
387
|
+
inputRefs.current[index]?.focus()
|
|
388
|
+
}}
|
|
389
|
+
{...props}
|
|
390
|
+
/>
|
|
391
|
+
))}
|
|
392
|
+
</Box>
|
|
393
|
+
|
|
394
|
+
<Box
|
|
395
|
+
width={20}
|
|
396
|
+
height={20}
|
|
397
|
+
borderRadius="50%"
|
|
398
|
+
bgcolor={isValid ? green.main : red.main}
|
|
399
|
+
position="static"
|
|
400
|
+
role="status"
|
|
401
|
+
aria-label={isValid ? 'Code is valid' : 'Code is invalid'}
|
|
402
|
+
alignSelf="center"
|
|
403
|
+
marginRight={2}
|
|
404
|
+
/>
|
|
405
|
+
</Box>
|
|
406
|
+
|
|
407
|
+
{showActionButtons && (
|
|
408
|
+
<Box
|
|
409
|
+
display="flex"
|
|
410
|
+
justifyContent={showSendResendButton ? 'space-between' : 'center'}
|
|
411
|
+
width={`${buttonContainerWidth}px`}
|
|
412
|
+
marginTop={1}
|
|
413
|
+
sx={{
|
|
414
|
+
// Center the button container if it's wider than input area
|
|
415
|
+
marginLeft:
|
|
416
|
+
buttonContainerWidth > inputAreaWidth
|
|
417
|
+
? `${-(buttonContainerWidth - inputAreaWidth) / 2}px`
|
|
418
|
+
: 0,
|
|
419
|
+
}}
|
|
420
|
+
>
|
|
421
|
+
{showSendResendButton && (
|
|
422
|
+
<CustomButton
|
|
423
|
+
text={codeSent ? 'Resend Code' : 'Send Code'}
|
|
424
|
+
fontcolor="white"
|
|
425
|
+
backgroundcolor="black"
|
|
426
|
+
width={
|
|
427
|
+
showSendResendButton
|
|
428
|
+
? `${buttonContainerWidth / 2 - 8}px`
|
|
429
|
+
: '100%'
|
|
430
|
+
}
|
|
431
|
+
height="40px"
|
|
432
|
+
{...sendResendButtonProps}
|
|
433
|
+
onClick={() => {
|
|
434
|
+
if (onSendResend) void onSendResend()
|
|
435
|
+
}}
|
|
436
|
+
// Send/Resend button is always enabled, preserve any custom disableButton setting
|
|
437
|
+
disableButton={sendResendButtonProps?.disableButton || 'false'}
|
|
438
|
+
/>
|
|
439
|
+
)}
|
|
440
|
+
<CustomButton
|
|
441
|
+
text="Verify Phone"
|
|
442
|
+
fontcolor="white"
|
|
443
|
+
backgroundcolor="black"
|
|
444
|
+
width={
|
|
445
|
+
showSendResendButton
|
|
446
|
+
? `${buttonContainerWidth / 2 - 8}px`
|
|
447
|
+
: '100%'
|
|
207
448
|
}
|
|
208
|
-
|
|
449
|
+
height="40px"
|
|
450
|
+
{...verifyButtonProps}
|
|
451
|
+
onClick={() => {
|
|
452
|
+
if (onVerify) void onVerify()
|
|
453
|
+
}}
|
|
454
|
+
disableButton={allFieldsFilled ? 'false' : 'true'}
|
|
209
455
|
/>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<Box
|
|
213
|
-
width={20}
|
|
214
|
-
height={20}
|
|
215
|
-
borderRadius="50%"
|
|
216
|
-
bgcolor={isValid ? green.main : red.main}
|
|
217
|
-
ml={2}
|
|
218
|
-
role="status"
|
|
219
|
-
aria-label={isValid ? 'Code is valid' : 'Code is invalid'}
|
|
220
|
-
/>
|
|
456
|
+
</Box>
|
|
457
|
+
)}
|
|
221
458
|
</Box>
|
|
222
459
|
)
|
|
223
460
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Provider } from 'jotai'
|
|
5
|
+
import { dataGridStore } from './utils/useInitializeGrid'
|
|
6
|
+
|
|
7
|
+
interface DataGridProviderProps {
|
|
8
|
+
children: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DataGridProvider({ children }: DataGridProviderProps) {
|
|
12
|
+
return <Provider store={dataGridStore}>{children}</Provider>
|
|
13
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import type { ColumnDef } from '../types'
|
|
11
11
|
import { useAtomValue } from 'jotai'
|
|
12
12
|
import { columnVisibilityAtom } from '../Jotai/atom'
|
|
13
|
+
import { dataGridStore } from './useInitializeGrid'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* A simple check to see if two arrays of ColumnDef differ
|
|
@@ -55,7 +56,9 @@ export function useComputeTableResize({
|
|
|
55
56
|
const [selectedOverflowField, setSelectedOverflowField] = useState('')
|
|
56
57
|
|
|
57
58
|
// We rely on Jotai for column visibility
|
|
58
|
-
const columnVisibility = useAtomValue(columnVisibilityAtom
|
|
59
|
+
const columnVisibility = useAtomValue(columnVisibilityAtom, {
|
|
60
|
+
store: dataGridStore,
|
|
61
|
+
})
|
|
59
62
|
|
|
60
63
|
/**
|
|
61
64
|
* measureTextWidth: Use a canvas to measure text length for column headers,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
import { useRef, useEffect } from 'react'
|
|
3
|
-
import { useSetAtom, useAtomValue } from 'jotai'
|
|
3
|
+
import { useSetAtom, useAtomValue, createStore } from 'jotai'
|
|
4
4
|
import {
|
|
5
5
|
columnsAtom,
|
|
6
6
|
columnVisibilityAtom,
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
} from '../Jotai/atom'
|
|
9
9
|
import type { ColumnDef, RowData } from '../types'
|
|
10
10
|
|
|
11
|
+
// Create a single shared store instance
|
|
12
|
+
export const dataGridStore = createStore()
|
|
13
|
+
|
|
11
14
|
interface UseInitializeGridProps {
|
|
12
15
|
columns: ColumnDef[]
|
|
13
16
|
providedRows: RowData[]
|
|
@@ -25,9 +28,14 @@ export function useInitializeGrid({
|
|
|
25
28
|
setRows,
|
|
26
29
|
}: UseInitializeGridProps) {
|
|
27
30
|
// We retrieve or modify atoms here, so that DataGrid doesn't need its own useEffect.
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
+
// Use the custom store instead of the default one
|
|
32
|
+
const setColumns = useSetAtom(columnsAtom, { store: dataGridStore })
|
|
33
|
+
const columnVisibility = useAtomValue(columnVisibilityAtom, {
|
|
34
|
+
store: dataGridStore,
|
|
35
|
+
})
|
|
36
|
+
const updateVisibility = useSetAtom(columnVisibilityActions, {
|
|
37
|
+
store: dataGridStore,
|
|
38
|
+
})
|
|
31
39
|
|
|
32
40
|
// We'll track whether we've run the "first-time" logic for columns and visibility
|
|
33
41
|
const initialized = useRef(false)
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import { useAtom, useSetAtom } from 'jotai'
|
|
3
3
|
import { columnVisibilityAtom, columnVisibilityActions } from '../Jotai/atom'
|
|
4
4
|
import type { ColumnDef } from '../types'
|
|
5
|
+
import { dataGridStore } from './useInitializeGrid'
|
|
5
6
|
|
|
6
7
|
type ColumnVisibilityModel = { [key: string]: boolean }
|
|
7
8
|
|
|
@@ -20,8 +21,12 @@ export const useManageColumn = ({
|
|
|
20
21
|
}: UseManageColumnProps) => {
|
|
21
22
|
const [tempVisibleColumns, setTempVisibleColumns] =
|
|
22
23
|
useState<ColumnVisibilityModel>({})
|
|
23
|
-
const [columnVisibility] = useAtom(columnVisibilityAtom
|
|
24
|
-
|
|
24
|
+
const [columnVisibility] = useAtom(columnVisibilityAtom, {
|
|
25
|
+
store: dataGridStore,
|
|
26
|
+
})
|
|
27
|
+
const updateVisibility = useSetAtom(columnVisibilityActions, {
|
|
28
|
+
store: dataGridStore,
|
|
29
|
+
})
|
|
25
30
|
const [searchInput, setSearchInput] = useState(initialSearchInput)
|
|
26
31
|
const [isAllChecked, setIsAllChecked] = useState(true)
|
|
27
32
|
const initialized = useRef(false)
|
|
@@ -565,8 +565,8 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
565
565
|
const getFilteredOptions = React.useCallback(() => {
|
|
566
566
|
const currentInputVal = inputValue.trim()
|
|
567
567
|
|
|
568
|
-
// HISTORY TAB
|
|
569
|
-
if (activeTab === 1) {
|
|
568
|
+
// HISTORY TAB - only apply when variant is complex
|
|
569
|
+
if (activeTab === 1 && variant === 'complex') {
|
|
570
570
|
if (combinedHistory.length === 0) {
|
|
571
571
|
// Show a placeholder message if no history
|
|
572
572
|
return [
|
|
@@ -654,7 +654,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
return filteredOpts
|
|
657
|
-
}, [inputValue, combinedHistory, options, activeTab])
|
|
657
|
+
}, [inputValue, combinedHistory, options, activeTab, variant])
|
|
658
658
|
|
|
659
659
|
// Create the footer component for the dropdown with tabs
|
|
660
660
|
const ListboxFooter = React.forwardRef<HTMLDivElement>((_, ref) => (
|
|
@@ -733,7 +733,7 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
733
733
|
style={{ pointerEvents: 'auto' }}
|
|
734
734
|
>
|
|
735
735
|
<ul {...other}>{children}</ul>
|
|
736
|
-
<ListboxFooter />
|
|
736
|
+
{variant === 'complex' && <ListboxFooter />}
|
|
737
737
|
</div>
|
|
738
738
|
)
|
|
739
739
|
})
|
|
@@ -747,6 +747,13 @@ const SearchableDropdown: React.FC<SearchableDropdownProps> = ({
|
|
|
747
747
|
[getFilteredOptions]
|
|
748
748
|
)
|
|
749
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
|
+
|
|
750
757
|
return (
|
|
751
758
|
<StyledFormControl
|
|
752
759
|
error={error}
|
|
@@ -4,6 +4,7 @@ import React, { useMemo, useEffect, useState, useCallback } from 'react'
|
|
|
4
4
|
import { Box, Stack } from '@mui/material'
|
|
5
5
|
import { useAtom } from 'jotai'
|
|
6
6
|
import { columnsAtom } from './jotai/atom'
|
|
7
|
+
import { JotaiProvider } from './jotai/provider'
|
|
7
8
|
|
|
8
9
|
import Toolbar from '../Toolbar'
|
|
9
10
|
// Removed old generic AddTask import
|
|
@@ -57,7 +58,7 @@ function mergeColumnsAndTasks(
|
|
|
57
58
|
})
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function
|
|
61
|
+
function ProjectBoardContent({
|
|
61
62
|
variant,
|
|
62
63
|
boardType,
|
|
63
64
|
columns,
|
|
@@ -158,7 +159,7 @@ function ProjectBoard({
|
|
|
158
159
|
setColumnState(newCols)
|
|
159
160
|
setAddTaskOpen(false)
|
|
160
161
|
|
|
161
|
-
// 5.b) Also call the parent
|
|
162
|
+
// 5.b) Also call the parent's onAdd, passing the same newTask data
|
|
162
163
|
onAdd(newTask)
|
|
163
164
|
},
|
|
164
165
|
[columnState, boardType, setColumnState, onAdd]
|
|
@@ -460,4 +461,13 @@ function ProjectBoard({
|
|
|
460
461
|
)
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
// Wrap the component with our custom JotaiProvider to avoid the "multiple instances" error
|
|
465
|
+
function ProjectBoard(props: ProjectBoardProps) {
|
|
466
|
+
return (
|
|
467
|
+
<JotaiProvider>
|
|
468
|
+
<ProjectBoardContent {...props} />
|
|
469
|
+
</JotaiProvider>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
463
473
|
export default React.memo(ProjectBoard)
|