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 , restoreAllItems
, restoreItem , restoreItem
, saveClientSettings , saveClientSettings
, searchShare
, sendMail , sendMail
, setAttachmentName , setAttachmentName
, setCollectiveSettings , setCollectiveSettings
@ -155,6 +156,7 @@ module Api exposing
, upload , upload
, uploadAmend , uploadAmend
, uploadSingle , uploadSingle
, verifyShare
, versionInfo , versionInfo
) )
@ -223,6 +225,8 @@ import Api.Model.SentMails exposing (SentMails)
import Api.Model.ShareData exposing (ShareData) import Api.Model.ShareData exposing (ShareData)
import Api.Model.ShareDetail exposing (ShareDetail) import Api.Model.ShareDetail exposing (ShareDetail)
import Api.Model.ShareList exposing (ShareList) 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.SimpleMail exposing (SimpleMail)
import Api.Model.SourceAndTags exposing (SourceAndTags) import Api.Model.SourceAndTags exposing (SourceAndTags)
import Api.Model.SourceList exposing (SourceList) 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 --- Helper

View File

@ -324,7 +324,7 @@ updateShare lmsg model =
Just id -> Just id ->
let let
result = result =
Page.Share.Update.update model.flags id lmsg model.shareModel Page.Share.Update.update model.flags model.uiSettings id lmsg model.shareModel
in in
( { model | shareModel = result.model } ( { model | shareModel = result.model }
, Cmd.map ShareMsg result.cmd , Cmd.map ShareMsg result.cmd

View File

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

View File

@ -8,6 +8,7 @@
module Data.Items exposing module Data.Items exposing
( concat ( concat
, first , first
, flatten
, idSet , idSet
, length , length
, replaceIn , replaceIn
@ -21,6 +22,11 @@ import Set exposing (Set)
import Util.List import Util.List
flatten : ItemLightList -> List ItemLight
flatten list =
List.concatMap .items list.groups
concat : ItemLightList -> ItemLightList -> ItemLightList concat : ItemLightList -> ItemLightList -> ItemLightList
concat l0 l1 = concat l0 l1 =
let let

View File

@ -7,16 +7,41 @@
module Messages.Page.Share exposing (..) module Messages.Page.Share exposing (..)
import Messages.Basics
import Messages.Comp.ItemCardList
import Messages.Comp.SearchMenu
type alias Texts = 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 : Texts
gb = 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 : Texts
de = 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 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 = 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 : Maybe String -> Flags -> ( Model, Cmd Msg )
init shareId flags = init shareId flags =
case shareId of case shareId of
Just id -> Just id ->
let ( emptyModel flags, Api.verifyShare flags (ShareSecret id Nothing) VerifyResp )
_ =
Debug.log "share" id
in
( {}, Cmd.none )
Nothing -> Nothing ->
( {}, Cmd.none ) ( emptyModel flags, Cmd.none )
type Msg 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) 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.Flags exposing (Flags)
import Data.ItemQuery as Q
import Data.SearchMode
import Data.UiSettings exposing (UiSettings)
import Page.Share.Data exposing (..) import Page.Share.Data exposing (..)
@ -18,6 +26,161 @@ type alias UpdateResult =
} }
update : Flags -> String -> Msg -> Model -> UpdateResult update : Flags -> UiSettings -> String -> Msg -> Model -> UpdateResult
update flags shareId msg model = update flags settings shareId msg model =
UpdateResult model Cmd.none Sub.none 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) 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.Flags exposing (Flags)
import Data.Items
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onInput, onSubmit)
import Messages.Page.Share exposing (Texts) import Messages.Page.Share exposing (Texts)
import Page.Share.Data exposing (..) 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 import Styles as S
viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg viewSidebar : Texts -> Bool -> Flags -> UiSettings -> Model -> Html Msg
viewSidebar _ visible _ _ _ = viewSidebar texts visible flags settings model =
div div
[ id "sidebar" [ 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 -> VersionInfo -> UiSettings -> Model -> Html Msg
viewContent texts flags _ model = 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 div
[ id "content" [ id "content"
, class "h-full flex flex-col" , class "h-full flex flex-col"
, class S.content , class S.content
] ]
[ h1 [ class S.header1 ] [ h1
[ text "Share Page!" [ 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 , authTask
, executeIn , executeIn
, jsonResolver , jsonResolver
, sharePost
) )
import Api.Model.AuthResult exposing (AuthResult) 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 : authPost :
{ url : String { url : String
, account : AuthResult , 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 : authPostTrack :
{ url : String { url : String
, account : AuthResult , account : AuthResult