diff options
author | Yorhel <git@yorhel.nl> | 2020-12-31 09:35:07 +0100 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2021-01-03 10:39:57 +0100 |
commit | 65e36750ec6ed95d82529146887dd82b2e9dde1c (patch) | |
tree | e28d26909bf2e0f94cebbca54ca9d96306b53253 /elm | |
parent | 61cb7cd185fbf9203041ca366a2eb915e746d6f4 (diff) |
AdvSearch: Add feature to save/load/delete queries
Diffstat (limited to 'elm')
-rw-r--r-- | elm/AdvSearch/Lib.elm | 8 | ||||
-rw-r--r-- | elm/AdvSearch/Main.elm | 212 | ||||
-rw-r--r-- | elm/Lib/Api.elm | 1 |
3 files changed, 172 insertions, 49 deletions
diff --git a/elm/AdvSearch/Lib.elm b/elm/AdvSearch/Lib.elm index 0820eeb6..36373ede 100644 --- a/elm/AdvSearch/Lib.elm +++ b/elm/AdvSearch/Lib.elm @@ -140,6 +140,14 @@ encQuery query = QTuple n o a b -> fint n ++ encTypeOp o 5 ++ fint a ++ fint b +showQType : QType -> String +showQType q = + case q of + V -> "v" + R -> "r" + C -> "c" + S -> "s" + showOp : Op -> String showOp op = case op of diff --git a/elm/AdvSearch/Main.elm b/elm/AdvSearch/Main.elm index 3565be81..1d1a03b8 100644 --- a/elm/AdvSearch/Main.elm +++ b/elm/AdvSearch/Main.elm @@ -6,10 +6,19 @@ import Html.Events exposing (..) import Browser import Set import Dict -import Array as A +import Task +import Browser.Dom as Dom +import Array as Array import Json.Encode as JE import Json.Decode as JD import Gen.Api as GApi +import Gen.AdvSearchSave as GASS +import Gen.AdvSearchDel as GASD +import Gen.AdvSearchLoad as GASL +import Lib.Html exposing (..) +import Lib.Api as Api +import Lib.DropDown as DD +import Lib.Autocomplete as A import AdvSearch.Lib exposing (..) import AdvSearch.Fields exposing (..) @@ -19,109 +28,214 @@ main = Browser.element { init = \e -> (init e, Cmd.none) , view = view , update = update - , subscriptions = \m -> Sub.map Field (fieldSub m.query) + , subscriptions = \m -> Sub.batch [ DD.sub m.saveDd, Sub.map Field (fieldSub m.query) ] } +type alias SQuery = { name: String, query: String } type alias Recv = - { query : JE.Value - , qtype : String - , uid : Maybe Int + { uid : Maybe Int , labels : List { id: Int, label: String } , defaultSpoil : Int - , producers : List GApi.ApiProducerResult - , staff : List GApi.ApiStaffResult - , tags : List GApi.ApiTagResult - , traits : List GApi.ApiTraitResult - , anime : List GApi.ApiAnimeResult + , saved : List SQuery + , query : GApi.ApiAdvSearchQuery } type alias Model = - { query : Field - , qtype : QType - , data : Data + { query : Field + , qtype : QType + , data : Data + , saved : List SQuery + , saveState : Api.State + , saveDd : DD.Config Msg + , saveAct : Int + , saveName : String + , saveDel : Set.Set String } type Msg - = Field FieldMsg + = Noop + | Field FieldMsg + | SaveToggle Bool + | SaveAct Int + | SaveName String + | SaveSave + | SaveSaved SQuery GApi.Response + | SaveLoad String + | SaveLoaded GApi.Response + | SaveDelSel String + | SaveDel + | SaveDeleted (Set.Set String) GApi.Response -- Add default set of fields (if they aren't present yet) and sort the list -normalize : Model -> Model -normalize model = +normalize : QType -> Field -> Data -> (Field, Data) +normalize qtype query odat = let present = List.foldl (\(n,_,_) a -> Set.insert n a) Set.empty - defaults pres = A.foldl (\f (al,dat,an) -> - if f.qtype == model.qtype && f.quick /= 0 && not (Set.member an pres) + defaults pres = Array.foldl (\f (al,dat,an) -> + if f.qtype == qtype && f.quick /= 0 && not (Set.member an pres) then let (ndat, nf) = fieldInit an dat in (nf::al, ndat, an+1) else (al,dat,an+1) - ) ([],model.data,0) fields + ) ([],odat,0) fields cmp (an,add,am) (bn,bdd,bm) = -- Sort active filters before empty ones, then order by 'quick', fallback to title - let aq = fieldToQuery model.data (an,add,am) /= Nothing - bq = fieldToQuery model.data (bn,bdd,bm) /= Nothing - af = A.get an fields - bf = A.get bn fields + let aq = fieldToQuery odat (an,add,am) /= Nothing + bq = fieldToQuery odat (bn,bdd,bm) /= Nothing + af = Array.get an fields + bf = Array.get bn fields ao = Maybe.andThen (\d -> if d.quick == 0 then Nothing else Just d.quick) af |> Maybe.withDefault 9999 bo = Maybe.andThen (\d -> if d.quick == 0 then Nothing else Just d.quick) bf |> Maybe.withDefault 9999 at = Maybe.map (\d -> d.title) af |> Maybe.withDefault "" bt = Maybe.map (\d -> d.title) bf |> Maybe.withDefault "" in if aq && not bq then LT else if not aq && bq then GT else if ao /= bo then compare ao bo else compare at bt - in case model.query of + in case query of (qid, qdd, FMNest qm) -> - let (nl, dat, _) = defaults (present qm.fields) + let (nl, ndat, _) = defaults (present qm.fields) nqm = { qm | fields = List.sortWith cmp (nl++qm.fields) } - in { model | query = (qid, qdd, FMNest nqm), data = dat } - _ -> model + in ((qid, qdd, FMNest nqm), ndat) + _ -> (query, odat) -init : Recv -> Model -init arg = +loadQuery : Data -> GApi.ApiAdvSearchQuery -> (QType, Field, Data) +loadQuery odat arg = let dat = { objid = 0 , level = 0 - , uid = arg.uid - , labels = (0, "Unlabeled") :: List.map (\e -> (e.id, e.label)) arg.labels - , defaultSpoil = arg.defaultSpoil - , producers = Dict.fromList <| List.map (\p -> (p.id,p)) <| arg.producers - , staff = Dict.fromList <| List.map (\s -> (s.id,s)) <| arg.staff - , tags = Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.tags - , traits = Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.traits - , anime = Dict.fromList <| List.map (\a -> (a.id,a)) <| arg.anime + , uid = odat.uid + , labels = odat.labels + , defaultSpoil = odat.defaultSpoil + , producers = Dict.union (Dict.fromList <| List.map (\p -> (p.id,p)) <| arg.producers) odat.producers + , staff = Dict.union (Dict.fromList <| List.map (\s -> (s.id,s)) <| arg.staff ) odat.staff + , tags = Dict.union (Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.tags ) odat.tags + , traits = Dict.union (Dict.fromList <| List.map (\t -> (t.id,t)) <| arg.traits ) odat.traits + , anime = Dict.union (Dict.fromList <| List.map (\a -> (a.id,a)) <| arg.anime ) odat.anime } qtype = if arg.qtype == "v" then V else R - (ndat, query) = JD.decodeValue decodeQuery arg.query |> Result.toMaybe |> Maybe.withDefault (QAnd []) |> fieldFromQuery qtype dat + (dat2, query) = JD.decodeValue decodeQuery arg.query |> Result.toMaybe |> Maybe.withDefault (QAnd []) |> fieldFromQuery qtype dat -- We always want the top-level query to be a Nest type. - addtoplvl = let (_,m) = fieldCreate -1 (Tuple.mapSecond FMNest (nestInit True qtype qtype [query] ndat)) in m - nquery = case query of + addtoplvl = let (_,m) = fieldCreate -1 (Tuple.mapSecond FMNest (nestInit True qtype qtype [query] dat2)) in m + query2 = case query of (_,_,FMNest m) -> if m.qtype == qtype then query else addtoplvl _ -> addtoplvl + dat3 = { dat2 | objid = dat2.objid + 5 } -- +5 for the creation of query2 -- Is this a "simple" query? i.e. one that consists of at most a single level of nesting - isSimple = case nquery of + isSimple = case query2 of (_,_,FMNest m) -> List.all (\f -> case f of (_,_,FMNest _) -> False _ -> True) m.fields _ -> True - model = { query = nquery - , qtype = qtype - , data = { ndat | objid = ndat.objid + 5 } -- +5 for the creation of nQuery - } - in if isSimple then normalize model else model + (query3, dat4) = if isSimple then normalize qtype query2 dat3 else (query2, dat3) + in (qtype, query3, dat4) + + +init : Recv -> Model +init arg = + let dat = { objid = 0 + , level = 0 + , uid = arg.uid + , labels = (0, "Unlabeled") :: List.map (\e -> (e.id, e.label)) arg.labels + , defaultSpoil = arg.defaultSpoil + , producers = Dict.empty + , staff = Dict.empty + , tags = Dict.empty + , traits = Dict.empty + , anime = Dict.empty + } + (qtype, query, ndat) = loadQuery dat arg.query + in { query = query + , qtype = qtype + , data = ndat + , saved = arg.saved + , saveState = Api.Normal + , saveDd = DD.init "advsearch_save" SaveToggle + , saveAct = 0 + , saveName = "" + , saveDel = Set.empty + } update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of + Noop -> (model, Cmd.none) Field m -> let (ndat, nm, nc) = fieldUpdate model.data m model.query in ({ model | data = ndat, query = nm }, Cmd.map Field nc) + SaveToggle b -> + let act = if model.saveAct == 0 && not (List.isEmpty model.saved) && fieldToQuery model.data model.query == Nothing then 1 else model.saveAct + in ( { model | saveDd = DD.toggle model.saveDd b, saveAct = act, saveDel = Set.empty } + , if b && act == 0 then Task.attempt (always Noop) (Dom.focus "advsearch_saveinput") else Cmd.none) + SaveAct n -> ({ model | saveAct = n, saveDel = Set.empty }, Cmd.none) + SaveName n -> ({ model | saveName = n }, Cmd.none) + SaveSave -> + case Maybe.map encQuery (fieldToQuery model.data model.query) of + Just q -> ({ model | saveState = Api.Loading }, GASS.send { name = model.saveName, qtype = showQType model.qtype, query = q } (SaveSaved { name = model.saveName, query = q }) ) + Nothing -> (model, Cmd.none) + SaveSaved q GApi.Success -> + let f rep lst = case lst of + (x::xs) -> + if x.name == q.name then q :: f True xs + else if not rep && x.name > q.name then q :: x :: f True xs + else x :: f rep xs + [] -> if rep then [] else [q] + in ({ model | saveState = Api.Normal, saveDd = DD.toggle model.saveDd False, saved = f False model.saved }, Cmd.none) + SaveSaved _ e -> ({ model | saveState = Api.Error e }, Cmd.none) + SaveLoad q -> ({ model | saveState = Api.Loading, saveDd = DD.toggle model.saveDd False }, GASL.send { qtype = showQType model.qtype, query = q } SaveLoaded) + SaveLoaded (GApi.AdvSearchQuery q) -> + let (_, query, dat) = loadQuery model.data q + in ({ model | saveState = Api.Normal, query = query, data = dat }, Cmd.none) + SaveLoaded e -> ({ model | saveState = Api.Error e }, Cmd.none) + SaveDelSel s -> ({ model | saveDel = (if Set.member s model.saveDel then Set.remove else Set.insert) s model.saveDel }, Cmd.none) + SaveDel -> ({ model | saveState = Api.Loading }, GASD.send { qtype = showQType model.qtype, name = Set.toList model.saveDel } (SaveDeleted model.saveDel)) + SaveDeleted d GApi.Success -> ({ model | saveState = Api.Normal, saveDel = Set.empty, saved = List.filter (\e -> not (Set.member e.name d)) model.saved }, Cmd.none) + SaveDeleted _ e -> ({ model | saveState = Api.Error e }, Cmd.none) + view : Model -> Html Msg -view model = div [ class "advsearch" ] - [ input [ type_ "hidden", id "f", name "f", value <| Maybe.withDefault "" <| Maybe.map encQuery (fieldToQuery model.data model.query) ] [] +view model = div [ class "advsearch" ] <| + let encQ = Maybe.withDefault "" <| Maybe.map encQuery (fieldToQuery model.data model.query) + in + [ input [ type_ "hidden", id "f", name "f", value encQ ] [] , Html.map Field (fieldView model.data model.query) - , input [ type_ "submit", class "submit", value "Search" ] [] + , div [ class "optbuttons" ] + [ if model.data.uid == Nothing then text "" else div [ class "elm_dd_button" ] + [ DD.view model.saveDd model.saveState (text "Save/Load") <| \() -> + [ div [ class "advheader", style "min-width" "250px" ] + [ div [ class "opts", style "margin-bottom" "5px" ] + [ if model.saveAct == 0 then b [] [ text "Save" ] else a [ href "#", onClickD (SaveAct 0) ] [ text "Save" ] + , if model.saveAct == 1 then b [] [ text "Load" ] else a [ href "#", onClickD (SaveAct 1) ] [ text "Load" ] + , if model.saveAct == 2 then b [] [ text "Delete" ] else a [ href "#", onClickD (SaveAct 2) ] [ text "Delete" ] + ] + , h3 [] [ text <| if model.saveAct == 0 then "Save current filter" else if model.saveAct == 1 then "Load filter" else "Delete saved filter" ] + ] + , case (model.saved, model.saveAct) of + (_, 0) -> + if encQ == "" then text "Nothing to save." else + form_ "" SaveSave False + [ inputText "advsearch_saveinput" model.saveName SaveName [ required True, maxlength 50, placeholder "Name...", style "width" "245px" ] + , if List.any (\e -> e.name == model.saveName) model.saved + then text "You already have a filter by that name, click save to overwrite it." + else text "" + , submitButton "Save" model.saveState True + ] + ([], _) -> text "You don't have any saved queries." + (l, 1) -> + div [] + [ if encQ == "" || List.any (\e -> encQ == e.query) l + then text "" else text "Unsaved changes will be lost when loading a saved filter." + , ul [] <| List.map (\e -> li [ style "overflow" "hidden", style "text-overflow" "ellipsis" ] [ a [ href "#", onClickD (SaveLoad e.query) ] [ text e.name ] ]) l + ] + (l, _) -> + div [] + [ ul [] <| List.map (\e -> li [ style "overflow" "hidden", style "text-overflow" "ellipsis" ] [ linkRadio (Set.member e.name model.saveDel) (always (SaveDelSel e.name)) [ text e.name ] ]) model.saved + , inputButton "Delete selected" SaveDel [ disabled (Set.isEmpty model.saveDel) ] + ] + ] + ] + , input [ type_ "submit", class "submit", value "Search" ] [] + ] ] diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm index 3e69a4e1..514ac6d2 100644 --- a/elm/Lib/Api.elm +++ b/elm/Lib/Api.elm @@ -58,6 +58,7 @@ showResponse res = CharResult _ -> unexp AnimeResult _ -> unexp ImageResult _ -> unexp + AdvSearchQuery _ -> unexp expectResponse : (Response -> msg) -> Http.Expect msg |