From 699cf091e66ec4bb4933b49cf9924028227c9d21 Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 10 Jan 2022 14:25:20 +0100 Subject: [PATCH] Allow bookmarks in periodic query notification --- .../backend/ops/OQueryBookmarks.scala | 47 +++-- .../joex/notify/PeriodicQueryTask.scala | 84 +++++++-- .../notification/api/PeriodicQueryArgs.scala | 3 +- .../src/main/resources/docspell-openapi.yml | 8 +- .../restapi/model/PeriodicQuerySettings.scala | 3 +- .../routes/PeriodicQueryRoutes.scala | 23 ++- .../store/records/RQueryBookmark.scala | 23 ++- .../scala/docspell/store/records/RShare.scala | 17 ++ .../src/main/elm/Comp/BookmarkDropdown.elm | 177 ++++++++++++++++++ .../webapp/src/main/elm/Comp/DetailEdit.elm | 2 +- modules/webapp/src/main/elm/Comp/Dropdown.elm | 9 +- .../main/elm/Comp/PeriodicQueryTaskForm.elm | 86 +++++++-- modules/webapp/src/main/elm/Comp/TagForm.elm | 6 +- .../webapp/src/main/elm/Comp/TagManage.elm | 13 +- .../main/elm/Data/PeriodicQuerySettings.elm | 16 +- .../elm/Messages/Comp/BookmarkDropdown.elm | 43 +++++ .../elm/Messages/Comp/BookmarkQueryForm.elm | 4 +- .../Messages/Comp/PeriodicQueryTaskForm.elm | 8 +- .../src/main/elm/Page/ManageData/View2.elm | 7 +- 19 files changed, 497 insertions(+), 82 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/BookmarkDropdown.elm create mode 100644 modules/webapp/src/main/elm/Messages/Comp/BookmarkDropdown.elm diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OQueryBookmarks.scala b/modules/backend/src/main/scala/docspell/backend/ops/OQueryBookmarks.scala index 8b8976ce..242b0d17 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OQueryBookmarks.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OQueryBookmarks.scala @@ -20,6 +20,8 @@ trait OQueryBookmarks[F[_]] { def getAll(account: AccountId): F[Vector[OQueryBookmarks.Bookmark]] + def findOne(account: AccountId, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]] + def create(account: AccountId, bookmark: OQueryBookmarks.NewBookmark): F[AddResult] def update( @@ -53,9 +55,15 @@ object OQueryBookmarks { def getAll(account: AccountId): F[Vector[Bookmark]] = store .transact(RQueryBookmark.allForUser(account)) - .map( - _.map(r => Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created)) - ) + .map(_.map(convert.toModel)) + + def findOne( + account: AccountId, + nameOrId: String + ): F[Option[OQueryBookmarks.Bookmark]] = + store + .transact(RQueryBookmark.findByNameOrId(account, nameOrId)) + .map(_.map(convert.toModel)) def create(account: AccountId, b: NewBookmark): F[AddResult] = { val record = @@ -65,23 +73,28 @@ object OQueryBookmarks { def update(account: AccountId, id: Ident, b: NewBookmark): F[UpdateResult] = UpdateResult.fromUpdate( - store.transact( - RQueryBookmark.update( - RQueryBookmark( - id, - b.name, - b.label, - None, // userId and some other values are not used - account.collective, - b.query, - Timestamp.Epoch - ) - ) - ) + store.transact(RQueryBookmark.update(convert.toRecord(account, id, b))) ) def delete(account: AccountId, bookmark: Ident): F[Unit] = store.transact(RQueryBookmark.deleteById(account.collective, bookmark)).as(()) - }) + + private object convert { + + def toModel(r: RQueryBookmark): Bookmark = + Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created) + + def toRecord(account: AccountId, id: Ident, b: NewBookmark): RQueryBookmark = + RQueryBookmark( + id, + b.name, + b.label, + None, // userId and some other values are not used + account.collective, + b.query, + Timestamp.Epoch + ) + + } } diff --git a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala index a21a8a0f..d69993a4 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala @@ -7,6 +7,7 @@ package docspell.joex.notify import cats.data.OptionT +import cats.data.{NonEmptyList => Nel} import cats.effect._ import cats.implicits._ @@ -17,10 +18,14 @@ import docspell.joex.scheduler.Task import docspell.notification.api.EventContext import docspell.notification.api.NotificationChannel import docspell.notification.api.PeriodicQueryArgs +import docspell.query.ItemQuery +import docspell.query.ItemQuery.Expr.AndExpr import docspell.query.ItemQueryParser import docspell.store.qb.Batch import docspell.store.queries.ListItem import docspell.store.queries.{QItem, Query} +import docspell.store.records.RQueryBookmark +import docspell.store.records.RShare import docspell.store.records.RUser object PeriodicQueryTask { @@ -54,22 +59,77 @@ object PeriodicQueryTask { ) .getOrElse(()) + private def queryString(q: ItemQuery.Expr) = + ItemQueryParser.asString(q) + + def makeQuery[F[_]: Sync](ctx: Context[F, Args])(cont: Query => F[Unit]): F[Unit] = { + def fromBookmark(id: String) = + ctx.store + .transact(RQueryBookmark.findByNameOrId(ctx.args.account, id)) + .map(_.map(_.query)) + .flatTap(q => + ctx.logger.debug(s"Loaded bookmark '$id': ${q.map(_.expr).map(queryString)}") + ) + + def fromShare(id: String) = + ctx.store + .transact(RShare.findOneByCollective(ctx.args.account.collective, Some(true), id)) + .map(_.map(_.query)) + .flatTap(q => + ctx.logger.debug(s"Loaded share '$id': ${q.map(_.expr).map(queryString)}") + ) + + def fromBookmarkOrShare(id: String) = + OptionT(fromBookmark(id)).orElse(OptionT(fromShare(id))).value + + def withQuery(bm: Option[ItemQuery], str: String): F[Unit] = + ItemQueryParser.parse(str) match { + case Right(q) => + val expr = bm.map(b => AndExpr(Nel.of(b.expr, q.expr))).getOrElse(q.expr) + val query = Query(Query.Fix(ctx.args.account, Some(expr), None)) + ctx.logger.debug(s"Running query: ${queryString(expr)}") *> cont(query) + + case Left(err) => + ctx.logger.error( + s"Item query is invalid, stopping: ${ctx.args.query.map(_.query)} - ${err.render}" + ) + } + + (ctx.args.bookmark, ctx.args.query) match { + case (Some(bm), Some(qstr)) => + ctx.logger.debug(s"Using bookmark $bm and query $qstr") *> + fromBookmarkOrShare(bm).flatMap(bq => withQuery(bq, qstr.query)) + + case (Some(bm), None) => + fromBookmarkOrShare(bm).flatMap { + case Some(bq) => + val query = Query(Query.Fix(ctx.args.account, Some(bq.expr), None)) + ctx.logger.debug(s"Using bookmark: ${queryString(bq.expr)}") *> cont(query) + + case None => + ctx.logger.error( + s"No bookmark found for id: $bm. Can't continue. Please fix the task query." + ) + } + + case (None, Some(qstr)) => + ctx.logger.debug(s"Using query: ${qstr.query}") *> withQuery(None, qstr.query) + + case (None, None) => + ctx.logger.error(s"No query provided for task $taskName!") + } + } + def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)( cont: Vector[ListItem] => F[Unit] ): F[Unit] = - ItemQueryParser.parse(ctx.args.query.query) match { - case Right(q) => - val query = Query(Query.Fix(ctx.args.account, Some(q.expr), None)) - val items = ctx.store - .transact(QItem.findItems(query, now.toUtcDate, 0, Batch.limit(limit))) - .compile - .to(Vector) + makeQuery(ctx) { query => + val items = ctx.store + .transact(QItem.findItems(query, now.toUtcDate, 0, Batch.limit(limit))) + .compile + .to(Vector) - items.flatMap(cont) - case Left(err) => - ctx.logger.error( - s"Item query is invalid, stopping: ${ctx.args.query} - ${err.render}" - ) + items.flatMap(cont) } def withEventContext[F[_]]( diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala index 3f79bf90..e8ffd089 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala @@ -15,7 +15,8 @@ import io.circe.{Decoder, Encoder} final case class PeriodicQueryArgs( account: AccountId, channel: ChannelOrRef, - query: ItemQueryString, + query: Option[ItemQueryString], + bookmark: Option[String], baseUrl: Option[LenientUri] ) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 894eb995..7dfdd6f6 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -8037,12 +8037,12 @@ extraSchemas: PeriodicQuerySettings: description: | - Settings for the periodc-query task. + Settings for the periodc-query task. At least one of `query` and + `bookmark` is required! required: - id - enabled - channel - - query - schedule properties: id: @@ -8065,6 +8065,10 @@ extraSchemas: query: type: string format: itemquery + bookmark: + type: string + description: | + Name or ID of bookmark to use. PeriodicDueItemsSettings: description: | diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala index 5045c62f..964ad125 100644 --- a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala +++ b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala @@ -21,7 +21,8 @@ final case class PeriodicQuerySettings( summary: Option[String], enabled: Boolean, channel: NotificationChannel, - query: ItemQuery, + query: Option[ItemQuery], + bookmark: Option[String], schedule: CalEvent ) {} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala index b55e59ad..93b90b6e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala @@ -117,11 +117,20 @@ object PeriodicQueryRoutes extends MailAddressCodec { Sync[F] .pure(for { ch <- NotificationChannel.convert(settings.channel) - qstr <- ItemQueryParser - .asString(settings.query.expr) - .left - .map(err => new IllegalArgumentException(s"Query not renderable: $err")) - } yield (ch, ItemQueryString(qstr))) + qstr <- settings.query match { + case Some(q) => + ItemQueryParser + .asString(q.expr) + .left + .map(err => new IllegalArgumentException(s"Query not renderable: $err")) + .map(Option.apply) + case None => + Right(None) + } + _ <- + if (qstr.nonEmpty || settings.bookmark.nonEmpty) Right(()) + else Left(new IllegalArgumentException("No query or bookmark provided")) + } yield (ch, qstr.map(ItemQueryString.apply))) .rethrow .map { case (channel, qstr) => UserTask( @@ -134,6 +143,7 @@ object PeriodicQueryRoutes extends MailAddressCodec { user, Right(channel), qstr, + settings.bookmark, Some(baseUrl / "app" / "item") ) ) @@ -155,7 +165,8 @@ object PeriodicQueryRoutes extends MailAddressCodec { task.summary, task.enabled, ch, - ItemQueryParser.parseUnsafe(task.args.query.query), + task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe), + task.args.bookmark, task.timer ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RQueryBookmark.scala b/modules/store/src/main/scala/docspell/store/records/RQueryBookmark.scala index a89cd49b..b16d5dd1 100644 --- a/modules/store/src/main/scala/docspell/store/records/RQueryBookmark.scala +++ b/modules/store/src/main/scala/docspell/store/records/RQueryBookmark.scala @@ -11,12 +11,12 @@ import cats.syntax.all._ import docspell.common._ import docspell.query.ItemQuery +import docspell.store.AddResult import docspell.store.qb.DSL._ import docspell.store.qb._ import doobie._ import doobie.implicits._ -import docspell.store.AddResult final case class RQueryBookmark( id: Ident, @@ -153,4 +153,25 @@ object RQueryBookmark { bm.cid === account.collective && (bm.userId.isNull || bm.userId.in(users)) ).build.query[RQueryBookmark].to[Vector] } + + def findByNameOrId( + account: AccountId, + nameOrId: String + ): ConnectionIO[Option[RQueryBookmark]] = { + val user = RUser.as("u") + val bm = RQueryBookmark.as("bm") + + val users = Select( + user.uid.s, + from(user), + user.cid === account.collective && user.login === account.user + ) + Select( + select(bm.all), + from(bm), + bm.cid === account.collective && + (bm.userId.isNull || bm.userId.in(users)) && + (bm.name === nameOrId || bm.id ==== nameOrId) + ).build.query[RQueryBookmark].option + } } diff --git a/modules/store/src/main/scala/docspell/store/records/RShare.scala b/modules/store/src/main/scala/docspell/store/records/RShare.scala index 5ddfdb6b..3aca8f81 100644 --- a/modules/store/src/main/scala/docspell/store/records/RShare.scala +++ b/modules/store/src/main/scala/docspell/store/records/RShare.scala @@ -138,6 +138,23 @@ object RShare { .option }) + def findOneByCollective( + cid: Ident, + enabled: Option[Boolean], + nameOrId: String + ): ConnectionIO[Option[RShare]] = { + val s = RShare.as("s") + val u = RUser.as("u") + + Select( + select(s.all), + from(s).innerJoin(u, u.uid === s.userId), + u.cid === cid && + (s.name === nameOrId || s.id ==== nameOrId) &&? + enabled.map(e => s.enabled === e) + ).build.query[RShare].option + } + def findAllByCollective( cid: Ident, ownerLogin: Option[Ident], diff --git a/modules/webapp/src/main/elm/Comp/BookmarkDropdown.elm b/modules/webapp/src/main/elm/Comp/BookmarkDropdown.elm new file mode 100644 index 00000000..134743ea --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/BookmarkDropdown.elm @@ -0,0 +1,177 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.BookmarkDropdown exposing (Item(..), Model, Msg, getSelected, getSelectedId, init, initWith, update, view) + +import Api +import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) +import Api.Model.ShareDetail exposing (ShareDetail) +import Comp.Dropdown exposing (Option) +import Data.Bookmarks exposing (AllBookmarks) +import Data.DropdownStyle +import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html) +import Http +import Messages.Comp.BookmarkDropdown exposing (Texts) +import Util.List + + +type Model + = Model (Comp.Dropdown.Model Item) + + +type Msg + = DropdownMsg (Comp.Dropdown.Msg Item) + | GetBookmarksResp (Maybe String) (Result Http.Error AllBookmarks) + + +initCmd : Flags -> Maybe String -> Cmd Msg +initCmd flags selected = + Api.getBookmarks flags (GetBookmarksResp selected) + + +type Item + = BM BookmarkedQuery + | Share ShareDetail + + +toItems : AllBookmarks -> List Item +toItems all = + List.map BM all.bookmarks + ++ List.map Share all.shares + + +initWith : AllBookmarks -> Maybe String -> Model +initWith bms selected = + let + items = + toItems bms + + findSel id = + Util.List.find + (\b -> + case b of + BM m -> + m.id == id + + Share s -> + s.id == id + ) + items + in + Model <| + Comp.Dropdown.makeSingleList + { options = items, selected = Maybe.andThen findSel selected } + + +init : Flags -> Maybe String -> ( Model, Cmd Msg ) +init flags selected = + ( Model Comp.Dropdown.makeSingle, initCmd flags selected ) + + +getSelected : Model -> Maybe Item +getSelected model = + case model of + Model dm -> + Comp.Dropdown.getSelected dm + |> List.head + + +getSelectedId : Model -> Maybe String +getSelectedId model = + let + id item = + case item of + BM b -> + b.id + + Share s -> + s.id + in + getSelected model |> Maybe.map id + + + +--- Update + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + let + dmodel = + case model of + Model a -> + a + in + case msg of + GetBookmarksResp sel (Ok all) -> + ( initWith all sel, Cmd.none ) + + GetBookmarksResp _ (Err err) -> + ( model, Cmd.none ) + + DropdownMsg lm -> + let + ( dm, dc ) = + Comp.Dropdown.update lm dmodel + in + ( Model dm, Cmd.map DropdownMsg dc ) + + + +--- View + + +itemOption : Texts -> Item -> Option +itemOption texts item = + case item of + BM b -> + { text = b.name + , additional = + if b.personal then + texts.personal + + else + texts.collective + } + + Share s -> + { text = Maybe.withDefault "-" s.name, additional = texts.share } + + +itemColor : Item -> String +itemColor item = + case item of + BM b -> + if b.personal then + "text-cyan-600 dark:text-indigo-300" + + else + "text-sky-600 dark:text-violet-300" + + Share _ -> + "text-blue-600 dark:text-purple-300" + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + viewSettings = + { makeOption = itemOption texts + , placeholder = texts.placeholder + , labelColor = \a -> \_ -> itemColor a + , style = Data.DropdownStyle.mainStyle + } + + dm = + case model of + Model a -> + a + in + Html.map DropdownMsg + (Comp.Dropdown.viewSingle2 viewSettings settings dm) diff --git a/modules/webapp/src/main/elm/Comp/DetailEdit.elm b/modules/webapp/src/main/elm/Comp/DetailEdit.elm index e9d9c759..43b5231a 100644 --- a/modules/webapp/src/main/elm/Comp/DetailEdit.elm +++ b/modules/webapp/src/main/elm/Comp/DetailEdit.elm @@ -834,7 +834,7 @@ viewIntern2 texts settings withButtons model = ] , case model.form of TM tm -> - Html.map TagMsg (Comp.TagForm.view2 texts.tagForm tm) + Html.map TagMsg (Comp.TagForm.view2 texts.tagForm settings tm) PMR pm -> Html.map PersonMsg (Comp.PersonForm.view2 texts.personForm True settings pm) diff --git a/modules/webapp/src/main/elm/Comp/Dropdown.elm b/modules/webapp/src/main/elm/Comp/Dropdown.elm index 75ce2fb3..f8e039b7 100644 --- a/modules/webapp/src/main/elm/Comp/Dropdown.elm +++ b/modules/webapp/src/main/elm/Comp/Dropdown.elm @@ -462,16 +462,17 @@ view2 cfg settings model = viewMultiple2 cfg settings model else - viewSingle2 cfg model + viewSingle2 cfg settings model -viewSingle2 : ViewSettings a -> Model a -> Html (Msg a) -viewSingle2 cfg model = +viewSingle2 : ViewSettings a -> UiSettings -> Model a -> Html (Msg a) +viewSingle2 cfg settings model = let renderItem item = a [ href "#" , class cfg.style.item + , class (cfg.labelColor item.value settings) , classList [ ( cfg.style.itemActive, item.active ) , ( "font-semibold", item.selected ) @@ -480,7 +481,7 @@ viewSingle2 cfg model = , onKeyUp KeyPress ] [ text <| (.value >> cfg.makeOption >> .text) item - , span [ class "text-gray-400 float-right" ] + , span [ class "text-gray-400 opacity-75 float-right" ] [ text <| (.value >> cfg.makeOption >> .additional) item ] ] diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm index 098a61aa..e5fe3b67 100644 --- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm +++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm @@ -17,6 +17,7 @@ module Comp.PeriodicQueryTaskForm exposing ) import Comp.Basic as B +import Comp.BookmarkDropdown import Comp.CalEventInput import Comp.ChannelForm import Comp.MenuBar as MB @@ -44,6 +45,7 @@ type alias Model = , scheduleModel : Comp.CalEventInput.Model , queryModel : Comp.PowerSearchInput.Model , channelModel : Comp.ChannelForm.Model + , bookmarkDropdown : Comp.BookmarkDropdown.Model , formState : FormState , loading : Int } @@ -75,6 +77,7 @@ type Msg | CalEventMsg Comp.CalEventInput.Msg | QueryMsg Comp.PowerSearchInput.Msg | ChannelMsg Comp.ChannelForm.Msg + | BookmarkMsg Comp.BookmarkDropdown.Msg | StartOnce | Cancel | RequestDelete @@ -93,11 +96,14 @@ initWith flags s = res = Comp.PowerSearchInput.update - (Comp.PowerSearchInput.setSearchString s.query) + (Comp.PowerSearchInput.setSearchString (Maybe.withDefault "" s.query)) Comp.PowerSearchInput.init ( cfm, cfc ) = Comp.ChannelForm.initWith flags s.channel + + ( bm, bc ) = + Comp.BookmarkDropdown.init flags s.bookmark in ( { settings = s , enabled = s.enabled @@ -105,6 +111,7 @@ initWith flags s = , scheduleModel = sm , queryModel = res.model , channelModel = cfm + , bookmarkDropdown = bm , formState = FormStateInitial , loading = 0 , summary = s.summary @@ -113,6 +120,7 @@ initWith flags s = [ Cmd.map CalEventMsg sc , Cmd.map QueryMsg res.cmd , Cmd.map ChannelMsg cfc + , Cmd.map BookmarkMsg bc ] ) @@ -128,6 +136,9 @@ init flags ct = ( cfm, cfc ) = Comp.ChannelForm.init flags ct + + ( bm, bc ) = + Comp.BookmarkDropdown.init flags Nothing in ( { settings = Data.PeriodicQuerySettings.empty ct , enabled = False @@ -135,6 +146,7 @@ init flags ct = , scheduleModel = sm , queryModel = Comp.PowerSearchInput.init , channelModel = cfm + , bookmarkDropdown = bm , formState = FormStateInitial , loading = 0 , summary = Nothing @@ -142,6 +154,7 @@ init flags ct = , Cmd.batch [ Cmd.map CalEventMsg scmd , Cmd.map ChannelMsg cfc + , Cmd.map BookmarkMsg bc ] ) @@ -172,27 +185,46 @@ makeSettings model = Nothing -> Err ValidateCalEventInvalid - queryString = - Result.fromMaybe ValidateQueryStringRequired model.queryModel.input + query = + let + qstr = + model.queryModel.input + + bm = + Comp.BookmarkDropdown.getSelectedId model.bookmarkDropdown + in + case ( qstr, bm ) of + ( Just _, Just _ ) -> + Result.Ok ( qstr, bm ) + + ( Just _, Nothing ) -> + Result.Ok ( qstr, bm ) + + ( Nothing, Just _ ) -> + Result.Ok ( qstr, bm ) + + ( Nothing, Nothing ) -> + Result.Err ValidateQueryStringRequired channelM = Result.fromMaybe ValidateChannelRequired (Comp.ChannelForm.getChannel model.channelModel) - make timer channel query = + make timer channel q = { prev | enabled = model.enabled , schedule = Data.CalEvent.makeEvent timer , summary = model.summary , channel = channel - , query = query + , query = Tuple.first q + , bookmark = Tuple.second q } in Result.map3 make schedule_ channelM - queryString + query withValidSettings : (PeriodicQuerySettings -> Action) -> Model -> UpdateResult @@ -257,6 +289,17 @@ update flags msg model = , sub = Sub.none } + BookmarkMsg lm -> + let + ( bm, bc ) = + Comp.BookmarkDropdown.update lm model.bookmarkDropdown + in + { model = { model | bookmarkDropdown = bm } + , action = NoAction + , cmd = Cmd.map BookmarkMsg bc + , sub = Sub.none + } + ToggleEnabled -> { model = { model @@ -344,9 +387,14 @@ view texts extraClasses settings model = (Comp.PowerSearchInput.viewResult [] model.queryModel) ] - formHeader txt = + formHeader txt req = h2 [ class S.formHeader, class "mt-2" ] [ text txt + , if req then + B.inputRequired + + else + span [] [] ] in div @@ -438,23 +486,29 @@ view texts extraClasses settings model = ] ] , div [ class "mb-4" ] - [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) + [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) False , Html.map ChannelMsg (Comp.ChannelForm.view texts.channelForm settings model.channelModel) ] , div [ class "mb-4" ] - [ formHeader texts.queryLabel - , label - [ for "sharequery" - , class S.inputLabel + [ formHeader texts.queryLabel True + , div [ class "mb-3" ] + [ label [ class S.inputLabel ] + [ text "Bookmark" ] + , Html.map BookmarkMsg (Comp.BookmarkDropdown.view texts.bookmarkDropdown settings model.bookmarkDropdown) ] - [ text texts.queryLabel - , B.inputRequired + , div [ class "mb-3" ] + [ label + [ for "sharequery" + , class S.inputLabel + ] + [ text texts.queryLabel + ] + , queryInput ] - , queryInput ] , div [ class "mb-4" ] - [ formHeader texts.schedule + [ formHeader texts.schedule False , label [ class S.inputLabel ] [ text texts.schedule , B.inputRequired diff --git a/modules/webapp/src/main/elm/Comp/TagForm.elm b/modules/webapp/src/main/elm/Comp/TagForm.elm index 1ec2280e..015645f9 100644 --- a/modules/webapp/src/main/elm/Comp/TagForm.elm +++ b/modules/webapp/src/main/elm/Comp/TagForm.elm @@ -20,6 +20,7 @@ import Comp.Basic as B import Comp.Dropdown import Data.DropdownStyle as DS import Data.Flags exposing (Flags) +import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onInput) @@ -126,8 +127,8 @@ update _ msg model = --- View2 -view2 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> UiSettings -> Model -> Html Msg +view2 texts settings model = let categoryCfg = { makeOption = \s -> Comp.Dropdown.mkOption s @@ -170,6 +171,7 @@ view2 texts model = , Html.map CatMsg (Comp.Dropdown.viewSingle2 categoryCfg + settings model.catDropdown ) ] diff --git a/modules/webapp/src/main/elm/Comp/TagManage.elm b/modules/webapp/src/main/elm/Comp/TagManage.elm index 3c067d20..422e3264 100644 --- a/modules/webapp/src/main/elm/Comp/TagManage.elm +++ b/modules/webapp/src/main/elm/Comp/TagManage.elm @@ -24,6 +24,7 @@ import Comp.TagTable import Comp.YesNoDimmer import Data.Flags exposing (Flags) import Data.TagOrder exposing (TagOrder) +import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onSubmit) @@ -247,13 +248,13 @@ update flags msg model = --- View2 -view2 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> UiSettings -> Model -> Html Msg +view2 texts settings model = if model.viewMode == Table then viewTable2 texts model else - viewForm2 texts model + viewForm2 texts settings model viewTable2 : Texts -> Model -> Html Msg @@ -290,8 +291,8 @@ viewTable2 texts model = ] -viewForm2 : Texts -> Model -> Html Msg -viewForm2 texts model = +viewForm2 : Texts -> UiSettings -> Model -> Html Msg +viewForm2 texts settings model = let newTag = model.tagFormModel.tag.id == "" @@ -373,7 +374,7 @@ viewForm2 texts model = FormErrorSubmit m -> text m ] - , Html.map FormMsg (Comp.TagForm.view2 texts.tagForm model.tagFormModel) + , Html.map FormMsg (Comp.TagForm.view2 texts.tagForm settings model.tagFormModel) , B.loadingDimmer { active = model.loading , label = texts.basics.loading diff --git a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm b/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm index 63db0087..6a3a5b1e 100644 --- a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm +++ b/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm @@ -18,7 +18,8 @@ type alias PeriodicQuerySettings = , enabled : Bool , summary : Maybe String , channel : NotificationChannel - , query : String + , query : Maybe String + , bookmark : Maybe String , schedule : String } @@ -29,19 +30,21 @@ empty ct = , enabled = False , summary = Nothing , channel = Data.NotificationChannel.empty ct - , query = "" + , query = Nothing + , bookmark = Nothing , schedule = "" } decoder : D.Decoder PeriodicQuerySettings decoder = - D.map6 PeriodicQuerySettings + D.map7 PeriodicQuerySettings (D.field "id" D.string) (D.field "enabled" D.bool) - (D.field "summary" (D.maybe D.string)) + (D.maybe (D.field "summary" D.string)) (D.field "channel" Data.NotificationChannel.decoder) - (D.field "query" D.string) + (D.maybe (D.field "query" D.string)) + (D.maybe (D.field "bookmark" D.string)) (D.field "schedule" D.string) @@ -52,6 +55,7 @@ encode s = , ( "enabled", E.bool s.enabled ) , ( "summary", Maybe.map E.string s.summary |> Maybe.withDefault E.null ) , ( "channel", Data.NotificationChannel.encode s.channel ) - , ( "query", E.string s.query ) + , ( "query", Maybe.map E.string s.query |> Maybe.withDefault E.null ) + , ( "bookmark", Maybe.map E.string s.bookmark |> Maybe.withDefault E.null ) , ( "schedule", E.string s.schedule ) ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/BookmarkDropdown.elm b/modules/webapp/src/main/elm/Messages/Comp/BookmarkDropdown.elm new file mode 100644 index 00000000..4934a839 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/BookmarkDropdown.elm @@ -0,0 +1,43 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.BookmarkDropdown exposing + ( Texts + , de + , gb + ) + +import Messages.Basics + + +type alias Texts = + { basics : Messages.Basics.Texts + , placeholder : String + , personal : String + , collective : String + , share : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , placeholder = "Bookmark…" + , personal = "Personal" + , collective = "Collective" + , share = "Share" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , placeholder = "Bookmark…" + , personal = "Persönlich" + , collective = "Kollektiv" + , share = "Freigabe" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/BookmarkQueryForm.elm b/modules/webapp/src/main/elm/Messages/Comp/BookmarkQueryForm.elm index e5d3605f..4547ab70 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/BookmarkQueryForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/BookmarkQueryForm.elm @@ -33,7 +33,7 @@ gb = , userLocationText = "The bookmarked query is just for you" , collectiveLocation = "Collective scope" , collectiveLocationText = "The bookmarked query can be used and edited by all users" - , nameExistsWarning = "A bookmark with this name exists!" + , nameExistsWarning = "A bookmark with this name exists! Choose another name." } @@ -45,5 +45,5 @@ de = , userLocationText = "Der Bookmark ist nur für dich" , collectiveLocation = "Kollektiv-Bookmark" , collectiveLocationText = "Der Bookmark kann von allen Benutzer verwendet werden" - , nameExistsWarning = "Der Bookmark existiert bereits!" + , nameExistsWarning = "Der Bookmark existiert bereits! Verwende einen anderen Namen." } diff --git a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm index 96f8cfac..e988ce72 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm @@ -14,6 +14,7 @@ module Messages.Comp.PeriodicQueryTaskForm exposing import Data.ChannelType exposing (ChannelType) import Http import Messages.Basics +import Messages.Comp.BookmarkDropdown import Messages.Comp.CalEventInput import Messages.Comp.ChannelForm import Messages.Comp.HttpError @@ -24,6 +25,7 @@ type alias Texts = { basics : Messages.Basics.Texts , calEventInput : Messages.Comp.CalEventInput.Texts , channelForm : Messages.Comp.ChannelForm.Texts + , bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts , httpError : Http.Error -> String , reallyDeleteTask : String , startOnce : String @@ -49,6 +51,7 @@ gb = , calEventInput = Messages.Comp.CalEventInput.gb , channelForm = Messages.Comp.ChannelForm.gb , httpError = Messages.Comp.HttpError.gb + , bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb , reallyDeleteTask = "Really delete this notification task?" , startOnce = "Start Once" , startTaskNow = "Start this task now" @@ -66,7 +69,7 @@ gb = , invalidCalEvent = "The calendar event is not valid." , queryLabel = "Query" , channelRequired = "A valid channel must be given." - , queryStringRequired = "A query string must be supplied" + , queryStringRequired = "A query string and/or bookmark must be supplied" , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct } @@ -77,6 +80,7 @@ de = , calEventInput = Messages.Comp.CalEventInput.de , channelForm = Messages.Comp.ChannelForm.de , httpError = Messages.Comp.HttpError.de + , bookmarkDropdown = Messages.Comp.BookmarkDropdown.de , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" , startOnce = "Jetzt starten" , startTaskNow = "Starte den Auftrag sofort" @@ -94,6 +98,6 @@ de = , invalidCalEvent = "Das Kalenderereignis ist nicht gültig." , queryLabel = "Abfrage" , channelRequired = "Ein Versandkanal muss angegeben werden." - , queryStringRequired = "Eine Suchabfrage muss angegeben werden." + , queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark muss angegeben werden." , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct } diff --git a/modules/webapp/src/main/elm/Page/ManageData/View2.elm b/modules/webapp/src/main/elm/Page/ManageData/View2.elm index dbc686f2..f4947bf1 100644 --- a/modules/webapp/src/main/elm/Page/ManageData/View2.elm +++ b/modules/webapp/src/main/elm/Page/ManageData/View2.elm @@ -146,7 +146,7 @@ viewContent texts flags settings model = ] (case model.currentTab of Just TagTab -> - viewTags texts model + viewTags texts settings model Just EquipTab -> viewEquip texts model @@ -180,8 +180,8 @@ menuEntryActive model tab = class "" -viewTags : Texts -> Model -> List (Html Msg) -viewTags texts model = +viewTags : Texts -> UiSettings -> Model -> List (Html Msg) +viewTags texts settings model = [ h2 [ class S.header1 , class "inline-flex items-center" @@ -194,6 +194,7 @@ viewTags texts model = , Html.map TagManageMsg (Comp.TagManage.view2 texts.tagManage + settings model.tagManageModel ) ]