Skip to content
This repository was archived by the owner on Jul 19, 2022. It is now read-only.

Catalog: Support searching for users #312

Merged
merged 1 commit into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/UI/Icon.elm
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,12 @@ clipboard =
[ path [ fill "currentColor", fillRule "evenodd", d "M8 2.25C8 2.11193 7.88807 2 7.75 2H6.25C6.11193 2 6 2.11193 6 2.25V2.75C6 2.88807 6.11193 3 6.25 3H7.75C7.88807 3 8 2.88807 8 2.75V2.25ZM6 1C5.44772 1 5 1.44772 5 2V3C5 3.55228 5.44772 4 6 4H8C8.55228 4 9 3.55228 9 3V2C9 1.44772 8.55228 1 8 1H6Z" ] []
, path [ fill "currentColor", fillRule "evenodd", d "M3 2.5C3 2.22386 3.22386 2 3.5 2C3.77614 2 4 2.22386 4 2.5V10.5C4 10.7761 4.22386 11 4.5 11H9.5C9.77614 11 10 10.7761 10 10.5V2.5C10 2.22386 10.2239 2 10.5 2C10.7761 2 11 2.22386 11 2.5V11C11 11.5523 10.5523 12 10 12H4C3.44772 12 3 11.5523 3 11V2.5Z" ] []
]


user : Icon msg
user =
Icon "user"
[]
[ path [ fill "currentColor", d "M7 7C3.97669 7 1.47565 9.60877 1.06051 13.0021C0.99344 13.5503 1.44772 14 2 14H12C12.5523 14 13.0066 13.5503 12.9395 13.0021C12.5243 9.60877 10.0233 7 7 7Z" ] []
, circle [ fill "currentColor", cx "7", cy "3", r "3" ] []
]
20 changes: 0 additions & 20 deletions src/UnisonShare/Catalog.elm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import FullyQualifiedName as FQN
import Json.Decode as Decode
import OrderedDict exposing (OrderedDict)
import Project exposing (ProjectListing)
import Simple.Fuzzy as Fuzzy
import UnisonShare.Catalog.CatalogMask as CatalogMask exposing (CatalogMask)


Expand Down Expand Up @@ -87,25 +86,6 @@ fromList items =
-- HELPERS


{-| Fuzzy search through a flattened catalog by project name and category
-}
search : Catalog -> String -> List ( ProjectListing, String )
search catalog_ query =
let
flat ( category, projects ) acc =
acc ++ List.map (\p -> ( p, category )) projects

normalize ( p, c ) =
p
|> Project.slugString
|> (++) c
in
catalog_
|> toList
|> List.foldl flat []
|> Fuzzy.filter normalize query


isEmpty : Catalog -> Bool
isEmpty (Catalog dict) =
OrderedDict.isEmpty dict
Expand Down
149 changes: 115 additions & 34 deletions src/UnisonShare/Page/Catalog.elm
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,45 @@ import Api
import Env exposing (Env)
import Html exposing (Html, div, h1, input, strong, table, tbody, td, text, tr)
import Html.Attributes exposing (autofocus, class, classList, placeholder)
import Html.Events exposing (onBlur, onClick, onFocus, onInput)
import Html.Events exposing (onBlur, onFocus, onInput, onMouseDown)
import Http
import KeyboardShortcut exposing (KeyboardShortcut(..))
import KeyboardShortcut.Key as Key exposing (Key(..))
import KeyboardShortcut.KeyboardEvent as KeyboardEvent exposing (KeyboardEvent)
import List.Extra as ListE
import Maybe.Extra as MaybeE
import Perspective
import Project exposing (ProjectListing)
import RemoteData exposing (RemoteData(..), WebData)
import SearchResults exposing (SearchResults(..))
import Simple.Fuzzy as Fuzzy
import Task
import UI
import UI.Card as Card
import UI.Click as Click
import UI.Icon as Icon
import UI.PageLayout as PageLayout exposing (PageLayout)
import UnisonShare.Catalog as Catalog exposing (Catalog)
import UnisonShare.Catalog.CatalogMask exposing (CatalogMask)
import UnisonShare.Route as Route
import UnisonShare.User as User exposing (Username)



-- MODEL


type alias SearchResult =
( ProjectListing, String )
type Match
= UserMatch Username
| ProjectMatch ProjectListing String


type alias CatalogSearchResults =
SearchResults SearchResult
SearchResults Match



-- TODO: Rename


type alias CatalogSearch =
Expand All @@ -43,6 +53,7 @@ type alias LoadedModel =
{ search : CatalogSearch
, hasFocus : Bool
, catalog : Catalog
, usernames : List Username
, keyboardShortcut : KeyboardShortcut.Model
}

Expand All @@ -53,14 +64,14 @@ type alias Model =

init : Env -> ( Model, Cmd Msg )
init env =
( Loading, fetchCatalog env )
( Loading, fetch env )


{-| Fetch the Catalog in sequence by first fetching the doc, then the
projectListings and finally merging them into a Catalog
-}
fetchCatalog : Env -> Cmd Msg
fetchCatalog env =
fetch : Env -> Cmd Msg
fetch env =
let
perspective =
Perspective.toCodebasePerspective env.perspective
Expand All @@ -73,8 +84,7 @@ fetchCatalog env =
|> Api.toTask env.apiBasePath Project.decodeListings
|> Task.map (\projects -> ( catalog, projects ))
)
|> Task.map (\( cm, ps ) -> Catalog.catalog cm ps)
|> Task.attempt FetchCatalogFinished
|> Task.attempt FetchFinished



Expand All @@ -86,25 +96,34 @@ type Msg
| UpdateFocus Bool
| ClearQuery
| SelectProject ProjectListing
| FetchCatalogFinished (Result Http.Error Catalog)
| SelectUser Username
| FetchFinished (Result Http.Error ( CatalogMask, List ProjectListing ))
| Keydown KeyboardEvent
| KeyboardShortcutMsg KeyboardShortcut.Msg


update : Env -> Msg -> Model -> ( Model, Cmd Msg )
update env msg model =
case ( msg, model ) of
( FetchCatalogFinished catalogResult, _ ) ->
case catalogResult of
( FetchFinished result, _ ) ->
case result of
Err e ->
( Failure e, Cmd.none )

Ok catalog ->
Ok ( mask, listings ) ->
let
usernames =
listings
|> List.map (.owner >> Project.ownerToString)
|> ListE.unique
|> List.map User.usernameFromString
|> MaybeE.values

initModel =
{ search = { query = "", results = SearchResults.empty }
, hasFocus = True
, catalog = catalog
, catalog = Catalog.catalog mask listings
, usernames = usernames
, keyboardShortcut = KeyboardShortcut.init env.operatingSystem
}
in
Expand All @@ -121,7 +140,7 @@ update env msg model =

else
query
|> Catalog.search m.catalog
|> search m.catalog m.usernames
|> SearchResults.fromList
in
( Success { m | search = { query = query, results = searchResults } }, Cmd.none )
Expand All @@ -132,6 +151,9 @@ update env msg model =
( SelectProject project, Success m ) ->
( Success m, Route.navigateToProject env.navKey project )

( SelectUser username, Success m ) ->
( Success m, Route.navigateToUsername env.navKey username )

( Keydown event, Success m ) ->
let
( keyboardShortcut, kCmd ) =
Expand Down Expand Up @@ -174,8 +196,7 @@ update env msg model =
navigate =
matches
|> SearchResults.focus
|> Tuple.first
|> Route.navigateToProject env.navKey
|> matchToNavigate env
in
( Success newModel, Cmd.batch [ cmd, navigate ] )

Expand All @@ -185,8 +206,7 @@ update env msg model =
let
navigate =
SearchResults.getAt (n - 1) m.search.results
|> Maybe.map Tuple.first
|> Maybe.map (Route.navigateToProject env.navKey)
|> Maybe.map (matchToNavigate env)
|> Maybe.withDefault Cmd.none
in
( Success newModel, Cmd.batch [ cmd, navigate ] )
Expand All @@ -209,8 +229,49 @@ update env msg model =


mapSearch : (CatalogSearchResults -> CatalogSearchResults) -> CatalogSearch -> CatalogSearch
mapSearch f search =
{ search | results = f search.results }
mapSearch f search_ =
{ search_ | results = f search_.results }


matchToNavigate : Env -> Match -> Cmd Msg
matchToNavigate env match =
case match of
UserMatch username ->
Route.navigateToUsername env.navKey username

ProjectMatch project _ ->
Route.navigateToProject env.navKey project


toMatches : Catalog -> List Username -> List Match
toMatches catalog users =
let
flat ( category, projects ) acc =
acc ++ List.map (\p -> ProjectMatch p category) projects

projectMatches =
catalog
|> Catalog.toList
|> List.foldl flat []

userMatches =
List.map UserMatch users
in
userMatches ++ projectMatches


search : Catalog -> List Username -> String -> List Match
search catalog users query =
let
normalize m =
case m of
UserMatch u ->
User.usernameToString u

ProjectMatch p _ ->
Project.slugString p
in
Fuzzy.filter normalize query (toMatches catalog users)



Expand Down Expand Up @@ -238,8 +299,11 @@ viewCategory ( category, projects ) =
|> Card.view


viewMatch : KeyboardShortcut.Model -> SearchResult -> Bool -> Maybe Key -> Html Msg
viewMatch keyboardShortcut ( project, category ) isFocused shortcut =
{-| View a match in the dropdown list. Use `onMouseDown` instead of `onClick`
to avoid competing with `onBlur` on the input
-}
viewMatch : KeyboardShortcut.Model -> Match -> Bool -> Maybe Key -> Html Msg
viewMatch keyboardShortcut match isFocused shortcut =
let
shortcutIndicator =
if isFocused then
Expand All @@ -253,14 +317,31 @@ viewMatch keyboardShortcut ( project, category ) isFocused shortcut =
Just key ->
KeyboardShortcut.view keyboardShortcut (Sequence (Just Key.Semicolon) key)
in
tr
[ classList [ ( "search-result", True ), ( "focused", isFocused ) ]
, onClick (SelectProject project)
]
[ td [ class "project-name" ] [ Project.viewProjectListing Click.Disabled project ]
, td [ class "category" ] [ text category ]
, td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ]
]
case match of
UserMatch username ->
tr
[ classList [ ( "search-result", True ), ( "focused", isFocused ) ]
, onMouseDown (SelectUser username)
]
[ td [ class "match-name" ]
[ div [ class "user-listing" ]
[ div [ class "avatar" ] [ Icon.view Icon.user ]
, text (User.usernameToString username)
]
]
, td [ class "category" ] [ text "User" ]
, td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ]
]

ProjectMatch project category ->
tr
[ classList [ ( "search-result", True ), ( "focused", isFocused ) ]
, onMouseDown (SelectProject project)
]
[ td [ class "match-name" ] [ Project.viewProjectListing Click.Disabled project ]
, td [ class "category" ] [ text category ]
, td [] [ div [ class "shortcut" ] [ shortcutIndicator ] ]
]


indexToShortcut : Int -> Maybe Key
Expand All @@ -276,7 +357,7 @@ indexToShortcut index =
n |> String.fromInt |> Key.fromString |> Just


viewMatches : KeyboardShortcut.Model -> SearchResults.Matches SearchResult -> Html Msg
viewMatches : KeyboardShortcut.Model -> SearchResults.Matches Match -> Html Msg
viewMatches keyboardShortcut matches =
let
matchItems =
Expand All @@ -295,7 +376,7 @@ viewSearchResults keyboardShortcut { query, results } =
resultsPane =
case results of
Empty ->
div [ class "empty-state" ] [ text ("No matching projects found for \"" ++ query ++ "\"") ]
div [ class "empty-state" ] [ text ("No matches found for \"" ++ query ++ "\"") ]

SearchResults matches ->
viewMatches keyboardShortcut matches
Expand Down Expand Up @@ -347,7 +428,7 @@ viewLoaded model =
[ div [ class "search-field" ]
[ Icon.view Icon.search
, input
[ placeholder "Search for projects"
[ placeholder "Search for projects and users"
, onInput UpdateQuery
, autofocus True
, onBlur (UpdateFocus False)
Expand Down
12 changes: 12 additions & 0 deletions src/UnisonShare/Route.elm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module UnisonShare.Route exposing
, navigateToLatestCodebase
, navigateToPerspective
, navigateToProject
, navigateToUser
, navigateToUsername
, perspectiveParams
, replacePerspective
, toDefinition
Expand Down Expand Up @@ -328,6 +330,16 @@ navigateToProject navKey project =
navigate navKey (forProject project)


navigateToUser : Nav.Key -> User.User a -> Cmd msg
navigateToUser navKey user_ =
navigate navKey (forUser user_)


navigateToUsername : Nav.Key -> User.Username -> Cmd msg
navigateToUsername navKey username_ =
navigate navKey (User username_)



-- TODO: this should go away in UnisonShare

Expand Down
Loading