@@ -2,7 +2,7 @@ import type {Key} from 'react';
2
2
import { useCallback , useMemo , useRef , useState } from 'react' ;
3
3
import styled from '@emotion/styled' ;
4
4
import { type AriaComboBoxProps } from '@react-aria/combobox' ;
5
- import { Item } from '@react-stately/collections' ;
5
+ import { Item , Section } from '@react-stately/collections' ;
6
6
import { useComboBoxState } from '@react-stately/combobox' ;
7
7
import type { CollectionChildren } from '@react-types/shared' ;
8
8
@@ -14,37 +14,93 @@ import {useSearchTokenCombobox} from 'sentry/components/searchQueryBuilder/token
14
14
import { IconSearch } from 'sentry/icons' ;
15
15
import { t } from 'sentry/locale' ;
16
16
import type { StoryTreeNode } from 'sentry/stories/view/storyTree' ;
17
- import { useStoryTree } from 'sentry/stories/view/storyTree' ;
18
17
import { space } from 'sentry/styles/space' ;
19
18
import { fzf } from 'sentry/utils/profiling/fzf/fzf' ;
20
19
import { useHotkeys } from 'sentry/utils/useHotkeys' ;
21
20
import { useNavigate } from 'sentry/utils/useNavigate' ;
22
21
23
- import { useStoryBookFiles } from './useStoriesLoader' ;
22
+ import { useStoryBookFilesByCategory } from './storySidebar' ;
23
+
24
+ interface StorySection {
25
+ key : string ;
26
+ label : string ;
27
+ options : StoryTreeNode [ ] ;
28
+ }
29
+
30
+ function isStorySection ( item : StoryTreeNode | StorySection ) : item is StorySection {
31
+ return 'options' in item ;
32
+ }
24
33
25
34
export function StorySearch ( ) {
26
35
const inputRef = useRef < HTMLInputElement | null > ( null ) ;
27
- const files = useStoryBookFiles ( ) ;
28
- const tree = useStoryTree ( files , { query : '' , representation : 'category' , type : 'flat' } ) ;
36
+ const { foundations , core , shared } = useStoryBookFilesByCategory ( ) ;
37
+
29
38
const storiesSearchHotkeys = useMemo ( ( ) => {
30
39
return [ { match : '/' , callback : ( ) => inputRef . current ?. focus ( ) } ] ;
31
40
} , [ ] ) ;
41
+
32
42
useHotkeys ( storiesSearchHotkeys ) ;
33
43
44
+ const sectionedItems = useMemo ( ( ) => {
45
+ const sections : StorySection [ ] = [ ] ;
46
+
47
+ if ( foundations . length > 0 ) {
48
+ sections . push ( {
49
+ key : 'foundations' ,
50
+ label : 'Foundations' ,
51
+ options : foundations ,
52
+ } ) ;
53
+ }
54
+
55
+ if ( core . length > 0 ) {
56
+ sections . push ( {
57
+ key : 'core' ,
58
+ label : 'Core' ,
59
+ options : core ,
60
+ } ) ;
61
+ }
62
+
63
+ if ( shared . length > 0 ) {
64
+ sections . push ( {
65
+ key : 'shared' ,
66
+ label : 'Shared' ,
67
+ options : shared ,
68
+ } ) ;
69
+ }
70
+
71
+ return sections ;
72
+ } , [ foundations , core , shared ] ) ;
73
+
34
74
return (
35
75
< SearchComboBox
36
76
label = { t ( 'Search stories' ) }
37
77
menuTrigger = "focus"
38
78
inputRef = { inputRef }
39
- defaultItems = { tree }
79
+ defaultItems = { sectionedItems }
40
80
>
41
- { item => (
42
- < Item
43
- key = { item . filesystemPath }
44
- textValue = { item . label }
45
- { ...( { label : item . label , hideCheck : true } as any ) }
46
- />
47
- ) }
81
+ { item => {
82
+ if ( isStorySection ( item ) ) {
83
+ return (
84
+ < Section key = { item . key } title = { < SectionTitle > { item . label } </ SectionTitle > } >
85
+ { item . options . map ( storyItem => (
86
+ < Item
87
+ key = { storyItem . filesystemPath }
88
+ textValue = { storyItem . label }
89
+ { ...( { label : storyItem . label , hideCheck : true } as any ) }
90
+ />
91
+ ) ) }
92
+ </ Section >
93
+ ) ;
94
+ }
95
+
96
+ return (
97
+ < Item
98
+ key = { item . filesystemPath }
99
+ textValue = { item . label }
100
+ { ...( { label : item . label , hideCheck : true } as any ) }
101
+ />
102
+ ) ;
103
+ } }
48
104
</ SearchComboBox >
49
105
) ;
50
106
}
@@ -67,10 +123,12 @@ function SearchInput(
67
123
) ;
68
124
}
69
125
126
+ type SearchComboBoxItem < T extends StoryTreeNode > = T | StorySection ;
127
+
70
128
interface SearchComboBoxProps < T extends StoryTreeNode >
71
- extends Omit < AriaComboBoxProps < T > , 'children' > {
72
- children : CollectionChildren < T > ;
73
- defaultItems : T [ ] ;
129
+ extends Omit < AriaComboBoxProps < SearchComboBoxItem < T > > , 'children' > {
130
+ children : CollectionChildren < SearchComboBoxItem < T > > ;
131
+ defaultItems : Array < SearchComboBoxItem < T > > ;
74
132
inputRef : React . RefObject < HTMLInputElement | null > ;
75
133
description ?: string | null ;
76
134
label ?: string ;
@@ -106,7 +164,9 @@ function SearchComboBox<T extends StoryTreeNode>(props: SearchComboBoxProps<T>)
106
164
onSelectionChange : handleSelectionChange ,
107
165
} ) ;
108
166
109
- const { inputProps, listBoxProps, labelProps} = useSearchTokenCombobox < T > (
167
+ const { inputProps, listBoxProps, labelProps} = useSearchTokenCombobox <
168
+ SearchComboBoxItem < T >
169
+ > (
110
170
{
111
171
...props ,
112
172
inputRef,
@@ -160,4 +220,15 @@ const StyledOverlay = styled(Overlay)`
160
220
width: 320px;
161
221
max-height: calc(100dvh - 128px);
162
222
overflow-y: auto;
223
+
224
+ /* Make section headers darker in this component */
225
+ p[id][aria-hidden='true'] {
226
+ color: ${ p => p . theme . textColor } ;
227
+ }
228
+ ` ;
229
+
230
+ const SectionTitle = styled ( 'span' ) `
231
+ color: ${ p => p . theme . textColor } ;
232
+ font-weight: 600;
233
+ text-transform: uppercase;
163
234
` ;
0 commit comments