From e5b90eff34d54e81001f9360b98186834d7ee52c Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Thu, 4 Jun 2020 21:14:49 +0200 Subject: [PATCH 1/7] Allow client to load items in batches --- .../scala/docspell/backend/ops/OItem.scala | 13 +++++++-- .../joex/notify/NotifyDueItemsTask.scala | 9 ++++-- .../src/main/resources/docspell-openapi.yml | 12 ++++++++ .../restserver/routes/ItemRoutes.scala | 10 +++++-- .../scala/docspell/store/queries/QItem.scala | 28 +++++++++++++++---- .../webapp/src/main/elm/Page/Home/Update.elm | 5 +++- 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala index c62cc064..fdc68c9d 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -12,6 +12,7 @@ import OItem.{ AttachmentArchiveData, AttachmentData, AttachmentSourceData, + Batch, ItemData, ListItem, Query @@ -24,7 +25,7 @@ trait OItem[F[_]] { def findItem(id: Ident, collective: Ident): F[Option[ItemData]] - def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] + def findItems(q: Query, batch: Batch): F[Vector[ListItem]] def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] @@ -84,6 +85,9 @@ object OItem { type Query = QItem.Query val Query = QItem.Query + type Batch = QItem.Batch + val Batch = QItem.Batch + type ListItem = QItem.ListItem val ListItem = QItem.ListItem @@ -138,8 +142,11 @@ object OItem { .transact(QItem.findItem(id)) .map(opt => opt.flatMap(_.filterCollective(collective))) - def findItems(q: Query, maxResults: Int): F[Vector[ListItem]] = - store.transact(QItem.findItems(q).take(maxResults.toLong)).compile.toVector + def findItems(q: Query, batch: Batch): F[Vector[ListItem]] = + store + .transact(QItem.findItems(q, batch).take(batch.limit.toLong)) + .compile + .toVector def findAttachment(id: Ident, collective: Ident): F[Option[AttachmentData[F]]] = store diff --git a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala index 2b789ff1..055d0d90 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala @@ -8,6 +8,7 @@ import emil.markdown._ import emil.javamail.syntax._ import docspell.common._ +import docspell.backend.ops.OItem.Batch import docspell.store.records._ import docspell.store.queries.QItem import docspell.joex.scheduler.{Context, Task} @@ -15,7 +16,7 @@ import cats.data.OptionT import docspell.joex.mail.EmilHeader object NotifyDueItemsTask { - val maxItems: Long = 7 + val maxItems: Int = 7 type Args = NotifyDueItemsArgs def apply[F[_]: Sync](cfg: MailSendConfig, emil: Emil[F]): Task[F, Args, Unit] = @@ -78,7 +79,11 @@ object NotifyDueItemsTask { dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong)), orderAsc = Some(_.dueDate) ) - res <- ctx.store.transact(QItem.findItems(q).take(maxItems)).compile.toVector + res <- + ctx.store + .transact(QItem.findItems(q, Batch.limit(maxItems)).take(maxItems.toLong)) + .compile + .toVector } yield res def makeMail[F[_]: Sync]( diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index e8591141..11c9ba49 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -3121,6 +3121,8 @@ components: - tagsInclude - tagsExclude - inbox + - offset + - limit properties: tagsInclude: type: array @@ -3134,6 +3136,16 @@ components: format: ident inbox: type: boolean + offset: + type: integer + format: int32 + limit: + type: integer + format: int32 + description: | + The maximum number of results to return. Note that this + limit is a soft limit, there is some hard limit on the + server, too. direction: type: string format: direction diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index c22d7bca..e49e4178 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -4,6 +4,7 @@ import cats.effect._ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OItem.Batch import docspell.common.{Ident, ItemState} import org.http4s.HttpRoutes import org.http4s.dsl.Http4sDsl @@ -27,9 +28,12 @@ object ItemRoutes { mask <- req.as[ItemSearch] _ <- logger.ftrace(s"Got search mask: $mask") query = Conversions.mkQuery(mask, user.account.collective) - _ <- logger.ftrace(s"Running query: $query") - items <- backend.item.findItems(query, 100) - resp <- Ok(Conversions.mkItemList(items)) + _ <- logger.ftrace(s"Running query: $query") + items <- backend.item.findItems( + query, + Batch(mask.offset, mask.limit).restrictLimitTo(500) + ) + resp <- Ok(Conversions.mkItemList(items)) } yield resp case GET -> Root / Ident(id) => diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 5abb2c92..4ce8b7f0 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -187,7 +187,22 @@ object QItem { ) } - def findItems(q: Query): Stream[ConnectionIO, ListItem] = { + case class Batch(offset: Int, limit: Int) { + def restrictLimitTo(n: Int): Batch = + Batch(offset, math.min(n, limit)) + } + + object Batch { + val all: Batch = Batch(0, Int.MaxValue) + + def page(n: Int, size: Int): Batch = + Batch(n * size, size) + + def limit(c: Int): Batch = + Batch(0, c) + } + + def findItems(q: Query, batch: Batch): Stream[ConnectionIO, ListItem] = { val IC = RItem.Columns val AC = RAttachment.Columns val PC = RPerson.Columns @@ -202,7 +217,7 @@ object QItem { IC.id.prefix("i").f, IC.name.prefix("i").f, IC.state.prefix("i").f, - coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f), + coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"i_date", IC.dueDate.prefix("i").f, IC.source.prefix("i").f, IC.incoming.prefix("i").f, @@ -310,11 +325,12 @@ object QItem { case Some(co) => orderBy(coalesce(co(IC).prefix("i").f, IC.created.prefix("i").f) ++ fr"ASC") case None => - orderBy( - coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"DESC" - ) + orderBy(fr"i_date DESC") } - val frag = query ++ fr"WHERE" ++ cond ++ order + val frag = + query ++ fr"WHERE" ++ cond ++ order ++ (if (batch == Batch.all) Fragment.empty + else + fr"OFFSET ${batch.offset} LIMIT ${batch.limit}") logger.trace(s"List items: $frag") frag.query[ListItem].stream } diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index b3e1e327..05c8d850 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -77,8 +77,11 @@ update key flags msg model = doSearch : Flags -> Model -> ( Model, Cmd Msg ) doSearch flags model = let - mask = + smask = Comp.SearchMenu.getItemSearch model.searchMenuModel + + mask = + { smask | limit = 100 } in ( { model | searchInProgress = True, viewMode = Listing } , Api.itemSearch flags mask ItemSearchResp From b1502695281b85690b8c3a3dad70fe53673309f4 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sat, 6 Jun 2020 02:08:20 +0200 Subject: [PATCH 2/7] Add a load-more button to item list --- modules/webapp/src/main/elm/App/Data.elm | 2 +- .../webapp/src/main/elm/Comp/ItemCardList.elm | 26 +++++++ modules/webapp/src/main/elm/Data/Items.elm | 67 +++++++++++++++++++ .../webapp/src/main/elm/Page/Home/Data.elm | 48 ++++++++++++- .../webapp/src/main/elm/Page/Home/Update.elm | 66 ++++++++++++++---- .../webapp/src/main/elm/Page/Home/View.elm | 31 +++++++++ modules/webapp/src/main/elm/Ports.elm | 4 ++ modules/webapp/src/main/elm/Util/List.elm | 8 +++ modules/webapp/src/main/webjar/docspell.js | 15 +++++ 9 files changed, 251 insertions(+), 16 deletions(-) create mode 100644 modules/webapp/src/main/elm/Data/Items.elm diff --git a/modules/webapp/src/main/elm/App/Data.elm b/modules/webapp/src/main/elm/App/Data.elm index d57fdef5..01aacda2 100644 --- a/modules/webapp/src/main/elm/App/Data.elm +++ b/modules/webapp/src/main/elm/App/Data.elm @@ -57,7 +57,7 @@ init key url flags = , key = key , page = page , version = Api.Model.VersionInfo.empty - , homeModel = Page.Home.Data.emptyModel + , homeModel = Page.Home.Data.init flags , loginModel = Page.Login.Data.emptyModel , manageDataModel = Page.ManageData.Data.emptyModel , collSettingsModel = Page.CollectiveSettings.Data.emptyModel diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 5518b645..fbbbece4 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -14,9 +14,11 @@ import Api.Model.ItemLightList exposing (ItemLightList) import Data.Direction import Data.Flags exposing (Flags) import Data.Icons as Icons +import Data.Items import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) +import Ports import Util.List import Util.String import Util.Time @@ -29,6 +31,7 @@ type alias Model = type Msg = SetResults ItemLightList + | AddResults ItemLightList | SelectItem ItemLight @@ -64,6 +67,28 @@ update _ msg model = in ( newModel, Cmd.none, Nothing ) + AddResults list -> + if list.groups == [] then + ( model, Cmd.none, Nothing ) + + else + let + firstNew = + Data.Items.first list + + scrollCmd = + case firstNew of + Just item -> + Ports.scrollToElem item.id + + Nothing -> + Cmd.none + + newModel = + { model | results = Data.Items.concat model.results list } + in + ( newModel, scrollCmd, Nothing ) + SelectItem item -> ( model, Cmd.none, Just item ) @@ -123,6 +148,7 @@ viewItem item = [ ( "ui fluid card", True ) , ( newColor, not isConfirmed ) ] + , id item.id , href "#" , onClick (SelectItem item) ] diff --git a/modules/webapp/src/main/elm/Data/Items.elm b/modules/webapp/src/main/elm/Data/Items.elm new file mode 100644 index 00000000..04b9aaad --- /dev/null +++ b/modules/webapp/src/main/elm/Data/Items.elm @@ -0,0 +1,67 @@ +module Data.Items exposing + ( concat + , first + , length + ) + +import Api.Model.ItemLight exposing (ItemLight) +import Api.Model.ItemLightGroup exposing (ItemLightGroup) +import Api.Model.ItemLightList exposing (ItemLightList) +import Util.List + + +concat : ItemLightList -> ItemLightList -> ItemLightList +concat l0 l1 = + let + lastOld = + lastGroup l0 + + firstNew = + List.head l1.groups + in + case ( lastOld, firstNew ) of + ( Nothing, Nothing ) -> + l0 + + ( Just _, Nothing ) -> + l0 + + ( Nothing, Just _ ) -> + l1 + + ( Just o, Just n ) -> + if o.name == n.name then + let + ng = + ItemLightGroup o.name (o.items ++ n.items) + + prev = + Util.List.dropRight 1 l0.groups + + suff = + List.drop 1 l1.groups + in + ItemLightList (prev ++ [ ng ] ++ suff) + + else + ItemLightList (l0.groups ++ l1.groups) + + +first : ItemLightList -> Maybe ItemLight +first list = + List.head list.groups + |> Maybe.map .items + |> Maybe.withDefault [] + |> List.head + + +length : ItemLightList -> Int +length list = + List.map (\g -> List.length g.items) list.groups + |> List.sum + + +lastGroup : ItemLightList -> Maybe ItemLightGroup +lastGroup list = + List.reverse list.groups + |> List.head diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index 2ca78dbb..baab5e9a 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -2,13 +2,19 @@ module Page.Home.Data exposing ( Model , Msg(..) , ViewMode(..) - , emptyModel + , doSearchCmd + , init , itemNav + , resultsBelowLimit + , searchLimit ) +import Api import Api.Model.ItemLightList exposing (ItemLightList) import Comp.ItemCardList import Comp.SearchMenu +import Data.Flags exposing (Flags) +import Data.Items import Http @@ -18,16 +24,22 @@ type alias Model = , searchInProgress : Bool , viewMode : ViewMode , menuCollapsed : Bool + , searchOffset : Int + , moreAvailable : Bool + , moreInProgress : Bool } -emptyModel : Model -emptyModel = +init : Flags -> Model +init _ = { searchMenuModel = Comp.SearchMenu.emptyModel , itemListModel = Comp.ItemCardList.init , searchInProgress = False , viewMode = Listing , menuCollapsed = False + , searchOffset = 0 + , moreAvailable = True + , moreInProgress = False } @@ -39,6 +51,7 @@ type Msg | ItemSearchResp (Result Http.Error ItemLightList) | DoSearch | ToggleSearchMenu + | LoadMore type ViewMode @@ -58,3 +71,32 @@ itemNav id model = { prev = Maybe.map .id prev , next = Maybe.map .id next } + + +searchLimit : Int +searchLimit = + 90 + + +doSearchCmd : Flags -> Int -> Comp.SearchMenu.Model -> Cmd Msg +doSearchCmd flags offset model = + let + smask = + Comp.SearchMenu.getItemSearch model + + mask = + { smask + | limit = searchLimit + , offset = offset + } + in + Api.itemSearch flags mask ItemSearchResp + + +resultsBelowLimit : Model -> Bool +resultsBelowLimit model = + let + len = + Data.Items.length model.itemListModel.results + in + len < searchLimit diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 05c8d850..92a5d600 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -1,6 +1,5 @@ module Page.Home.Update exposing (update) -import Api import Browser.Navigation as Nav import Comp.ItemCardList import Comp.SearchMenu @@ -21,7 +20,11 @@ update key flags msg model = model ResetSearch -> - update key flags (SearchMenuMsg Comp.SearchMenu.ResetForm) model + let + nm = + { model | searchOffset = 0 } + in + update key flags (SearchMenuMsg Comp.SearchMenu.ResetForm) nm SearchMenuMsg m -> let @@ -57,32 +60,71 @@ update key flags msg model = ItemSearchResp (Ok list) -> let + noff = + model.searchOffset + searchLimit + m = - { model | searchInProgress = False, viewMode = Listing } + { model + | searchInProgress = False + , moreInProgress = False + , searchOffset = noff + , viewMode = Listing + , moreAvailable = list.groups /= [] + } in - update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m + if list.groups == [] then + ( m, Cmd.none ) + + else if model.searchOffset == 0 then + update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m + + else + update key flags (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m ItemSearchResp (Err _) -> - ( { model | searchInProgress = False }, Cmd.none ) + ( { model + | searchInProgress = False + } + , Cmd.none + ) DoSearch -> - doSearch flags model + let + nm = + { model | searchOffset = 0 } + in + doSearch flags nm ToggleSearchMenu -> ( { model | menuCollapsed = not model.menuCollapsed } , Cmd.none ) + LoadMore -> + if model.moreAvailable then + doSearchMore flags model + + else + ( model, Cmd.none ) + doSearch : Flags -> Model -> ( Model, Cmd Msg ) doSearch flags model = let - smask = - Comp.SearchMenu.getItemSearch model.searchMenuModel - - mask = - { smask | limit = 100 } + cmd = + doSearchCmd flags model.searchOffset model.searchMenuModel in ( { model | searchInProgress = True, viewMode = Listing } - , Api.itemSearch flags mask ItemSearchResp + , cmd + ) + + +doSearchMore : Flags -> Model -> ( Model, Cmd Msg ) +doSearchMore flags model = + let + cmd = + doSearchCmd flags model.searchOffset model.searchMenuModel + in + ( { model | moreInProgress = True, viewMode = Listing } + , cmd ) diff --git a/modules/webapp/src/main/elm/Page/Home/View.elm b/modules/webapp/src/main/elm/Page/Home/View.elm index dbc89548..29fe81ff 100644 --- a/modules/webapp/src/main/elm/Page/Home/View.elm +++ b/modules/webapp/src/main/elm/Page/Home/View.elm @@ -61,6 +61,7 @@ view model = , not model.menuCollapsed ) , ( "sixteen wide column", model.menuCollapsed ) + , ( "item-card-list", True ) ] ] [ div @@ -90,6 +91,36 @@ view model = Detail -> div [] [] ] + , div + [ classList + [ ( "sixteen wide column", True ) + ] + ] + [ div [ class "ui basic center aligned segment" ] + [ button + [ classList + [ ( "ui basic tiny button", True ) + , ( "disabled", not model.moreAvailable ) + , ( "hidden invisible", resultsBelowLimit model ) + ] + , disabled (not model.moreAvailable || model.moreInProgress || model.searchInProgress) + , title "Load more items" + , href "#" + , onClick LoadMore + ] + [ if model.moreInProgress then + i [ class "loading spinner icon" ] [] + + else + i [ class "angle double down icon" ] [] + , if model.moreAvailable then + text "Load moreā¦" + + else + text "That's all" + ] + ] + ] ] diff --git a/modules/webapp/src/main/elm/Ports.elm b/modules/webapp/src/main/elm/Ports.elm index 94b3b8e1..100852c3 100644 --- a/modules/webapp/src/main/elm/Ports.elm +++ b/modules/webapp/src/main/elm/Ports.elm @@ -1,5 +1,6 @@ port module Ports exposing ( removeAccount + , scrollToElem , setAccount , setAllProgress , setProgress @@ -18,3 +19,6 @@ port setProgress : ( String, Int ) -> Cmd msg port setAllProgress : ( String, Int ) -> Cmd msg + + +port scrollToElem : String -> Cmd msg diff --git a/modules/webapp/src/main/elm/Util/List.elm b/modules/webapp/src/main/elm/Util/List.elm index fca6efce..c7df91ca 100644 --- a/modules/webapp/src/main/elm/Util/List.elm +++ b/modules/webapp/src/main/elm/Util/List.elm @@ -1,5 +1,6 @@ module Util.List exposing ( distinct + , dropRight , find , findIndexed , findNext @@ -80,3 +81,10 @@ findNext pred list = |> Maybe.map Tuple.second |> Maybe.map (\i -> i + 1) |> Maybe.andThen (get list) + + +dropRight : Int -> List a -> List a +dropRight n list = + List.reverse list + |> List.drop n + |> List.reverse diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js index 9bea1f26..162c5b36 100644 --- a/modules/webapp/src/main/webjar/docspell.js +++ b/modules/webapp/src/main/webjar/docspell.js @@ -30,3 +30,18 @@ elmApp.ports.setAllProgress.subscribe(function(input) { $("."+id).progress({percent: percent}); }, 100); }); + +elmApp.ports.scrollToElem.subscribe(function(id) { + if (id && id != "") { + window.setTimeout(function() { + var el = document.getElementById(id); + if (el) { + if (el["scrollIntoViewIfNeeded"]) { + el.scrollIntoViewIfNeeded(); + } else { + el.scrollIntoView(); + } + } + }, 20); + } +}); From d5819eab350e70d86b52b9d798403e6bcd0e39b1 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sat, 6 Jun 2020 11:13:33 +0200 Subject: [PATCH 3/7] Fix offset/limit clause for mariadb MariaDB wants first limit and then offset (optionally), postgres doesn't care. --- modules/store/src/main/scala/docspell/store/queries/QItem.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 4ce8b7f0..607e14ec 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -330,7 +330,7 @@ object QItem { val frag = query ++ fr"WHERE" ++ cond ++ order ++ (if (batch == Batch.all) Fragment.empty else - fr"OFFSET ${batch.offset} LIMIT ${batch.limit}") + fr"LIMIT ${batch.limit} OFFSET ${batch.offset}") logger.trace(s"List items: $frag") frag.query[ListItem].stream } From 071ab60a5c58476dfa2f78678adaf56b5cdd161b Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sat, 6 Jun 2020 15:15:29 +0200 Subject: [PATCH 4/7] Remove i_date query binding --- .../src/main/scala/docspell/store/queries/QItem.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 607e14ec..174c25b1 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -217,7 +217,7 @@ object QItem { IC.id.prefix("i").f, IC.name.prefix("i").f, IC.state.prefix("i").f, - coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"i_date", + coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f), IC.dueDate.prefix("i").f, IC.source.prefix("i").f, IC.incoming.prefix("i").f, @@ -325,13 +325,15 @@ object QItem { case Some(co) => orderBy(coalesce(co(IC).prefix("i").f, IC.created.prefix("i").f) ++ fr"ASC") case None => - orderBy(fr"i_date DESC") + orderBy( + coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"DESC" + ) } val frag = query ++ fr"WHERE" ++ cond ++ order ++ (if (batch == Batch.all) Fragment.empty else fr"LIMIT ${batch.limit} OFFSET ${batch.offset}") - logger.trace(s"List items: $frag") + logger.trace(s"List $batch items: $frag") frag.query[ListItem].stream } From 77e8a51acdab15b5705cd555f0e01096e1fdb26f Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sat, 6 Jun 2020 15:15:53 +0200 Subject: [PATCH 5/7] Fix updating item list when there are no results --- .../webapp/src/main/elm/Page/Home/Update.elm | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 92a5d600..effd80cf 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -56,7 +56,9 @@ update key flags msg model = Nothing -> Cmd.none in - ( { model | itemListModel = m2 }, Cmd.batch [ Cmd.map ItemCardListMsg c2, cmd ] ) + ( { model | itemListModel = m2 } + , Cmd.batch [ Cmd.map ItemCardListMsg c2, cmd ] + ) ItemSearchResp (Ok list) -> let @@ -72,10 +74,7 @@ update key flags msg model = , moreAvailable = list.groups /= [] } in - if list.groups == [] then - ( m, Cmd.none ) - - else if model.searchOffset == 0 then + if model.searchOffset == 0 then update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m else @@ -112,9 +111,13 @@ doSearch : Flags -> Model -> ( Model, Cmd Msg ) doSearch flags model = let cmd = - doSearchCmd flags model.searchOffset model.searchMenuModel + doSearchCmd flags 0 model.searchMenuModel in - ( { model | searchInProgress = True, viewMode = Listing } + ( { model + | searchInProgress = True + , viewMode = Listing + , searchOffset = 0 + } , cmd ) From 6abdb95f022ae78efe39ea00daf1a99481283198 Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sat, 6 Jun 2020 20:52:23 +0200 Subject: [PATCH 6/7] Reformatting --- .../src/main/scala/docspell/store/queries/QItem.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 174c25b1..12ef6dd1 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -329,10 +329,12 @@ object QItem { coalesce(IC.itemDate.prefix("i").f, IC.created.prefix("i").f) ++ fr"DESC" ) } + val limitOffset = + if (batch == Batch.all) Fragment.empty + else fr"LIMIT ${batch.limit} OFFSET ${batch.offset}" + val frag = - query ++ fr"WHERE" ++ cond ++ order ++ (if (batch == Batch.all) Fragment.empty - else - fr"LIMIT ${batch.limit} OFFSET ${batch.offset}") + query ++ fr"WHERE" ++ cond ++ order ++ limitOffset logger.trace(s"List $batch items: $frag") frag.query[ListItem].stream } From 79fc5a30a1fbbe325f2c440ccc53e6080b55c30e Mon Sep 17 00:00:00 2001 From: Eike Kettner <eike.kettner@posteo.de> Date: Sun, 7 Jun 2020 00:51:11 +0200 Subject: [PATCH 7/7] Introduce ui settings and let user set page size for item search --- modules/webapp/src/main/elm/App/Data.elm | 2 + modules/webapp/src/main/elm/App/Update.elm | 26 +++- .../src/main/elm/Comp/UiSettingsForm.elm | 93 ++++++++++++++ .../src/main/elm/Comp/UiSettingsManage.elm | 119 ++++++++++++++++++ .../webapp/src/main/elm/Data/UiSettings.elm | 63 ++++++++++ modules/webapp/src/main/elm/Main.elm | 13 +- .../webapp/src/main/elm/Page/Home/Data.elm | 25 ++-- .../webapp/src/main/elm/Page/Home/Update.elm | 40 ++++-- .../src/main/elm/Page/UserSettings/Data.elm | 7 ++ .../src/main/elm/Page/UserSettings/Update.elm | 99 +++++++++------ .../src/main/elm/Page/UserSettings/View.elm | 19 +++ modules/webapp/src/main/elm/Ports.elm | 52 +++++++- modules/webapp/src/main/webjar/docspell.js | 38 ++++++ 13 files changed, 530 insertions(+), 66 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/UiSettingsForm.elm create mode 100644 modules/webapp/src/main/elm/Comp/UiSettingsManage.elm create mode 100644 modules/webapp/src/main/elm/Data/UiSettings.elm diff --git a/modules/webapp/src/main/elm/App/Data.elm b/modules/webapp/src/main/elm/App/Data.elm index 01aacda2..863d7048 100644 --- a/modules/webapp/src/main/elm/App/Data.elm +++ b/modules/webapp/src/main/elm/App/Data.elm @@ -11,6 +11,7 @@ import Api.Model.VersionInfo exposing (VersionInfo) import Browser exposing (UrlRequest) import Browser.Navigation exposing (Key) import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) import Http import Page exposing (Page(..)) import Page.CollectiveSettings.Data @@ -90,6 +91,7 @@ type Msg | LogoutResp (Result Http.Error ()) | SessionCheckResp (Result Http.Error AuthResult) | ToggleNavMenu + | GetUiSettings UiSettings isSignedIn : Flags -> Bool diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 60a7ffd1..596583f9 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -40,7 +40,7 @@ update msg model = ( m, c, s ) = updateWithSub msg model in - ( { m | subs = s }, c ) + ( { m | subs = Sub.batch [ m.subs, s ] }, c ) updateWithSub : Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) @@ -92,7 +92,10 @@ updateWithSub msg model = ) LogoutResp _ -> - ( { model | loginModel = Page.Login.Data.emptyModel }, Page.goto (LoginPage Nothing), Sub.none ) + ( { model | loginModel = Page.Login.Data.emptyModel } + , Page.goto (LoginPage Nothing) + , Sub.none + ) SessionCheckResp res -> case res of @@ -171,6 +174,14 @@ updateWithSub msg model = ToggleNavMenu -> ( { model | navMenuOpen = not model.navMenuOpen }, Cmd.none, Sub.none ) + GetUiSettings settings -> + Util.Update.andThen1 + [ updateUserSettings (Page.UserSettings.Data.GetUiSettings settings) + , updateHome (Page.Home.Data.GetUiSettings settings) + ] + model + |> noSub + updateItemDetail : Page.ItemDetail.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateItemDetail lmsg model = @@ -241,10 +252,17 @@ updateQueue lmsg model = updateUserSettings : Page.UserSettings.Data.Msg -> Model -> ( Model, Cmd Msg ) updateUserSettings lmsg model = let - ( lm, lc ) = + ( lm, lc, ls ) = Page.UserSettings.Update.update model.flags lmsg model.userSettingsModel in - ( { model | userSettingsModel = lm } + ( { model + | userSettingsModel = lm + , subs = + Sub.batch + [ model.subs + , Sub.map UserSettingsMsg ls + ] + } , Cmd.map UserSettingsMsg lc ) diff --git a/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm new file mode 100644 index 00000000..7ed59c81 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm @@ -0,0 +1,93 @@ +module Comp.UiSettingsForm exposing + ( Model + , Msg + , init + , initWith + , update + , view + ) + +import Comp.IntField +import Data.UiSettings exposing (StoredUiSettings, UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) + + +type alias Model = + { defaults : UiSettings + , input : StoredUiSettings + , searchPageSizeModel : Comp.IntField.Model + } + + +initWith : UiSettings -> Model +initWith defaults = + { defaults = defaults + , input = Data.UiSettings.toStoredUiSettings defaults + , searchPageSizeModel = + Comp.IntField.init + (Just 10) + (Just 500) + False + "Item search page" + } + + +init : Model +init = + initWith Data.UiSettings.defaults + + +changeInput : (StoredUiSettings -> StoredUiSettings) -> Model -> StoredUiSettings +changeInput change model = + change model.input + + +type Msg + = SearchPageSizeMsg Comp.IntField.Msg + + +getSettings : Model -> UiSettings +getSettings model = + Data.UiSettings.merge model.input model.defaults + + + +--- Update + + +update : Msg -> Model -> ( Model, Maybe UiSettings ) +update msg model = + case msg of + SearchPageSizeMsg lm -> + let + ( m, n ) = + Comp.IntField.update lm model.searchPageSizeModel + + model_ = + { model + | searchPageSizeModel = m + , input = changeInput (\s -> { s | itemSearchPageSize = n }) model + } + + nextSettings = + Maybe.map (\_ -> getSettings model_) n + in + ( model_, nextSettings ) + + + +--- View + + +view : Model -> Html Msg +view model = + div [ class "ui form" ] + [ Html.map SearchPageSizeMsg + (Comp.IntField.viewWithInfo + "Maximum results in one page when searching items." + model.input.itemSearchPageSize + "" + model.searchPageSizeModel + ) + ] diff --git a/modules/webapp/src/main/elm/Comp/UiSettingsManage.elm b/modules/webapp/src/main/elm/Comp/UiSettingsManage.elm new file mode 100644 index 00000000..579b88c3 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/UiSettingsManage.elm @@ -0,0 +1,119 @@ +module Comp.UiSettingsManage exposing + ( Model + , Msg + , init + , update + , view + ) + +import Api.Model.BasicResult exposing (BasicResult) +import Comp.UiSettingsForm +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Ports + + +type alias Model = + { formModel : Comp.UiSettingsForm.Model + , settings : Maybe UiSettings + , message : Maybe BasicResult + } + + +type Msg + = UiSettingsFormMsg Comp.UiSettingsForm.Msg + | Submit + | SettingsSaved + + +init : UiSettings -> Model +init defaults = + { formModel = Comp.UiSettingsForm.initWith defaults + , settings = Nothing + , message = Nothing + } + + + +--- update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) +update flags msg model = + case msg of + UiSettingsFormMsg lm -> + let + ( m_, sett ) = + Comp.UiSettingsForm.update lm model.formModel + in + ( { model + | formModel = m_ + , settings = sett + , message = Nothing + } + , Cmd.none + , Sub.none + ) + + Submit -> + case model.settings of + Just s -> + ( { model | message = Nothing } + , Ports.storeUiSettings flags s + , Ports.onUiSettingsSaved SettingsSaved + ) + + Nothing -> + ( { model | message = Just (BasicResult False "Settings unchanged or invalid.") } + , Cmd.none + , Sub.none + ) + + SettingsSaved -> + ( { model | message = Just (BasicResult True "Settings saved.") } + , Cmd.none + , Sub.none + ) + + + +--- View + + +isError : Model -> Bool +isError model = + Maybe.map .success model.message == Just False + + +isSuccess : Model -> Bool +isSuccess model = + Maybe.map .success model.message == Just True + + +view : String -> Model -> Html Msg +view classes model = + div [ class classes ] + [ Html.map UiSettingsFormMsg (Comp.UiSettingsForm.view model.formModel) + , div [ class "ui divider" ] [] + , button + [ class "ui primary button" + , onClick Submit + ] + [ text "Submit" + ] + , div + [ classList + [ ( "ui message", True ) + , ( "success", isSuccess model ) + , ( "error", isError model ) + , ( "hidden invisible", model.message == Nothing ) + ] + ] + [ Maybe.map .message model.message + |> Maybe.withDefault "" + |> text + ] + ] diff --git a/modules/webapp/src/main/elm/Data/UiSettings.elm b/modules/webapp/src/main/elm/Data/UiSettings.elm new file mode 100644 index 00000000..7498f088 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/UiSettings.elm @@ -0,0 +1,63 @@ +module Data.UiSettings exposing + ( StoredUiSettings + , UiSettings + , defaults + , merge + , mergeDefaults + , toStoredUiSettings + ) + +{-| Settings for the web ui. All fields should be optional, since it +is loaded from local storage. + +Making fields optional, allows it to evolve without breaking previous +versions. Also if a user is logged out, an empty object is send to +force default settings. + +-} + + +type alias StoredUiSettings = + { itemSearchPageSize : Maybe Int + } + + +{-| Settings for the web ui. These fields are all mandatory, since +there is always a default value. + +When loaded from local storage, all optional fields can fallback to a +default value, converting the StoredUiSettings into a UiSettings. + +-} +type alias UiSettings = + { itemSearchPageSize : Int + } + + +defaults : UiSettings +defaults = + { itemSearchPageSize = 90 + } + + +merge : StoredUiSettings -> UiSettings -> UiSettings +merge given fallback = + { itemSearchPageSize = + choose given.itemSearchPageSize fallback.itemSearchPageSize + } + + +mergeDefaults : StoredUiSettings -> UiSettings +mergeDefaults given = + merge given defaults + + +toStoredUiSettings : UiSettings -> StoredUiSettings +toStoredUiSettings settings = + { itemSearchPageSize = Just settings.itemSearchPageSize + } + + +choose : Maybe a -> a -> a +choose m1 m2 = + Maybe.withDefault m2 m1 diff --git a/modules/webapp/src/main/elm/Main.elm b/modules/webapp/src/main/elm/Main.elm index 0206cee9..fea343a8 100644 --- a/modules/webapp/src/main/elm/Main.elm +++ b/modules/webapp/src/main/elm/Main.elm @@ -11,6 +11,7 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Page +import Ports import Url exposing (Url) @@ -59,7 +60,12 @@ init flags url key = Cmd.none in ( m - , Cmd.batch [ cmd, Api.versionInfo flags VersionResp, sessionCheck ] + , Cmd.batch + [ cmd + , Api.versionInfo flags VersionResp + , sessionCheck + , Ports.getUiSettings flags + ] ) @@ -76,4 +82,7 @@ viewDoc model = subscriptions : Model -> Sub Msg subscriptions model = - model.subs + Sub.batch + [ model.subs + , Ports.loadUiSettings GetUiSettings + ] diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index baab5e9a..0dc9baf0 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -6,7 +6,6 @@ module Page.Home.Data exposing , init , itemNav , resultsBelowLimit - , searchLimit ) import Api @@ -15,6 +14,7 @@ import Comp.ItemCardList import Comp.SearchMenu import Data.Flags exposing (Flags) import Data.Items +import Data.UiSettings exposing (UiSettings) import Http @@ -27,6 +27,7 @@ type alias Model = , searchOffset : Int , moreAvailable : Bool , moreInProgress : Bool + , uiSettings : UiSettings } @@ -40,6 +41,7 @@ init _ = , searchOffset = 0 , moreAvailable = True , moreInProgress = False + , uiSettings = Data.UiSettings.defaults } @@ -49,9 +51,11 @@ type Msg | ResetSearch | ItemCardListMsg Comp.ItemCardList.Msg | ItemSearchResp (Result Http.Error ItemLightList) + | ItemSearchAddResp (Result Http.Error ItemLightList) | DoSearch | ToggleSearchMenu | LoadMore + | GetUiSettings UiSettings type ViewMode @@ -73,24 +77,23 @@ itemNav id model = } -searchLimit : Int -searchLimit = - 90 - - -doSearchCmd : Flags -> Int -> Comp.SearchMenu.Model -> Cmd Msg +doSearchCmd : Flags -> Int -> Model -> Cmd Msg doSearchCmd flags offset model = let smask = - Comp.SearchMenu.getItemSearch model + Comp.SearchMenu.getItemSearch model.searchMenuModel mask = { smask - | limit = searchLimit + | limit = model.uiSettings.itemSearchPageSize , offset = offset } in - Api.itemSearch flags mask ItemSearchResp + if offset == 0 then + Api.itemSearch flags mask ItemSearchResp + + else + Api.itemSearch flags mask ItemSearchAddResp resultsBelowLimit : Model -> Bool @@ -99,4 +102,4 @@ resultsBelowLimit model = len = Data.Items.length model.itemListModel.results in - len < searchLimit + len < model.uiSettings.itemSearchPageSize diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index effd80cf..33d9a7d3 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -15,7 +15,6 @@ update key flags msg model = Init -> Util.Update.andThen1 [ update key flags (SearchMenuMsg Comp.SearchMenu.Init) - , doSearch flags ] model @@ -63,7 +62,22 @@ update key flags msg model = ItemSearchResp (Ok list) -> let noff = - model.searchOffset + searchLimit + model.uiSettings.itemSearchPageSize + + m = + { model + | searchInProgress = False + , searchOffset = noff + , viewMode = Listing + , moreAvailable = list.groups /= [] + } + in + update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m + + ItemSearchAddResp (Ok list) -> + let + noff = + model.searchOffset + model.uiSettings.itemSearchPageSize m = { model @@ -74,11 +88,14 @@ update key flags msg model = , moreAvailable = list.groups /= [] } in - if model.searchOffset == 0 then - update key flags (ItemCardListMsg (Comp.ItemCardList.SetResults list)) m + update key flags (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m - else - update key flags (ItemCardListMsg (Comp.ItemCardList.AddResults list)) m + ItemSearchAddResp (Err _) -> + ( { model + | moreInProgress = False + } + , Cmd.none + ) ItemSearchResp (Err _) -> ( { model @@ -106,12 +123,19 @@ update key flags msg model = else ( model, Cmd.none ) + GetUiSettings settings -> + let + m_ = + { model | uiSettings = settings } + in + doSearch flags m_ + doSearch : Flags -> Model -> ( Model, Cmd Msg ) doSearch flags model = let cmd = - doSearchCmd flags 0 model.searchMenuModel + doSearchCmd flags 0 model in ( { model | searchInProgress = True @@ -126,7 +150,7 @@ doSearchMore : Flags -> Model -> ( Model, Cmd Msg ) doSearchMore flags model = let cmd = - doSearchCmd flags model.searchOffset model.searchMenuModel + doSearchCmd flags model.searchOffset model in ( { model | moreInProgress = True, viewMode = Listing } , cmd diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm index 96cbeadf..9b66d955 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm @@ -10,7 +10,9 @@ import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationForm import Comp.ScanMailboxManage +import Comp.UiSettingsManage import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) type alias Model = @@ -20,6 +22,7 @@ type alias Model = , imapSettingsModel : Comp.ImapSettingsManage.Model , notificationModel : Comp.NotificationForm.Model , scanMailboxModel : Comp.ScanMailboxManage.Model + , uiSettingsModel : Comp.UiSettingsManage.Model } @@ -31,6 +34,7 @@ emptyModel flags = , imapSettingsModel = Comp.ImapSettingsManage.emptyModel , notificationModel = Tuple.first (Comp.NotificationForm.init flags) , scanMailboxModel = Tuple.first (Comp.ScanMailboxManage.init flags) + , uiSettingsModel = Comp.UiSettingsManage.init Data.UiSettings.defaults } @@ -40,6 +44,7 @@ type Tab | ImapSettingsTab | NotificationTab | ScanMailboxTab + | UiSettingsTab type Msg @@ -49,3 +54,5 @@ type Msg | NotificationMsg Comp.NotificationForm.Msg | ImapSettingsMsg Comp.ImapSettingsManage.Msg | ScanMailboxMsg Comp.ScanMailboxManage.Msg + | GetUiSettings UiSettings + | UiSettingsMsg Comp.UiSettingsManage.Msg diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 41295f05..23441716 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -5,75 +5,76 @@ import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationForm import Comp.ScanMailboxManage +import Comp.UiSettingsManage import Data.Flags exposing (Flags) import Page.UserSettings.Data exposing (..) -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) update flags msg model = case msg of SetTab t -> let m = { model | currentTab = Just t } - - ( m2, cmd ) = - case t of - EmailSettingsTab -> - let - ( em, c ) = - Comp.EmailSettingsManage.init flags - in - ( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c ) - - ImapSettingsTab -> - let - ( em, c ) = - Comp.ImapSettingsManage.init flags - in - ( { m | imapSettingsModel = em }, Cmd.map ImapSettingsMsg c ) - - ChangePassTab -> - ( m, Cmd.none ) - - NotificationTab -> - let - initCmd = - Cmd.map NotificationMsg - (Tuple.second (Comp.NotificationForm.init flags)) - in - ( m, initCmd ) - - ScanMailboxTab -> - let - initCmd = - Cmd.map ScanMailboxMsg - (Tuple.second (Comp.ScanMailboxManage.init flags)) - in - ( m, initCmd ) in - ( m2, cmd ) + case t of + EmailSettingsTab -> + let + ( em, c ) = + Comp.EmailSettingsManage.init flags + in + ( { m | emailSettingsModel = em }, Cmd.map EmailSettingsMsg c, Sub.none ) + + ImapSettingsTab -> + let + ( em, c ) = + Comp.ImapSettingsManage.init flags + in + ( { m | imapSettingsModel = em }, Cmd.map ImapSettingsMsg c, Sub.none ) + + ChangePassTab -> + ( m, Cmd.none, Sub.none ) + + NotificationTab -> + let + initCmd = + Cmd.map NotificationMsg + (Tuple.second (Comp.NotificationForm.init flags)) + in + ( m, initCmd, Sub.none ) + + ScanMailboxTab -> + let + initCmd = + Cmd.map ScanMailboxMsg + (Tuple.second (Comp.ScanMailboxManage.init flags)) + in + ( m, initCmd, Sub.none ) + + UiSettingsTab -> + ( m, Cmd.none, Sub.none ) ChangePassMsg m -> let ( m2, c2 ) = Comp.ChangePasswordForm.update flags m model.changePassModel in - ( { model | changePassModel = m2 }, Cmd.map ChangePassMsg c2 ) + ( { model | changePassModel = m2 }, Cmd.map ChangePassMsg c2, Sub.none ) EmailSettingsMsg m -> let ( m2, c2 ) = Comp.EmailSettingsManage.update flags m model.emailSettingsModel in - ( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2 ) + ( { model | emailSettingsModel = m2 }, Cmd.map EmailSettingsMsg c2, Sub.none ) ImapSettingsMsg m -> let ( m2, c2 ) = Comp.ImapSettingsManage.update flags m model.imapSettingsModel in - ( { model | imapSettingsModel = m2 }, Cmd.map ImapSettingsMsg c2 ) + ( { model | imapSettingsModel = m2 }, Cmd.map ImapSettingsMsg c2, Sub.none ) NotificationMsg lm -> let @@ -82,6 +83,7 @@ update flags msg model = in ( { model | notificationModel = m2 } , Cmd.map NotificationMsg c2 + , Sub.none ) ScanMailboxMsg lm -> @@ -91,4 +93,21 @@ update flags msg model = in ( { model | scanMailboxModel = m2 } , Cmd.map ScanMailboxMsg c2 + , Sub.none + ) + + GetUiSettings settings -> + ( { model | uiSettingsModel = Comp.UiSettingsManage.init settings } + , Cmd.none + , Sub.none + ) + + UiSettingsMsg lm -> + let + ( m2, c2, s2 ) = + Comp.UiSettingsManage.update flags lm model.uiSettingsModel + in + ( { model | uiSettingsModel = m2 } + , Cmd.map UiSettingsMsg c2 + , Sub.map UiSettingsMsg s2 ) diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View.elm b/modules/webapp/src/main/elm/Page/UserSettings/View.elm index 12bdc5de..6b1dba06 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View.elm @@ -5,6 +5,7 @@ import Comp.EmailSettingsManage import Comp.ImapSettingsManage import Comp.NotificationForm import Comp.ScanMailboxManage +import Comp.UiSettingsManage import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) @@ -26,6 +27,7 @@ view model = , makeTab model ImapSettingsTab "E-Mail Settings (IMAP)" "mail icon" , makeTab model NotificationTab "Notification Task" "bullhorn icon" , makeTab model ScanMailboxTab "Scan Mailbox Task" "envelope open outline icon" + , makeTab model UiSettingsTab "UI Settings" "cog icon" ] ] ] @@ -47,6 +49,9 @@ view model = Just ScanMailboxTab -> viewScanMailboxManage model + Just UiSettingsTab -> + viewUiSettings model + Nothing -> [] ) @@ -66,6 +71,20 @@ makeTab model tab header icon = ] +viewUiSettings : Model -> List (Html Msg) +viewUiSettings model = + [ h2 [ class "ui header" ] + [ i [ class "cog icon" ] [] + , text "UI Settings" + ] + , p [] + [ text "These settings only affect the web ui. They are stored in the browser, " + , text "so they are separated between browsers and devices." + ] + , Html.map UiSettingsMsg (Comp.UiSettingsManage.view "ui segment" model.uiSettingsModel) + ] + + viewEmailSettings : Model -> List (Html Msg) viewEmailSettings model = [ h2 [ class "ui header" ] diff --git a/modules/webapp/src/main/elm/Ports.elm b/modules/webapp/src/main/elm/Ports.elm index 100852c3..2c711b8e 100644 --- a/modules/webapp/src/main/elm/Ports.elm +++ b/modules/webapp/src/main/elm/Ports.elm @@ -1,14 +1,22 @@ port module Ports exposing - ( removeAccount + ( getUiSettings + , loadUiSettings + , onUiSettingsSaved + , removeAccount , scrollToElem , setAccount , setAllProgress , setProgress + , storeUiSettings ) import Api.Model.AuthResult exposing (AuthResult) +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (StoredUiSettings, UiSettings) +{-| Save the result of authentication to local storage. +-} port setAccount : AuthResult -> Cmd msg @@ -22,3 +30,45 @@ port setAllProgress : ( String, Int ) -> Cmd msg port scrollToElem : String -> Cmd msg + + +port saveUiSettings : ( AuthResult, UiSettings ) -> Cmd msg + + +port receiveUiSettings : (StoredUiSettings -> msg) -> Sub msg + + +port requestUiSettings : ( AuthResult, UiSettings ) -> Cmd msg + + +port uiSettingsSaved : (() -> msg) -> Sub msg + + +onUiSettingsSaved : msg -> Sub msg +onUiSettingsSaved m = + uiSettingsSaved (\_ -> m) + + +storeUiSettings : Flags -> UiSettings -> Cmd msg +storeUiSettings flags settings = + case flags.account of + Just ar -> + saveUiSettings ( ar, settings ) + + Nothing -> + Cmd.none + + +loadUiSettings : (UiSettings -> msg) -> Sub msg +loadUiSettings tagger = + receiveUiSettings (Data.UiSettings.mergeDefaults >> tagger) + + +getUiSettings : Flags -> Cmd msg +getUiSettings flags = + case flags.account of + Just ar -> + requestUiSettings ( ar, Data.UiSettings.defaults ) + + Nothing -> + Cmd.none diff --git a/modules/webapp/src/main/webjar/docspell.js b/modules/webapp/src/main/webjar/docspell.js index 162c5b36..229eecbb 100644 --- a/modules/webapp/src/main/webjar/docspell.js +++ b/modules/webapp/src/main/webjar/docspell.js @@ -5,6 +5,7 @@ var elmApp = Elm.Main.init({ flags: elmFlags }); + elmApp.ports.setAccount.subscribe(function(authResult) { console.log("Add account from local storage"); localStorage.setItem("account", JSON.stringify(authResult)); @@ -45,3 +46,40 @@ elmApp.ports.scrollToElem.subscribe(function(id) { }, 20); } }); + +elmApp.ports.saveUiSettings.subscribe(function(args) { + if (Array.isArray(args) && args.length == 2) { + var authResult = args[0]; + var settings = args[1]; + if (authResult && settings) { + var key = authResult.collective + "/" + authResult.user + "/uiSettings"; + console.log("Save ui settings to local storage"); + localStorage.setItem(key, JSON.stringify(settings)); + elmApp.ports.receiveUiSettings.send(settings); + elmApp.ports.uiSettingsSaved.send(null); + } + } +}); + +elmApp.ports.requestUiSettings.subscribe(function(args) { + console.log("Requesting ui settings"); + if (Array.isArray(args) && args.length == 2) { + var account = args[0]; + var defaults = args[1]; + var collective = account ? account.collective : null; + var user = account ? account.user : null; + if (collective && user) { + var key = collective + "/" + user + "/uiSettings"; + var settings = localStorage.getItem(key); + var data = settings ? JSON.parse(settings) : null; + if (data && defaults) { + $.extend(defaults, data); + elmApp.ports.receiveUiSettings.send(defaults); + } else if (defaults) { + elmApp.ports.receiveUiSettings.send(defaults); + } + } else if (defaults) { + elmApp.ports.receiveUiSettings.send(defaults); + } + } +});