diff --git a/build.sbt b/build.sbt index 94827571..9088695e 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ val elmCompileMode = settingKey[ElmCompileMode]("How to compile elm sources") val scalafixSettings = Seq( semanticdbEnabled := true, // enable SemanticDB - semanticdbVersion := "4.4.0",//scalafixSemanticdb.revision + semanticdbVersion := scalafixSemanticdb.revision, //"4.4.0" ThisBuild / scalafixDependencies ++= Dependencies.organizeImports ) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala index f761570c..5efc5221 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OMail.scala @@ -68,6 +68,8 @@ object OMail { item: Ident, subject: String, recipients: List[MailAddress], + cc: List[MailAddress], + bcc: List[MailAddress], body: String, attach: AttachSelection ) @@ -230,6 +232,8 @@ object OMail { val fields: Seq[Trans[F]] = Seq( From(sett.mailFrom), Tos(m.recipients), + Ccs(m.cc), + Bccs(m.bcc), XMailer.emil, Subject(m.subject), TextBody[F](m.body) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 4d0cbb04..f28ccd07 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4012,6 +4012,8 @@ components: is ignored, if `addAllAttachments` is set to `true`. required: - recipients + - cc + - bcc - subject - body - addAllAttachments @@ -4021,6 +4023,14 @@ components: type: array items: type: string + cc: + type: array + items: + type: string + bcc: + type: array + items: + type: string subject: type: string body: diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala index 0dd67cc9..1c6a40c7 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/MailSendRoutes.scala @@ -39,11 +39,13 @@ object MailSendRoutes { def convertIn(item: Ident, s: SimpleMail): Either[String, ItemMail] = for { rec <- s.recipients.traverse(MailAddress.parse) + cc <- s.cc.traverse(MailAddress.parse) + bcc <- s.bcc.traverse(MailAddress.parse) fileIds <- s.attachmentIds.traverse(Ident.fromString) sel = if (s.addAllAttachments) AttachSelection.All else AttachSelection.Selected(fileIds) - } yield ItemMail(item, s.subject, rec, s.body, sel) + } yield ItemMail(item, s.subject, rec, cc, bcc, s.body, sel) def convertOut(res: SendResult): BasicResult = res match { diff --git a/modules/webapp/src/main/elm/App/View.elm b/modules/webapp/src/main/elm/App/View.elm index 1c2f1ec0..63e4839f 100644 --- a/modules/webapp/src/main/elm/App/View.elm +++ b/modules/webapp/src/main/elm/App/View.elm @@ -198,7 +198,7 @@ loginInfo model = div [ class "right menu" ] (case model.flags.account of Just acc -> - [ a + [ div [ class "ui dropdown icon link item" , href "#" , onClick ToggleUserMenu @@ -238,7 +238,7 @@ loginInfo model = ] ] ] - , a + , div [ class "ui dropdown icon link item" , onClick ToggleNavMenu , href "#" @@ -296,10 +296,10 @@ loginInfo model = ] , div [ class "divider" ] [] , a - [ class "icon item" - , href "https://docspell.org/doc" + [ class "item" + , href "https://docspell.org/docs" , target "_new" - , title "Opens https://docspell.org/doc" + , title "Opens https://docspell.org/docs" ] [ i [ class "help icon" ] [] , text "Help" diff --git a/modules/webapp/src/main/elm/Comp/ItemMail.elm b/modules/webapp/src/main/elm/Comp/ItemMail.elm index 83f5ccd8..fb62176d 100644 --- a/modules/webapp/src/main/elm/Comp/ItemMail.elm +++ b/modules/webapp/src/main/elm/Comp/ItemMail.elm @@ -28,6 +28,10 @@ type alias Model = , subject : String , recipients : List String , recipientsModel : Comp.EmailInput.Model + , ccRecipients : List String + , ccRecipientsModel : Comp.EmailInput.Model + , bccRecipients : List String + , bccRecipientsModel : Comp.EmailInput.Model , body : String , attachAll : Bool , formError : Maybe String @@ -37,6 +41,8 @@ type alias Model = type Msg = SetSubject String | RecipientMsg Comp.EmailInput.Msg + | CCRecipientMsg Comp.EmailInput.Msg + | BCCRecipientMsg Comp.EmailInput.Msg | SetBody String | ConnMsg (Comp.Dropdown.Msg String) | ConnResp (Result Http.Error EmailSettingsList) @@ -67,6 +73,10 @@ emptyModel = , subject = "" , recipients = [] , recipientsModel = Comp.EmailInput.init + , ccRecipients = [] + , ccRecipientsModel = Comp.EmailInput.init + , bccRecipients = [] + , bccRecipientsModel = Comp.EmailInput.init , body = "" , attachAll = True , formError = Nothing @@ -83,6 +93,8 @@ clear model = { model | subject = "" , recipients = [] + , ccRecipients = [] + , bccRecipients = [] , body = "" } @@ -103,6 +115,26 @@ update flags msg model = , FormNone ) + CCRecipientMsg m -> + let + ( em, ec, rec ) = + Comp.EmailInput.update flags model.ccRecipients m model.ccRecipientsModel + in + ( { model | ccRecipients = rec, ccRecipientsModel = em } + , Cmd.map CCRecipientMsg ec + , FormNone + ) + + BCCRecipientMsg m -> + let + ( em, ec, rec ) = + Comp.EmailInput.update flags model.bccRecipients m model.bccRecipientsModel + in + ( { model | bccRecipients = rec, bccRecipientsModel = em } + , Cmd.map BCCRecipientMsg ec + , FormNone + ) + SetBody str -> ( { model | body = str }, Cmd.none, FormNone ) @@ -153,8 +185,18 @@ update flags msg model = case ( model.formError, Comp.Dropdown.getSelected model.connectionModel ) of ( Nothing, conn :: [] ) -> let + emptyMail = + Api.Model.SimpleMail.empty + sm = - SimpleMail model.recipients model.subject model.body model.attachAll [] + { emptyMail + | recipients = model.recipients + , cc = model.ccRecipients + , bcc = model.bccRecipients + , subject = model.subject + , body = model.body + , addAllAttachments = model.attachAll + } in ( model, Cmd.none, FormSend { conn = conn, mail = sm } ) @@ -195,6 +237,18 @@ view settings model = ] , Html.map RecipientMsg (Comp.EmailInput.view model.recipients model.recipientsModel) ] + , div [ class "field" ] + [ label [] + [ text "CC(s)" + ] + , Html.map CCRecipientMsg (Comp.EmailInput.view model.ccRecipients model.ccRecipientsModel) + ] + , div [ class "field" ] + [ label [] + [ text "BCC(s)" + ] + , Html.map BCCRecipientMsg (Comp.EmailInput.view model.bccRecipients model.bccRecipientsModel) + ] , div [ class "field" ] [ label [] [ text "Subject" ] , input diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index 333d1442..7837b5b8 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -1,6 +1,7 @@ module Page.Home.Data exposing ( Model , Msg(..) + , SearchParam , SearchType(..) , SelectActionMode(..) , SelectViewModel @@ -49,8 +50,8 @@ type alias Model = , moreInProgress : Bool , throttle : Throttle Msg , searchTypeDropdown : Comp.FixedDropdown.Model SearchType - , searchType : SearchType - , searchTypeForm : SearchType + , searchTypeDropdownValue : SearchType + , lastSearchType : SearchType , contentOnlySearch : Maybe String , dragDropData : DD.DragDropData , scrollToCard : Maybe String @@ -104,8 +105,8 @@ init flags viewMode = , searchTypeDropdown = Comp.FixedDropdown.initMap searchTypeString searchTypeOptions - , searchType = BasicSearch - , searchTypeForm = defaultSearchType flags + , searchTypeDropdownValue = defaultSearchType flags + , lastSearchType = BasicSearch , contentOnlySearch = Nothing , dragDropData = DD.DragDropData DD.init Nothing @@ -156,14 +157,14 @@ type Msg | ItemCardListMsg Comp.ItemCardList.Msg | ItemSearchResp Bool (Result Http.Error ItemLightList) | ItemSearchAddResp (Result Http.Error ItemLightList) - | DoSearch + | DoSearch SearchType | ToggleSearchMenu | ToggleSelectView | LoadMore | UpdateThrottle | SetBasicSearch String | SearchTypeMsg (Comp.FixedDropdown.Msg SearchType) - | KeyUpMsg (Maybe KeyCode) + | KeyUpSearchbarMsg (Maybe KeyCode) | SetContentOnly String | ScrollResult (Result Dom.Error ()) | ClearItemDetailId @@ -182,7 +183,6 @@ type Msg type SearchType = BasicSearch - | ContentSearch | ContentOnlySearch @@ -192,15 +192,21 @@ type SelectActionMode | EditSelected +type alias SearchParam = + { flags : Flags + , searchType : SearchType + , pageSize : Int + , offset : Int + , scroll : Bool + } + + searchTypeString : SearchType -> String searchTypeString st = case st of BasicSearch -> "Names" - ContentSearch -> - "Contents" - ContentOnlySearch -> "Contents Only" @@ -219,54 +225,51 @@ itemNav id model = } -doSearchCmd : Flags -> UiSettings -> Int -> Bool -> Model -> Cmd Msg -doSearchCmd flags settings offset scroll model = - case model.searchType of +doSearchCmd : SearchParam -> Model -> Cmd Msg +doSearchCmd param model = + case param.searchType of BasicSearch -> - doSearchDefaultCmd flags settings offset scroll model - - ContentSearch -> - doSearchDefaultCmd flags settings offset scroll model + doSearchDefaultCmd param model ContentOnlySearch -> - doSearchIndexCmd flags settings offset scroll model + doSearchIndexCmd param model -doSearchDefaultCmd : Flags -> UiSettings -> Int -> Bool -> Model -> Cmd Msg -doSearchDefaultCmd flags settings offset scroll model = +doSearchDefaultCmd : SearchParam -> Model -> Cmd Msg +doSearchDefaultCmd param model = let smask = Comp.SearchMenu.getItemSearch model.searchMenuModel mask = { smask - | limit = settings.itemSearchPageSize - , offset = offset + | limit = param.pageSize + , offset = param.offset } in - if offset == 0 then - Api.itemSearch flags mask (ItemSearchResp scroll) + if param.offset == 0 then + Api.itemSearch param.flags mask (ItemSearchResp param.scroll) else - Api.itemSearch flags mask ItemSearchAddResp + Api.itemSearch param.flags mask ItemSearchAddResp -doSearchIndexCmd : Flags -> UiSettings -> Int -> Bool -> Model -> Cmd Msg -doSearchIndexCmd flags settings offset scroll model = +doSearchIndexCmd : SearchParam -> Model -> Cmd Msg +doSearchIndexCmd param model = case model.contentOnlySearch of Just q -> let mask = { query = q - , limit = settings.itemSearchPageSize - , offset = offset + , limit = param.pageSize + , offset = param.offset } in - if offset == 0 then - Api.itemIndexSearch flags mask (ItemSearchResp scroll) + if param.offset == 0 then + Api.itemIndexSearch param.flags mask (ItemSearchResp param.scroll) else - Api.itemIndexSearch flags mask ItemSearchAddResp + Api.itemIndexSearch param.flags mask ItemSearchAddResp Nothing -> -- If there is no fulltext query, render simply the most @@ -276,9 +279,9 @@ doSearchIndexCmd flags settings offset scroll model = Api.Model.ItemSearch.empty mask = - { emptyMask | limit = settings.itemSearchPageSize } + { emptyMask | limit = param.pageSize } in - Api.itemSearch flags mask (ItemSearchResp scroll) + Api.itemSearch param.flags mask (ItemSearchResp param.scroll) resultsBelowLimit : UiSettings -> Model -> Bool diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 454c7790..a2f5dc6d 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -6,7 +6,6 @@ import Api.Model.ItemLightList exposing (ItemLightList) import Api.Model.ItemSearch import Browser.Navigation as Nav import Comp.FixedDropdown -import Comp.ItemCard import Comp.ItemCardList import Comp.ItemDetail.EditMenu exposing (SaveNameState(..)) import Comp.ItemDetail.FormChange exposing (FormChange(..)) @@ -28,7 +27,6 @@ import Time import Util.Html exposing (KeyCode(..)) import Util.ItemDragDrop as DD import Util.Maybe -import Util.String import Util.Update @@ -36,9 +34,18 @@ update : Maybe String -> Nav.Key -> Flags -> UiSettings -> Msg -> Model -> ( Mod update mId key flags settings msg model = case msg of Init -> + let + searchParam = + { flags = flags + , searchType = model.lastSearchType + , pageSize = settings.itemSearchPageSize + , offset = 0 + , scroll = True + } + in Util.Update.andThen2 [ update mId key flags settings (SearchMenuMsg Comp.SearchMenu.Init) - , doSearch flags settings True + , doSearch searchParam ] model @@ -47,7 +54,6 @@ update mId key flags settings msg model = nm = { model | searchOffset = 0 - , searchType = defaultSearchType flags , contentOnlySearch = Nothing } in @@ -64,7 +70,7 @@ update mId key flags settings msg model = model.searchMenuModel dropCmd = - DD.makeUpdateCmd flags (\_ -> DoSearch) nextState.dragDrop.dropped + DD.makeUpdateCmd flags (\_ -> DoSearch model.lastSearchType) nextState.dragDrop.dropped newModel = { model @@ -74,7 +80,7 @@ update mId key flags settings msg model = ( m2, c2, s2 ) = if nextState.stateChange && not model.searchInProgress then - doSearch flags settings False newModel + doSearch (SearchParam flags BasicSearch settings.itemSearchPageSize 0 False) newModel else withSub ( newModel, Cmd.none ) @@ -181,16 +187,24 @@ update mId key flags settings msg model = , Cmd.none ) - DoSearch -> + DoSearch stype -> let nm = { model | searchOffset = 0 } + + param = + { flags = flags + , searchType = stype + , pageSize = settings.itemSearchPageSize + , offset = 0 + , scroll = False + } in if model.searchInProgress then withSub ( model, Cmd.none ) else - doSearch flags settings False nm + doSearch param nm ToggleSearchMenu -> let @@ -247,13 +261,10 @@ update mId key flags settings msg model = SetBasicSearch str -> let smMsg = - case model.searchTypeForm of + case model.searchTypeDropdownValue of BasicSearch -> SearchMenuMsg (Comp.SearchMenu.SetAllName str) - ContentSearch -> - SearchMenuMsg (Comp.SearchMenu.SetFulltext str) - ContentOnlySearch -> SetContentOnly str in @@ -271,12 +282,12 @@ update mId key flags settings msg model = Comp.FixedDropdown.update lm model.searchTypeDropdown mvChange = - Util.Maybe.filter (\a -> a /= model.searchTypeForm) mv + Util.Maybe.filter (\a -> a /= model.searchTypeDropdownValue) mv m0 = { model | searchTypeDropdown = sm - , searchTypeForm = Maybe.withDefault model.searchTypeForm mv + , searchTypeDropdownValue = Maybe.withDefault model.searchTypeDropdownValue mv } next = @@ -303,10 +314,10 @@ update mId key flags settings msg model = Nothing -> withSub ( m0, Cmd.none ) - KeyUpMsg (Just Enter) -> - update mId key flags settings DoSearch model + KeyUpSearchbarMsg (Just Enter) -> + update mId key flags settings (DoSearch model.searchTypeDropdownValue) model - KeyUpMsg _ -> + KeyUpSearchbarMsg _ -> withSub ( model, Cmd.none ) ScrollResult _ -> @@ -393,8 +404,16 @@ update mId key flags settings msg model = let nm = { model | viewMode = SearchView } + + param = + { flags = flags + , searchType = model.lastSearchType + , pageSize = settings.itemSearchPageSize + , offset = 0 + , scroll = False + } in - doSearch flags settings False nm + doSearch param nm else noSub ( model, Cmd.none ) @@ -543,7 +562,7 @@ update mId key flags settings msg model = model_ = { model | viewMode = viewMode } in - update mId key flags settings DoSearch model_ + update mId key flags settings (DoSearch model.lastSearchType) model_ @@ -651,33 +670,24 @@ loadEditModel flags = Cmd.map EditMenuMsg (Comp.ItemDetail.EditMenu.loadModel flags) -doSearch : Flags -> UiSettings -> Bool -> Model -> ( Model, Cmd Msg, Sub Msg ) -doSearch flags settings scroll model = +doSearch : SearchParam -> Model -> ( Model, Cmd Msg, Sub Msg ) +doSearch param model = let - stype = - if - not (menuCollapsed model) - || Util.String.isNothingOrBlank model.contentOnlySearch - then - BasicSearch - - else - model.searchTypeForm - - model_ = - { model | searchType = stype } + param_ = + { param | offset = 0 } searchCmd = - doSearchCmd flags settings 0 scroll model_ + doSearchCmd param_ model ( newThrottle, cmd ) = Throttle.try searchCmd model.throttle in withSub - ( { model_ + ( { model | searchInProgress = cmd /= Cmd.none , searchOffset = 0 , throttle = newThrottle + , lastSearchType = param.searchType } , cmd ) @@ -711,8 +721,16 @@ linkTargetMsg linkTarget = doSearchMore : Flags -> UiSettings -> Model -> ( Model, Cmd Msg ) doSearchMore flags settings model = let + param = + { flags = flags + , searchType = model.lastSearchType + , pageSize = settings.itemSearchPageSize + , offset = model.searchOffset + , scroll = False + } + cmd = - doSearchCmd flags settings model.searchOffset False model + doSearchCmd param model in ( { model | moreInProgress = True } , cmd diff --git a/modules/webapp/src/main/elm/Page/Home/View.elm b/modules/webapp/src/main/elm/Page/Home/View.elm index fada780f..f2adfe63 100644 --- a/modules/webapp/src/main/elm/Page/Home/View.elm +++ b/modules/webapp/src/main/elm/Page/Home/View.elm @@ -83,7 +83,7 @@ view flags settings model = ] , a [ class "borderless item" - , onClick DoSearch + , onClick (DoSearch BasicSearch) , title "Run search query" , href "" , disabled model.searchInProgress @@ -281,17 +281,14 @@ viewSearchBar flags model = let searchTypeItem = Comp.FixedDropdown.Item - model.searchTypeForm - (searchTypeString model.searchTypeForm) + model.searchTypeDropdownValue + (searchTypeString model.searchTypeDropdownValue) searchInput = - case model.searchTypeForm of + case model.searchTypeDropdownValue of BasicSearch -> model.searchMenuModel.allNameModel - ContentSearch -> - model.searchMenuModel.fulltextModel - ContentOnlySearch -> model.contentOnlySearch @@ -329,9 +326,9 @@ viewSearchBar flags model = , ( "loading spinner icon", model.searchInProgress ) ] , href "#" - , onClick DoSearch + , onClick (DoSearch model.searchTypeDropdownValue) ] - (if hasMoreSearch model && model.searchTypeForm == BasicSearch then + (if hasMoreSearch model && model.searchTypeDropdownValue == BasicSearch then [ i [ class "icons search-corner-icons" ] [ i [ class "tiny blue circle icon" ] [] ] @@ -344,7 +341,7 @@ viewSearchBar flags model = [ type_ "text" , placeholder "Quick Search …" , onInput SetBasicSearch - , Util.Html.onKeyUpCode KeyUpMsg + , Util.Html.onKeyUpCode KeyUpSearchbarMsg , Maybe.map value searchInput |> Maybe.withDefault (value "") ] @@ -384,13 +381,10 @@ hasMoreSearch model = Comp.SearchMenu.getItemSearch model.searchMenuModel is_ = - case model.searchType of + case model.lastSearchType of BasicSearch -> { is | allNames = Nothing } - ContentSearch -> - { is | fullText = Nothing } - ContentOnlySearch -> Api.Model.ItemSearch.empty in