@@ -8,12 +8,15 @@ import { FC, useCallback, useEffect, useMemo, useState } from "react";
8
8
import { Combobox , ComboboxElement , ComboboxSelectedItem } from "./podkit/combobox/Combobox" ;
9
9
import RepositorySVG from "../icons/Repository.svg" ;
10
10
import { ReactComponent as RepositoryIcon } from "../icons/RepositoryWithColor.svg" ;
11
+ import { ReactComponent as GitpodRepositoryTemplate } from "../icons/GitpodRepositoryTemplate.svg" ;
12
+ import GitpodRepositoryTemplateSVG from "../icons/GitpodRepositoryTemplate.svg" ;
11
13
import { MiddleDot } from "./typography/MiddleDot" ;
12
14
import { useUnifiedRepositorySearch } from "../data/git-providers/unified-repositories-search-query" ;
13
15
import { useAuthProviderDescriptions } from "../data/auth-providers/auth-provider-descriptions-query" ;
14
16
import { ReactComponent as Exclamation2 } from "../images/exclamation2.svg" ;
15
17
import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb" ;
16
18
import { SuggestedRepository } from "@gitpod/public-api/lib/gitpod/v1/scm_pb" ;
19
+ import { PREDEFINED_REPOS } from "../data/git-providers/predefined-repos" ;
17
20
18
21
interface RepositoryFinderProps {
19
22
selectedContextURL ?: string ;
@@ -22,6 +25,7 @@ interface RepositoryFinderProps {
22
25
expanded ?: boolean ;
23
26
excludeConfigurations ?: boolean ;
24
27
onlyConfigurations ?: boolean ;
28
+ showExamples ?: boolean ;
25
29
onChange ?: ( repo : SuggestedRepository ) => void ;
26
30
}
27
31
@@ -32,6 +36,7 @@ export default function RepositoryFinder({
32
36
expanded,
33
37
excludeConfigurations = false ,
34
38
onlyConfigurations = false ,
39
+ showExamples = false ,
35
40
onChange,
36
41
} : RepositoryFinderProps ) {
37
42
const [ searchString , setSearchString ] = useState ( "" ) ;
@@ -44,35 +49,75 @@ export default function RepositoryFinder({
44
49
searchString,
45
50
excludeConfigurations : excludeConfigurations ,
46
51
onlyConfigurations : onlyConfigurations ,
52
+ showExamples : showExamples ,
47
53
} ) ;
48
54
49
55
const authProviders = useAuthProviderDescriptions ( ) ;
50
56
57
+ // This approach creates a memoized Map of the predefined repos,
58
+ // which can be more efficient for lookups if we would have a large number of predefined repos
59
+ const memoizedPredefinedRepos = useMemo ( ( ) => {
60
+ return new Map ( PREDEFINED_REPOS . map ( ( repo ) => [ repo . url , repo ] ) ) ;
61
+ } , [ ] ) ;
62
+
51
63
const handleSelectionChange = useCallback (
52
64
( selectedID : string ) => {
53
- // selectedId is either configurationId or repo url
54
- const matchingSuggestion = repos ?. find ( ( repo ) => {
55
- if ( repo . configurationId ) {
56
- return repo . configurationId === selectedID ;
57
- }
65
+ const matchingSuggestion = repos ?. find (
66
+ ( repo ) => repo . configurationId === selectedID || repo . url === selectedID ,
67
+ ) ;
58
68
59
- return repo . url === selectedID ;
60
- } ) ;
61
69
if ( matchingSuggestion ) {
62
70
onChange ?.( matchingSuggestion ) ;
63
71
return ;
64
72
}
65
73
66
- onChange ?.(
67
- new SuggestedRepository ( {
68
- url : selectedID ,
69
- } ) ,
70
- ) ;
74
+ const matchingPredefinedRepo = memoizedPredefinedRepos . get ( selectedID ) ;
75
+ if ( matchingPredefinedRepo ) {
76
+ onChange ?.(
77
+ new SuggestedRepository ( {
78
+ url : matchingPredefinedRepo . url ,
79
+ repoName : matchingPredefinedRepo . repoName ,
80
+ } ) ,
81
+ ) ;
82
+ return ;
83
+ }
84
+
85
+ onChange ?.( new SuggestedRepository ( { url : selectedID } ) ) ;
71
86
} ,
72
- [ onChange , repos ] ,
87
+ [ onChange , repos , memoizedPredefinedRepos ] ,
73
88
) ;
74
89
75
90
const [ selectedSuggestion , setSelectedSuggestion ] = useState < SuggestedRepository | undefined > ( undefined ) ;
91
+ const [ hasStartedSearching , setHasStartedSearching ] = useState ( false ) ;
92
+ const [ isShowingExamples , setIsShowingExamples ] = useState ( showExamples ) ;
93
+
94
+ type PredefinedRepositoryOptionProps = {
95
+ repo : {
96
+ url : string ;
97
+ repoName : string ;
98
+ description : string ;
99
+ repoPath : string ;
100
+ } ;
101
+ } ;
102
+
103
+ const PredefinedRepositoryOption : FC < PredefinedRepositoryOptionProps > = ( { repo } ) => {
104
+ return (
105
+ < div className = "flex flex-col overflow-hidden" aria-label = { `Demo: ${ repo . url } ` } >
106
+ < div className = "flex items-center" >
107
+ < GitpodRepositoryTemplate className = "w-5 h-5 text-pk-content-tertiary mr-2" />
108
+ < span className = "text-sm font-semibold" > { repo . repoName } </ span >
109
+ < MiddleDot className = "px-0.5 text-pk-content-tertiary" />
110
+ < span
111
+ className = "text-sm whitespace-nowrap truncate overflow-ellipsis text-pk-content-secondary"
112
+ title = { repo . repoPath }
113
+ >
114
+ { repo . repoPath }
115
+ </ span >
116
+ </ div >
117
+ < span className = "text-xs text-pk-content-secondary ml-7" > { repo . description } </ span >
118
+ </ div >
119
+ ) ;
120
+ } ;
76
121
77
122
// Resolve the selected context url & configurationId id props to a suggestion entry
78
123
useEffect ( ( ) => {
@@ -86,9 +131,17 @@ export default function RepositoryFinder({
86
131
87
132
// If no match, it's a context url that was typed/pasted in, so treat it like a suggestion w/ just a url
88
133
if ( ! match && selectedContextURL ) {
89
- match = new SuggestedRepository ( {
90
- url : selectedContextURL ,
91
- } ) ;
134
+ const predefinedMatch = PREDEFINED_REPOS . find ( ( repo ) => repo . url === selectedContextURL ) ;
135
+ if ( predefinedMatch ) {
136
+ match = new SuggestedRepository ( {
137
+ url : predefinedMatch . url ,
138
+ repoName : predefinedMatch . repoName ,
139
+ } ) ;
140
+ } else {
141
+ match = new SuggestedRepository ( {
142
+ url : selectedContextURL ,
143
+ } ) ;
144
+ }
92
145
}
93
146
94
147
// This means we found a matching configuration, but the context url is different
@@ -112,7 +165,7 @@ export default function RepositoryFinder({
112
165
113
166
// If we put the selectedSuggestion in the dependency array, it will cause an infinite loop
114
167
// eslint-disable-next-line react-hooks/exhaustive-deps
115
- } , [ repos , selectedContextURL , selectedConfigurationId ] ) ;
168
+ } , [ repos , selectedContextURL , selectedConfigurationId , isShowingExamples ] ) ;
116
169
117
170
const displayName = useMemo ( ( ) => {
118
171
if ( ! selectedSuggestion ) {
@@ -126,27 +179,44 @@ export default function RepositoryFinder({
126
179
return selectedSuggestion ?. configurationName ;
127
180
} , [ selectedSuggestion ] ) ;
128
181
182
+ const handleSearchChange = ( value : string ) => {
183
+ setSearchString ( value ) ;
184
+ if ( value . length > 0 ) {
185
+ setIsShowingExamples ( false ) ;
186
+ if ( ! hasStartedSearching ) {
187
+ setHasStartedSearching ( true ) ;
188
+ }
189
+ } else {
190
+ setIsShowingExamples ( showExamples ) ;
191
+ }
192
+ } ;
193
+
129
194
const getElements = useCallback (
130
- // searchString ignore here as list is already pre-filtered against it
131
- // w/ mirrored state via useUnifiedRepositorySearch
132
- ( searchString : string ) => {
133
- const result = repos . map ( ( repo ) => {
134
- return {
135
- id : repo . configurationId || repo . url ,
136
- element : < SuggestedRepositoryOption repo = { repo } /> ,
195
+ ( searchString : string ) : ComboboxElement [ ] => {
196
+ if ( isShowingExamples && searchString . length === 0 ) {
197
+ return PREDEFINED_REPOS . map ( ( repo ) => ( {
198
+ id : repo . url ,
199
+ element : < PredefinedRepositoryOption repo = { repo } /> ,
137
200
isSelectable : true ,
138
- } as ComboboxElement ;
139
- } ) ;
201
+ } ) ) ;
202
+ }
203
+
204
+ const result = repos . map ( ( repo ) => ( {
205
+ id : repo . configurationId || repo . url ,
206
+ element : < SuggestedRepositoryOption repo = { repo } /> ,
207
+ isSelectable : true ,
208
+ } ) ) ;
209
+
140
210
if ( hasMore ) {
141
- // add an element that tells the user to refine the search
142
211
result . push ( {
143
212
id : "more" ,
144
213
element : (
145
214
< div className = "text-sm text-pk-content-tertiary" > Repo missing? Try refining your search.</ div >
146
215
) ,
147
216
isSelectable : false ,
148
- } as ComboboxElement ) ;
217
+ } ) ;
149
218
}
219
+
150
220
if (
151
221
searchString . length >= 3 &&
152
222
authProviders . data ?. some ( ( p ) => p . type === AuthProviderType . BITBUCKET_SERVER ) &&
@@ -156,16 +226,15 @@ export default function RepositoryFinder({
156
226
result . push ( {
157
227
id : "bitbucket-server" ,
158
228
element : (
159
- < div className = "text-sm text-pk-content-tertiary" >
160
- < div className = "flex items-center" >
161
- < Exclamation2 className = "w-4 h-4" > </ Exclamation2 >
162
- < span className = "ml-2" > Bitbucket Server only supports searching by prefix.</ span >
163
- </ div >
229
+ < div className = "text-sm text-pk-content-tertiary flex items-center" >
230
+ < Exclamation2 className = "w-4 h-4 mr-2" />
231
+ < span > Bitbucket Server only supports searching by prefix.</ span >
164
232
</ div >
165
233
) ,
166
234
isSelectable : false ,
167
- } as ComboboxElement ) ;
235
+ } ) ;
168
236
}
237
+
169
238
if ( searchString . length < 3 ) {
170
239
// add an element that tells the user to type more
171
240
result . push ( {
@@ -176,13 +245,19 @@ export default function RepositoryFinder({
176
245
</ div >
177
246
) ,
178
247
isSelectable : false ,
179
- } as ComboboxElement ) ;
248
+ } ) ;
180
249
}
250
+
181
251
return result ;
182
252
} ,
183
- [ repos , hasMore , authProviders . data , onlyConfigurations ] ,
253
+ [ repos , hasMore , authProviders . data , onlyConfigurations , isShowingExamples ] ,
184
254
) ;
185
255
256
+ const resolveIcon = useCallback ( ( contextUrl ?: string ) => {
257
+ if ( ! contextUrl ) return RepositorySVG ;
258
+ return PREDEFINED_REPOS . some ( ( repo ) => repo . url === contextUrl ) ? GitpodRepositoryTemplateSVG : RepositorySVG ;
259
+ } , [ ] ) ;
260
+
186
261
return (
187
262
< Combobox
188
263
getElements = { getElements }
@@ -192,11 +267,11 @@ export default function RepositoryFinder({
192
267
disabled = { disabled }
193
268
// Only consider the isLoading prop if we're including projects in list
194
269
loading = { isLoading || isSearching }
195
- searchPlaceholder = "Paste repository URL or type to find suggestions "
196
- onSearchChange = { setSearchString }
270
+ searchPlaceholder = "Search repos and demos or paste a repo URL "
271
+ onSearchChange = { handleSearchChange }
197
272
>
198
273
< ComboboxSelectedItem
199
- icon = { RepositorySVG }
274
+ icon = { resolveIcon ( selectedContextURL ) }
200
275
htmlTitle = { displayContextUrl ( selectedContextURL ) || "Repository" }
201
276
title = { < div className = "truncate" > { displayName || "Select a repository" } </ div > }
202
277
subtitle = {
0 commit comments