on master help sign in
Got search working
b71b0faiko
frontend/Main.elm−1
@@ -134,7 +134,6 @@ subscriptions _ =
134
134
135init : ( Model, Cmd Msg )
135init : ( Model, Cmd Msg )
136init =
136init =
137− -- Cipher.init |> Tuple.mapBoth (\p -> { notifications = [], page = CipherModel p }) (Cmd.map CipherMsg)
138 ( { notifications = []
137 ( { notifications = []
139 , pageStack = Nonempty.singleton LoadingPage
138 , pageStack = Nonempty.singleton LoadingPage
140 }
139 }
frontend/Pages/Ciphers.elm+36−14
@@ -5,16 +5,19 @@ import Html exposing (..)
5import Html.Attributes as Attr
5import Html.Attributes as Attr
6import Html.Events as Ev
6import Html.Events as Ev
7import Page exposing (..)
7import Page exposing (..)
8+import Search
8import Utils exposing (..)
9import Utils exposing (..)
9
10
10
11
11type alias Model =
12type alias Model =
12 { ciphers : Bridge.Sub_LoadCiphers_List
13 { ciphers : Bridge.Sub_LoadCiphers_List
14+ , search : String
13 }
15 }
14
16
15
17
16type Msg
18type Msg
17 = Noop
19 = Noop
20+ | UpdateSearch String
18
21
19
22
20type alias Callbacks msg =
23type alias Callbacks msg =
@@ -25,7 +28,7 @@ type alias Callbacks msg =
25page : Callbacks emsg -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
28page : Callbacks emsg -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
26page callbacks liftMsg =
29page callbacks liftMsg =
27 { init = \ciphers -> Tuple.mapSecond (Cmd.map liftMsg) (init ciphers)
30 { init = \ciphers -> Tuple.mapSecond (Cmd.map liftMsg) (init ciphers)
28− , view = view callbacks
31+ , view = view callbacks liftMsg
29 , update = \msg model -> update callbacks liftMsg msg model
32 , update = \msg model -> update callbacks liftMsg msg model
30 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
33 , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
31 , title = always "Passwords"
34 , title = always "Passwords"
@@ -34,7 +37,7 @@ page callbacks liftMsg =
34
37
35init : Bridge.Sub_LoadCiphers_List -> ( Model, Cmd Msg )
38init : Bridge.Sub_LoadCiphers_List -> ( Model, Cmd Msg )
36init ciphers =
39init ciphers =
37− ( { ciphers = ciphers }
40+ ( { ciphers = ciphers, search = "" }
38 , Cmd.none
41 , Cmd.none
39 )
42 )
40
43
@@ -45,24 +48,43 @@ update {} liftMsg msg model =
45 Noop ->
48 Noop ->
46 ( Ok model, Cmd.none )
49 ( Ok model, Cmd.none )
47
50
51+ UpdateSearch s ->
52+ ( Ok { model | search = s }, Cmd.none )
53+
48
54
49subscriptions : Model -> Sub Msg
55subscriptions : Model -> Sub Msg
50subscriptions model =
56subscriptions model =
51 Sub.none
57 Sub.none
52
58
53
59
54−view : Callbacks emsg -> Model -> List (Html emsg)
60+view : Callbacks emsg -> (Msg -> emsg) -> Model -> List (Html emsg)
55−view { selected } { ciphers } =
61+view { selected } liftMsg { ciphers, search } =
62+ [ input
63+ [ Attr.type_ "search"
64+ , Attr.value search
65+ , Ev.onInput (liftMsg << UpdateSearch)
66+ , Attr.placeholder "Search"
67+ , Attr.attribute "autocomplete" "off"
68+ ]
69+ []
56− [ ul [ Attr.class "p-list--divided" ]
70+ , ul [ Attr.class "p-list--divided" ]
71+ (ciphers
72+ |> Search.searchList (cipherSearch search)
57− (List.map
73+ |> List.map
58− (\{ name, date, id } ->
74+ (\{ name, date, id } ->
59− li [ Attr.class "p-list__item cipher-row", Ev.onClick (selected id) ]
75+ li [ Attr.class "p-list__item cipher-row", Ev.onClick (selected id) ]
60− [ div [ Attr.class "cipher-row-container" ]
76+ [ div [ Attr.class "cipher-row-container" ]
61− [ p [] [ text name ]
77+ [ p [] [ text name ]
62− , p [ Attr.class "p-text--small u-align-text--right u-text--muted" ] [ text date ]
78+ , p [ Attr.class "p-text--small u-align-text--right u-text--muted" ] [ text date ]
79+ ]
63 ]
80 ]
64− ]
65− )
81+ )
66− ciphers
67 )
82 )
68 ]
83 ]
84+
85+
86+cipherSearch : String -> Search.Search Bridge.Sub_LoadCiphers
87+cipherSearch query =
88+ Search.Search
89+ [ ( query, \q { name } -> Search.search q name )
90+ ]
frontend/Pages/Login.elm+21−3
@@ -71,11 +71,29 @@ view : Model -> List (Html Msg)
71view model =
71view model =
72 [ form []
72 [ form []
73 [ label [] [ text "Server" ]
73 [ label [] [ text "Server" ]
74+ , input
75+ [ Attr.type_ "url"
76+ , Attr.value model.server
74− , input [ Attr.type_ "url", Attr.value model.server, Ev.onInput UpdateServer ] []
77+ , Ev.onInput UpdateServer
78+ , Attr.attribute "autocomplete" "url"
79+ ]
80+ []
75 , label [] [ text "Email" ]
81 , label [] [ text "Email" ]
82+ , input
83+ [ Attr.type_ "email"
84+ , Attr.value model.email
76− , input [ Attr.type_ "email", Attr.value model.email, Ev.onInput UpdateEmail ] []
85+ , Ev.onInput UpdateEmail
86+ , Attr.attribute "autocomplete" "email"
87+ ]
88+ []
77 , label [] [ text "Password" ]
89 , label [] [ text "Password" ]
90+ , input
91+ [ Attr.type_ "password"
92+ , Attr.value model.password
78− , input [ Attr.type_ "password", Attr.value model.password, Ev.onInput UpdatePassword ] []
93+ , Ev.onInput UpdatePassword
94+ , Attr.attribute "autocomplete" "current-password"
95+ ]
96+ []
79 ]
97 ]
80 , alignRight [ button [ Ev.onClick Submit ] [ text "Log in" ] ]
98 , alignRight [ button [ Ev.onClick Submit ] [ text "Log in" ] ]
81 ]
99 ]
frontend/Pages/MasterPassword.elm+7−1
@@ -79,7 +79,13 @@ view model =
79 ]
79 ]
80 , form []
80 , form []
81 [ label [] [ text "Password" ]
81 [ label [] [ text "Password" ]
82+ , input
83+ [ Attr.type_ "password"
84+ , Attr.value model.password
82− , input [ Attr.type_ "password", Attr.value model.password, Ev.onInput UpdatePassword ] []
85+ , Ev.onInput UpdatePassword
86+ , Attr.attribute "autocomplete" "current-password"
87+ ]
88+ []
83 ]
89 ]
84 , alignRight [ button [ Ev.onClick Submit ] [ text "Log in" ] ]
90 , alignRight [ button [ Ev.onClick Submit ] [ text "Log in" ] ]
85 ]
91 ]
frontend/Search.elmnew file+182
@@ -0,0 +1,182 @@
1+module Search exposing
2+ ( Query
3+ , Search(..)
4+ , SearchResult
5+ , combine
6+ , contramap
7+ , search
8+ , searchList
9+ )
10+
11+import Set exposing (Set)
12+
13+
14+ignoredCharacters : Set Char
15+ignoredCharacters =
16+ Set.fromList [ '_', '-', '.' ]
17+
18+
19+prepareString : String -> String
20+prepareString =
21+ String.filter (\c -> Set.member c ignoredCharacters |> not)
22+ >> String.toLower
23+
24+
25+type SearchResult
26+ = Match Int
27+ | NoMatch {- | A search result for an empty query. It is for the most part just ignored, hance the name. -}
28+ | Ignore
29+
30+
31+{-| Used to combine different search results within one search entry.
32+-}
33+combine : List SearchResult -> SearchResult
34+combine ress =
35+ case ress of
36+ [] ->
37+ NoMatch
38+
39+ _ ->
40+ if List.all ((==) Ignore) ress then
41+ Ignore
42+
43+ else
44+ let
45+ go : List SearchResult -> SearchResult
46+ go rr =
47+ case rr of
48+ [] ->
49+ NoMatch
50+
51+ NoMatch :: xs ->
52+ go xs
53+
54+ (Match score) :: xs ->
55+ case go xs of
56+ NoMatch ->
57+ Match score
58+
59+ Match score_ ->
60+ Match (score + score_)
61+
62+ Ignore ->
63+ Match score
64+
65+ Ignore :: xs ->
66+ go xs
67+ in
68+ go ress
69+
70+
71+type alias Haystack =
72+ String
73+
74+
75+type alias Needle =
76+ String
77+
78+
79+type Query
80+ = Query Needle
81+
82+
83+{-| Constructs queries from an input string. (Splits on spaces and requires all words to match)
84+-}
85+constructQueries : String -> List Query
86+constructQueries =
87+ prepareString >> String.words >> List.filter (String.isEmpty >> not) >> List.map Query
88+
89+
90+{-| Search a search term within a search entry.
91+-}
92+search : Query -> Haystack -> SearchResult
93+search (Query needle) haystack =
94+ if String.contains needle (prepareString haystack) then
95+ Match 1
96+
97+ else
98+ NoMatch
99+
100+
101+{-| Requires all search results to be matched
102+-}
103+searchAll : List SearchResult -> SearchResult
104+searchAll =
105+ let
106+ combineQueries : SearchResult -> SearchResult -> SearchResult
107+ combineQueries x y =
108+ case ( x, y ) of
109+ ( NoMatch, _ ) ->
110+ NoMatch
111+
112+ ( _, NoMatch ) ->
113+ NoMatch
114+
115+ ( Ignore, a ) ->
116+ a
117+
118+ ( a, Ignore ) ->
119+ a
120+
121+ ( Match a, Match b ) ->
122+ Match (a + b)
123+ in
124+ List.foldl combineQueries Ignore
125+
126+
127+{-| A list of search inputs with the definition of what it means to search for that query.
128+-}
129+type Search a
130+ = Search (List ( String, Query -> a -> SearchResult ))
131+
132+
133+contramap : (b -> a) -> Search a -> Search b
134+contramap f (Search s) =
135+ List.map (\( q, g ) -> ( q, \query b -> g query (f b) )) s
136+ |> Search
137+
138+
139+{-| The main function of the module. Search a list with the given `Search`.
140+-}
141+searchList : Search a -> List a -> List a
142+searchList (Search searches) aa =
143+ let
144+ searchByTerm =
145+ searches
146+ |> List.concatMap
147+ (\( s, f ) ->
148+ let
149+ qq =
150+ constructQueries s
151+ in
152+ List.map (\q a -> f q a) qq
153+ )
154+
155+ searchEntry a =
156+ List.map (\f -> f a) searchByTerm |> searchAll
157+ in
158+ aa
159+ |> List.map (\a -> ( searchEntry a, a ))
160+ |> sortResult Tuple.first
161+ |> List.map Tuple.second
162+
163+
164+sortResult : (a -> SearchResult) -> List a -> List a
165+sortResult f =
166+ List.filterMap
167+ (\a ->
168+ case f a of
169+ Match score ->
170+ Just ( score, a )
171+
172+ NoMatch ->
173+ Nothing
174+
175+ Ignore ->
176+ Just ( 0, a )
177+ )
178+ -- Here we assume that the sort is stable which is not true for old
179+ -- versions of ECMAScript which would result in undeterministic
180+ -- ordering for empty queries.
181+ >> List.sortBy Tuple.first
182+ >> List.map Tuple.second