Refactor the loading and request management (#230)
This commit is contained in:
parent
ecf71f5932
commit
060daed760
154
src/Main.elm
154
src/Main.elm
|
@ -108,50 +108,68 @@ updateWith toPage toMsg model ( subModel, subCmd ) =
|
|||
)
|
||||
|
||||
|
||||
submitQuery :
|
||||
Model
|
||||
-> ( Model, Cmd Msg )
|
||||
-> ( Model, Cmd Msg )
|
||||
submitQuery old ( new, cmd ) =
|
||||
attemptQuery : ( Model, Cmd Msg ) -> ( Model, Cmd Msg )
|
||||
attemptQuery (( model, _ ) as pair) =
|
||||
let
|
||||
triggerSearch _ newModel msg makeRequest =
|
||||
if
|
||||
(newModel.query /= Nothing)
|
||||
&& (newModel.query /= Just "")
|
||||
&& List.member newModel.channel Search.channels
|
||||
then
|
||||
( new
|
||||
, Cmd.batch
|
||||
[ cmd
|
||||
, makeRequest
|
||||
new.elasticsearch
|
||||
newModel.channel
|
||||
(Maybe.withDefault "" newModel.query)
|
||||
newModel.from
|
||||
newModel.size
|
||||
newModel.sort
|
||||
|> Cmd.map msg
|
||||
]
|
||||
-- We intentially throw away Cmd
|
||||
-- because we don't want to perform any effects
|
||||
-- in this cases where route itself doesn't change
|
||||
noEffects =
|
||||
Tuple.mapSecond (always Cmd.none)
|
||||
|
||||
submitQuery msg makeRequest searchModel =
|
||||
Tuple.mapSecond
|
||||
(\cmd ->
|
||||
Cmd.batch
|
||||
[ cmd
|
||||
, Cmd.map msg <|
|
||||
makeRequest
|
||||
model.elasticsearch
|
||||
searchModel.channel
|
||||
(Maybe.withDefault "" searchModel.query)
|
||||
searchModel.from
|
||||
searchModel.size
|
||||
searchModel.sort
|
||||
]
|
||||
)
|
||||
pair
|
||||
in
|
||||
case model.page of
|
||||
Packages searchModel ->
|
||||
if Search.shouldLoad searchModel then
|
||||
submitQuery PackagesMsg Page.Packages.makeRequest searchModel
|
||||
|
||||
else
|
||||
( new, cmd )
|
||||
in
|
||||
case ( old.page, new.page ) of
|
||||
( Packages oldModel, Packages newModel ) ->
|
||||
triggerSearch oldModel newModel PackagesMsg Page.Packages.makeRequest
|
||||
noEffects pair
|
||||
|
||||
( NotFound, Packages newModel ) ->
|
||||
triggerSearch newModel newModel PackagesMsg Page.Packages.makeRequest
|
||||
Options searchModel ->
|
||||
if Search.shouldLoad searchModel then
|
||||
submitQuery OptionsMsg Page.Options.makeRequest searchModel
|
||||
|
||||
( Options oldModel, Options newModel ) ->
|
||||
triggerSearch oldModel newModel OptionsMsg Page.Options.makeRequest
|
||||
else
|
||||
noEffects pair
|
||||
|
||||
( NotFound, Options newModel ) ->
|
||||
triggerSearch newModel newModel OptionsMsg Page.Options.makeRequest
|
||||
_ ->
|
||||
pair
|
||||
|
||||
( _, _ ) ->
|
||||
( new, cmd )
|
||||
|
||||
pageMatch : Page -> Page -> Bool
|
||||
pageMatch m1 m2 =
|
||||
case ( m1, m2 ) of
|
||||
( NotFound, NotFound ) ->
|
||||
True
|
||||
|
||||
( Home _, Home _ ) ->
|
||||
True
|
||||
|
||||
( Packages _, Packages _ ) ->
|
||||
True
|
||||
|
||||
( Options _, Options _ ) ->
|
||||
True
|
||||
|
||||
_ ->
|
||||
False
|
||||
|
||||
|
||||
changeRouteTo :
|
||||
|
@ -159,49 +177,6 @@ changeRouteTo :
|
|||
-> Url.Url
|
||||
-> ( Model, Cmd Msg )
|
||||
changeRouteTo currentModel url =
|
||||
let
|
||||
attempteQuery ( newModel, cmd ) =
|
||||
let
|
||||
-- We intentially throw away Cmd
|
||||
-- because we don't want to perform any effects
|
||||
-- in this cases where route itself doesn't change
|
||||
noEffects =
|
||||
( newModel, Cmd.none )
|
||||
in
|
||||
case ( currentModel.route, newModel.route ) of
|
||||
( Route.Packages arg1, Route.Packages arg2 ) ->
|
||||
if
|
||||
(arg1.channel /= arg2.channel)
|
||||
|| (arg1.query /= arg2.query)
|
||||
|| (arg1.from /= arg2.from)
|
||||
|| (arg1.size /= arg2.size)
|
||||
|| (arg1.sort /= arg2.sort)
|
||||
then
|
||||
submitQuery newModel ( newModel, cmd )
|
||||
|
||||
else
|
||||
noEffects
|
||||
|
||||
( Route.Options arg1, Route.Options arg2 ) ->
|
||||
if
|
||||
(arg1.channel /= arg2.channel)
|
||||
|| (arg1.query /= arg2.query)
|
||||
|| (arg1.from /= arg2.from)
|
||||
|| (arg1.size /= arg2.size)
|
||||
|| (arg1.sort /= arg2.sort)
|
||||
then
|
||||
submitQuery newModel ( newModel, cmd )
|
||||
|
||||
else
|
||||
noEffects
|
||||
|
||||
( a, b ) ->
|
||||
if a /= b then
|
||||
submitQuery newModel ( newModel, cmd )
|
||||
|
||||
else
|
||||
noEffects
|
||||
in
|
||||
case Route.fromUrl url of
|
||||
Nothing ->
|
||||
( { currentModel | page = NotFound }
|
||||
|
@ -212,6 +187,13 @@ changeRouteTo currentModel url =
|
|||
let
|
||||
model =
|
||||
{ currentModel | route = route }
|
||||
|
||||
avoidReinit ( newModel, cmd ) =
|
||||
if pageMatch currentModel.page newModel.page then
|
||||
( model, Cmd.none )
|
||||
|
||||
else
|
||||
( newModel, cmd )
|
||||
in
|
||||
case route of
|
||||
Route.NotFound ->
|
||||
|
@ -234,7 +216,8 @@ changeRouteTo currentModel url =
|
|||
in
|
||||
Page.Packages.init searchArgs modelPage
|
||||
|> updateWith Packages PackagesMsg model
|
||||
|> attempteQuery
|
||||
|> avoidReinit
|
||||
|> attemptQuery
|
||||
|
||||
Route.Options searchArgs ->
|
||||
let
|
||||
|
@ -248,7 +231,8 @@ changeRouteTo currentModel url =
|
|||
in
|
||||
Page.Options.init searchArgs modelPage
|
||||
|> updateWith Options OptionsMsg model
|
||||
|> attempteQuery
|
||||
|> avoidReinit
|
||||
|> attemptQuery
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
|
@ -263,7 +247,13 @@ update msg model =
|
|||
|
||||
Browser.External href ->
|
||||
( model
|
||||
, Browser.Navigation.load href
|
||||
, case href of
|
||||
-- ignore links with no `href` attribute
|
||||
"" ->
|
||||
Cmd.none
|
||||
|
||||
_ ->
|
||||
Browser.Navigation.load href
|
||||
)
|
||||
|
||||
( ChangedUrl url, _ ) ->
|
||||
|
|
262
src/Search.elm
262
src/Search.elm
|
@ -12,6 +12,7 @@ module Search exposing
|
|||
, init
|
||||
, makeRequest
|
||||
, makeRequestBody
|
||||
, shouldLoad
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
@ -148,10 +149,25 @@ init args model =
|
|||
|> fromSortId
|
||||
|> Maybe.withDefault Relevance
|
||||
}
|
||||
|> ensureLoading
|
||||
, Browser.Dom.focus "search-query-input" |> Task.attempt (\_ -> NoOp)
|
||||
)
|
||||
|
||||
|
||||
shouldLoad : Model a -> Bool
|
||||
shouldLoad model =
|
||||
model.result == RemoteData.Loading
|
||||
|
||||
|
||||
ensureLoading : Model a -> Model a
|
||||
ensureLoading model =
|
||||
if model.query /= Nothing && model.query /= Just "" && List.member model.channel channels then
|
||||
{ model | result = RemoteData.Loading }
|
||||
|
||||
else
|
||||
model
|
||||
|
||||
|
||||
|
||||
-- ---------------------------
|
||||
-- UPDATE
|
||||
|
@ -166,6 +182,7 @@ type Msg a
|
|||
| QueryInputSubmit
|
||||
| QueryResponse (RemoteData.WebData (SearchResult a))
|
||||
| ShowDetails String
|
||||
| ChangePage Int
|
||||
|
||||
|
||||
update :
|
||||
|
@ -186,19 +203,15 @@ update toRoute navKey msg model =
|
|||
| sort = fromSortId sortId |> Maybe.withDefault Relevance
|
||||
, from = 0
|
||||
}
|
||||
|> ensureLoading
|
||||
|> pushUrl toRoute navKey
|
||||
|
||||
ChannelChange channel ->
|
||||
{ model
|
||||
| channel = channel
|
||||
, result =
|
||||
if model.query == Nothing || model.query == Just "" then
|
||||
RemoteData.NotAsked
|
||||
|
||||
else
|
||||
RemoteData.Loading
|
||||
, from = 0
|
||||
}
|
||||
|> ensureLoading
|
||||
|> pushUrl toRoute navKey
|
||||
|
||||
QueryInput query ->
|
||||
|
@ -207,10 +220,8 @@ update toRoute navKey msg model =
|
|||
)
|
||||
|
||||
QueryInputSubmit ->
|
||||
{ model
|
||||
| result = RemoteData.Loading
|
||||
, from = 0
|
||||
}
|
||||
{ model | from = 0 }
|
||||
|> ensureLoading
|
||||
|> pushUrl toRoute navKey
|
||||
|
||||
QueryResponse result ->
|
||||
|
@ -229,6 +240,11 @@ update toRoute navKey msg model =
|
|||
}
|
||||
|> pushUrl toRoute navKey
|
||||
|
||||
ChangePage from ->
|
||||
{ model | from = from }
|
||||
|> ensureLoading
|
||||
|> pushUrl toRoute navKey
|
||||
|
||||
|
||||
pushUrl : Route.SearchRoute -> Browser.Navigation.Key -> Model a -> ( Model a, Cmd msg )
|
||||
pushUrl toRoute navKey model =
|
||||
|
@ -487,20 +503,26 @@ view { toRoute, categoryName } title model viewSuccess outMsg =
|
|||
]
|
||||
]
|
||||
]
|
||||
, case model.result of
|
||||
RemoteData.NotAsked ->
|
||||
div [] [ text "" ]
|
||||
, div [] <|
|
||||
case model.result of
|
||||
RemoteData.NotAsked ->
|
||||
[ text "" ]
|
||||
|
||||
RemoteData.Loading ->
|
||||
div [ class "loader" ] [ text "Loading..." ]
|
||||
RemoteData.Loading ->
|
||||
[ p [] [ em [] [ text "Searching..." ] ]
|
||||
, div []
|
||||
[ viewSortSelection outMsg model
|
||||
, viewPager outMsg model 0 toRoute
|
||||
]
|
||||
, div [ class "loader-wrapper" ] [ div [ class "loader" ] [ text "Loading..." ] ]
|
||||
, viewPager outMsg model 0 toRoute
|
||||
]
|
||||
|
||||
RemoteData.Success result ->
|
||||
if result.hits.total.value == 0 then
|
||||
div []
|
||||
RemoteData.Success result ->
|
||||
if result.hits.total.value == 0 then
|
||||
[ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ]
|
||||
|
||||
else
|
||||
div []
|
||||
else
|
||||
[ p []
|
||||
[ em []
|
||||
[ text
|
||||
|
@ -525,138 +547,128 @@ view { toRoute, categoryName } title model viewSuccess outMsg =
|
|||
)
|
||||
]
|
||||
]
|
||||
, form [ class "form-horizontal pull-right" ]
|
||||
[ div
|
||||
[ class "control-group"
|
||||
]
|
||||
[ label [ class "control-label" ] [ text "Sort by:" ]
|
||||
, div
|
||||
[ class "controls" ]
|
||||
[ select
|
||||
[ onInput (\x -> outMsg (SortChange x))
|
||||
]
|
||||
(List.map
|
||||
(\sort ->
|
||||
option
|
||||
[ selected (model.sort == sort)
|
||||
, value (toSortId sort)
|
||||
]
|
||||
[ text <| toSortTitle sort ]
|
||||
)
|
||||
sortBy
|
||||
)
|
||||
]
|
||||
]
|
||||
, div []
|
||||
[ viewSortSelection outMsg model
|
||||
, viewPager outMsg model result.hits.total.value toRoute
|
||||
]
|
||||
, viewPager outMsg model result toRoute
|
||||
, viewSuccess model.channel model.show result
|
||||
, viewPager outMsg model result toRoute
|
||||
, viewPager outMsg model result.hits.total.value toRoute
|
||||
]
|
||||
|
||||
RemoteData.Failure error ->
|
||||
let
|
||||
( errorTitle, errorMessage ) =
|
||||
case error of
|
||||
Http.BadUrl text ->
|
||||
( "Bad Url!", text )
|
||||
RemoteData.Failure error ->
|
||||
let
|
||||
( errorTitle, errorMessage ) =
|
||||
case error of
|
||||
Http.BadUrl text ->
|
||||
( "Bad Url!", text )
|
||||
|
||||
Http.Timeout ->
|
||||
( "Timeout!", "Request to the server timeout." )
|
||||
Http.Timeout ->
|
||||
( "Timeout!", "Request to the server timeout." )
|
||||
|
||||
Http.NetworkError ->
|
||||
( "Network Error!", "A network request bonsaisearch.net domain failed. This is either due to a content blocker or a networking issue." )
|
||||
Http.NetworkError ->
|
||||
( "Network Error!", "A network request bonsaisearch.net domain failed. This is either due to a content blocker or a networking issue." )
|
||||
|
||||
Http.BadStatus code ->
|
||||
( "Bad Status", "Server returned " ++ String.fromInt code )
|
||||
Http.BadStatus code ->
|
||||
( "Bad Status", "Server returned " ++ String.fromInt code )
|
||||
|
||||
Http.BadBody text ->
|
||||
( "Bad Body", text )
|
||||
in
|
||||
div [ class "alert alert-error" ]
|
||||
[ h4 [] [ text errorTitle ]
|
||||
, text errorMessage
|
||||
Http.BadBody text ->
|
||||
( "Bad Body", text )
|
||||
in
|
||||
[ div [ class "alert alert-error" ]
|
||||
[ h4 [] [ text errorTitle ]
|
||||
, text errorMessage
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewSortSelection : (Msg a -> b) -> Model a -> Html b
|
||||
viewSortSelection outMsg model =
|
||||
form [ class "form-horizontal pull-right sort-form" ]
|
||||
[ div [ class "control-group sort-group" ]
|
||||
[ label [ class "control-label" ] [ text "Sort by:" ]
|
||||
, div
|
||||
[ class "controls" ]
|
||||
[ select
|
||||
[ onInput (\x -> outMsg (SortChange x))
|
||||
]
|
||||
(List.map
|
||||
(\sort ->
|
||||
option
|
||||
[ selected (model.sort == sort)
|
||||
, value (toSortId sort)
|
||||
]
|
||||
[ text <| toSortTitle sort ]
|
||||
)
|
||||
sortBy
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewPager :
|
||||
(Msg a -> b)
|
||||
-> Model a
|
||||
-> SearchResult a
|
||||
-> Int
|
||||
-> Route.SearchRoute
|
||||
-> Html b
|
||||
viewPager _ model result toRoute =
|
||||
ul [ class "pager" ]
|
||||
[ li
|
||||
[ classList
|
||||
[ ( "disabled", model.from == 0 )
|
||||
]
|
||||
]
|
||||
[ a
|
||||
[ href <|
|
||||
if model.from == 0 then
|
||||
""
|
||||
viewPager outMsg model total toRoute =
|
||||
Html.map outMsg <|
|
||||
ul [ class "pager" ]
|
||||
[ li [ classList [ ( "disabled", model.from == 0 ) ] ]
|
||||
[ a
|
||||
[ Html.Events.onClick <|
|
||||
if model.from == 0 then
|
||||
NoOp
|
||||
|
||||
else
|
||||
createUrl toRoute { model | from = 0 }
|
||||
else
|
||||
ChangePage 0
|
||||
]
|
||||
[ text "First" ]
|
||||
]
|
||||
[ text "First" ]
|
||||
]
|
||||
, li
|
||||
[ classList
|
||||
[ ( "disabled", model.from == 0 )
|
||||
]
|
||||
]
|
||||
[ a
|
||||
[ href <|
|
||||
if model.from - model.size < 0 then
|
||||
""
|
||||
, li [ classList [ ( "disabled", model.from == 0 ) ] ]
|
||||
[ a
|
||||
[ Html.Events.onClick <|
|
||||
if model.from - model.size < 0 then
|
||||
NoOp
|
||||
|
||||
else
|
||||
createUrl toRoute { model | from = model.from - model.size }
|
||||
else
|
||||
ChangePage <| model.from - model.size
|
||||
]
|
||||
[ text "Previous" ]
|
||||
]
|
||||
[ text "Previous" ]
|
||||
]
|
||||
, li
|
||||
[ classList
|
||||
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
||||
]
|
||||
]
|
||||
[ a
|
||||
[ href <|
|
||||
if model.from + model.size >= result.hits.total.value then
|
||||
""
|
||||
, li [ classList [ ( "disabled", model.from + model.size >= total ) ] ]
|
||||
[ a
|
||||
[ Html.Events.onClick <|
|
||||
if model.from + model.size >= total then
|
||||
NoOp
|
||||
|
||||
else
|
||||
createUrl toRoute { model | from = model.from + model.size }
|
||||
else
|
||||
ChangePage <| model.from + model.size
|
||||
]
|
||||
[ text "Next" ]
|
||||
]
|
||||
[ text "Next" ]
|
||||
]
|
||||
, li
|
||||
[ classList
|
||||
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
||||
]
|
||||
]
|
||||
[ a
|
||||
[ href <|
|
||||
if model.from + model.size >= result.hits.total.value then
|
||||
""
|
||||
, li [ classList [ ( "disabled", model.from + model.size >= total ) ] ]
|
||||
[ a
|
||||
[ Html.Events.onClick <|
|
||||
if model.from + model.size >= total then
|
||||
NoOp
|
||||
|
||||
else
|
||||
let
|
||||
remainder =
|
||||
if remainderBy model.size result.hits.total.value == 0 then
|
||||
1
|
||||
else
|
||||
let
|
||||
remainder =
|
||||
if remainderBy model.size total == 0 then
|
||||
1
|
||||
|
||||
else
|
||||
0
|
||||
in
|
||||
createUrl toRoute
|
||||
{ model | from = ((result.hits.total.value // model.size) - remainder) * model.size }
|
||||
else
|
||||
0
|
||||
in
|
||||
ChangePage <| ((total // model.size) - remainder) * model.size
|
||||
]
|
||||
[ text "Last" ]
|
||||
]
|
||||
[ text "Last" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
body {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#content {
|
||||
|
@ -89,6 +90,21 @@ header .navbar.navbar-static-top {
|
|||
}
|
||||
}
|
||||
|
||||
.sort-form,
|
||||
.sort-form > .sort-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.pager {
|
||||
& > li > a {
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.loader-wrapper {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loader,
|
||||
.loader:before,
|
||||
.loader:after {
|
||||
|
|
Loading…
Reference in a new issue