From c6032ff27965d2fa310436d750ca1efd127aafde Mon Sep 17 00:00:00 2001
From: Eike Kettner <eike.kettner@posteo.de>
Date: Sun, 7 Mar 2021 23:46:31 +0100
Subject: [PATCH] Check query in client

---
 .../src/main/elm/Comp/PowerSearchInput.elm    | 177 ++++++++++++++++++
 .../webapp/src/main/elm/Data/ItemQuery.elm    |   4 +-
 .../src/main/elm/Data/QueryParseResult.elm    |  14 ++
 .../webapp/src/main/elm/Page/Home/Data.elm    |   9 +-
 .../webapp/src/main/elm/Page/Home/Update.elm  |  24 ++-
 .../webapp/src/main/elm/Page/Home/View2.elm   |  18 +-
 modules/webapp/src/main/elm/Ports.elm         |  12 +-
 modules/webapp/src/main/elm/Styles.elm        |   7 +-
 modules/webapp/src/main/webjar/docspell.js    |  26 +++
 9 files changed, 267 insertions(+), 24 deletions(-)
 create mode 100644 modules/webapp/src/main/elm/Comp/PowerSearchInput.elm
 create mode 100644 modules/webapp/src/main/elm/Data/QueryParseResult.elm

diff --git a/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm
new file mode 100644
index 00000000..2a868fa0
--- /dev/null
+++ b/modules/webapp/src/main/elm/Comp/PowerSearchInput.elm
@@ -0,0 +1,177 @@
+module Comp.PowerSearchInput exposing
+    ( Action(..)
+    , Model
+    , Msg
+    , init
+    , update
+    , viewInput
+    , viewResult
+    )
+
+import Data.DropdownStyle
+import Data.QueryParseResult exposing (QueryParseResult)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onInput)
+import Ports
+import Styles as S
+import Throttle exposing (Throttle)
+import Time
+import Util.Html exposing (KeyCode(..))
+import Util.Maybe
+
+
+type alias Model =
+    { input : Maybe String
+    , result : QueryParseResult
+    , parseThrottle : Throttle Msg
+    }
+
+
+init : Model
+init =
+    { input = Nothing
+    , result = Data.QueryParseResult.success
+    , parseThrottle = Throttle.create 1
+    }
+
+
+type Msg
+    = SetSearch String
+    | KeyUpMsg (Maybe KeyCode)
+    | ParseResultMsg QueryParseResult
+    | UpdateThrottle
+
+
+type Action
+    = NoAction
+    | SubmitSearch
+
+
+type alias Result =
+    { model : Model
+    , cmd : Cmd Msg
+    , action : Action
+    , subs : Sub Msg
+    }
+
+
+
+--- Update
+
+
+update : Msg -> Model -> Result
+update msg model =
+    case msg of
+        SetSearch str ->
+            let
+                parseCmd =
+                    Ports.checkSearchQueryString str
+
+                parseSub =
+                    Ports.receiveCheckQueryResult ParseResultMsg
+
+                ( newThrottle, cmd ) =
+                    Throttle.try parseCmd model.parseThrottle
+
+                model_ =
+                    { model
+                        | input = Util.Maybe.fromString str
+                        , parseThrottle = newThrottle
+                        , result =
+                            if str == "" then
+                                Data.QueryParseResult.success
+
+                            else
+                                model.result
+                    }
+            in
+            { model = model_
+            , cmd = cmd
+            , action = NoAction
+            , subs = Sub.batch [ throttleUpdate model_, parseSub ]
+            }
+
+        KeyUpMsg (Just Enter) ->
+            Result model Cmd.none SubmitSearch Sub.none
+
+        KeyUpMsg _ ->
+            let
+                parseSub =
+                    Ports.receiveCheckQueryResult ParseResultMsg
+            in
+            Result model Cmd.none NoAction (Sub.batch [ throttleUpdate model, parseSub ])
+
+        ParseResultMsg lm ->
+            Result { model | result = lm } Cmd.none NoAction Sub.none
+
+        UpdateThrottle ->
+            let
+                parseSub =
+                    Ports.receiveCheckQueryResult ParseResultMsg
+
+                ( newThrottle, cmd ) =
+                    Throttle.update model.parseThrottle
+
+                model_ =
+                    { model | parseThrottle = newThrottle }
+            in
+            { model = model_
+            , cmd = cmd
+            , action = NoAction
+            , subs = Sub.batch [ throttleUpdate model_, parseSub ]
+            }
+
+
+throttleUpdate : Model -> Sub Msg
+throttleUpdate model =
+    Throttle.ifNeeded
+        (Time.every 100 (\_ -> UpdateThrottle))
+        model.parseThrottle
+
+
+
+--- View
+
+
+viewInput : List (Attribute Msg) -> Model -> Html Msg
+viewInput attrs model =
+    input
+        (attrs
+            ++ [ type_ "text"
+               , placeholder "Search query …"
+               , onInput SetSearch
+               , Util.Html.onKeyUpCode KeyUpMsg
+               , Maybe.map value model.input
+                    |> Maybe.withDefault (value "")
+               , class S.textInput
+               , class "text-sm "
+               ]
+        )
+        []
+
+
+viewResult : List ( String, Bool ) -> Model -> Html Msg
+viewResult classes model =
+    div
+        [ classList [ ( "hidden", model.result.success ) ]
+        , classList classes
+        , class resultStyle
+        ]
+        [ p [ class "font-mono text-sm" ]
+            [ text model.result.input
+            ]
+        , pre [ class "font-mono text-sm" ]
+            [ List.repeat model.result.index " "
+                |> String.join ""
+                |> text
+            , text "^"
+            ]
+        , ul []
+            (List.map (\line -> li [] [ text line ]) model.result.messages)
+        ]
+
+
+resultStyle : String
+resultStyle =
+    S.warnMessageColors ++ " absolute left-0 max-h-44 w-full overflow-y-auto z-50 shadow-lg transition duration-200 top-9 border-0 border-b border-l border-r rounded-b px-2 py-2"
diff --git a/modules/webapp/src/main/elm/Data/ItemQuery.elm b/modules/webapp/src/main/elm/Data/ItemQuery.elm
index d01464b8..54a54e8e 100644
--- a/modules/webapp/src/main/elm/Data/ItemQuery.elm
+++ b/modules/webapp/src/main/elm/Data/ItemQuery.elm
@@ -106,8 +106,8 @@ render q =
                     "="
 
         quoteStr =
-            --TODO escape quotes
-            surround "\""
+            String.replace "\"" "\\\""
+                >> surround "\""
     in
     case q of
         And inner ->
diff --git a/modules/webapp/src/main/elm/Data/QueryParseResult.elm b/modules/webapp/src/main/elm/Data/QueryParseResult.elm
new file mode 100644
index 00000000..bb0046ba
--- /dev/null
+++ b/modules/webapp/src/main/elm/Data/QueryParseResult.elm
@@ -0,0 +1,14 @@
+module Data.QueryParseResult exposing (QueryParseResult, success)
+
+
+type alias QueryParseResult =
+    { success : Bool
+    , input : String
+    , index : Int
+    , messages : List String
+    }
+
+
+success : QueryParseResult
+success =
+    QueryParseResult True "" 0 []
diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm
index 809d2526..7e6246cb 100644
--- a/modules/webapp/src/main/elm/Page/Home/Data.elm
+++ b/modules/webapp/src/main/elm/Page/Home/Data.elm
@@ -27,6 +27,7 @@ import Comp.ItemCardList
 import Comp.ItemDetail.FormChange exposing (FormChange)
 import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..))
 import Comp.LinkTarget exposing (LinkTarget)
+import Comp.PowerSearchInput
 import Comp.SearchMenu
 import Comp.YesNoDimmer
 import Data.Flags exposing (Flags)
@@ -56,7 +57,7 @@ type alias Model =
     , dragDropData : DD.DragDropData
     , scrollToCard : Maybe String
     , searchStats : SearchStats
-    , powerSearchInput : Maybe String
+    , powerSearchInput : Comp.PowerSearchInput.Model
     }
 
 
@@ -122,7 +123,7 @@ init flags viewMode =
     , scrollToCard = Nothing
     , viewMode = viewMode
     , searchStats = Api.Model.SearchStats.empty
-    , powerSearchInput = Nothing
+    , powerSearchInput = Comp.PowerSearchInput.init
     }
 
 
@@ -196,7 +197,7 @@ type Msg
     | SetLinkTarget LinkTarget
     | SearchStatsResp (Result Http.Error SearchStats)
     | TogglePreviewFullWidth
-    | SetPowerSearch String
+    | PowerSearchMsg Comp.PowerSearchInput.Msg
     | KeyUpPowerSearchbarMsg (Maybe KeyCode)
 
 
@@ -247,7 +248,7 @@ doSearchDefaultCmd param model =
             Q.request <|
                 Q.and
                     [ Comp.SearchMenu.getItemQuery model.searchMenuModel
-                    , Maybe.map Q.Fragment model.powerSearchInput
+                    , Maybe.map Q.Fragment model.powerSearchInput.input
                     ]
 
         mask =
diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm
index d161d58c..9d8efee9 100644
--- a/modules/webapp/src/main/elm/Page/Home/Update.elm
+++ b/modules/webapp/src/main/elm/Page/Home/Update.elm
@@ -1,15 +1,14 @@
 module Page.Home.Update exposing (update)
 
 import Api
-import Api.Model.IdList exposing (IdList)
 import Api.Model.ItemLightList exposing (ItemLightList)
-import Api.Model.ItemQuery
 import Browser.Navigation as Nav
 import Comp.FixedDropdown
 import Comp.ItemCardList
 import Comp.ItemDetail.FormChange exposing (FormChange(..))
 import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..))
 import Comp.LinkTarget exposing (LinkTarget)
+import Comp.PowerSearchInput
 import Comp.SearchMenu
 import Comp.YesNoDimmer
 import Data.Flags exposing (Flags)
@@ -54,7 +53,7 @@ update mId key flags settings msg model =
         ResetSearch ->
             let
                 nm =
-                    { model | searchOffset = 0, powerSearchInput = Nothing }
+                    { model | searchOffset = 0, powerSearchInput = Comp.PowerSearchInput.init }
             in
             update mId key flags settings (SearchMenuMsg Comp.SearchMenu.ResetForm) nm
 
@@ -580,8 +579,23 @@ update mId key flags settings msg model =
             in
             noSub ( model, cmd )
 
-        SetPowerSearch str ->
-            noSub ( { model | powerSearchInput = Util.Maybe.fromString str }, Cmd.none )
+        PowerSearchMsg lm ->
+            let
+                result =
+                    Comp.PowerSearchInput.update lm model.powerSearchInput
+
+                cmd_ =
+                    Cmd.map PowerSearchMsg result.cmd
+
+                model_ =
+                    { model | powerSearchInput = result.model }
+            in
+            case result.action of
+                Comp.PowerSearchInput.NoAction ->
+                    ( model_, cmd_, Sub.map PowerSearchMsg result.subs )
+
+                Comp.PowerSearchInput.SubmitSearch ->
+                    update mId key flags settings (DoSearch model_.searchTypeDropdownValue) model_
 
         KeyUpPowerSearchbarMsg (Just Enter) ->
             update mId key flags settings (DoSearch model.searchTypeDropdownValue) model
diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm
index 82db65c7..4b6d6f8e 100644
--- a/modules/webapp/src/main/elm/Page/Home/View2.elm
+++ b/modules/webapp/src/main/elm/Page/Home/View2.elm
@@ -3,6 +3,7 @@ module Page.Home.View2 exposing (viewContent, viewSidebar)
 import Comp.Basic as B
 import Comp.ItemCardList
 import Comp.MenuBar as MB
+import Comp.PowerSearchInput
 import Comp.SearchMenu
 import Comp.SearchStatsView
 import Comp.YesNoDimmer
@@ -135,17 +136,12 @@ defaultMenuBar _ settings model =
         powerSearchBar =
             div
                 [ class "relative flex flex-grow flex-row" ]
-                [ input
-                    [ type_ "text"
-                    , placeholder "Search query …"
-                    , onInput SetPowerSearch
-                    , Util.Html.onKeyUpCode KeyUpPowerSearchbarMsg
-                    , Maybe.map value model.powerSearchInput
-                        |> Maybe.withDefault (value "")
-                    , class S.textInput
-                    , class "text-sm "
-                    ]
-                    []
+                [ Html.map PowerSearchMsg
+                    (Comp.PowerSearchInput.viewInput []
+                        model.powerSearchInput
+                    )
+                , Html.map PowerSearchMsg
+                    (Comp.PowerSearchInput.viewResult [] model.powerSearchInput)
                 ]
     in
     MB.view
diff --git a/modules/webapp/src/main/elm/Ports.elm b/modules/webapp/src/main/elm/Ports.elm
index a8874539..9f630a81 100644
--- a/modules/webapp/src/main/elm/Ports.elm
+++ b/modules/webapp/src/main/elm/Ports.elm
@@ -1,8 +1,10 @@
 port module Ports exposing
-    ( getUiSettings
+    ( checkSearchQueryString
+    , getUiSettings
     , initClipboard
     , loadUiSettings
     , onUiSettingsSaved
+    , receiveCheckQueryResult
     , removeAccount
     , setAccount
     , setUiTheme
@@ -10,7 +12,9 @@ port module Ports exposing
     )
 
 import Api.Model.AuthResult exposing (AuthResult)
+import Api.Model.BasicResult exposing (BasicResult)
 import Data.Flags exposing (Flags)
+import Data.QueryParseResult exposing (QueryParseResult)
 import Data.UiSettings exposing (StoredUiSettings, UiSettings)
 import Data.UiTheme exposing (UiTheme)
 
@@ -38,6 +42,12 @@ port uiSettingsSaved : (() -> msg) -> Sub msg
 port internalSetUiTheme : String -> Cmd msg
 
 
+port checkSearchQueryString : String -> Cmd msg
+
+
+port receiveCheckQueryResult : (QueryParseResult -> msg) -> Sub msg
+
+
 setUiTheme : UiTheme -> Cmd msg
 setUiTheme theme =
     internalSetUiTheme (Data.UiTheme.toString theme)
diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm
index 56a727bc..2d917c49 100644
--- a/modules/webapp/src/main/elm/Styles.elm
+++ b/modules/webapp/src/main/elm/Styles.elm
@@ -43,7 +43,12 @@ errorMessage =
 
 warnMessage : String
 warnMessage =
-    " border border-yellow-800 bg-yellow-50 text-yellow-800 dark:border-amber-200 dark:bg-amber-800 dark:text-amber-200 dark:bg-opacity-25 px-2 py-2 rounded "
+    warnMessageColors ++ " border dark:bg-opacity-25 px-2 py-2 rounded "
+
+
+warnMessageColors : String
+warnMessageColors =
+    " border-yellow-800 bg-yellow-50 text-yellow-800 dark:border-amber-200 dark:bg-amber-800 dark:text-amber-200 "
 
 
 infoMessage : String
diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js
index 17b1901f..d6fcc4c6 100644
--- a/modules/webapp/src/main/webjar/docspell.js
+++ b/modules/webapp/src/main/webjar/docspell.js
@@ -97,3 +97,29 @@ elmApp.ports.initClipboard.subscribe(function(args) {
         docspell_clipboards[page] = new ClipboardJS(sel);
     }
 });
+
+elmApp.ports.checkSearchQueryString.subscribe(function(args) {
+    var qStr = args;
+    if (qStr && DsItemQueryParser && DsItemQueryParser['parseToFailure']) {
+        var result = DsItemQueryParser.parseToFailure(qStr);
+        var answer;
+        if (result) {
+            answer =
+                { success: false,
+                  input: result.input,
+                  index: result.failedAt,
+                  messages: result.messages
+                };
+
+        } else {
+            answer =
+                { success: true,
+                  input: qStr,
+                  index: 0,
+                  messages: []
+                };
+        }
+        console.log("Sending: " + answer.success);
+        elmApp.ports.receiveCheckQueryResult.send(answer);
+    }
+});