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); + } +});