@@ -4,35 +4,45 @@ import Api
4
4
import Env exposing (Env )
5
5
import Html exposing (Html , div , h1 , input , strong , table , tbody , td , text , tr )
6
6
import Html.Attributes exposing (autofocus , class , classList , placeholder )
7
- import Html.Events exposing (onBlur , onClick , onFocus , onInput )
7
+ import Html.Events exposing (onBlur , onFocus , onInput , onMouseDown )
8
8
import Http
9
9
import KeyboardShortcut exposing (KeyboardShortcut (..) )
10
10
import KeyboardShortcut.Key as Key exposing (Key (..) )
11
11
import KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent )
12
+ import List.Extra as ListE
13
+ import Maybe.Extra as MaybeE
12
14
import Perspective
13
15
import Project exposing (ProjectListing )
14
16
import RemoteData exposing (RemoteData (..) , WebData )
15
17
import SearchResults exposing (SearchResults (..) )
18
+ import Simple.Fuzzy as Fuzzy
16
19
import Task
17
20
import UI
18
21
import UI.Card as Card
19
22
import UI.Click as Click
20
23
import UI.Icon as Icon
21
24
import UI.PageLayout as PageLayout exposing (PageLayout )
22
25
import UnisonShare.Catalog as Catalog exposing (Catalog )
26
+ import UnisonShare.Catalog.CatalogMask exposing (CatalogMask )
23
27
import UnisonShare.Route as Route
28
+ import UnisonShare.User as User exposing (Username )
24
29
25
30
26
31
27
32
-- MODEL
28
33
29
34
30
- type alias SearchResult =
31
- ( ProjectListing , String )
35
+ type Match
36
+ = UserMatch Username
37
+ | ProjectMatch ProjectListing String
32
38
33
39
34
40
type alias CatalogSearchResults =
35
- SearchResults SearchResult
41
+ SearchResults Match
42
+
43
+
44
+
45
+ -- TODO: Rename
36
46
37
47
38
48
type alias CatalogSearch =
@@ -43,6 +53,7 @@ type alias LoadedModel =
43
53
{ search : CatalogSearch
44
54
, hasFocus : Bool
45
55
, catalog : Catalog
56
+ , usernames : List Username
46
57
, keyboardShortcut : KeyboardShortcut . Model
47
58
}
48
59
@@ -53,14 +64,14 @@ type alias Model =
53
64
54
65
init : Env -> ( Model , Cmd Msg )
55
66
init env =
56
- ( Loading , fetchCatalog env )
67
+ ( Loading , fetch env )
57
68
58
69
59
70
{- | Fetch the Catalog in sequence by first fetching the doc, then the
60
71
projectListings and finally merging them into a Catalog
61
72
-}
62
- fetchCatalog : Env -> Cmd Msg
63
- fetchCatalog env =
73
+ fetch : Env -> Cmd Msg
74
+ fetch env =
64
75
let
65
76
perspective =
66
77
Perspective . toCodebasePerspective env. perspective
@@ -73,8 +84,7 @@ fetchCatalog env =
73
84
|> Api . toTask env. apiBasePath Project . decodeListings
74
85
|> Task . map ( \ projects -> ( catalog, projects ))
75
86
)
76
- |> Task . map ( \ ( cm, ps ) -> Catalog . catalog cm ps)
77
- |> Task . attempt FetchCatalogFinished
87
+ |> Task . attempt FetchFinished
78
88
79
89
80
90
@@ -86,25 +96,34 @@ type Msg
86
96
| UpdateFocus Bool
87
97
| ClearQuery
88
98
| SelectProject ProjectListing
89
- | FetchCatalogFinished ( Result Http . Error Catalog )
99
+ | SelectUser Username
100
+ | FetchFinished ( Result Http . Error ( CatalogMask , List ProjectListing ))
90
101
| Keydown KeyboardEvent
91
102
| KeyboardShortcutMsg KeyboardShortcut . Msg
92
103
93
104
94
105
update : Env -> Msg -> Model -> ( Model , Cmd Msg )
95
106
update env msg model =
96
107
case ( msg, model ) of
97
- ( FetchCatalogFinished catalogResult , _ ) ->
98
- case catalogResult of
108
+ ( FetchFinished result , _ ) ->
109
+ case result of
99
110
Err e ->
100
111
( Failure e, Cmd . none )
101
112
102
- Ok catalog ->
113
+ Ok ( mask , listings ) ->
103
114
let
115
+ usernames =
116
+ listings
117
+ |> List . map ( . owner >> Project . ownerToString)
118
+ |> ListE . unique
119
+ |> List . map User . usernameFromString
120
+ |> MaybeE . values
121
+
104
122
initModel =
105
123
{ search = { query = " " , results = SearchResults . empty }
106
124
, hasFocus = True
107
- , catalog = catalog
125
+ , catalog = Catalog . catalog mask listings
126
+ , usernames = usernames
108
127
, keyboardShortcut = KeyboardShortcut . init env. operatingSystem
109
128
}
110
129
in
@@ -121,7 +140,7 @@ update env msg model =
121
140
122
141
else
123
142
query
124
- |> Catalog . search m. catalog
143
+ |> search m. catalog m . usernames
125
144
|> SearchResults . fromList
126
145
in
127
146
( Success { m | search = { query = query, results = searchResults } }, Cmd . none )
@@ -132,6 +151,9 @@ update env msg model =
132
151
( SelectProject project, Success m ) ->
133
152
( Success m, Route . navigateToProject env. navKey project )
134
153
154
+ ( SelectUser username, Success m ) ->
155
+ ( Success m, Route . navigateToUsername env. navKey username )
156
+
135
157
( Keydown event, Success m ) ->
136
158
let
137
159
( keyboardShortcut, kCmd ) =
@@ -174,8 +196,7 @@ update env msg model =
174
196
navigate =
175
197
matches
176
198
|> SearchResults . focus
177
- |> Tuple . first
178
- |> Route . navigateToProject env. navKey
199
+ |> matchToNavigate env
179
200
in
180
201
( Success newModel, Cmd . batch [ cmd, navigate ] )
181
202
@@ -185,8 +206,7 @@ update env msg model =
185
206
let
186
207
navigate =
187
208
SearchResults . getAt ( n - 1 ) m. search. results
188
- |> Maybe . map Tuple . first
189
- |> Maybe . map ( Route . navigateToProject env. navKey)
209
+ |> Maybe . map ( matchToNavigate env)
190
210
|> Maybe . withDefault Cmd . none
191
211
in
192
212
( Success newModel, Cmd . batch [ cmd, navigate ] )
@@ -209,8 +229,49 @@ update env msg model =
209
229
210
230
211
231
mapSearch : (CatalogSearchResults -> CatalogSearchResults ) -> CatalogSearch -> CatalogSearch
212
- mapSearch f search =
213
- { search | results = f search. results }
232
+ mapSearch f search_ =
233
+ { search_ | results = f search_. results }
234
+
235
+
236
+ matchToNavigate : Env -> Match -> Cmd Msg
237
+ matchToNavigate env match =
238
+ case match of
239
+ UserMatch username ->
240
+ Route . navigateToUsername env. navKey username
241
+
242
+ ProjectMatch project _ ->
243
+ Route . navigateToProject env. navKey project
244
+
245
+
246
+ toMatches : Catalog -> List Username -> List Match
247
+ toMatches catalog users =
248
+ let
249
+ flat ( category, projects ) acc =
250
+ acc ++ List . map ( \ p -> ProjectMatch p category) projects
251
+
252
+ projectMatches =
253
+ catalog
254
+ |> Catalog . toList
255
+ |> List . foldl flat []
256
+
257
+ userMatches =
258
+ List . map UserMatch users
259
+ in
260
+ userMatches ++ projectMatches
261
+
262
+
263
+ search : Catalog -> List Username -> String -> List Match
264
+ search catalog users query =
265
+ let
266
+ normalize m =
267
+ case m of
268
+ UserMatch u ->
269
+ User . usernameToString u
270
+
271
+ ProjectMatch p _ ->
272
+ Project . slugString p
273
+ in
274
+ Fuzzy . filter normalize query ( toMatches catalog users)
214
275
215
276
216
277
@@ -238,8 +299,11 @@ viewCategory ( category, projects ) =
238
299
|> Card . view
239
300
240
301
241
- viewMatch : KeyboardShortcut .Model -> SearchResult -> Bool -> Maybe Key -> Html Msg
242
- viewMatch keyboardShortcut ( project, category ) isFocused shortcut =
302
+ {- | View a match in the dropdown list. Use `onMouseDown` instead of `onClick`
303
+ to avoid competing with `onBlur` on the input
304
+ -}
305
+ viewMatch : KeyboardShortcut .Model -> Match -> Bool -> Maybe Key -> Html Msg
306
+ viewMatch keyboardShortcut match isFocused shortcut =
243
307
let
244
308
shortcutIndicator =
245
309
if isFocused then
@@ -253,14 +317,31 @@ viewMatch keyboardShortcut ( project, category ) isFocused shortcut =
253
317
Just key ->
254
318
KeyboardShortcut . view keyboardShortcut ( Sequence ( Just Key . Semicolon ) key)
255
319
in
256
- tr
257
- [ classList [ ( " search-result" , True ) , ( " focused" , isFocused ) ]
258
- , onClick ( SelectProject project)
259
- ]
260
- [ td [ class " project-name" ] [ Project . viewProjectListing Click . Disabled project ]
261
- , td [ class " category" ] [ text category ]
262
- , td [] [ div [ class " shortcut" ] [ shortcutIndicator ] ]
263
- ]
320
+ case match of
321
+ UserMatch username ->
322
+ tr
323
+ [ classList [ ( " search-result" , True ) , ( " focused" , isFocused ) ]
324
+ , onMouseDown ( SelectUser username)
325
+ ]
326
+ [ td [ class " match-name" ]
327
+ [ div [ class " user-listing" ]
328
+ [ div [ class " avatar" ] [ Icon . view Icon . user ]
329
+ , text ( User . usernameToString username)
330
+ ]
331
+ ]
332
+ , td [ class " category" ] [ text " User" ]
333
+ , td [] [ div [ class " shortcut" ] [ shortcutIndicator ] ]
334
+ ]
335
+
336
+ ProjectMatch project category ->
337
+ tr
338
+ [ classList [ ( " search-result" , True ) , ( " focused" , isFocused ) ]
339
+ , onMouseDown ( SelectProject project)
340
+ ]
341
+ [ td [ class " match-name" ] [ Project . viewProjectListing Click . Disabled project ]
342
+ , td [ class " category" ] [ text category ]
343
+ , td [] [ div [ class " shortcut" ] [ shortcutIndicator ] ]
344
+ ]
264
345
265
346
266
347
indexToShortcut : Int -> Maybe Key
@@ -276,7 +357,7 @@ indexToShortcut index =
276
357
n |> String . fromInt |> Key . fromString |> Just
277
358
278
359
279
- viewMatches : KeyboardShortcut .Model -> SearchResults .Matches SearchResult -> Html Msg
360
+ viewMatches : KeyboardShortcut .Model -> SearchResults .Matches Match -> Html Msg
280
361
viewMatches keyboardShortcut matches =
281
362
let
282
363
matchItems =
@@ -295,7 +376,7 @@ viewSearchResults keyboardShortcut { query, results } =
295
376
resultsPane =
296
377
case results of
297
378
Empty ->
298
- div [ class " empty-state" ] [ text ( " No matching projects found for \" " ++ query ++ " \" " ) ]
379
+ div [ class " empty-state" ] [ text ( " No matches found for \" " ++ query ++ " \" " ) ]
299
380
300
381
SearchResults matches ->
301
382
viewMatches keyboardShortcut matches
@@ -347,7 +428,7 @@ viewLoaded model =
347
428
[ div [ class " search-field" ]
348
429
[ Icon . view Icon . search
349
430
, input
350
- [ placeholder " Search for projects"
431
+ [ placeholder " Search for projects and users "
351
432
, onInput UpdateQuery
352
433
, autofocus True
353
434
, onBlur ( UpdateFocus False )
0 commit comments