Basic search view for shares

This commit is contained in:
eikek 2021-10-05 01:06:52 +02:00
parent a286556116
commit 83dd675e4f
12 changed files with 577 additions and 23 deletions

View File

@ -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

View File

@ -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

View File

@ -432,6 +432,7 @@ viewShare texts shareId model =
, Html.map ShareMsg
(Share.viewContent texts.share
model.flags
model.version
model.uiSettings
model.shareModel
)

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}

View File

@ -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)
]

View File

@ -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
}

View File

@ -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

View File

@ -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
]
]
]

View File

@ -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