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.
@@ -580,3 +580,167 @@ export const AllAttributesDisplay: Story = {
580
580
  expect(hasAllLevels).toBe(true)
581
581
  },
582
582
  }
583
+
584
+ /**
585
+ * 15) With Search History
586
+ * Uses userEvent => keep `async`.
587
+ */
588
+ export const WithSearchHistory: Story = {
589
+ args: {
590
+ label: 'Search with History',
591
+ options: complexSampleOptions,
592
+ placeholder: 'Search with history...',
593
+ variant: 'complex',
594
+ // Provide pre-populated search history
595
+ searchHistory: [
596
+ { text: 'apple', timestamp: new Date(Date.now() - 5 * 60000) }, // 5 minutes ago
597
+ { text: 'broccoli', timestamp: new Date(Date.now() - 60 * 60000) }, // 1 hour ago
598
+ {
599
+ text: 'custom search',
600
+ timestamp: new Date(Date.now() - 24 * 60 * 60000),
601
+ }, // 1 day ago
602
+ ],
603
+ maxHistoryItems: 5,
604
+ },
605
+ play: async ({ canvasElement }) => {
606
+ const canvas = within(canvasElement)
607
+
608
+ // Open the dropdown
609
+ const input = canvas.getByRole('combobox')
610
+ await userEvent.click(input)
611
+
612
+ // Switch to history tab
613
+ const historyTab = canvas.getByRole('tab', { name: /HISTORY/i })
614
+ await userEvent.click(historyTab)
615
+
616
+ // Get dropdown options after switching to history tab
617
+ const listboxItems = getDropdownOptions()
618
+
619
+ // Check if history items are displayed
620
+ const hasAppleHistory = listboxItems.some(
621
+ item => item.textContent && item.textContent.includes('apple')
622
+ )
623
+
624
+ const hasTimestamp = listboxItems.some(
625
+ item =>
626
+ item.textContent &&
627
+ (item.textContent.includes('minutes ago') ||
628
+ item.textContent.includes('hour ago') ||
629
+ item.textContent.includes('day ago'))
630
+ )
631
+
632
+ expect(hasAppleHistory).toBe(true)
633
+ expect(hasTimestamp).toBe(true)
634
+
635
+ // Try entering a new search term
636
+ await userEvent.clear(input)
637
+ await userEvent.type(input, 'new search')
638
+ await userEvent.tab() // Blur to trigger adding to history
639
+
640
+ // Reopen and check history tab again
641
+ await userEvent.click(input)
642
+ await userEvent.click(historyTab)
643
+
644
+ // Should now include our new search
645
+ const updatedListboxItems = getDropdownOptions()
646
+ const hasNewSearch = updatedListboxItems.some(
647
+ item => item.textContent && item.textContent.includes('new search')
648
+ )
649
+
650
+ expect(hasNewSearch).toBe(true)
651
+ },
652
+ }
653
+
654
+ /**
655
+ * 16) With Search Callback
656
+ * Uses userEvent => keep `async`.
657
+ */
658
+ export const WithSearchCallback: Story = {
659
+ args: {
660
+ label: 'Search with Callback',
661
+ options: sampleOptions,
662
+ placeholder: 'Search with callback...',
663
+ variant: 'simple',
664
+ // This callback will be triggered when searches are performed
665
+ onSearch: (searchTerm: string, timestamp?: Date) => {
666
+ console.log(
667
+ `Search term: ${searchTerm}, Time: ${timestamp?.toISOString() || 'No timestamp'}`
668
+ )
669
+ },
670
+ },
671
+ play: async ({ canvasElement }) => {
672
+ const canvas = within(canvasElement)
673
+
674
+ // Perform a search
675
+ const input = canvas.getByRole('combobox')
676
+ await userEvent.click(input)
677
+ await userEvent.type(input, 'test search')
678
+ await userEvent.tab() // Blur to trigger search
679
+
680
+ // Since we can't easily verify the callback in Storybook tests,
681
+ // we'll just ensure the component functions correctly
682
+ expect(input).toHaveValue('test search')
683
+ },
684
+ }
685
+
686
+ /**
687
+ * 17) Tab Navigation
688
+ * Uses userEvent => keep `async`.
689
+ */
690
+ export const TabNavigation: Story = {
691
+ args: {
692
+ label: 'Tab Navigation Demo',
693
+ options: sampleOptions,
694
+ placeholder: 'Explore tabs...',
695
+ variant: 'simple',
696
+ // Provide some search history
697
+ searchHistory: [
698
+ 'previous search one',
699
+ 'previous search two',
700
+ 'previous search three',
701
+ ],
702
+ maxHistoryItems: 10,
703
+ },
704
+ play: async ({ canvasElement }) => {
705
+ const canvas = within(canvasElement)
706
+
707
+ // Open the dropdown
708
+ const input = canvas.getByRole('combobox')
709
+ await userEvent.click(input)
710
+
711
+ // First check that we're on the default "ALL OPTIONS" tab
712
+ // and can see the regular options
713
+ const allOptionsTab = canvas.getByRole('tab', { name: /ALL OPTIONS/i })
714
+ expect(allOptionsTab).toHaveAttribute('aria-selected', 'true')
715
+
716
+ // We should see the original options
717
+ const initialListboxItems = getDropdownOptions()
718
+ const hasAppleOption = initialListboxItems.some(
719
+ item => item.textContent && item.textContent.includes('apple')
720
+ )
721
+ expect(hasAppleOption).toBe(true)
722
+
723
+ // Now switch to the HISTORY tab
724
+ const historyTab = canvas.getByRole('tab', { name: /HISTORY/i })
725
+ await userEvent.click(historyTab)
726
+ expect(historyTab).toHaveAttribute('aria-selected', 'true')
727
+
728
+ // Now we should see the history items
729
+ const historyListboxItems = getDropdownOptions()
730
+ const hasHistoryItems = historyListboxItems.some(
731
+ item => item.textContent && item.textContent.includes('previous search')
732
+ )
733
+ expect(hasHistoryItems).toBe(true)
734
+
735
+ // Now switch back to ALL OPTIONS tab
736
+ await userEvent.click(allOptionsTab)
737
+ expect(allOptionsTab).toHaveAttribute('aria-selected', 'true')
738
+
739
+ // We should see the original options again
740
+ const finalListboxItems = getDropdownOptions()
741
+ const hasAppleOptionAgain = finalListboxItems.some(
742
+ item => item.textContent && item.textContent.includes('apple')
743
+ )
744
+ expect(hasAppleOptionAgain).toBe(true)
745
+ },
746
+ }
@@ -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)
@@ -0,0 +1,23 @@
1
+ 'use client'
2
+
3
+ import React from 'react'
4
+ import { Provider, createStore } from 'jotai'
5
+ import { columnsAtom } from './atom'
6
+
7
+ // Create a custom store
8
+ export const store = createStore()
9
+
10
+ // Initialize the store with our atoms to ensure they're properly registered
11
+ store.set(columnsAtom, [])
12
+
13
+ /**
14
+ * A Jotai Provider component that uses a custom store to prevent
15
+ * "multiple instances" error in Next.js applications.
16
+ */
17
+ export function JotaiProvider({
18
+ children,
19
+ }: {
20
+ children: React.ReactNode
21
+ }): React.ReactElement {
22
+ return <Provider store={store}>{children}</Provider>
23
+ }
@@ -1,8 +1,9 @@
1
- import React, { useMemo } from 'react'
1
+ import React, { useMemo, useEffect } from 'react'
2
2
  import QRCode from 'react-qr-code'
3
- import { Box, Typography, Paper, Theme, CircularProgress } from '@mui/material'
3
+ import { Box, Paper } from '@mui/material'
4
4
  import { SxProps } from '@mui/system'
5
5
  import { authenticator } from 'otplib'
6
+ import Typography from '../Typography'
6
7
 
7
8
  /**
8
9
  * Props for the QRCodeComponent
@@ -11,7 +12,7 @@ import { authenticator } from 'otplib'
11
12
  * @property {string} appName - The name of the application for MFA
12
13
  * @property {number} [size] - The size of the QR code in pixels
13
14
  * @property {string} [title] - An optional title to display above the QR code
14
- * @property {SxProps<Theme>} [sx] - Custom styles to apply to the component
15
+ * @property {SxProps} [sx] - Custom styles to apply to the component
15
16
  * @property {(secret: string) => void} [onSecretGenerated] - Callback function to receive the generated secret
16
17
  */
17
18
  export interface QRCodeProps {
@@ -19,7 +20,7 @@ export interface QRCodeProps {
19
20
  appName: string
20
21
  size?: number
21
22
  title?: string
22
- sx?: SxProps<Theme>
23
+ sx?: SxProps
23
24
  onSecretGenerated?: (secret: string) => void
24
25
  }
25
26
 
@@ -38,11 +39,15 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
38
39
  encodeURIComponent(appName),
39
40
  generatedSecret
40
41
  )
41
- if (onSecretGenerated) {
42
- onSecretGenerated(generatedSecret)
43
- }
44
42
  return { secret: generatedSecret, otpAuth: otpAuthUrl }
45
- }, [username, appName, onSecretGenerated])
43
+ }, [username, appName])
44
+
45
+ // Move the callback to useEffect to avoid state updates during render
46
+ useEffect(() => {
47
+ if (onSecretGenerated && secret) {
48
+ onSecretGenerated(secret)
49
+ }
50
+ }, [secret, onSecretGenerated])
46
51
 
47
52
  // Calculate responsive size
48
53
  const responsiveSize = useMemo(() => {
@@ -52,9 +57,11 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
52
57
  if (!otpAuth) {
53
58
  return (
54
59
  <Box sx={{ ...sx, p: 2 }} role="alert">
55
- <Typography color="error">
56
- Error: Failed to generate QR code
57
- </Typography>
60
+ <Typography
61
+ text="Error: Failed to generate QR code"
62
+ fontcolor="error"
63
+ fontvariant="merriparagraph"
64
+ />
58
65
  </Box>
59
66
  )
60
67
  }
@@ -63,17 +70,20 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
63
70
  <Paper
64
71
  elevation={3}
65
72
  sx={{
66
- ...sx,
67
73
  p: 3,
68
74
  display: 'inline-block',
69
75
  maxWidth: '100%',
70
76
  boxSizing: 'border-box',
77
+ ...sx,
71
78
  }}
72
79
  >
73
80
  {title && (
74
- <Typography variant="h6" gutterBottom align="center">
75
- {title}
76
- </Typography>
81
+ <Typography
82
+ text={title}
83
+ fontvariant="merrih5"
84
+ align="center"
85
+ gutterBottom
86
+ />
77
87
  )}
78
88
  <Box
79
89
  sx={{
@@ -85,25 +95,13 @@ const QRCodeComponent: React.FC<QRCodeProps> = React.memo(
85
95
  margin: 'auto',
86
96
  }}
87
97
  >
88
- <React.Suspense
89
- fallback={
90
- <CircularProgress
91
- size={responsiveSize / 4}
92
- aria-label="Loading QR Code"
93
- />
94
- }
95
- >
96
- <QRCode
97
- value={otpAuth}
98
- size={responsiveSize}
99
- style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
100
- aria-label={`QR Code for ${title || 'MFA Setup'}`}
101
- />
102
- </React.Suspense>
98
+ <QRCode
99
+ value={otpAuth}
100
+ size={responsiveSize}
101
+ style={{ height: 'auto', maxWidth: '100%', width: '100%' }}
102
+ aria-label={`QR Code for ${title || 'MFA Setup'}`}
103
+ />
103
104
  </Box>
104
- <Typography variant="body2" align="center" sx={{ mt: 2 }}>
105
- Secret: {secret}
106
- </Typography>
107
105
  </Paper>
108
106
  )
109
107
  }