diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index a9619b1e..b5323195 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -113,6 +113,7 @@ module Api exposing , restoreAllItems , restoreItem , saveClientSettings + , searchShare , sendMail , setAttachmentName , setCollectiveSettings @@ -155,6 +156,7 @@ module Api exposing , upload , uploadAmend , uploadSingle + , verifyShare , versionInfo ) @@ -223,6 +225,8 @@ import Api.Model.SentMails exposing (SentMails) import Api.Model.ShareData exposing (ShareData) import Api.Model.ShareDetail exposing (ShareDetail) import Api.Model.ShareList exposing (ShareList) +import Api.Model.ShareSecret exposing (ShareSecret) +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) import Api.Model.SimpleMail exposing (SimpleMail) import Api.Model.SourceAndTags exposing (SourceAndTags) import Api.Model.SourceList exposing (SourceList) @@ -2264,6 +2268,26 @@ deleteShare flags id receive = } +verifyShare : Flags -> ShareSecret -> (Result Http.Error ShareVerifyResult -> msg) -> Cmd msg +verifyShare flags secret receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/open/share/verify" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.ShareSecret.encode secret) + , expect = Http.expectJson receive Api.Model.ShareVerifyResult.decoder + } + + +searchShare : Flags -> String -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg +searchShare flags token search receive = + Http2.sharePost + { url = flags.config.baseUrl ++ "/api/v1/share/search" + , token = token + , body = Http.jsonBody (Api.Model.ItemQuery.encode search) + , expect = Http.expectJson receive Api.Model.ItemLightList.decoder + } + + --- Helper diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 5408c581..2b051d84 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -324,7 +324,7 @@ updateShare lmsg model = Just id -> let result = - Page.Share.Update.update model.flags id lmsg model.shareModel + Page.Share.Update.update model.flags model.uiSettings id lmsg model.shareModel in ( { model | shareModel = result.model } , Cmd.map ShareMsg result.cmd diff --git a/modules/webapp/src/main/elm/App/View2.elm b/modules/webapp/src/main/elm/App/View2.elm index d80be6e3..06b99462 100644 --- a/modules/webapp/src/main/elm/App/View2.elm +++ b/modules/webapp/src/main/elm/App/View2.elm @@ -432,6 +432,7 @@ viewShare texts shareId model = , Html.map ShareMsg (Share.viewContent texts.share model.flags + model.version model.uiSettings model.shareModel ) diff --git a/modules/webapp/src/main/elm/Data/Items.elm b/modules/webapp/src/main/elm/Data/Items.elm index a2459b7b..0940781c 100644 --- a/modules/webapp/src/main/elm/Data/Items.elm +++ b/modules/webapp/src/main/elm/Data/Items.elm @@ -8,6 +8,7 @@ module Data.Items exposing ( concat , first + , flatten , idSet , length , replaceIn @@ -21,6 +22,11 @@ import Set exposing (Set) import Util.List +flatten : ItemLightList -> List ItemLight +flatten list = + List.concatMap .items list.groups + + concat : ItemLightList -> ItemLightList -> ItemLightList concat l0 l1 = let diff --git a/modules/webapp/src/main/elm/Messages/Page/Share.elm b/modules/webapp/src/main/elm/Messages/Page/Share.elm index b6044543..cee461cd 100644 --- a/modules/webapp/src/main/elm/Messages/Page/Share.elm +++ b/modules/webapp/src/main/elm/Messages/Page/Share.elm @@ -7,16 +7,41 @@ module Messages.Page.Share exposing (..) +import Messages.Basics +import Messages.Comp.ItemCardList +import Messages.Comp.SearchMenu + type alias Texts = - {} + { searchMenu : Messages.Comp.SearchMenu.Texts + , basics : Messages.Basics.Texts + , itemCardList : Messages.Comp.ItemCardList.Texts + , passwordRequired : String + , password : String + , passwordSubmitButton : String + , passwordFailed : String + } gb : Texts gb = - {} + { searchMenu = Messages.Comp.SearchMenu.gb + , basics = Messages.Basics.gb + , itemCardList = Messages.Comp.ItemCardList.gb + , passwordRequired = "Password required" + , password = "Password" + , passwordSubmitButton = "Submit" + , passwordFailed = "Das Passwort ist falsch" + } de : Texts de = - {} + { searchMenu = Messages.Comp.SearchMenu.de + , basics = Messages.Basics.de + , itemCardList = Messages.Comp.ItemCardList.de + , passwordRequired = "Passwort benötigt" + , password = "Passwort" + , passwordSubmitButton = "Submit" + , passwordFailed = "Password is wrong" + } diff --git a/modules/webapp/src/main/elm/Page/Share/Data.elm b/modules/webapp/src/main/elm/Page/Share/Data.elm index a0aa5f76..0689e9dd 100644 --- a/modules/webapp/src/main/elm/Page/Share/Data.elm +++ b/modules/webapp/src/main/elm/Page/Share/Data.elm @@ -5,28 +5,83 @@ -} -module Page.Share.Data exposing (Model, Msg, init) +module Page.Share.Data exposing (Mode(..), Model, Msg(..), PageError(..), init) +import Api +import Api.Model.ItemLightList exposing (ItemLightList) +import Api.Model.ShareSecret exposing (ShareSecret) +import Api.Model.ShareVerifyResult exposing (ShareVerifyResult) +import Comp.ItemCardList +import Comp.PowerSearchInput +import Comp.SearchMenu import Data.Flags exposing (Flags) +import Http + + +type Mode + = ModeInitial + | ModePassword + | ModeShare + + +type PageError + = PageErrorNone + | PageErrorHttp Http.Error + | PageErrorAuthFail + + +type alias PasswordModel = + { password : String + , passwordFailed : Bool + } type alias Model = - {} + { mode : Mode + , verifyResult : ShareVerifyResult + , passwordModel : PasswordModel + , pageError : PageError + , items : ItemLightList + , searchMenuModel : Comp.SearchMenu.Model + , powerSearchInput : Comp.PowerSearchInput.Model + , searchInProgress : Bool + , itemListModel : Comp.ItemCardList.Model + } + + +emptyModel : Flags -> Model +emptyModel flags = + { mode = ModeInitial + , verifyResult = Api.Model.ShareVerifyResult.empty + , passwordModel = + { password = "" + , passwordFailed = False + } + , pageError = PageErrorNone + , items = Api.Model.ItemLightList.empty + , searchMenuModel = Comp.SearchMenu.init flags + , powerSearchInput = Comp.PowerSearchInput.init + , searchInProgress = False + , itemListModel = Comp.ItemCardList.init + } init : Maybe String -> Flags -> ( Model, Cmd Msg ) init shareId flags = case shareId of Just id -> - let - _ = - Debug.log "share" id - in - ( {}, Cmd.none ) + ( emptyModel flags, Api.verifyShare flags (ShareSecret id Nothing) VerifyResp ) Nothing -> - ( {}, Cmd.none ) + ( emptyModel flags, Cmd.none ) type Msg - = Msg + = VerifyResp (Result Http.Error ShareVerifyResult) + | SearchResp (Result Http.Error ItemLightList) + | SetPassword String + | SubmitPassword + | SearchMenuMsg Comp.SearchMenu.Msg + | PowerSearchMsg Comp.PowerSearchInput.Msg + | ResetSearch + | ItemListMsg Comp.ItemCardList.Msg diff --git a/modules/webapp/src/main/elm/Page/Share/Menubar.elm b/modules/webapp/src/main/elm/Page/Share/Menubar.elm new file mode 100644 index 00000000..10751839 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Menubar.elm @@ -0,0 +1,60 @@ +module Page.Share.Menubar exposing (view) + +import Comp.Basic as B +import Comp.MenuBar as MB +import Comp.PowerSearchInput +import Comp.SearchMenu +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) +import Styles as S + + +view : Texts -> Model -> Html Msg +view texts model = + let + btnStyle = + S.secondaryBasicButton ++ " text-sm" + + searchInput = + Comp.SearchMenu.textSearchString + model.searchMenuModel.textSearchModel + + powerSearchBar = + div + [ class "relative flex flex-grow flex-row" ] + [ Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewInput + { placeholder = texts.basics.searchPlaceholder + , extraAttrs = [] + } + model.powerSearchInput + ) + , Html.map PowerSearchMsg + (Comp.PowerSearchInput.viewResult [] model.powerSearchInput) + ] + in + MB.view + { end = + [ MB.CustomElement <| + B.secondaryBasicButton + { label = "" + , icon = + if model.searchInProgress then + "fa fa-sync animate-spin" + + else + "fa fa-sync" + , disabled = model.searchInProgress + , handler = onClick ResetSearch + , attrs = [ href "#" ] + } + ] + , start = + [ MB.CustomElement <| + powerSearchBar + ] + , rootClasses = "mb-2 pt-1 dark:bg-bluegray-700 items-center text-sm" + } diff --git a/modules/webapp/src/main/elm/Page/Share/Results.elm b/modules/webapp/src/main/elm/Page/Share/Results.elm new file mode 100644 index 00000000..b513e936 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Results.elm @@ -0,0 +1,23 @@ +module Page.Share.Results exposing (view) + +import Comp.ItemCardList +import Data.ItemSelection +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + viewCfg = + { current = Nothing + , selection = Data.ItemSelection.Inactive + } + in + div [] + [ Html.map ItemListMsg + (Comp.ItemCardList.view2 texts.itemCardList viewCfg settings model.itemListModel) + ] diff --git a/modules/webapp/src/main/elm/Page/Share/Sidebar.elm b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm new file mode 100644 index 00000000..abd0d8f4 --- /dev/null +++ b/modules/webapp/src/main/elm/Page/Share/Sidebar.elm @@ -0,0 +1,32 @@ +module Page.Share.Sidebar exposing (..) + +import Comp.SearchMenu +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Page.Share exposing (Texts) +import Page.Share.Data exposing (Model, Msg(..)) +import Util.ItemDragDrop + + +view : Texts -> Flags -> UiSettings -> Model -> Html Msg +view texts flags settings model = + div + [ class "flex flex-col" + ] + [ Html.map SearchMenuMsg + (Comp.SearchMenu.viewDrop2 texts.searchMenu + ddDummy + flags + settings + model.searchMenuModel + ) + ] + + +ddDummy : Util.ItemDragDrop.DragDropData +ddDummy = + { model = Util.ItemDragDrop.init + , dropped = Nothing + } diff --git a/modules/webapp/src/main/elm/Page/Share/Update.elm b/modules/webapp/src/main/elm/Page/Share/Update.elm index 0f1dadbb..f7be7d99 100644 --- a/modules/webapp/src/main/elm/Page/Share/Update.elm +++ b/modules/webapp/src/main/elm/Page/Share/Update.elm @@ -7,7 +7,15 @@ module Page.Share.Update exposing (UpdateResult, update) +import Api +import Api.Model.ItemQuery +import Comp.ItemCardList +import Comp.PowerSearchInput +import Comp.SearchMenu import Data.Flags exposing (Flags) +import Data.ItemQuery as Q +import Data.SearchMode +import Data.UiSettings exposing (UiSettings) import Page.Share.Data exposing (..) @@ -18,6 +26,161 @@ type alias UpdateResult = } -update : Flags -> String -> Msg -> Model -> UpdateResult -update flags shareId msg model = - UpdateResult model Cmd.none Sub.none +update : Flags -> UiSettings -> String -> Msg -> Model -> UpdateResult +update flags settings shareId msg model = + case msg of + VerifyResp (Ok res) -> + if res.success then + let + eq = + Api.Model.ItemQuery.empty + + iq = + { eq | withDetails = Just True } + in + noSub + ( { model + | pageError = PageErrorNone + , mode = ModeShare + , verifyResult = res + , searchInProgress = True + } + , makeSearchCmd flags model + ) + + else if res.passwordRequired then + if model.mode == ModePassword then + noSub + ( { model + | pageError = PageErrorNone + , passwordModel = + { password = "" + , passwordFailed = True + } + } + , Cmd.none + ) + + else + noSub + ( { model + | pageError = PageErrorNone + , mode = ModePassword + } + , Cmd.none + ) + + else + noSub + ( { model | pageError = PageErrorAuthFail } + , Cmd.none + ) + + VerifyResp (Err err) -> + noSub ( { model | pageError = PageErrorHttp err }, Cmd.none ) + + SearchResp (Ok list) -> + update flags + settings + shareId + (ItemListMsg (Comp.ItemCardList.SetResults list)) + { model | searchInProgress = False } + + SearchResp (Err err) -> + noSub ( { model | pageError = PageErrorHttp err, searchInProgress = False }, Cmd.none ) + + SetPassword pw -> + let + pm = + model.passwordModel + in + noSub ( { model | passwordModel = { pm | password = pw } }, Cmd.none ) + + SubmitPassword -> + let + secret = + { shareId = shareId + , password = Just model.passwordModel.password + } + in + noSub ( model, Api.verifyShare flags secret VerifyResp ) + + SearchMenuMsg lm -> + let + res = + Comp.SearchMenu.update flags settings lm model.searchMenuModel + + nextModel = + { model | searchMenuModel = res.model } + + ( initSearch, searchCmd ) = + if res.stateChange && not model.searchInProgress then + ( True, makeSearchCmd flags nextModel ) + + else + ( False, Cmd.none ) + in + noSub + ( { nextModel | searchInProgress = initSearch } + , Cmd.batch [ Cmd.map SearchMenuMsg res.cmd, searchCmd ] + ) + + PowerSearchMsg lm -> + let + res = + Comp.PowerSearchInput.update lm model.powerSearchInput + + nextModel = + { model | powerSearchInput = res.model } + + ( initSearch, searchCmd ) = + case res.action of + Comp.PowerSearchInput.NoAction -> + ( False, Cmd.none ) + + Comp.PowerSearchInput.SubmitSearch -> + ( True, makeSearchCmd flags nextModel ) + in + { model = { nextModel | searchInProgress = initSearch } + , cmd = Cmd.batch [ Cmd.map PowerSearchMsg res.cmd, searchCmd ] + , sub = Sub.map PowerSearchMsg res.subs + } + + ResetSearch -> + let + nm = + { model | powerSearchInput = Comp.PowerSearchInput.init } + in + update flags settings shareId (SearchMenuMsg Comp.SearchMenu.ResetForm) nm + + ItemListMsg lm -> + let + ( im, ic ) = + Comp.ItemCardList.update flags lm model.itemListModel + in + noSub ( { model | itemListModel = im }, Cmd.map ItemListMsg ic ) + + +noSub : ( Model, Cmd Msg ) -> UpdateResult +noSub ( m, c ) = + UpdateResult m c Sub.none + + +makeSearchCmd : Flags -> Model -> Cmd Msg +makeSearchCmd flags model = + let + xq = + Q.and + [ Comp.SearchMenu.getItemQuery model.searchMenuModel + , Maybe.map Q.Fragment model.powerSearchInput.input + ] + + request mq = + { offset = Nothing + , limit = Nothing + , withDetails = Just True + , query = Q.renderMaybe mq + , searchMode = Just (Data.SearchMode.asString Data.SearchMode.Normal) + } + in + Api.searchShare flags model.verifyResult.token (request xq) SearchResp diff --git a/modules/webapp/src/main/elm/Page/Share/View.elm b/modules/webapp/src/main/elm/Page/Share/View.elm index 0d5cf016..5c0f941b 100644 --- a/modules/webapp/src/main/elm/Page/Share/View.elm +++ b/modules/webapp/src/main/elm/Page/Share/View.elm @@ -7,32 +7,155 @@ module Page.Share.View exposing (viewContent, viewSidebar) +import Api.Model.VersionInfo exposing (VersionInfo) +import Comp.Basic as B import Data.Flags exposing (Flags) +import Data.Items import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onInput, onSubmit) import Messages.Page.Share exposing (Texts) import Page.Share.Data exposing (..) +import Page.Share.Menubar as Menubar +import Page.Share.Results as Results +import Page.Share.Sidebar as Sidebar import Styles as S viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg -viewSidebar _ visible _ _ _ = +viewSidebar texts visible flags settings model = div [ id "sidebar" - , classList [ ( "hidden", not visible ) ] + , class S.sidebar + , class S.sidebarBg + , classList [ ( "hidden", not visible || model.mode /= ModeShare ) ] + ] + [ Sidebar.view texts flags settings model ] - [ text "sidebar" ] -viewContent : Texts -> Flags -> UiSettings -> Model -> Html Msg -viewContent texts flags _ model = +viewContent : Texts -> Flags -> VersionInfo -> UiSettings -> Model -> Html Msg +viewContent texts flags versionInfo uiSettings model = + case model.mode of + ModeInitial -> + div + [ id "content" + , class "h-full w-full flex flex-col text-5xl" + , class S.content + ] + [ B.loadingDimmer + { active = model.pageError == PageErrorNone + , label = "" + } + ] + + ModePassword -> + passwordContent texts flags versionInfo model + + ModeShare -> + mainContent texts flags uiSettings model + + + +--- Helpers + + +mainContent : Texts -> Flags -> UiSettings -> Model -> Html Msg +mainContent texts _ settings model = div [ id "content" , class "h-full flex flex-col" , class S.content ] - [ h1 [ class S.header1 ] - [ text "Share Page!" + [ h1 + [ class S.header1 + , classList [ ( "hidden", model.verifyResult.name == Nothing ) ] + ] + [ text <| Maybe.withDefault "" model.verifyResult.name + ] + , Menubar.view texts model + , Results.view texts settings model + ] + + +passwordContent : Texts -> Flags -> VersionInfo -> Model -> Html Msg +passwordContent texts flags versionInfo model = + div + [ id "content" + , class "h-full flex flex-col items-center justify-center w-full" + , class S.content + ] + [ div [ class ("flex flex-col px-4 sm:px-6 md:px-8 lg:px-10 py-8 rounded-md " ++ S.box) ] + [ div [ class "self-center" ] + [ img + [ class "w-16 py-2" + , src (flags.config.docspellAssetPath ++ "/img/logo-96.png") + ] + [] + ] + , div [ class "font-medium self-center text-xl sm:text-2xl" ] + [ text texts.passwordRequired + ] + , Html.form + [ action "#" + , onSubmit SubmitPassword + , autocomplete False + ] + [ div [ class "flex flex-col my-3" ] + [ label + [ for "password" + , class S.inputLabel + ] + [ text texts.password + ] + , div [ class "relative" ] + [ div [ class S.inputIcon ] + [ i [ class "fa fa-lock" ] [] + ] + , input + [ type_ "password" + , name "password" + , autocomplete False + , autofocus True + , tabindex 1 + , onInput SetPassword + , value model.passwordModel.password + , class ("pl-10 pr-4 py-2 rounded-lg" ++ S.textInput) + , placeholder texts.password + ] + [] + ] + ] + , div [ class "flex flex-col my-3" ] + [ button + [ type_ "submit" + , class S.primaryButton + ] + [ text texts.passwordSubmitButton + ] + ] + , div + [ class S.errorMessage + , classList [ ( "hidden", not model.passwordModel.passwordFailed ) ] + ] + [ text texts.passwordFailed + ] + ] + ] + , a + [ class "inline-flex items-center mt-4 text-xs opacity-50 hover:opacity-90" + , href "https://docspell.org" + , target "_new" + ] + [ img + [ src (flags.config.docspellAssetPath ++ "/img/logo-mc-96.png") + , class "w-3 h-3 mr-1" + ] + [] + , span [] + [ text "Docspell " + , text versionInfo.version + ] ] ] diff --git a/modules/webapp/src/main/elm/Util/Http.elm b/modules/webapp/src/main/elm/Util/Http.elm index 550cbd7a..dd965b23 100644 --- a/modules/webapp/src/main/elm/Util/Http.elm +++ b/modules/webapp/src/main/elm/Util/Http.elm @@ -14,6 +14,7 @@ module Util.Http exposing , authTask , executeIn , jsonResolver + , sharePost ) import Api.Model.AuthResult exposing (AuthResult) @@ -49,6 +50,28 @@ authReq req = } +shareReq : + { url : String + , token : String + , method : String + , headers : List Http.Header + , body : Http.Body + , expect : Http.Expect msg + , tracker : Maybe String + } + -> Cmd msg +shareReq req = + Http.request + { url = req.url + , method = req.method + , headers = Http.header "Docspell-Share-Auth" req.token :: req.headers + , expect = req.expect + , body = req.body + , timeout = Nothing + , tracker = req.tracker + } + + authPost : { url : String , account : AuthResult @@ -68,6 +91,25 @@ authPost req = } +sharePost : + { url : String + , token : String + , body : Http.Body + , expect : Http.Expect msg + } + -> Cmd msg +sharePost req = + shareReq + { url = req.url + , token = req.token + , body = req.body + , expect = req.expect + , method = "POST" + , headers = [] + , tracker = Nothing + } + + authPostTrack : { url : String , account : AuthResult