Support search engine escaping of spaces (+) (#215)
This commit is contained in:
parent
adbb224490
commit
e412085ea8
|
@ -1,7 +1,5 @@
|
|||
module Main exposing (main)
|
||||
|
||||
--exposing (UrlRequest(..))
|
||||
|
||||
import Browser
|
||||
import Browser.Navigation
|
||||
import Html
|
||||
|
|
|
@ -43,6 +43,7 @@ import Html.Parser.Util
|
|||
import Json.Decode
|
||||
import Json.Encode
|
||||
import Regex
|
||||
import Route
|
||||
import Search
|
||||
|
||||
|
||||
|
@ -102,7 +103,7 @@ update navKey msg model =
|
|||
let
|
||||
( newModel, newCmd ) =
|
||||
Search.update
|
||||
"options"
|
||||
Route.Options
|
||||
navKey
|
||||
subMsg
|
||||
model
|
||||
|
@ -116,8 +117,7 @@ update navKey msg model =
|
|||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Search.view
|
||||
"options"
|
||||
Search.view { toRoute = Route.Options, categoryName = "options" }
|
||||
"Search NixOS options"
|
||||
model
|
||||
viewSuccess
|
||||
|
|
|
@ -43,6 +43,7 @@ import Json.Decode
|
|||
import Json.Decode.Pipeline
|
||||
import Json.Encode
|
||||
import Regex
|
||||
import Route
|
||||
import Search
|
||||
|
||||
|
||||
|
@ -139,7 +140,7 @@ update navKey msg model =
|
|||
let
|
||||
( newModel, newCmd ) =
|
||||
Search.update
|
||||
"packages"
|
||||
Route.Packages
|
||||
navKey
|
||||
subMsg
|
||||
model
|
||||
|
@ -153,8 +154,7 @@ update navKey msg model =
|
|||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
Search.view
|
||||
"packages"
|
||||
Search.view { toRoute = Route.Packages, categoryName = "packages" }
|
||||
"Search NixOS packages"
|
||||
model
|
||||
viewSuccess
|
||||
|
|
118
src/Route.elm
118
src/Route.elm
|
@ -1,9 +1,11 @@
|
|||
module Route exposing (Route(..), fromUrl, href, replaceUrl)
|
||||
module Route exposing (Route(..), fromUrl, href, replaceUrl, routeToString)
|
||||
|
||||
import Browser.Navigation
|
||||
import Html
|
||||
import Html.Attributes
|
||||
import Route.SearchQuery
|
||||
import Url
|
||||
import Url.Builder exposing (QueryParameter)
|
||||
import Url.Parser exposing ((<?>))
|
||||
import Url.Parser.Query
|
||||
|
||||
|
@ -19,30 +21,32 @@ type Route
|
|||
| Options (Maybe String) (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) (Maybe String)
|
||||
|
||||
|
||||
parser : Url.Parser.Parser (Route -> msg) msg
|
||||
parser =
|
||||
parser : Url.Url -> Url.Parser.Parser (Route -> msg) msg
|
||||
parser url =
|
||||
let
|
||||
rawQuery =
|
||||
Route.SearchQuery.toRawQuery url
|
||||
|
||||
withSearchQuery : (a -> Maybe String -> b) -> a -> b
|
||||
withSearchQuery f channel =
|
||||
f channel <|
|
||||
Maybe.andThen Route.SearchQuery.searchQueryToString <|
|
||||
Maybe.andThen (Route.SearchQuery.searchString "query") rawQuery
|
||||
in
|
||||
Url.Parser.oneOf
|
||||
[ Url.Parser.map
|
||||
Home
|
||||
Url.Parser.top
|
||||
, Url.Parser.map
|
||||
NotFound
|
||||
(Url.Parser.s "not-found")
|
||||
, Url.Parser.map
|
||||
Packages
|
||||
[ Url.Parser.map Home Url.Parser.top
|
||||
, Url.Parser.map NotFound (Url.Parser.s "not-found")
|
||||
, Url.Parser.map (withSearchQuery Packages)
|
||||
(Url.Parser.s "packages"
|
||||
<?> Url.Parser.Query.string "channel"
|
||||
<?> Url.Parser.Query.string "query"
|
||||
<?> Url.Parser.Query.string "show"
|
||||
<?> Url.Parser.Query.int "from"
|
||||
<?> Url.Parser.Query.int "size"
|
||||
<?> Url.Parser.Query.string "sort"
|
||||
)
|
||||
, Url.Parser.map
|
||||
Options
|
||||
, Url.Parser.map (withSearchQuery Options)
|
||||
(Url.Parser.s "options"
|
||||
<?> Url.Parser.Query.string "channel"
|
||||
<?> Url.Parser.Query.string "query"
|
||||
<?> Url.Parser.Query.string "show"
|
||||
<?> Url.Parser.Query.int "from"
|
||||
<?> Url.Parser.Query.int "size"
|
||||
|
@ -71,7 +75,7 @@ fromUrl url =
|
|||
-- This makes it *literally* the path, so we can proceed
|
||||
-- with parsing as if it had been a normal path all along.
|
||||
--{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
|
||||
Url.Parser.parse parser url
|
||||
Url.Parser.parse (parser url) url
|
||||
|
||||
|
||||
|
||||
|
@ -79,41 +83,63 @@ fromUrl url =
|
|||
|
||||
|
||||
routeToString : Route -> String
|
||||
routeToString page =
|
||||
routeToString =
|
||||
let
|
||||
( path, query ) =
|
||||
routeToPieces page
|
||||
buildString ( path, query, searchQuery ) =
|
||||
Route.SearchQuery.absolute path query <|
|
||||
Maybe.withDefault [] <|
|
||||
Maybe.map List.singleton searchQuery
|
||||
in
|
||||
"/" ++ String.join "/" path ++ "?" ++ String.join "&" (List.filterMap Basics.identity query)
|
||||
buildString << routeToPieces
|
||||
|
||||
|
||||
routeToPieces : Route -> ( List String, List (Maybe String) )
|
||||
routeToPieces : Route -> ( List String, List QueryParameter, Maybe ( String, Route.SearchQuery.SearchQuery ) )
|
||||
routeToPieces page =
|
||||
case page of
|
||||
Home ->
|
||||
( [], [] )
|
||||
let
|
||||
channelQ =
|
||||
Maybe.map (Url.Builder.string "channel")
|
||||
|
||||
NotFound ->
|
||||
( [ "not-found" ], [] )
|
||||
queryQ =
|
||||
Maybe.map (Route.SearchQuery.toSearchQuery "query")
|
||||
|
||||
Packages channel query show from size sort ->
|
||||
( [ "packages" ]
|
||||
, [ channel
|
||||
, query
|
||||
, show
|
||||
, Maybe.map String.fromInt from
|
||||
, Maybe.map String.fromInt size
|
||||
, sort
|
||||
]
|
||||
)
|
||||
showQ =
|
||||
Maybe.map (Url.Builder.string "show")
|
||||
|
||||
Options channel query show from size sort ->
|
||||
( [ "options" ]
|
||||
, [ channel
|
||||
, query
|
||||
, show
|
||||
, Maybe.map String.fromInt from
|
||||
, Maybe.map String.fromInt size
|
||||
, sort
|
||||
]
|
||||
)
|
||||
fromQ =
|
||||
Maybe.map (Url.Builder.int "from")
|
||||
|
||||
sizeQ =
|
||||
Maybe.map (Url.Builder.int "size")
|
||||
|
||||
sortQ =
|
||||
Maybe.map (Url.Builder.string "sort")
|
||||
in
|
||||
(\( path, urlQ, searchQuery ) -> ( path, List.filterMap identity urlQ, searchQuery )) <|
|
||||
case page of
|
||||
Home ->
|
||||
( [], [], Nothing )
|
||||
|
||||
NotFound ->
|
||||
( [ "not-found" ], [], Nothing )
|
||||
|
||||
Packages channel query show from size sort ->
|
||||
( [ "packages" ]
|
||||
, [ channelQ channel
|
||||
, showQ show
|
||||
, fromQ from
|
||||
, sizeQ size
|
||||
, sortQ sort
|
||||
]
|
||||
, queryQ query
|
||||
)
|
||||
|
||||
Options channel query show from size sort ->
|
||||
( [ "options" ]
|
||||
, [ channelQ channel
|
||||
, showQ show
|
||||
, fromQ from
|
||||
, sizeQ size
|
||||
, sortQ sort
|
||||
]
|
||||
, queryQ query
|
||||
)
|
||||
|
|
87
src/Route/SearchQuery.elm
Normal file
87
src/Route/SearchQuery.elm
Normal file
|
@ -0,0 +1,87 @@
|
|||
module Route.SearchQuery exposing
|
||||
( RawQuery
|
||||
, SearchQuery
|
||||
, absolute
|
||||
, searchQueryToString
|
||||
, searchString
|
||||
, toRawQuery
|
||||
, toSearchQuery
|
||||
)
|
||||
|
||||
import Dict exposing (Dict)
|
||||
import Url
|
||||
import Url.Builder
|
||||
|
||||
|
||||
|
||||
-- RawQuery
|
||||
|
||||
|
||||
type RawQuery
|
||||
= RawQuery (Dict String String)
|
||||
|
||||
|
||||
chunk : String -> String -> Maybe ( String, String )
|
||||
chunk sep str =
|
||||
case String.split sep str of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
[ key ] ->
|
||||
Just ( key, "" )
|
||||
|
||||
key :: xs ->
|
||||
Just ( key, String.join sep xs )
|
||||
|
||||
|
||||
toRawQuery : Url.Url -> Maybe RawQuery
|
||||
toRawQuery =
|
||||
Maybe.map (RawQuery << Dict.fromList << List.filterMap (chunk "=") << String.split "&")
|
||||
<< .query
|
||||
|
||||
|
||||
|
||||
-- SearchQuery
|
||||
|
||||
|
||||
{-| This is type safe wrapper for working with search queries in url
|
||||
-}
|
||||
type SearchQuery
|
||||
= SearchQuery String
|
||||
|
||||
|
||||
searchString : String -> RawQuery -> Maybe SearchQuery
|
||||
searchString name (RawQuery dict) =
|
||||
Maybe.map SearchQuery <| Dict.get name dict
|
||||
|
||||
|
||||
searchQueryToString : SearchQuery -> Maybe String
|
||||
searchQueryToString (SearchQuery str) =
|
||||
Url.percentDecode <| String.replace "+" "%20" str
|
||||
|
||||
|
||||
toSearchQuery : String -> String -> ( String, SearchQuery )
|
||||
toSearchQuery name query =
|
||||
( name, SearchQuery <| String.replace "%20" "+" <| Url.percentEncode query )
|
||||
|
||||
|
||||
{-| Build absolute URL with support for search query strings
|
||||
-}
|
||||
absolute : List String -> List Url.Builder.QueryParameter -> List ( String, SearchQuery ) -> String
|
||||
absolute path query searchQuery =
|
||||
let
|
||||
searchStrings =
|
||||
List.map (\( name, SearchQuery val ) -> name ++ "=" ++ val) searchQuery
|
||||
|> String.join "&"
|
||||
in
|
||||
Url.Builder.absolute path query
|
||||
|> (\str ->
|
||||
str
|
||||
++ (case query of
|
||||
[] ->
|
||||
"?" ++ searchStrings
|
||||
|
||||
_ ->
|
||||
"&" ++ searchStrings
|
||||
)
|
||||
)
|
|
@ -62,11 +62,23 @@ import Http
|
|||
import Json.Decode
|
||||
import Json.Encode
|
||||
import RemoteData
|
||||
import Route exposing (Route)
|
||||
import Set
|
||||
import Task
|
||||
import Url
|
||||
import Url.Builder
|
||||
|
||||
|
||||
type alias SearchRoute =
|
||||
Maybe String
|
||||
-> Maybe String
|
||||
-> Maybe String
|
||||
-> Maybe Int
|
||||
-> Maybe Int
|
||||
-> Maybe String
|
||||
-> Route
|
||||
|
||||
|
||||
type alias Model a =
|
||||
{ channel : String
|
||||
, query : Maybe String
|
||||
|
@ -174,12 +186,12 @@ type Msg a
|
|||
|
||||
|
||||
update :
|
||||
String
|
||||
SearchRoute
|
||||
-> Browser.Navigation.Key
|
||||
-> Msg a
|
||||
-> Model a
|
||||
-> ( Model a, Cmd (Msg a) )
|
||||
update path navKey msg model =
|
||||
update toRoute navKey msg model =
|
||||
case msg of
|
||||
NoOp ->
|
||||
( model
|
||||
|
@ -193,7 +205,7 @@ update path navKey msg model =
|
|||
in
|
||||
( { model | sort = sort }
|
||||
, createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -218,7 +230,7 @@ update path navKey msg model =
|
|||
|
||||
else
|
||||
createUrl
|
||||
path
|
||||
toRoute
|
||||
channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -240,7 +252,7 @@ update path navKey msg model =
|
|||
else
|
||||
( { model | result = RemoteData.Loading }
|
||||
, createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -258,7 +270,7 @@ update path navKey msg model =
|
|||
ShowDetails selected ->
|
||||
( model
|
||||
, createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
(if model.show == Just selected then
|
||||
|
@ -275,8 +287,10 @@ update path navKey msg model =
|
|||
)
|
||||
|
||||
|
||||
{-| TODO: Sort should be part of Route type
|
||||
-}
|
||||
createUrl :
|
||||
String
|
||||
SearchRoute
|
||||
-> String
|
||||
-> Maybe String
|
||||
-> Maybe String
|
||||
|
@ -284,30 +298,9 @@ createUrl :
|
|||
-> Int
|
||||
-> Sort
|
||||
-> String
|
||||
createUrl path channel query show from size sort =
|
||||
[ Url.Builder.int "from" from
|
||||
, Url.Builder.int "size" size
|
||||
, Url.Builder.string "sort" <| toSortId sort
|
||||
, Url.Builder.string "channel" channel
|
||||
]
|
||||
|> List.append
|
||||
(query
|
||||
|> Maybe.map
|
||||
(\x ->
|
||||
[ Url.Builder.string "query" x ]
|
||||
)
|
||||
|> Maybe.withDefault []
|
||||
)
|
||||
|> List.append
|
||||
(show
|
||||
|> Maybe.map
|
||||
(\x ->
|
||||
[ Url.Builder.string "show" x
|
||||
]
|
||||
)
|
||||
|> Maybe.withDefault []
|
||||
)
|
||||
|> Url.Builder.absolute [ path ]
|
||||
createUrl toRoute channel query show from size sort =
|
||||
toRoute (Just channel) query show (Just from) (Just size) (Just <| toSortId sort)
|
||||
|> Route.routeToString
|
||||
|
||||
|
||||
|
||||
|
@ -320,6 +313,7 @@ type Channel
|
|||
| Release_20_03
|
||||
| Release_20_09
|
||||
|
||||
|
||||
type alias ChannelDetails =
|
||||
{ id : String
|
||||
, title : String
|
||||
|
@ -455,13 +449,15 @@ fromSortId id =
|
|||
|
||||
|
||||
view :
|
||||
String
|
||||
{ toRoute : SearchRoute
|
||||
, categoryName : String
|
||||
}
|
||||
-> String
|
||||
-> Model a
|
||||
-> (String -> Maybe String -> SearchResult a -> Html b)
|
||||
-> (Msg a -> b)
|
||||
-> Html b
|
||||
view path title model viewSuccess outMsg =
|
||||
view { toRoute, categoryName } title model viewSuccess outMsg =
|
||||
div
|
||||
[ class "search-page"
|
||||
]
|
||||
|
@ -520,14 +516,15 @@ view path title model viewSuccess outMsg =
|
|||
[ type_ "text"
|
||||
, id "search-query-input"
|
||||
, autofocus True
|
||||
, placeholder <| "Search for " ++ path
|
||||
, onInput (\x -> outMsg (QueryInput x))
|
||||
, placeholder <| "Search for " ++ categoryName
|
||||
, onInput (outMsg << QueryInput)
|
||||
, value <| Maybe.withDefault "" model.query
|
||||
]
|
||||
[]
|
||||
, div [ class "loader" ] []
|
||||
, div [ class "btn-group" ]
|
||||
[ button [ class "btn" ] [ text "Search" ]
|
||||
[ button [ class "btn", type_ "submit" ]
|
||||
[ text "Search" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -542,8 +539,7 @@ view path title model viewSuccess outMsg =
|
|||
RemoteData.Success result ->
|
||||
if result.hits.total.value == 0 then
|
||||
div []
|
||||
[ h4 [] [ text <| "No " ++ path ++ " found!" ]
|
||||
]
|
||||
[ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ]
|
||||
|
||||
else
|
||||
div []
|
||||
|
@ -594,9 +590,9 @@ view path title model viewSuccess outMsg =
|
|||
]
|
||||
]
|
||||
]
|
||||
, viewPager outMsg model result path
|
||||
, viewPager outMsg model result toRoute
|
||||
, viewSuccess model.channel model.show result
|
||||
, viewPager outMsg model result path
|
||||
, viewPager outMsg model result toRoute
|
||||
]
|
||||
|
||||
RemoteData.Failure error ->
|
||||
|
@ -629,9 +625,9 @@ viewPager :
|
|||
(Msg a -> b)
|
||||
-> Model a
|
||||
-> SearchResult a
|
||||
-> String
|
||||
-> SearchRoute
|
||||
-> Html b
|
||||
viewPager _ model result path =
|
||||
viewPager _ model result toRoute =
|
||||
ul [ class "pager" ]
|
||||
[ li
|
||||
[ classList
|
||||
|
@ -645,7 +641,7 @@ viewPager _ model result path =
|
|||
else
|
||||
href <|
|
||||
createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -667,7 +663,7 @@ viewPager _ model result path =
|
|||
|
||||
else
|
||||
createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -689,7 +685,7 @@ viewPager _ model result path =
|
|||
|
||||
else
|
||||
createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
@ -719,7 +715,7 @@ viewPager _ model result path =
|
|||
0
|
||||
in
|
||||
createUrl
|
||||
path
|
||||
toRoute
|
||||
model.channel
|
||||
model.query
|
||||
model.show
|
||||
|
|
Loading…
Reference in a new issue