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
|
@@ -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
|
|
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)
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
56
|
-
Error: Failed to generate QR code
|
|
57
|
-
|
|
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
|
|
75
|
-
{title}
|
|
76
|
-
|
|
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
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
}
|