2020-06-03 19:02:12 +00:00
|
|
|
module Search exposing
|
2020-05-08 13:24:58 +00:00
|
|
|
( Model
|
|
|
|
, Msg(..)
|
|
|
|
, Options
|
|
|
|
, ResultItem
|
2020-07-02 12:27:49 +00:00
|
|
|
, SearchResult
|
2020-07-10 07:49:43 +00:00
|
|
|
, Sort(..)
|
2020-06-09 23:53:17 +00:00
|
|
|
, channelDetailsFromId
|
2020-05-08 13:24:58 +00:00
|
|
|
, decodeResult
|
2020-07-10 07:49:43 +00:00
|
|
|
, fromSortId
|
2020-05-08 13:24:58 +00:00
|
|
|
, init
|
|
|
|
, makeRequest
|
2020-06-19 06:53:49 +00:00
|
|
|
, makeRequestBody
|
2020-05-08 13:24:58 +00:00
|
|
|
, update
|
|
|
|
, view
|
|
|
|
)
|
|
|
|
|
2020-07-02 12:27:49 +00:00
|
|
|
import Array
|
2020-05-08 13:24:58 +00:00
|
|
|
import Base64
|
2020-07-02 12:27:49 +00:00
|
|
|
import Browser.Dom
|
2020-05-08 13:24:58 +00:00
|
|
|
import Browser.Navigation
|
2020-07-02 12:27:49 +00:00
|
|
|
import Debouncer.Messages
|
|
|
|
import Dict
|
2020-05-08 13:24:58 +00:00
|
|
|
import Html
|
|
|
|
exposing
|
|
|
|
( Html
|
2020-05-11 19:56:10 +00:00
|
|
|
, a
|
2020-05-08 13:24:58 +00:00
|
|
|
, button
|
|
|
|
, div
|
2020-05-11 19:56:10 +00:00
|
|
|
, em
|
2020-05-08 13:24:58 +00:00
|
|
|
, form
|
|
|
|
, h1
|
2020-05-11 22:00:23 +00:00
|
|
|
, h4
|
2020-05-08 13:24:58 +00:00
|
|
|
, input
|
2020-07-10 07:49:43 +00:00
|
|
|
, label
|
2020-05-11 19:56:10 +00:00
|
|
|
, li
|
2020-07-10 07:49:43 +00:00
|
|
|
, option
|
2020-05-11 19:56:10 +00:00
|
|
|
, p
|
2020-07-10 07:49:43 +00:00
|
|
|
, select
|
2020-05-11 20:42:57 +00:00
|
|
|
, strong
|
2020-05-08 13:24:58 +00:00
|
|
|
, text
|
2020-05-11 19:56:10 +00:00
|
|
|
, ul
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
import Html.Attributes
|
|
|
|
exposing
|
2020-06-12 10:57:47 +00:00
|
|
|
( attribute
|
2020-07-08 14:19:08 +00:00
|
|
|
, autocomplete
|
2020-07-06 12:27:37 +00:00
|
|
|
, autofocus
|
2020-06-12 10:57:47 +00:00
|
|
|
, class
|
2020-05-11 19:56:10 +00:00
|
|
|
, classList
|
|
|
|
, href
|
2020-07-02 12:27:49 +00:00
|
|
|
, id
|
2020-07-09 16:07:07 +00:00
|
|
|
, placeholder
|
2020-07-10 07:49:43 +00:00
|
|
|
, selected
|
2020-05-08 13:24:58 +00:00
|
|
|
, type_
|
|
|
|
, value
|
|
|
|
)
|
|
|
|
import Html.Events
|
|
|
|
exposing
|
2020-05-11 19:56:10 +00:00
|
|
|
( custom
|
|
|
|
, onClick
|
|
|
|
, onInput
|
2020-05-08 13:24:58 +00:00
|
|
|
, onSubmit
|
|
|
|
)
|
|
|
|
import Http
|
|
|
|
import Json.Decode
|
|
|
|
import Json.Encode
|
2020-07-02 12:27:49 +00:00
|
|
|
import Keyboard
|
|
|
|
import Keyboard.Events
|
2020-05-08 13:24:58 +00:00
|
|
|
import RemoteData
|
2020-07-02 12:27:49 +00:00
|
|
|
import Task
|
2020-05-08 13:24:58 +00:00
|
|
|
import Url.Builder
|
|
|
|
|
|
|
|
|
|
|
|
type alias Model a =
|
2020-05-11 20:42:57 +00:00
|
|
|
{ channel : String
|
|
|
|
, query : Maybe String
|
2020-07-02 12:27:49 +00:00
|
|
|
, queryDebounce : Debouncer.Messages.Debouncer (Msg a)
|
|
|
|
, querySuggest : RemoteData.WebData (SearchResult a)
|
|
|
|
, querySelectedSuggestion : Maybe String
|
|
|
|
, result : RemoteData.WebData (SearchResult a)
|
2020-06-10 12:15:54 +00:00
|
|
|
, show : Maybe String
|
2020-05-11 19:56:10 +00:00
|
|
|
, from : Int
|
|
|
|
, size : Int
|
2020-07-10 07:49:43 +00:00
|
|
|
, sort : Sort
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-07-02 12:27:49 +00:00
|
|
|
type alias SearchResult a =
|
2020-05-08 13:24:58 +00:00
|
|
|
{ hits : ResultHits a
|
2020-07-02 12:27:49 +00:00
|
|
|
, suggest : Maybe (SearchSuggest a)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchSuggest a =
|
|
|
|
{ query : Maybe (List (SearchSuggestQuery a))
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias SearchSuggestQuery a =
|
|
|
|
{ text : String
|
|
|
|
, offset : Int
|
|
|
|
, length : Int
|
|
|
|
, options : List (ResultItem a)
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultHits a =
|
|
|
|
{ total : ResultHitsTotal
|
|
|
|
, max_score : Maybe Float
|
|
|
|
, hits : List (ResultItem a)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultHitsTotal =
|
|
|
|
{ value : Int
|
2020-07-02 12:27:49 +00:00
|
|
|
, relation : String
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultItem a =
|
|
|
|
{ index : String
|
|
|
|
, id : String
|
2020-07-10 07:49:43 +00:00
|
|
|
, score : Maybe Float
|
2020-05-08 13:24:58 +00:00
|
|
|
, source : a
|
2020-07-02 12:27:49 +00:00
|
|
|
, text : Maybe String
|
2020-06-18 10:24:52 +00:00
|
|
|
, matched_queries : Maybe (List String)
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-07-10 07:49:43 +00:00
|
|
|
type Sort
|
|
|
|
= Relevance
|
|
|
|
| AlphabeticallyAsc
|
|
|
|
| AlphabeticallyDesc
|
|
|
|
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
init :
|
|
|
|
Maybe String
|
|
|
|
-> Maybe String
|
2020-05-11 20:42:57 +00:00
|
|
|
-> Maybe String
|
2020-05-11 19:56:10 +00:00
|
|
|
-> Maybe Int
|
|
|
|
-> Maybe Int
|
2020-07-10 07:49:43 +00:00
|
|
|
-> Maybe String
|
2020-06-11 16:29:51 +00:00
|
|
|
-> Maybe (Model a)
|
2020-07-06 12:27:37 +00:00
|
|
|
-> ( Model a, Cmd (Msg a) )
|
2020-07-10 07:49:43 +00:00
|
|
|
init channel query show from size sort model =
|
2020-06-11 16:29:51 +00:00
|
|
|
let
|
|
|
|
defaultChannel =
|
|
|
|
model
|
|
|
|
|> Maybe.map (\x -> x.channel)
|
|
|
|
|> Maybe.withDefault "unstable"
|
|
|
|
|
|
|
|
defaultFrom =
|
|
|
|
model
|
|
|
|
|> Maybe.map (\x -> x.from)
|
|
|
|
|> Maybe.withDefault 0
|
|
|
|
|
|
|
|
defaultSize =
|
|
|
|
model
|
|
|
|
|> Maybe.map (\x -> x.size)
|
|
|
|
|> Maybe.withDefault 15
|
|
|
|
in
|
|
|
|
( { channel = Maybe.withDefault defaultChannel channel
|
2020-07-02 12:27:49 +00:00
|
|
|
, queryDebounce =
|
|
|
|
Debouncer.Messages.manual
|
2020-07-09 17:00:28 +00:00
|
|
|
|> Debouncer.Messages.settleWhenQuietFor (Just <| Debouncer.Messages.fromSeconds 0.4)
|
2020-07-02 12:27:49 +00:00
|
|
|
|> Debouncer.Messages.toDebouncer
|
2020-05-11 20:42:57 +00:00
|
|
|
, query = query
|
2020-07-06 12:27:37 +00:00
|
|
|
, querySuggest =
|
|
|
|
query
|
|
|
|
|> Maybe.map
|
|
|
|
(\selected ->
|
|
|
|
if String.endsWith "." selected then
|
|
|
|
model
|
|
|
|
|> Maybe.map .querySuggest
|
|
|
|
|> Maybe.withDefault RemoteData.NotAsked
|
|
|
|
|
|
|
|
else
|
|
|
|
RemoteData.NotAsked
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault RemoteData.NotAsked
|
2020-07-02 12:27:49 +00:00
|
|
|
, querySelectedSuggestion = Nothing
|
2020-06-11 16:29:51 +00:00
|
|
|
, result =
|
|
|
|
model
|
|
|
|
|> Maybe.map (\x -> x.result)
|
|
|
|
|> Maybe.withDefault RemoteData.NotAsked
|
2020-06-10 12:15:54 +00:00
|
|
|
, show = show
|
2020-06-11 16:29:51 +00:00
|
|
|
, from = Maybe.withDefault defaultFrom from
|
|
|
|
, size = Maybe.withDefault defaultSize size
|
2020-07-10 07:49:43 +00:00
|
|
|
, sort =
|
|
|
|
sort
|
|
|
|
|> Maybe.withDefault ""
|
|
|
|
|> fromSortId
|
|
|
|
|> Maybe.withDefault Relevance
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
2020-07-06 12:27:37 +00:00
|
|
|
, Browser.Dom.focus "search-query-input" |> Task.attempt (\_ -> NoOp)
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- UPDATE
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
|
|
|
type Msg a
|
2020-05-11 19:56:10 +00:00
|
|
|
= NoOp
|
2020-07-10 07:49:43 +00:00
|
|
|
| SortChange String
|
2020-05-11 20:42:57 +00:00
|
|
|
| ChannelChange String
|
2020-07-02 12:27:49 +00:00
|
|
|
| QueryInputDebounce (Debouncer.Messages.Msg (Msg a))
|
2020-05-11 19:56:10 +00:00
|
|
|
| QueryInput String
|
2020-07-02 12:27:49 +00:00
|
|
|
| QueryInputSuggestionsSubmit
|
|
|
|
| QueryInputSuggestionsResponse (RemoteData.WebData (SearchResult a))
|
2020-07-06 12:27:37 +00:00
|
|
|
| QueryInputSubmit
|
2020-07-02 12:27:49 +00:00
|
|
|
| QueryResponse (RemoteData.WebData (SearchResult a))
|
2020-05-08 13:24:58 +00:00
|
|
|
| ShowDetails String
|
2020-07-02 12:27:49 +00:00
|
|
|
| SuggestionsMoveDown
|
|
|
|
| SuggestionsMoveUp
|
|
|
|
| SuggestionsSelect
|
|
|
|
| SuggestionsClickSelect String
|
|
|
|
| SuggestionsClose
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
update :
|
|
|
|
String
|
|
|
|
-> Browser.Navigation.Key
|
2020-07-02 12:27:49 +00:00
|
|
|
-> String
|
|
|
|
-> Options
|
|
|
|
-> Json.Decode.Decoder a
|
2020-05-08 13:24:58 +00:00
|
|
|
-> Msg a
|
|
|
|
-> Model a
|
|
|
|
-> ( Model a, Cmd (Msg a) )
|
2020-07-02 12:27:49 +00:00
|
|
|
update path navKey result_type options decodeResultItemSource msg model =
|
|
|
|
let
|
|
|
|
requestQuerySuggestionsTracker =
|
|
|
|
"query-" ++ result_type ++ "-suggestions"
|
|
|
|
in
|
2020-05-08 13:24:58 +00:00
|
|
|
case msg of
|
2020-05-11 19:56:10 +00:00
|
|
|
NoOp ->
|
|
|
|
( model
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-07-10 07:49:43 +00:00
|
|
|
SortChange sortId ->
|
|
|
|
let
|
|
|
|
sort =
|
|
|
|
fromSortId sortId |> Maybe.withDefault Relevance
|
|
|
|
in
|
|
|
|
( { model | sort = sort }
|
|
|
|
, createUrl
|
|
|
|
path
|
|
|
|
model.channel
|
|
|
|
model.query
|
|
|
|
model.show
|
|
|
|
0
|
|
|
|
model.size
|
|
|
|
sort
|
|
|
|
|> Browser.Navigation.pushUrl navKey
|
|
|
|
)
|
|
|
|
|
2020-05-11 20:42:57 +00:00
|
|
|
ChannelChange channel ->
|
2020-06-10 11:14:36 +00:00
|
|
|
( { model
|
|
|
|
| channel = channel
|
2020-06-12 10:57:47 +00:00
|
|
|
, result =
|
|
|
|
if model.query == Nothing || model.query == Just "" then
|
|
|
|
RemoteData.NotAsked
|
|
|
|
|
|
|
|
else
|
|
|
|
RemoteData.Loading
|
2020-06-10 11:14:36 +00:00
|
|
|
}
|
2020-06-12 10:57:47 +00:00
|
|
|
, if model.query == Nothing || model.query == Just "" then
|
|
|
|
Cmd.none
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
|
|
|
channel
|
|
|
|
model.query
|
|
|
|
model.show
|
|
|
|
0
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-06-12 10:57:47 +00:00
|
|
|
|> Browser.Navigation.pushUrl navKey
|
2020-05-11 20:42:57 +00:00
|
|
|
)
|
|
|
|
|
2020-07-02 12:27:49 +00:00
|
|
|
QueryInputDebounce subMsg ->
|
|
|
|
Debouncer.Messages.update
|
|
|
|
(update path navKey result_type options decodeResultItemSource)
|
|
|
|
{ mapMsg = QueryInputDebounce
|
|
|
|
, getDebouncer = .queryDebounce
|
|
|
|
, setDebouncer = \debouncer m -> { m | queryDebounce = debouncer }
|
|
|
|
}
|
|
|
|
subMsg
|
|
|
|
model
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
QueryInput query ->
|
2020-07-02 12:27:49 +00:00
|
|
|
update path
|
|
|
|
navKey
|
|
|
|
result_type
|
|
|
|
options
|
|
|
|
decodeResultItemSource
|
|
|
|
(QueryInputDebounce (Debouncer.Messages.provideInput QueryInputSuggestionsSubmit))
|
|
|
|
{ model
|
|
|
|
| query = Just query
|
2020-07-09 17:00:28 +00:00
|
|
|
, querySuggest = RemoteData.Loading
|
2020-07-02 12:27:49 +00:00
|
|
|
, querySelectedSuggestion = Nothing
|
|
|
|
}
|
|
|
|
|> Tuple.mapSecond
|
|
|
|
(\cmd ->
|
|
|
|
if RemoteData.isLoading model.querySuggest then
|
|
|
|
Cmd.batch
|
|
|
|
[ cmd
|
|
|
|
, Http.cancel requestQuerySuggestionsTracker
|
|
|
|
]
|
|
|
|
|
|
|
|
else
|
|
|
|
cmd
|
|
|
|
)
|
|
|
|
|
|
|
|
QueryInputSuggestionsSubmit ->
|
|
|
|
let
|
|
|
|
body =
|
|
|
|
Http.jsonBody
|
|
|
|
(Json.Encode.object
|
|
|
|
[ ( "from", Json.Encode.int 0 )
|
|
|
|
, ( "size", Json.Encode.int 0 )
|
|
|
|
, ( "suggest"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "query"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "text", Json.Encode.string (Maybe.withDefault "" model.query) )
|
|
|
|
, ( "completion"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "field", Json.Encode.string (result_type ++ "_suggestions") )
|
|
|
|
, ( "skip_duplicates", Json.Encode.bool True )
|
|
|
|
, ( "size", Json.Encode.int 1000 )
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
in
|
|
|
|
( { model
|
2020-07-06 12:27:37 +00:00
|
|
|
| querySuggest =
|
|
|
|
model.query
|
|
|
|
|> Maybe.map
|
|
|
|
(\selected ->
|
|
|
|
if String.endsWith "." selected then
|
|
|
|
model.querySuggest
|
|
|
|
|
|
|
|
else
|
|
|
|
RemoteData.NotAsked
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault RemoteData.NotAsked
|
2020-07-02 12:27:49 +00:00
|
|
|
, querySelectedSuggestion = Nothing
|
|
|
|
}
|
|
|
|
, makeRequest
|
|
|
|
body
|
|
|
|
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ model.channel)
|
|
|
|
decodeResultItemSource
|
|
|
|
options
|
|
|
|
QueryInputSuggestionsResponse
|
|
|
|
(Just requestQuerySuggestionsTracker)
|
|
|
|
)
|
|
|
|
|
|
|
|
QueryInputSuggestionsResponse querySuggest ->
|
|
|
|
( { model
|
|
|
|
| querySuggest = querySuggest
|
|
|
|
, querySelectedSuggestion = Nothing
|
|
|
|
}
|
2020-05-08 13:24:58 +00:00
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-07-06 12:27:37 +00:00
|
|
|
QueryInputSubmit ->
|
2020-07-09 16:15:34 +00:00
|
|
|
if model.query == Nothing || model.query == Just "" then
|
|
|
|
( model, Cmd.none )
|
|
|
|
|
|
|
|
else
|
|
|
|
( { model | result = RemoteData.Loading }
|
|
|
|
, createUrl
|
|
|
|
path
|
|
|
|
model.channel
|
|
|
|
model.query
|
|
|
|
model.show
|
|
|
|
0
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-07-09 16:15:34 +00:00
|
|
|
|> Browser.Navigation.pushUrl navKey
|
|
|
|
)
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
QueryResponse result ->
|
|
|
|
( { model | result = result }
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
ShowDetails selected ->
|
2020-05-11 19:33:20 +00:00
|
|
|
( model
|
2020-05-11 20:42:57 +00:00
|
|
|
, createUrl
|
|
|
|
path
|
|
|
|
model.channel
|
2020-05-11 19:33:20 +00:00
|
|
|
model.query
|
2020-06-10 12:15:54 +00:00
|
|
|
(if model.show == Just selected then
|
2020-05-11 19:33:20 +00:00
|
|
|
Nothing
|
|
|
|
|
|
|
|
else
|
|
|
|
Just selected
|
|
|
|
)
|
2020-05-11 19:56:10 +00:00
|
|
|
model.from
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-05-11 19:33:20 +00:00
|
|
|
|> Browser.Navigation.pushUrl navKey
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
|
2020-07-02 12:27:49 +00:00
|
|
|
SuggestionsMoveDown ->
|
|
|
|
( { model
|
|
|
|
| querySelectedSuggestion =
|
|
|
|
getMovedSuggestion
|
|
|
|
model.query
|
|
|
|
model.querySuggest
|
|
|
|
model.querySelectedSuggestion
|
|
|
|
(\x -> x + 1)
|
|
|
|
}
|
|
|
|
, scrollToSelected "dropdown-menu"
|
|
|
|
)
|
|
|
|
|
|
|
|
SuggestionsMoveUp ->
|
|
|
|
( { model
|
|
|
|
| querySelectedSuggestion =
|
|
|
|
getMovedSuggestion
|
|
|
|
model.query
|
|
|
|
model.querySuggest
|
|
|
|
model.querySelectedSuggestion
|
|
|
|
(\x -> x - 1)
|
|
|
|
}
|
|
|
|
, scrollToSelected "dropdown-menu"
|
|
|
|
)
|
|
|
|
|
|
|
|
SuggestionsSelect ->
|
|
|
|
case model.querySelectedSuggestion of
|
|
|
|
Just selected ->
|
|
|
|
update path
|
|
|
|
navKey
|
|
|
|
result_type
|
|
|
|
options
|
|
|
|
decodeResultItemSource
|
|
|
|
(SuggestionsClickSelect selected)
|
|
|
|
model
|
|
|
|
|
|
|
|
Nothing ->
|
|
|
|
( model
|
2020-07-06 12:27:37 +00:00
|
|
|
, Task.attempt (\_ -> QueryInputSubmit) (Task.succeed ())
|
2020-07-02 12:27:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
SuggestionsClickSelect selected ->
|
|
|
|
( { model
|
2020-07-06 12:27:37 +00:00
|
|
|
| querySuggest =
|
|
|
|
if String.endsWith "." selected then
|
|
|
|
model.querySuggest
|
|
|
|
|
|
|
|
else
|
|
|
|
RemoteData.NotAsked
|
2020-07-02 12:27:49 +00:00
|
|
|
, querySelectedSuggestion = Nothing
|
|
|
|
, query = Just selected
|
|
|
|
}
|
2020-07-06 12:27:37 +00:00
|
|
|
, Cmd.batch
|
|
|
|
[ Task.attempt (\_ -> QueryInputSuggestionsSubmit) (Task.succeed ())
|
|
|
|
, Task.attempt (\_ -> QueryInputSubmit) (Task.succeed ())
|
|
|
|
]
|
2020-07-02 12:27:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
SuggestionsClose ->
|
|
|
|
( { model
|
|
|
|
| querySuggest = RemoteData.NotAsked
|
|
|
|
, querySelectedSuggestion = Nothing
|
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
scrollToSelected :
|
|
|
|
String
|
|
|
|
-> Cmd (Msg a)
|
|
|
|
scrollToSelected id =
|
|
|
|
let
|
|
|
|
scroll y =
|
|
|
|
Browser.Dom.setViewportOf id 0 y
|
|
|
|
|> Task.onError (\_ -> Task.succeed ())
|
|
|
|
in
|
|
|
|
Task.sequence
|
|
|
|
[ Browser.Dom.getElement (id ++ "-selected")
|
|
|
|
|> Task.map (\x -> ( x.element.y, x.element.height ))
|
|
|
|
, Browser.Dom.getElement id
|
|
|
|
|> Task.map (\x -> ( x.element.y, x.element.height ))
|
|
|
|
, Browser.Dom.getViewportOf id
|
|
|
|
|> Task.map (\x -> ( x.viewport.y, x.viewport.height ))
|
|
|
|
]
|
|
|
|
|> Task.andThen
|
|
|
|
(\x ->
|
|
|
|
case x of
|
|
|
|
( elementY, elementHeight ) :: ( viewportY, viewportHeight ) :: ( viewportScrollTop, _ ) :: [] ->
|
|
|
|
let
|
|
|
|
scrollTop =
|
|
|
|
scroll (viewportScrollTop + (elementY - viewportY))
|
|
|
|
|
|
|
|
scrollBottom =
|
|
|
|
scroll (viewportScrollTop + (elementY - viewportY) + (elementHeight - viewportHeight))
|
|
|
|
in
|
|
|
|
if elementHeight > viewportHeight then
|
|
|
|
scrollTop
|
|
|
|
|
|
|
|
else if elementY < viewportY then
|
|
|
|
scrollTop
|
|
|
|
|
|
|
|
else if elementY + elementHeight > viewportY + viewportHeight then
|
|
|
|
scrollBottom
|
|
|
|
|
|
|
|
else
|
|
|
|
Task.succeed ()
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Task.succeed ()
|
|
|
|
)
|
|
|
|
|> Task.attempt (\_ -> NoOp)
|
|
|
|
|
|
|
|
|
|
|
|
getMovedSuggestion :
|
|
|
|
Maybe String
|
|
|
|
-> RemoteData.WebData (SearchResult a)
|
|
|
|
-> Maybe String
|
|
|
|
-> (Int -> Int)
|
|
|
|
-> Maybe String
|
|
|
|
getMovedSuggestion query querySuggest querySelectedSuggestion moveIndex =
|
|
|
|
let
|
|
|
|
suggestions =
|
|
|
|
getSuggestions query querySuggest
|
|
|
|
|> List.filterMap .text
|
|
|
|
|
|
|
|
getIndex key =
|
|
|
|
suggestions
|
|
|
|
|> List.indexedMap (\i a -> ( a, i ))
|
|
|
|
|> Dict.fromList
|
|
|
|
|> Dict.get key
|
|
|
|
|> Maybe.map moveIndex
|
|
|
|
|> Maybe.map
|
|
|
|
(\x ->
|
|
|
|
if x < 0 then
|
|
|
|
x + List.length suggestions
|
|
|
|
|
|
|
|
else
|
|
|
|
x
|
|
|
|
)
|
|
|
|
|
|
|
|
getKey index =
|
|
|
|
suggestions
|
|
|
|
|> Array.fromList
|
|
|
|
|> Array.get index
|
|
|
|
in
|
|
|
|
querySelectedSuggestion
|
|
|
|
|> Maybe.andThen getIndex
|
|
|
|
|> Maybe.withDefault 0
|
|
|
|
|> getKey
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
|
2020-05-11 19:56:10 +00:00
|
|
|
createUrl :
|
|
|
|
String
|
2020-05-11 20:42:57 +00:00
|
|
|
-> String
|
2020-05-11 19:56:10 +00:00
|
|
|
-> Maybe String
|
|
|
|
-> Maybe String
|
|
|
|
-> Int
|
|
|
|
-> Int
|
2020-07-10 07:49:43 +00:00
|
|
|
-> Sort
|
2020-05-11 19:56:10 +00:00
|
|
|
-> String
|
2020-07-10 07:49:43 +00:00
|
|
|
createUrl path channel query show from size sort =
|
2020-05-11 19:56:10 +00:00
|
|
|
[ Url.Builder.int "from" from
|
|
|
|
, Url.Builder.int "size" size
|
2020-07-10 07:49:43 +00:00
|
|
|
, Url.Builder.string "sort" <| toSortId sort
|
2020-05-11 20:42:57 +00:00
|
|
|
, Url.Builder.string "channel" channel
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
2020-05-08 13:24:58 +00:00
|
|
|
|> List.append
|
|
|
|
(query
|
|
|
|
|> Maybe.map
|
|
|
|
(\x ->
|
|
|
|
[ Url.Builder.string "query" x ]
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
)
|
|
|
|
|> List.append
|
2020-06-10 12:15:54 +00:00
|
|
|
(show
|
2020-05-08 13:24:58 +00:00
|
|
|
|> Maybe.map
|
|
|
|
(\x ->
|
2020-06-10 12:15:54 +00:00
|
|
|
[ Url.Builder.string "show" x
|
2020-05-08 13:24:58 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
)
|
|
|
|
|> Url.Builder.absolute [ path ]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- VIEW
|
|
|
|
|
|
|
|
|
2020-06-09 23:53:17 +00:00
|
|
|
type Channel
|
|
|
|
= Unstable
|
|
|
|
| Release_19_09
|
|
|
|
| Release_20_03
|
|
|
|
|
|
|
|
|
|
|
|
type alias ChannelDetails =
|
|
|
|
{ id : String
|
|
|
|
, title : String
|
|
|
|
, jobset : String
|
2020-06-10 00:01:27 +00:00
|
|
|
, branch : String
|
2020-06-09 23:53:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
channelDetails : Channel -> ChannelDetails
|
|
|
|
channelDetails channel =
|
|
|
|
case channel of
|
|
|
|
Unstable ->
|
2020-06-10 00:01:27 +00:00
|
|
|
ChannelDetails "unstable" "unstable" "nixos/trunk-combined" "nixos-unstable"
|
2020-06-09 23:53:17 +00:00
|
|
|
|
|
|
|
Release_19_09 ->
|
2020-06-10 00:01:27 +00:00
|
|
|
ChannelDetails "19.09" "19.09" "nixos/release-19.09" "nixos-19.09"
|
2020-06-09 23:53:17 +00:00
|
|
|
|
|
|
|
Release_20_03 ->
|
2020-06-10 00:01:27 +00:00
|
|
|
ChannelDetails "20.03" "20.03" "nixos/release-20.03" "nixos-20.03"
|
2020-06-09 23:53:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
channelFromId : String -> Maybe Channel
|
|
|
|
channelFromId channel_id =
|
|
|
|
case channel_id of
|
|
|
|
"unstable" ->
|
|
|
|
Just Unstable
|
|
|
|
|
|
|
|
"19.09" ->
|
|
|
|
Just Release_19_09
|
|
|
|
|
|
|
|
"20.03" ->
|
|
|
|
Just Release_20_03
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
|
|
|
|
channelDetailsFromId : String -> Maybe ChannelDetails
|
|
|
|
channelDetailsFromId channel_id =
|
|
|
|
channelFromId channel_id
|
|
|
|
|> Maybe.map channelDetails
|
|
|
|
|
|
|
|
|
|
|
|
channels : List String
|
|
|
|
channels =
|
2020-07-09 16:08:06 +00:00
|
|
|
[ "19.09"
|
2020-06-09 23:53:17 +00:00
|
|
|
, "20.03"
|
2020-07-09 16:08:06 +00:00
|
|
|
, "unstable"
|
2020-06-09 23:53:17 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-07-10 07:49:43 +00:00
|
|
|
sortBy : List Sort
|
|
|
|
sortBy =
|
|
|
|
[ Relevance
|
|
|
|
, AlphabeticallyAsc
|
|
|
|
, AlphabeticallyDesc
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
toSortQuery :
|
|
|
|
Sort
|
|
|
|
-> String
|
|
|
|
-> ( String, Json.Encode.Value )
|
|
|
|
toSortQuery sort field =
|
|
|
|
( "sort"
|
|
|
|
, case sort of
|
|
|
|
AlphabeticallyAsc ->
|
|
|
|
Json.Encode.list Json.Encode.object
|
|
|
|
[ [ ( field, Json.Encode.string "asc" )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
AlphabeticallyDesc ->
|
|
|
|
Json.Encode.list Json.Encode.object
|
|
|
|
[ [ ( field, Json.Encode.string "desc" )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
Relevance ->
|
|
|
|
Json.Encode.list Json.Encode.string
|
|
|
|
[ "_score"
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
toSortTitle : Sort -> String
|
|
|
|
toSortTitle sort =
|
|
|
|
case sort of
|
|
|
|
AlphabeticallyAsc ->
|
|
|
|
"Alphabetically Ascending"
|
|
|
|
|
|
|
|
AlphabeticallyDesc ->
|
|
|
|
"Alphabetically Descending"
|
|
|
|
|
|
|
|
Relevance ->
|
|
|
|
"Relevance"
|
|
|
|
|
|
|
|
|
|
|
|
toSortId : Sort -> String
|
|
|
|
toSortId sort =
|
|
|
|
case sort of
|
|
|
|
AlphabeticallyAsc ->
|
|
|
|
"alpha_asc"
|
|
|
|
|
|
|
|
AlphabeticallyDesc ->
|
|
|
|
"alpha_desc"
|
|
|
|
|
|
|
|
Relevance ->
|
|
|
|
"relevance"
|
|
|
|
|
|
|
|
|
|
|
|
fromSortId : String -> Maybe Sort
|
|
|
|
fromSortId id =
|
|
|
|
case id of
|
|
|
|
"alpha_asc" ->
|
|
|
|
Just AlphabeticallyAsc
|
|
|
|
|
|
|
|
"alpha_desc" ->
|
|
|
|
Just AlphabeticallyDesc
|
|
|
|
|
|
|
|
"relevance" ->
|
|
|
|
Just Relevance
|
|
|
|
|
|
|
|
_ ->
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
|
2020-07-02 12:27:49 +00:00
|
|
|
getSuggestions :
|
|
|
|
Maybe String
|
|
|
|
-> RemoteData.WebData (SearchResult a)
|
|
|
|
-> List (ResultItem a)
|
|
|
|
getSuggestions query querySuggest =
|
|
|
|
let
|
|
|
|
maybeList f x =
|
|
|
|
x
|
|
|
|
|> Maybe.map f
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
in
|
|
|
|
case querySuggest of
|
|
|
|
RemoteData.Success result ->
|
2020-07-08 15:05:57 +00:00
|
|
|
let
|
|
|
|
suggestions =
|
|
|
|
result.suggest
|
|
|
|
|> maybeList (\x -> x.query |> maybeList (List.map .options))
|
|
|
|
|> List.concat
|
|
|
|
|> List.filter
|
|
|
|
(\x ->
|
|
|
|
if String.endsWith "." (Maybe.withDefault "" query) then
|
|
|
|
x.text /= query
|
|
|
|
|
|
|
|
else
|
|
|
|
True
|
|
|
|
)
|
|
|
|
|
|
|
|
firstItemText items =
|
|
|
|
items
|
|
|
|
|> List.head
|
|
|
|
|> Maybe.andThen .text
|
|
|
|
in
|
|
|
|
if List.length suggestions == 1 && firstItemText suggestions == query then
|
|
|
|
[]
|
|
|
|
|
|
|
|
else
|
|
|
|
suggestions
|
2020-07-02 12:27:49 +00:00
|
|
|
|
|
|
|
_ ->
|
|
|
|
[]
|
|
|
|
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
view :
|
2020-05-11 19:56:10 +00:00
|
|
|
String
|
|
|
|
-> String
|
2020-05-08 13:24:58 +00:00
|
|
|
-> Model a
|
2020-07-02 12:27:49 +00:00
|
|
|
-> (String -> Maybe String -> SearchResult a -> Html b)
|
2020-05-08 13:24:58 +00:00
|
|
|
-> (Msg a -> b)
|
|
|
|
-> Html b
|
2020-05-11 19:56:10 +00:00
|
|
|
view path title model viewSuccess outMsg =
|
2020-07-02 12:27:49 +00:00
|
|
|
let
|
|
|
|
suggestions =
|
|
|
|
getSuggestions model.query model.querySuggest
|
|
|
|
|
|
|
|
viewSuggestion x =
|
|
|
|
li
|
|
|
|
[]
|
|
|
|
[ a
|
|
|
|
([ href "#" ]
|
|
|
|
|> List.append
|
|
|
|
(x.text
|
|
|
|
|> Maybe.map (\text -> [ onClick <| outMsg (SuggestionsClickSelect text) ])
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
)
|
|
|
|
|> List.append
|
|
|
|
(if x.text == model.querySelectedSuggestion then
|
|
|
|
[ id "dropdown-menu-selected" ]
|
|
|
|
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
[ text <| Maybe.withDefault "" x.text ]
|
|
|
|
]
|
|
|
|
in
|
2020-07-09 17:00:28 +00:00
|
|
|
div
|
|
|
|
[ classList
|
|
|
|
[ ( "search-page", True )
|
|
|
|
, ( "with-suggestions", RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 )
|
|
|
|
, ( "with-suggestions-loading"
|
|
|
|
, (model.query /= Nothing)
|
|
|
|
&& (model.query /= Just "")
|
|
|
|
&& not (RemoteData.isSuccess model.querySuggest || RemoteData.isNotAsked model.querySuggest)
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
2020-05-11 19:56:10 +00:00
|
|
|
[ h1 [ class "page-header" ] [ text title ]
|
2020-07-02 12:27:49 +00:00
|
|
|
, div
|
2020-07-09 17:00:28 +00:00
|
|
|
[ class "search-backdrop"
|
|
|
|
, onClick <| outMsg SuggestionsClose
|
|
|
|
]
|
|
|
|
[]
|
|
|
|
, div
|
|
|
|
[ class "search-input"
|
2020-07-02 12:27:49 +00:00
|
|
|
]
|
2020-07-06 12:27:37 +00:00
|
|
|
[ form [ onSubmit (outMsg QueryInputSubmit) ]
|
2020-06-12 10:57:47 +00:00
|
|
|
[ p
|
|
|
|
[]
|
2020-05-11 20:42:57 +00:00
|
|
|
[ strong []
|
2020-06-12 10:57:47 +00:00
|
|
|
[ text "Channel: " ]
|
|
|
|
, div
|
|
|
|
[ class "btn-group"
|
|
|
|
, attribute "data-toggle" "buttons-radio"
|
|
|
|
]
|
2020-06-09 23:53:17 +00:00
|
|
|
(List.filterMap
|
|
|
|
(\channel_id ->
|
|
|
|
channelDetailsFromId channel_id
|
|
|
|
|> Maybe.map
|
|
|
|
(\channel ->
|
2020-06-12 10:57:47 +00:00
|
|
|
button
|
|
|
|
[ type_ "button"
|
|
|
|
, classList
|
|
|
|
[ ( "btn", True )
|
|
|
|
, ( "active", channel.id == model.channel )
|
|
|
|
]
|
|
|
|
, onClick <| outMsg (ChannelChange channel.id)
|
2020-06-09 23:53:17 +00:00
|
|
|
]
|
|
|
|
[ text channel.title ]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
channels
|
|
|
|
)
|
2020-06-12 10:57:47 +00:00
|
|
|
]
|
|
|
|
, p
|
|
|
|
[ class "input-append"
|
|
|
|
]
|
|
|
|
[ input
|
2020-07-02 12:27:49 +00:00
|
|
|
([ type_ "text"
|
2020-07-06 12:27:37 +00:00
|
|
|
, id "search-query-input"
|
2020-07-08 14:19:08 +00:00
|
|
|
, autocomplete False
|
2020-07-06 12:27:37 +00:00
|
|
|
, autofocus True
|
2020-07-09 16:07:07 +00:00
|
|
|
, placeholder <| "Search for " ++ path
|
2020-07-02 12:27:49 +00:00
|
|
|
, onInput (\x -> outMsg (QueryInput x))
|
|
|
|
, value <| Maybe.withDefault "" model.query
|
|
|
|
]
|
|
|
|
|> List.append
|
|
|
|
(if RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 then
|
|
|
|
[ Keyboard.Events.custom Keyboard.Events.Keydown
|
|
|
|
{ preventDefault = True
|
|
|
|
, stopPropagation = True
|
|
|
|
}
|
|
|
|
([ ( Keyboard.ArrowDown, SuggestionsMoveDown )
|
|
|
|
, ( Keyboard.ArrowUp, SuggestionsMoveUp )
|
|
|
|
, ( Keyboard.Tab, SuggestionsMoveDown )
|
|
|
|
, ( Keyboard.Enter, SuggestionsSelect )
|
|
|
|
, ( Keyboard.Escape, SuggestionsClose )
|
|
|
|
]
|
|
|
|
|> List.map (\( k, m ) -> ( k, outMsg m ))
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
else if RemoteData.isNotAsked model.querySuggest then
|
|
|
|
[ Keyboard.Events.custom Keyboard.Events.Keydown
|
|
|
|
{ preventDefault = True
|
|
|
|
, stopPropagation = True
|
|
|
|
}
|
|
|
|
([ ( Keyboard.ArrowDown, QueryInputSuggestionsSubmit )
|
|
|
|
, ( Keyboard.ArrowUp, QueryInputSuggestionsSubmit )
|
|
|
|
]
|
|
|
|
|> List.map (\( k, m ) -> ( k, outMsg m ))
|
|
|
|
)
|
|
|
|
]
|
|
|
|
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
)
|
2020-06-12 10:57:47 +00:00
|
|
|
[]
|
2020-07-02 12:27:49 +00:00
|
|
|
, div [ class "loader" ] []
|
2020-06-12 10:57:47 +00:00
|
|
|
, div [ class "btn-group" ]
|
|
|
|
[ button [ class "btn" ] [ text "Search" ]
|
|
|
|
]
|
2020-05-11 20:42:57 +00:00
|
|
|
]
|
2020-07-02 12:27:49 +00:00
|
|
|
, ul
|
|
|
|
[ id "dropdown-menu", class "dropdown-menu" ]
|
|
|
|
(if RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 then
|
|
|
|
List.map viewSuggestion suggestions
|
|
|
|
|
|
|
|
else
|
|
|
|
[]
|
|
|
|
)
|
2020-05-08 13:24:58 +00:00
|
|
|
]
|
|
|
|
]
|
|
|
|
, case model.result of
|
|
|
|
RemoteData.NotAsked ->
|
2020-05-11 22:00:23 +00:00
|
|
|
div [] [ text "" ]
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
RemoteData.Loading ->
|
2020-06-11 16:50:53 +00:00
|
|
|
div [ class "loader" ] [ text "Loading..." ]
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
RemoteData.Success result ->
|
2020-05-11 22:00:23 +00:00
|
|
|
if result.hits.total.value == 0 then
|
|
|
|
div []
|
|
|
|
[ h4 [] [ text <| "No " ++ path ++ " found!" ]
|
|
|
|
]
|
|
|
|
|
|
|
|
else
|
|
|
|
div []
|
|
|
|
[ p []
|
|
|
|
[ em []
|
|
|
|
[ text
|
|
|
|
("Showing results "
|
|
|
|
++ String.fromInt model.from
|
|
|
|
++ "-"
|
|
|
|
++ String.fromInt
|
|
|
|
(if model.from + model.size > result.hits.total.value then
|
|
|
|
result.hits.total.value
|
|
|
|
|
|
|
|
else
|
|
|
|
model.from + model.size
|
|
|
|
)
|
|
|
|
++ " of "
|
2020-06-15 07:51:09 +00:00
|
|
|
++ (if result.hits.total.value == 10000 then
|
|
|
|
"more than 10000 results, please provide more precise search terms."
|
|
|
|
|
|
|
|
else
|
|
|
|
String.fromInt result.hits.total.value
|
|
|
|
++ "."
|
|
|
|
)
|
2020-05-11 22:00:23 +00:00
|
|
|
)
|
|
|
|
]
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
2020-07-10 07:49:43 +00:00
|
|
|
, 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
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]
|
2020-05-11 22:00:23 +00:00
|
|
|
, viewPager outMsg model result path
|
2020-06-10 12:15:54 +00:00
|
|
|
, viewSuccess model.channel model.show result
|
2020-05-11 22:00:23 +00:00
|
|
|
, viewPager outMsg model result path
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
RemoteData.Failure error ->
|
2020-05-11 22:00:23 +00:00
|
|
|
let
|
|
|
|
( errorTitle, errorMessage ) =
|
|
|
|
case error of
|
|
|
|
Http.BadUrl text ->
|
|
|
|
( "Bad Url!", text )
|
|
|
|
|
|
|
|
Http.Timeout ->
|
|
|
|
( "Timeout!", "Request to the server timeout." )
|
|
|
|
|
|
|
|
Http.NetworkError ->
|
2020-06-10 00:05:04 +00:00
|
|
|
( "Network Error!", "A network request bonsaisearch.net domain failed. This is either due to a content blocker or a networking issue." )
|
2020-05-11 22:00:23 +00:00
|
|
|
|
|
|
|
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
|
2020-05-08 13:24:58 +00:00
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-05-11 19:56:10 +00:00
|
|
|
viewPager :
|
|
|
|
(Msg a -> b)
|
|
|
|
-> Model a
|
2020-07-02 12:27:49 +00:00
|
|
|
-> SearchResult a
|
2020-05-11 19:56:10 +00:00
|
|
|
-> String
|
|
|
|
-> Html b
|
2020-07-02 12:27:49 +00:00
|
|
|
viewPager _ model result path =
|
2020-05-11 19:56:10 +00:00
|
|
|
ul [ class "pager" ]
|
|
|
|
[ li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from == 0 )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ if model.from == 0 then
|
|
|
|
href "#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
href <|
|
|
|
|
createUrl
|
|
|
|
path
|
2020-05-11 20:42:57 +00:00
|
|
|
model.channel
|
2020-05-11 19:56:10 +00:00
|
|
|
model.query
|
2020-06-10 12:15:54 +00:00
|
|
|
model.show
|
2020-05-11 19:56:10 +00:00
|
|
|
0
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
|
|
|
[ text "First" ]
|
|
|
|
]
|
|
|
|
, li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from == 0 )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ href <|
|
|
|
|
if model.from - model.size < 0 then
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
2020-05-11 20:42:57 +00:00
|
|
|
model.channel
|
2020-05-11 19:56:10 +00:00
|
|
|
model.query
|
2020-06-10 12:15:54 +00:00
|
|
|
model.show
|
2020-05-11 19:56:10 +00:00
|
|
|
(model.from - model.size)
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
|
|
|
[ 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
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
2020-05-11 20:42:57 +00:00
|
|
|
model.channel
|
2020-05-11 19:56:10 +00:00
|
|
|
model.query
|
2020-06-10 12:15:54 +00:00
|
|
|
model.show
|
2020-05-11 19:56:10 +00:00
|
|
|
(model.from + model.size)
|
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
|
|
|
[ 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
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
2020-06-11 15:18:52 +00:00
|
|
|
let
|
|
|
|
remainder =
|
|
|
|
if remainderBy model.size result.hits.total.value == 0 then
|
|
|
|
1
|
|
|
|
|
|
|
|
else
|
|
|
|
0
|
|
|
|
in
|
2020-05-11 19:56:10 +00:00
|
|
|
createUrl
|
|
|
|
path
|
2020-05-11 20:42:57 +00:00
|
|
|
model.channel
|
2020-05-11 19:56:10 +00:00
|
|
|
model.query
|
2020-06-10 12:15:54 +00:00
|
|
|
model.show
|
2020-06-11 15:18:52 +00:00
|
|
|
(((result.hits.total.value // model.size) - remainder) * model.size)
|
2020-05-11 19:56:10 +00:00
|
|
|
model.size
|
2020-07-10 07:49:43 +00:00
|
|
|
model.sort
|
2020-05-11 19:56:10 +00:00
|
|
|
]
|
|
|
|
[ text "Last" ]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
-- API
|
|
|
|
|
|
|
|
|
|
|
|
type alias Options =
|
2020-06-03 23:33:54 +00:00
|
|
|
{ mappingSchemaVersion : Int
|
|
|
|
, url : String
|
2020-05-08 13:24:58 +00:00
|
|
|
, username : String
|
|
|
|
, password : String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-06-19 06:53:49 +00:00
|
|
|
filter_by_type :
|
|
|
|
String
|
|
|
|
-> ( String, Json.Encode.Value )
|
|
|
|
filter_by_type type_ =
|
|
|
|
( "term"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "type"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "value", Json.Encode.string type_ )
|
|
|
|
, ( "_name", Json.Encode.string <| "filter_" ++ type_ ++ "s" )
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
filter_by_query : String -> String -> List (List ( String, Json.Encode.Value ))
|
|
|
|
filter_by_query field queryRaw =
|
|
|
|
let
|
|
|
|
query =
|
|
|
|
queryRaw
|
|
|
|
|> String.trim
|
|
|
|
in
|
|
|
|
query
|
|
|
|
|> String.replace "." " "
|
|
|
|
|> String.words
|
|
|
|
|> List.indexedMap
|
|
|
|
(\i query_word ->
|
|
|
|
let
|
|
|
|
isLast =
|
|
|
|
List.length (String.words query) == i + 1
|
|
|
|
in
|
|
|
|
[ if isLast then
|
|
|
|
( "bool"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "should"
|
|
|
|
, Json.Encode.list Json.Encode.object
|
|
|
|
[ [ ( "match"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( field
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "query", Json.Encode.string query_word )
|
|
|
|
, ( "fuzziness", Json.Encode.string "1" )
|
|
|
|
, ( "_name", Json.Encode.string <| "filter_queries_" ++ String.fromInt (i + 1) ++ "_should_match" )
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
, [ ( "match_bool_prefix"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( field
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "query", Json.Encode.string query_word )
|
|
|
|
, ( "_name"
|
|
|
|
, Json.Encode.string <| "filter_queries_" ++ String.fromInt (i + 1) ++ "_should_prefix"
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
else
|
|
|
|
( "match_bool_prefix"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( field
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "query", Json.Encode.string query_word )
|
|
|
|
, ( "_name"
|
|
|
|
, Json.Encode.string <| "filter_queries_" ++ String.fromInt (i + 1) ++ "_prefix"
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
makeRequestBody :
|
|
|
|
String
|
|
|
|
-> Int
|
|
|
|
-> Int
|
2020-07-10 07:49:43 +00:00
|
|
|
-> Sort
|
|
|
|
-> String
|
2020-06-19 06:53:49 +00:00
|
|
|
-> String
|
|
|
|
-> String
|
|
|
|
-> List (List ( String, Json.Encode.Value ))
|
|
|
|
-> Http.Body
|
2020-07-10 07:49:43 +00:00
|
|
|
makeRequestBody query from sizeRaw sort type_ sort_field query_field should_queries =
|
2020-07-02 12:27:49 +00:00
|
|
|
let
|
|
|
|
-- you can not request more then 10000 results otherwise it will return 404
|
|
|
|
size =
|
|
|
|
if from + sizeRaw > 10000 then
|
|
|
|
10000 - from
|
|
|
|
|
|
|
|
else
|
|
|
|
sizeRaw
|
|
|
|
in
|
2020-06-19 06:53:49 +00:00
|
|
|
Http.jsonBody
|
|
|
|
(Json.Encode.object
|
|
|
|
[ ( "from"
|
|
|
|
, Json.Encode.int from
|
|
|
|
)
|
|
|
|
, ( "size"
|
|
|
|
, Json.Encode.int size
|
|
|
|
)
|
2020-07-10 07:49:43 +00:00
|
|
|
, toSortQuery sort sort_field
|
2020-06-19 06:53:49 +00:00
|
|
|
, ( "query"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "bool"
|
|
|
|
, Json.Encode.object
|
|
|
|
[ ( "filter"
|
|
|
|
, Json.Encode.list Json.Encode.object
|
|
|
|
(List.append
|
|
|
|
[ [ filter_by_type type_ ] ]
|
|
|
|
(filter_by_query query_field query)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
, ( "should"
|
|
|
|
, Json.Encode.list Json.Encode.object should_queries
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
makeRequest :
|
2020-06-19 06:53:49 +00:00
|
|
|
Http.Body
|
2020-05-08 13:24:58 +00:00
|
|
|
-> String
|
|
|
|
-> Json.Decode.Decoder a
|
|
|
|
-> Options
|
2020-07-02 12:27:49 +00:00
|
|
|
-> (RemoteData.WebData (SearchResult a) -> Msg a)
|
|
|
|
-> Maybe String
|
2020-05-08 13:24:58 +00:00
|
|
|
-> Cmd (Msg a)
|
2020-07-02 12:27:49 +00:00
|
|
|
makeRequest body index decodeResultItemSource options responseMsg tracker =
|
2020-05-08 13:24:58 +00:00
|
|
|
Http.riskyRequest
|
|
|
|
{ method = "POST"
|
|
|
|
, headers =
|
|
|
|
[ Http.header "Authorization" ("Basic " ++ Base64.encode (options.username ++ ":" ++ options.password))
|
|
|
|
]
|
|
|
|
, url = options.url ++ "/" ++ index ++ "/_search"
|
2020-06-19 06:53:49 +00:00
|
|
|
, body = body
|
2020-05-08 13:24:58 +00:00
|
|
|
, expect =
|
|
|
|
Http.expectJson
|
2020-07-02 12:27:49 +00:00
|
|
|
(RemoteData.fromResult >> responseMsg)
|
2020-05-08 13:24:58 +00:00
|
|
|
(decodeResult decodeResultItemSource)
|
|
|
|
, timeout = Nothing
|
2020-07-02 12:27:49 +00:00
|
|
|
, tracker = tracker
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- JSON
|
|
|
|
|
|
|
|
|
|
|
|
decodeResult :
|
|
|
|
Json.Decode.Decoder a
|
2020-07-02 12:27:49 +00:00
|
|
|
-> Json.Decode.Decoder (SearchResult a)
|
2020-05-08 13:24:58 +00:00
|
|
|
decodeResult decodeResultItemSource =
|
2020-07-02 12:27:49 +00:00
|
|
|
Json.Decode.map2 SearchResult
|
2020-05-08 13:24:58 +00:00
|
|
|
(Json.Decode.field "hits" (decodeResultHits decodeResultItemSource))
|
2020-07-02 12:27:49 +00:00
|
|
|
(Json.Decode.maybe (Json.Decode.field "suggest" (decodeSuggest decodeResultItemSource)))
|
|
|
|
|
|
|
|
|
|
|
|
decodeSuggest : Json.Decode.Decoder a -> Json.Decode.Decoder (SearchSuggest a)
|
|
|
|
decodeSuggest decodeResultItemSource =
|
|
|
|
Json.Decode.map SearchSuggest
|
|
|
|
(Json.Decode.maybe (Json.Decode.field "query" (Json.Decode.list (decodeSuggestQuery decodeResultItemSource))))
|
|
|
|
|
|
|
|
|
|
|
|
decodeSuggestQuery : Json.Decode.Decoder a -> Json.Decode.Decoder (SearchSuggestQuery a)
|
|
|
|
decodeSuggestQuery decodeResultItemSource =
|
|
|
|
Json.Decode.map4 SearchSuggestQuery
|
|
|
|
(Json.Decode.field "text" Json.Decode.string)
|
|
|
|
(Json.Decode.field "offset" Json.Decode.int)
|
|
|
|
(Json.Decode.field "length" Json.Decode.int)
|
|
|
|
(Json.Decode.field "options" (Json.Decode.list (decodeResultItem decodeResultItemSource)))
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
decodeResultHits : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultHits a)
|
|
|
|
decodeResultHits decodeResultItemSource =
|
|
|
|
Json.Decode.map3 ResultHits
|
|
|
|
(Json.Decode.field "total" decodeResultHitsTotal)
|
|
|
|
(Json.Decode.field "max_score" (Json.Decode.nullable Json.Decode.float))
|
|
|
|
(Json.Decode.field "hits" (Json.Decode.list (decodeResultItem decodeResultItemSource)))
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultHitsTotal : Json.Decode.Decoder ResultHitsTotal
|
|
|
|
decodeResultHitsTotal =
|
|
|
|
Json.Decode.map2 ResultHitsTotal
|
|
|
|
(Json.Decode.field "value" Json.Decode.int)
|
|
|
|
(Json.Decode.field "relation" Json.Decode.string)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultItem : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultItem a)
|
|
|
|
decodeResultItem decodeResultItemSource =
|
2020-07-02 12:27:49 +00:00
|
|
|
Json.Decode.map6 ResultItem
|
2020-05-08 13:24:58 +00:00
|
|
|
(Json.Decode.field "_index" Json.Decode.string)
|
|
|
|
(Json.Decode.field "_id" Json.Decode.string)
|
2020-07-10 07:49:43 +00:00
|
|
|
(Json.Decode.field "_score" (Json.Decode.nullable Json.Decode.float))
|
2020-05-08 13:24:58 +00:00
|
|
|
(Json.Decode.field "_source" decodeResultItemSource)
|
2020-07-02 12:27:49 +00:00
|
|
|
(Json.Decode.maybe (Json.Decode.field "text" Json.Decode.string))
|
2020-06-18 10:24:52 +00:00
|
|
|
(Json.Decode.maybe (Json.Decode.field "matched_queries" (Json.Decode.list Json.Decode.string)))
|