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.
@@ -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
- const prevInput = document.querySelector<HTMLInputElement>(
91
- `input[name=code${index}]`
92
- )
93
- if (prevInput) {
94
- prevInput.focus()
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
- const prevInput = document.querySelector<HTMLInputElement>(
99
- `input[name=code${index}]`
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
- const nextInput = document.querySelector<HTMLInputElement>(
107
- `input[name=code${index + 2}]`
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
- if (inputValue.length <= 1) {
145
- handleCodeChange(event, index)
146
- if (inputValue) {
147
- const nextInput = document.querySelector<HTMLInputElement>(
148
- `input[name=code${index + 2}]`
149
- )
150
- if (nextInput) {
151
- nextInput.focus()
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[] = value
166
- ? value.split('')
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="row"
173
- alignItems="center"
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 display="flex" gap={1}>
178
- {Array.from({ length: codeLength }, (_, index) => (
179
- <Input
180
- key={index}
181
- name={`code${index + 1}`}
182
- value={digits[index] || ''}
183
- inputProps={{
184
- maxLength: 1,
185
- pattern: '[0-9]*',
186
- inputMode: 'numeric',
187
- 'aria-label': `Code Digit ${index + 1}`,
188
- 'aria-required': ariaRequired,
189
- 'aria-invalid': ariaInvalid,
190
- }}
191
- sx={{
192
- border: '1px solid',
193
- borderColor: 'black',
194
- borderRadius: 1,
195
- width: 50,
196
- height: 50,
197
- input: {
198
- textAlign: 'center',
199
- color: 'black',
200
- },
201
- }}
202
- onChange={(event: ChangeEvent<HTMLInputElement>) =>
203
- handleChange(event, index)
204
- }
205
- onKeyDown={(event: KeyboardEvent<HTMLInputElement>) =>
206
- handleKeyDownWrapper(event, index)
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
- {...props}
449
+ height="40px"
450
+ {...verifyButtonProps}
451
+ onClick={() => {
452
+ if (onVerify) void onVerify()
453
+ }}
454
+ disableButton={allFieldsFilled ? 'false' : 'true'}
209
455
  />
210
- ))}
211
- </Box>
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
- const setColumns = useSetAtom(columnsAtom)
29
- const columnVisibility = useAtomValue(columnVisibilityAtom)
30
- const updateVisibility = useSetAtom(columnVisibilityActions)
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
- const updateVisibility = useSetAtom(columnVisibilityActions)
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 ProjectBoard({
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 parents onAdd, passing the same newTask data
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)