SearchMenu uses query string instead of json form

This commit is contained in:
Eike Kettner 2021-03-01 17:01:18 +01:00
parent f8307f77c6
commit 889e4f4fb0
7 changed files with 305 additions and 95 deletions

View File

@ -1696,22 +1696,22 @@ itemIndexSearch flags query receive =
} }
itemSearch : Flags -> ItemSearch -> (Result Http.Error ItemLightList -> msg) -> Cmd msg itemSearch : Flags -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg
itemSearch flags search receive = itemSearch flags search receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/item/searchFormWithTags" { url = flags.config.baseUrl ++ "/api/v1/sec/item/search"
, account = getAccount flags , account = getAccount flags
, body = Http.jsonBody (Api.Model.ItemSearch.encode search) , body = Http.jsonBody (Api.Model.ItemQuery.encode search)
, expect = Http.expectJson receive Api.Model.ItemLightList.decoder , expect = Http.expectJson receive Api.Model.ItemLightList.decoder
} }
itemSearchStats : Flags -> ItemSearch -> (Result Http.Error SearchStats -> msg) -> Cmd msg itemSearchStats : Flags -> ItemQuery -> (Result Http.Error SearchStats -> msg) -> Cmd msg
itemSearchStats flags search receive = itemSearchStats flags search receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/item/searchFormStats" { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchStats"
, account = getAccount flags , account = getAccount flags
, body = Http.jsonBody (Api.Model.ItemSearch.encode search) , body = Http.jsonBody (Api.Model.ItemQuery.encode search)
, expect = Http.expectJson receive Api.Model.SearchStats.decoder , expect = Http.expectJson receive Api.Model.SearchStats.decoder
} }

View File

@ -3,7 +3,7 @@ module Comp.SearchMenu exposing
, Msg(..) , Msg(..)
, NextState , NextState
, TextSearchModel , TextSearchModel
, getItemSearch , getItemQuery
, init , init
, isFulltextSearch , isFulltextSearch
, isNamesSearch , isNamesSearch
@ -21,7 +21,7 @@ import Api.Model.EquipmentList exposing (EquipmentList)
import Api.Model.FolderStats exposing (FolderStats) import Api.Model.FolderStats exposing (FolderStats)
import Api.Model.IdName exposing (IdName) import Api.Model.IdName exposing (IdName)
import Api.Model.ItemFieldValue exposing (ItemFieldValue) import Api.Model.ItemFieldValue exposing (ItemFieldValue)
import Api.Model.ItemSearch exposing (ItemSearch) import Api.Model.ItemQuery exposing (ItemQuery)
import Api.Model.PersonList exposing (PersonList) import Api.Model.PersonList exposing (PersonList)
import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.ReferenceList exposing (ReferenceList)
import Api.Model.SearchStats exposing (SearchStats) import Api.Model.SearchStats exposing (SearchStats)
@ -38,6 +38,7 @@ import Data.DropdownStyle as DS
import Data.Fields import Data.Fields
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.Icons as Icons import Data.Icons as Icons
import Data.ItemQuery as Q exposing (ItemQuery)
import Data.PersonUse import Data.PersonUse
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import DatePicker exposing (DatePicker) import DatePicker exposing (DatePicker)
@ -234,11 +235,21 @@ getDirection model =
Nothing Nothing
getItemSearch : Model -> ItemSearch getItemQuery : Model -> Maybe ItemQuery
getItemSearch model = getItemQuery model =
let let
e = when flag body =
Api.Model.ItemSearch.empty if flag then
Just body
else
Nothing
whenNot flag body =
when (not flag) body
whenNotEmpty list f =
whenNot (List.isEmpty list) (f list)
amendWildcards s = amendWildcards s =
if String.startsWith "\"" s && String.endsWith "\"" s then if String.startsWith "\"" s && String.endsWith "\"" s then
@ -254,35 +265,52 @@ getItemSearch model =
textSearch = textSearch =
textSearchValue model.textSearchModel textSearchValue model.textSearchModel
in in
{ e Q.and
| tagsInclude = model.tagSelection.includeTags |> List.map .tag |> List.map .id [ when model.inboxCheckbox (Q.Inbox True)
, tagsExclude = model.tagSelection.excludeTags |> List.map .tag |> List.map .id , whenNotEmpty (model.tagSelection.includeTags |> List.map (.tag >> .id))
, corrPerson = Comp.Dropdown.getSelected model.corrPersonModel |> List.map .id |> List.head (Q.TagIds Q.AllMatch)
, corrOrg = Comp.Dropdown.getSelected model.orgModel |> List.map .id |> List.head , whenNotEmpty (model.tagSelection.excludeTags |> List.map (.tag >> .id))
, concPerson = Comp.Dropdown.getSelected model.concPersonModel |> List.map .id |> List.head (\ids -> Q.Not (Q.TagIds Q.AnyMatch ids))
, concEquip = Comp.Dropdown.getSelected model.concEquipmentModel |> List.map .id |> List.head , whenNotEmpty (model.tagSelection.includeCats |> List.map .name)
, folder = model.selectedFolder |> Maybe.map .id (Q.CatNames Q.AllMatch)
, direction = , whenNotEmpty (model.tagSelection.excludeCats |> List.map .name)
Comp.Dropdown.getSelected model.directionModel (\ids -> Q.Not <| Q.CatNames Q.AnyMatch ids)
|> List.head , model.selectedFolder |> Maybe.map .id |> Maybe.map (Q.FolderId Q.Eq)
|> Maybe.map Data.Direction.toString , Comp.Dropdown.getSelected model.orgModel
, inbox = model.inboxCheckbox |> List.map .id
, dateFrom = model.fromDate |> List.head
, dateUntil = model.untilDate |> Maybe.map (Q.CorrOrgId Q.Eq)
, dueDateFrom = model.fromDueDate , Comp.Dropdown.getSelected model.corrPersonModel
, dueDateUntil = model.untilDueDate |> List.map .id
, name = |> List.head
model.nameModel |> Maybe.map (Q.CorrPersId Q.Eq)
|> Maybe.map amendWildcards , Comp.Dropdown.getSelected model.concPersonModel
, allNames = |> List.map .id
textSearch.nameSearch |> List.head
|> Maybe.map amendWildcards |> Maybe.map (Q.ConcPersId Q.Eq)
, fullText = textSearch.fullText , Comp.Dropdown.getSelected model.concEquipmentModel
, tagCategoriesInclude = model.tagSelection.includeCats |> List.map .name |> List.map .id
, tagCategoriesExclude = model.tagSelection.excludeCats |> List.map .name |> List.head
, customValues = Data.CustomFieldChange.toFieldValues model.customValues |> Maybe.map (Q.ConcEquipId Q.Eq)
, source = model.sourceModel , whenNotEmpty (Data.CustomFieldChange.toFieldValues model.customValues)
} (List.map (Q.CustomField Q.Like) >> Q.And)
, Maybe.map (Q.DateMs Q.Gte) model.fromDate
, Maybe.map (Q.DateMs Q.Lte) model.untilDate
, Maybe.map (Q.DueDateMs Q.Gte) model.fromDueDate
, Maybe.map (Q.DueDateMs Q.Lte) model.untilDueDate
, Maybe.map (Q.Source Q.Like) model.sourceModel
, model.nameModel
|> Maybe.map amendWildcards
|> Maybe.map (Q.ItemName Q.Like)
, textSearch.nameSearch
|> Maybe.map amendWildcards
|> Maybe.map Q.AllNames
, Comp.Dropdown.getSelected model.directionModel
|> List.head
|> Maybe.map Q.Dir
, textSearch.fullText
|> Maybe.map Q.Contents
]
resetModel : Model -> Model resetModel : Model -> Model
@ -437,7 +465,7 @@ updateDrop ddm flags settings msg model =
{ model = mdp { model = mdp
, cmd = , cmd =
Cmd.batch Cmd.batch
[ Api.itemSearchStats flags Api.Model.ItemSearch.empty GetAllTagsResp [ Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp
, Api.getOrgLight flags GetOrgResp , Api.getOrgLight flags GetOrgResp
, Api.getEquipments flags "" GetEquipResp , Api.getEquipments flags "" GetEquipResp
, Api.getPersons flags "" GetPersonResp , Api.getPersons flags "" GetPersonResp
@ -450,7 +478,7 @@ updateDrop ddm flags settings msg model =
ResetForm -> ResetForm ->
{ model = resetModel model { model = resetModel model
, cmd = Api.itemSearchStats flags Api.Model.ItemSearch.empty GetAllTagsResp , cmd = Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp
, stateChange = True , stateChange = True
, dragDrop = DD.DragDropData ddm Nothing , dragDrop = DD.DragDropData ddm Nothing
} }

View File

@ -10,7 +10,6 @@ module Data.CustomFieldChange exposing
import Api.Model.CustomField exposing (CustomField) import Api.Model.CustomField exposing (CustomField)
import Api.Model.CustomFieldValue exposing (CustomFieldValue) import Api.Model.CustomFieldValue exposing (CustomFieldValue)
import Api.Model.ItemFieldValue exposing (ItemFieldValue)
import Dict exposing (Dict) import Dict exposing (Dict)

View File

@ -0,0 +1,202 @@
module Data.ItemQuery exposing
( AttrMatch(..)
, ItemQuery(..)
, TagMatch(..)
, and
, render
, renderMaybe
, request
)
{-| Models the query language for the purpose of generating a query string.
-}
import Api.Model.CustomFieldValue exposing (CustomFieldValue)
import Api.Model.ItemQuery as RQ
import Data.Direction exposing (Direction)
type TagMatch
= AnyMatch
| AllMatch
type AttrMatch
= Eq
| Neq
| Lt
| Gt
| Lte
| Gte
| Like
type ItemQuery
= Inbox Bool
| And (List ItemQuery)
| Or (List ItemQuery)
| Not ItemQuery
| TagIds TagMatch (List String)
| CatNames TagMatch (List String)
| FolderId AttrMatch String
| CorrOrgId AttrMatch String
| CorrPersId AttrMatch String
| ConcPersId AttrMatch String
| ConcEquipId AttrMatch String
| CustomField AttrMatch CustomFieldValue
| DateMs AttrMatch Int
| DueDateMs AttrMatch Int
| Source AttrMatch String
| Dir Direction
| ItemIdIn (List String)
| ItemName AttrMatch String
| AllNames String
| Contents String
and : List (Maybe ItemQuery) -> Maybe ItemQuery
and list =
case List.filterMap identity list of
[] ->
Nothing
es ->
Just (And es)
request : Maybe ItemQuery -> RQ.ItemQuery
request mq =
{ offset = Nothing
, limit = Nothing
, withDetails = Just True
, query = renderMaybe mq
}
renderMaybe : Maybe ItemQuery -> String
renderMaybe mq =
Maybe.map render mq
|> Maybe.withDefault ""
render : ItemQuery -> String
render q =
let
boolStr flag =
if flag then
"yes"
else
"no"
between left right str =
left ++ str ++ right
surround lr str =
between lr lr str
tagMatchStr tm =
case tm of
AnyMatch ->
":"
AllMatch ->
"="
quoteStr =
--TODO escape quotes
surround "\""
in
case q of
And inner ->
List.map render inner
|> String.join " "
|> between "(& " " )"
Or inner ->
List.map render inner
|> String.join " "
|> between "(| " " )"
Not inner ->
"!" ++ render inner
Inbox flag ->
"inbox:" ++ boolStr flag
TagIds m ids ->
List.map quoteStr ids
|> String.join ","
|> between ("tag.id" ++ tagMatchStr m) ""
CatNames m ids ->
List.map quoteStr ids
|> String.join ","
|> between ("cat" ++ tagMatchStr m) ""
FolderId m id ->
"folder.id" ++ attrMatch m ++ quoteStr id
CorrOrgId m id ->
"correspondent.org.id" ++ attrMatch m ++ quoteStr id
CorrPersId m id ->
"correspondent.person.id" ++ attrMatch m ++ quoteStr id
ConcPersId m id ->
"concerning.person.id" ++ attrMatch m ++ quoteStr id
ConcEquipId m id ->
"concerning.equip.id" ++ attrMatch m ++ quoteStr id
CustomField m kv ->
"f:" ++ kv.field ++ attrMatch m ++ quoteStr kv.value
DateMs m ms ->
"date" ++ attrMatch m ++ "ms" ++ String.fromInt ms
DueDateMs m ms ->
"due" ++ attrMatch m ++ "ms" ++ String.fromInt ms
Source m str ->
"source" ++ attrMatch m ++ quoteStr str
Dir dir ->
"incoming:" ++ boolStr (dir == Data.Direction.Incoming)
ItemIdIn ids ->
"id~=" ++ String.join "," ids
ItemName m str ->
"name" ++ attrMatch m ++ quoteStr str
AllNames str ->
"$names:" ++ quoteStr str
Contents str ->
"content:" ++ quoteStr str
attrMatch : AttrMatch -> String
attrMatch am =
case am of
Eq ->
"="
Neq ->
"!="
Like ->
":"
Gt ->
">"
Gte ->
">="
Lt ->
"<"
Lte ->
"<="

View File

@ -31,6 +31,7 @@ import Comp.SearchMenu
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemNav exposing (ItemNav) import Data.ItemNav exposing (ItemNav)
import Data.ItemQuery as Q
import Data.Items import Data.Items
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Http import Http
@ -239,12 +240,13 @@ doSearchDefaultCmd : SearchParam -> Model -> Cmd Msg
doSearchDefaultCmd param model = doSearchDefaultCmd param model =
let let
smask = smask =
Comp.SearchMenu.getItemSearch model.searchMenuModel Q.request
(Comp.SearchMenu.getItemQuery model.searchMenuModel)
mask = mask =
{ smask { smask
| limit = param.pageSize | limit = Just param.pageSize
, offset = param.offset , offset = Just param.offset
} }
in in
if param.offset == 0 then if param.offset == 0 then

View File

@ -3,7 +3,7 @@ module Page.Home.Update exposing (update)
import Api import Api
import Api.Model.IdList exposing (IdList) import Api.Model.IdList exposing (IdList)
import Api.Model.ItemLightList exposing (ItemLightList) import Api.Model.ItemLightList exposing (ItemLightList)
import Api.Model.ItemSearch import Api.Model.ItemQuery
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Comp.FixedDropdown import Comp.FixedDropdown
import Comp.ItemCardList import Comp.ItemCardList
@ -13,6 +13,7 @@ import Comp.LinkTarget exposing (LinkTarget)
import Comp.SearchMenu import Comp.SearchMenu
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemQuery as Q
import Data.ItemSelection import Data.ItemSelection
import Data.Items import Data.Items
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
@ -648,16 +649,15 @@ loadChangedItems flags ids =
else else
let let
searchInit =
Api.Model.ItemSearch.empty
idList = idList =
IdList (Set.toList ids) Set.toList ids
searchInit =
Q.request (Just <| Q.ItemIdIn idList)
search = search =
{ searchInit { searchInit
| itemSubset = Just idList | limit = Just <| Set.size ids
, limit = Set.size ids
} }
in in
Api.itemSearch flags search ReplaceChangedItemsResp Api.itemSearch flags search ReplaceChangedItemsResp

View File

@ -320,8 +320,9 @@ viewSearchBar flags model =
[ a [ a
[ classList [ classList
[ ( "search-menu-toggle ui icon button", True ) [ ( "search-menu-toggle ui icon button", True )
, ( "primary", not (searchMenuFilled model) )
, ( "secondary", searchMenuFilled model ) -- , ( "primary", not (searchMenuFilled model) )
-- , ( "secondary", searchMenuFilled model )
] ]
, onClick ToggleSearchMenu , onClick ToggleSearchMenu
, href "#" , href "#"
@ -332,24 +333,23 @@ viewSearchBar flags model =
, div [ class "right menu" ] , div [ class "right menu" ]
[ div [ class "fitted item" ] [ div [ class "fitted item" ]
[ div [ class "ui left icon right action input" ] [ div [ class "ui left icon right action input" ]
[ i [ -- i
[ classList -- [ classList
[ ( "search link icon", not model.searchInProgress ) -- [ ( "search link icon", not model.searchInProgress )
, ( "loading spinner icon", model.searchInProgress ) -- , ( "loading spinner icon", model.searchInProgress )
] -- ]
, href "#" -- , href "#"
, onClick (DoSearch model.searchTypeDropdownValue) -- , onClick (DoSearch model.searchTypeDropdownValue)
] -- ]
(if hasMoreSearch model then -- (if hasMoreSearch model then
[ i [ class "icons search-corner-icons" ] -- [ i [ class "icons search-corner-icons" ]
[ i [ class "tiny blue circle icon" ] [] -- [ i [ class "tiny blue circle icon" ] []
] -- ]
] -- ]
-- else
else -- []
[] -- )
) input
, input
[ type_ "text" [ type_ "text"
, placeholder , placeholder
(case model.searchTypeDropdownValue of (case model.searchTypeDropdownValue of
@ -384,27 +384,6 @@ viewSearchBar flags model =
] ]
searchMenuFilled : Model -> Bool
searchMenuFilled model =
let
is =
Comp.SearchMenu.getItemSearch model.searchMenuModel
in
is /= Api.Model.ItemSearch.empty
hasMoreSearch : Model -> Bool
hasMoreSearch model =
let
is =
Comp.SearchMenu.getItemSearch model.searchMenuModel
is_ =
{ is | allNames = Nothing, fullText = Nothing }
in
is_ /= Api.Model.ItemSearch.empty
deleteAllDimmer : Comp.YesNoDimmer.Settings deleteAllDimmer : Comp.YesNoDimmer.Settings
deleteAllDimmer = deleteAllDimmer =
{ message = "Really delete all selected items?" { message = "Really delete all selected items?"