4
4
* See License.AGPL.txt in the project root for license information.
5
5
*/
6
6
7
- import { User } from "@gitpod/gitpod-protocol" ;
8
- import React , { useContext , useEffect , useState } from "react" ;
7
+ import { useEffect , useMemo , useState } from "react" ;
9
8
import { getGitpodService } from "../service/service" ;
10
- import { UserContext } from "../user-context" ;
11
-
12
- type SearchResult = string ;
13
- type SearchData = SearchResult [ ] ;
9
+ import { DropDown2 , DropDown2Element } from "./DropDown2" ;
14
10
15
11
const LOCAL_STORAGE_KEY = "open-in-gitpod-search-data" ;
16
- const MAX_DISPLAYED_ITEMS = 20 ;
17
-
18
- export default function RepositoryFinder ( props : { initialQuery ?: string } ) {
19
- const { user } = useContext ( UserContext ) ;
20
- const [ searchQuery , setSearchQuery ] = useState < string > ( props . initialQuery || "" ) ;
21
- const [ searchResults , setSearchResults ] = useState < SearchResult [ ] > ( [ ] ) ;
22
- const [ selectedSearchResult , setSelectedSearchResult ] = useState < SearchResult | undefined > ( ) ;
23
-
24
- const onResults = ( results : SearchResult [ ] ) => {
25
- if ( JSON . stringify ( results ) !== JSON . stringify ( searchResults ) ) {
26
- setSearchResults ( results ) ;
27
- setSelectedSearchResult ( results [ 0 ] ) ;
28
- }
29
- } ;
30
-
31
- const search = async ( query : string ) => {
32
- setSearchQuery ( query ) ;
33
- await findResults ( query , onResults ) ;
34
- if ( await refreshSearchData ( query , user ) ) {
35
- // Re-run search if the underlying search data has changed
36
- await findResults ( query , onResults ) ;
37
- }
38
- } ;
39
12
40
- useEffect ( ( ) => {
41
- search ( "" ) ;
42
- } , [ ] ) ;
43
-
44
- // Up/Down keyboard navigation between results
45
- const onKeyDown = ( event : React . KeyboardEvent ) => {
46
- if ( ! selectedSearchResult ) {
47
- return ;
48
- }
49
- const selectedIndex = searchResults . indexOf ( selectedSearchResult ) ;
50
- const select = ( index : number ) => {
51
- // Implement a true modulus in order to "wrap around" (e.g. `select(-1)` should select the last result)
52
- // Source: https://stackoverflow.com/a/4467559/3461173
53
- const n = Math . min ( searchResults . length , MAX_DISPLAYED_ITEMS ) ;
54
- setSelectedSearchResult ( searchResults [ ( ( index % n ) + n ) % n ] ) ;
55
- } ;
56
- if ( event . key === "ArrowDown" ) {
57
- event . preventDefault ( ) ;
58
- select ( selectedIndex + 1 ) ;
59
- return ;
60
- }
61
- if ( event . key === "ArrowUp" ) {
62
- event . preventDefault ( ) ;
63
- select ( selectedIndex - 1 ) ;
64
- return ;
65
- }
66
- } ;
13
+ interface RepositoryFinderProps {
14
+ initialValue ?: string ;
15
+ maxDisplayItems ?: number ;
16
+ setSelection : ( selection : string ) => void ;
17
+ }
67
18
19
+ export default function RepositoryFinder ( props : RepositoryFinderProps ) {
20
+ const [ suggestedContextURLs , setSuggestedContextURLs ] = useState < string [ ] > ( loadSearchData ( ) ) ;
68
21
useEffect ( ( ) => {
69
- const element = document . querySelector ( `a[href='/#${ selectedSearchResult } ']` ) ;
70
- if ( element ) {
71
- element . scrollIntoView ( { behavior : "smooth" , block : "nearest" } ) ;
72
- }
73
- } , [ selectedSearchResult ] ) ;
74
-
75
- const onSubmit = ( event : React . FormEvent ) => {
76
- event . preventDefault ( ) ;
77
- if ( selectedSearchResult ) {
78
- window . location . href = "/#" + selectedSearchResult ;
79
- }
80
- } ;
22
+ getGitpodService ( )
23
+ . server . getSuggestedContextURLs ( )
24
+ . then ( ( urls ) => {
25
+ setSuggestedContextURLs ( urls ) ;
26
+ saveSearchData ( urls ) ;
27
+ } ) ;
28
+ } , [ suggestedContextURLs ] ) ;
29
+
30
+ const getElements = useMemo (
31
+ ( ) => ( searchString : string ) => {
32
+ const result = [ ...suggestedContextURLs ] ;
33
+ try {
34
+ // If the searchString is a URL, and it's not present in the proposed results, "artificially" add it here.
35
+ new URL ( searchString ) ;
36
+ if ( ! result . includes ( searchString ) ) {
37
+ result . push ( searchString ) ;
38
+ }
39
+ } catch { }
40
+ return result
41
+ . filter ( ( e ) => e . toLowerCase ( ) . indexOf ( searchString . toLowerCase ( ) ) !== - 1 )
42
+ . map (
43
+ ( e ) =>
44
+ ( {
45
+ id : e ,
46
+ element : < div className = "px-4 py-3 text-ellipsis overflow-hidden" > { e } </ div > ,
47
+ isSelectable : true ,
48
+ } as DropDown2Element ) ,
49
+ ) ;
50
+ } ,
51
+ [ suggestedContextURLs ] ,
52
+ ) ;
81
53
82
54
return (
83
- < form onSubmit = { onSubmit } >
84
- < div className = "flex px-4 rounded-xl border border-gray-300 dark:border-gray-500" >
85
- < div className = "py-4" >
86
- < svg xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 16 16" width = "16" height = "16" >
87
- < path
88
- fill = "#A8A29E"
89
- d = "M6 2a4 4 0 100 8 4 4 0 000-8zM0 6a6 6 0 1110.89 3.477l4.817 4.816a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 010 6z"
90
- />
91
- </ svg >
92
- </ div >
93
- < input
94
- type = "search"
95
- className = "flex-grow"
96
- placeholder = "Paste repository URL or type to find suggestions"
97
- autoFocus
98
- value = { searchQuery }
99
- onChange = { ( e ) => search ( e . target . value ) }
100
- onKeyDown = { onKeyDown }
101
- />
102
- </ div >
103
- < div className = "mt-3 -mx-5 px-5 flex flex-col space-y-2 h-64 overflow-y-auto" >
104
- { searchResults . slice ( 0 , MAX_DISPLAYED_ITEMS ) . map ( ( result , index ) => (
105
- < a
106
- className = {
107
- `px-4 py-3 rounded-xl` +
108
- ( result === selectedSearchResult ? " bg-gray-600 text-gray-50 dark:bg-gray-700" : "" )
109
- }
110
- href = { `/#${ result } ` }
111
- key = { `search-result-${ index } ` }
112
- onMouseEnter = { ( ) => setSelectedSearchResult ( result ) }
113
- >
114
- { searchQuery . length < 2 ? (
115
- < span > { result } </ span >
116
- ) : (
117
- result . split ( searchQuery ) . map ( ( segment , index ) => (
118
- < span >
119
- { index === 0 ? < > </ > : < strong > { searchQuery } </ strong > }
120
- { segment }
121
- </ span >
122
- ) )
123
- ) }
124
- </ a >
125
- ) ) }
126
- { searchResults . length > MAX_DISPLAYED_ITEMS && (
127
- < span className = "mt-3 px-4 py-2 text-sm text-gray-400 dark:text-gray-500" >
128
- { searchResults . length - MAX_DISPLAYED_ITEMS } more result
129
- { searchResults . length - MAX_DISPLAYED_ITEMS === 1 ? "" : "s" } found
130
- </ span >
131
- ) }
132
- </ div >
133
- </ form >
55
+ < DropDown2
56
+ getElements = { getElements }
57
+ onSelectionChange = { props . setSelection }
58
+ searchPlaceholder = "Paste repository URL or type to find suggestions"
59
+ >
60
+ < div className = "m-3 text-ellipsis overflow-hidden" > { props . initialValue || "Select Repository" } </ div >
61
+ </ DropDown2 >
134
62
) ;
135
63
}
136
64
137
- function loadSearchData ( ) : SearchData {
65
+ function loadSearchData ( ) : string [ ] {
138
66
const string = localStorage . getItem ( LOCAL_STORAGE_KEY ) ;
139
67
if ( ! string ) {
140
68
return [ ] ;
@@ -148,45 +76,18 @@ function loadSearchData(): SearchData {
148
76
}
149
77
}
150
78
151
- function saveSearchData ( searchData : SearchData ) : void {
79
+ function saveSearchData ( searchData : string [ ] ) : void {
152
80
try {
153
81
window . localStorage . setItem ( LOCAL_STORAGE_KEY , JSON . stringify ( searchData ) ) ;
154
82
} catch ( error ) {
155
83
console . warn ( "Could not save search data into local storage" , error ) ;
156
84
}
157
85
}
158
86
159
- let refreshSearchDataPromise : Promise < boolean > | undefined ;
160
- export async function refreshSearchData ( query : string , user : User | undefined ) : Promise < boolean > {
161
- if ( refreshSearchDataPromise ) {
162
- // Another refresh is already in progress, no need to run another one in parallel.
163
- return refreshSearchDataPromise ;
164
- }
165
- refreshSearchDataPromise = actuallyRefreshSearchData ( query , user ) ;
166
- const didChange = await refreshSearchDataPromise ;
167
- refreshSearchDataPromise = undefined ;
168
- return didChange ;
169
- }
170
-
171
- // Fetch all possible search results and cache them into local storage
172
- async function actuallyRefreshSearchData ( query : string , user : User | undefined ) : Promise < boolean > {
173
- const oldData = loadSearchData ( ) ;
174
- const newData = await getGitpodService ( ) . server . getSuggestedContextURLs ( ) ;
175
- if ( JSON . stringify ( oldData ) !== JSON . stringify ( newData ) ) {
176
- saveSearchData ( newData ) ;
177
- return true ;
178
- }
179
- return false ;
180
- }
181
-
182
- async function findResults ( query : string , onResults : ( results : string [ ] ) => void ) {
183
- const searchData = loadSearchData ( ) ;
184
- try {
185
- // If the query is a URL, and it's not present in the proposed results, "artificially" add it here.
186
- new URL ( query ) ;
187
- if ( ! searchData . includes ( query ) ) {
188
- searchData . push ( query ) ;
189
- }
190
- } catch { }
191
- onResults ( searchData . filter ( ( result ) => result . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ) ) ;
87
+ export function refreshSearchData ( ) {
88
+ getGitpodService ( )
89
+ . server . getSuggestedContextURLs ( )
90
+ . then ( ( urls ) => {
91
+ saveSearchData ( urls ) ;
92
+ } ) ;
192
93
}
0 commit comments