diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index c18e72e8..fd217fb5 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -81,7 +81,7 @@ object BackendApp { itemSearchImpl <- OItemSearch(store) fulltextImpl <- OFulltext(itemSearchImpl, ftsClient, store, queue, joexImpl) mailImpl <- OMail(store, javaEmil) - userTaskImpl <- OUserTask(utStore, queue, joexImpl) + userTaskImpl <- OUserTask(utStore, store, queue, joexImpl) folderImpl <- OFolder(store) customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala index 9485e4e0..8f8d0ab3 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala @@ -6,15 +6,16 @@ package docspell.backend.ops -import cats.data.OptionT +import cats.data.{NonEmptyList, OptionT} import cats.effect._ import cats.implicits._ import fs2.Stream import docspell.common._ -import docspell.notification.api.PeriodicDueItemsArgs -import docspell.notification.api.PeriodicQueryArgs +import docspell.notification.api.{ChannelRef, PeriodicDueItemsArgs, PeriodicQueryArgs} +import docspell.store.Store import docspell.store.queue.JobQueue +import docspell.store.records.RNotificationChannel import docspell.store.usertask._ import io.circe.Encoder @@ -83,7 +84,8 @@ trait OUserTask[F[_]] { object OUserTask { def apply[F[_]: Async]( - store: UserTaskStore[F], + taskStore: UserTaskStore[F], + store: Store[F], queue: JobQueue[F], joex: OJoex[F] ): Resource[F, OUserTask[F]] = @@ -100,7 +102,7 @@ object OUserTask { } yield () def getScanMailbox(scope: UserTaskScope): Stream[F, UserTask[ScanMailboxArgs]] = - store + taskStore .getByName[ScanMailboxArgs](scope, ScanMailboxArgs.taskName) def findScanMailbox( @@ -111,8 +113,8 @@ object OUserTask { def deleteTask(scope: UserTaskScope, id: Ident): F[Unit] = (for { - _ <- store.getByIdRaw(scope, id) - _ <- OptionT.liftF(store.deleteTask(scope, id)) + _ <- taskStore.getByIdRaw(scope, id) + _ <- OptionT.liftF(taskStore.deleteTask(scope, id)) } yield ()).getOrElse(()) def submitScanMailbox( @@ -121,21 +123,28 @@ object OUserTask { task: UserTask[ScanMailboxArgs] ): F[Unit] = for { - _ <- store.updateTask[ScanMailboxArgs](scope, subject, task) + _ <- taskStore.updateTask[ScanMailboxArgs](scope, subject, task) _ <- joex.notifyAllNodes } yield () def getNotifyDueItems( scope: UserTaskScope ): Stream[F, UserTask[PeriodicDueItemsArgs]] = - store + taskStore .getByName[PeriodicDueItemsArgs](scope, PeriodicDueItemsArgs.taskName) + .evalMap(ut => + resolveChannels(ut.args.channels) + .map(chs => ut.mapArgs(_.copy(channels = chs))) + ) def findNotifyDueItems( id: Ident, scope: UserTaskScope ): OptionT[F, UserTask[PeriodicDueItemsArgs]] = OptionT(getNotifyDueItems(scope).find(_.id == id).compile.last) + .semiflatMap(ut => + resolveChannels(ut.args.channels).map(ch => ut.mapArgs(_.copy(channels = ch))) + ) def submitNotifyDueItems( scope: UserTaskScope, @@ -143,18 +152,26 @@ object OUserTask { task: UserTask[PeriodicDueItemsArgs] ): F[Unit] = for { - _ <- store.updateTask[PeriodicDueItemsArgs](scope, subject, task) + _ <- taskStore.updateTask[PeriodicDueItemsArgs](scope, subject, task) _ <- joex.notifyAllNodes } yield () def getPeriodicQuery(scope: UserTaskScope): Stream[F, UserTask[PeriodicQueryArgs]] = - store.getByName[PeriodicQueryArgs](scope, PeriodicQueryArgs.taskName) + taskStore + .getByName[PeriodicQueryArgs](scope, PeriodicQueryArgs.taskName) + .evalMap(ut => + resolveChannels(ut.args.channels) + .map(chs => ut.mapArgs(_.copy(channels = chs))) + ) def findPeriodicQuery( id: Ident, scope: UserTaskScope ): OptionT[F, UserTask[PeriodicQueryArgs]] = OptionT(getPeriodicQuery(scope).find(_.id == id).compile.last) + .semiflatMap(ut => + resolveChannels(ut.args.channels).map(ch => ut.mapArgs(_.copy(channels = ch))) + ) def submitPeriodicQuery( scope: UserTaskScope, @@ -162,9 +179,18 @@ object OUserTask { task: UserTask[PeriodicQueryArgs] ): F[Unit] = for { - _ <- store.updateTask[PeriodicQueryArgs](scope, subject, task) + _ <- taskStore.updateTask[PeriodicQueryArgs](scope, subject, task) _ <- joex.notifyAllNodes } yield () - }) + // When retrieving arguments containing channel references, we must update + // details because they could have changed in the db. There are no separate + // database models for each user task, so rather a hacky compromise + private def resolveChannels( + refs: NonEmptyList[ChannelRef] + ): F[NonEmptyList[ChannelRef]] = + store.transact(RNotificationChannel.resolveRefs(refs)).map { resolved => + NonEmptyList.fromList(resolved).getOrElse(refs) + } + }) } diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelType.scala b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelType.scala index 4403f2be..cb5d177c 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelType.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelType.scala @@ -6,6 +6,7 @@ package docspell.notification.api +import cats.Order import cats.data.{NonEmptyList => Nel} import io.circe.Decoder @@ -43,4 +44,7 @@ object ChannelType { Decoder.decodeString.emap(fromString) implicit val jsonEncoder: Encoder[ChannelType] = Encoder.encodeString.contramap(_.name) + + implicit val order: Order[ChannelType] = + Order.by(_.name) } diff --git a/modules/store/src/main/scala/docspell/store/qb/DSL.scala b/modules/store/src/main/scala/docspell/store/qb/DSL.scala index 22e17dbd..e5cc1a9a 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala @@ -56,6 +56,9 @@ trait DSL extends DoobieMeta { def union(s1: Select, sn: Select*): Select = Select.Union(s1, sn.toVector) + def union(selects: Nel[Select]): Select = + Select.Union(selects.head, selects.tail.toVector) + def intersect(s1: Select, sn: Select*): Select = Select.Intersect(s1, sn.toVector) diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala index b51e366c..586bd82a 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala @@ -6,11 +6,12 @@ package docspell.store.records -import cats.data.OptionT +import cats.data.{NonEmptyList => Nel, OptionT} import cats.implicits._ import docspell.common._ import docspell.notification.api.{Channel, ChannelRef, ChannelType} +import docspell.store.qb.DSL._ import doobie._ @@ -130,6 +131,22 @@ object RNotificationChannel { RNotificationChannelHttp.getById(userId)(ref.id).map(_.map(Http.apply)) } + def resolveRefs(refs: Nel[ChannelRef]): ConnectionIO[List[ChannelRef]] = { + val byType = refs.groupByNem(_.channelType) + val queries = byType.toNel + .map { + case (ChannelType.Mail, refs) => + RNotificationChannelMail.findRefs(refs.map(_.id)) + case (ChannelType.Matrix, refs) => + RNotificationChannelMatrix.findRefs(refs.map(_.id)) + case (ChannelType.Gotify, refs) => + RNotificationChannelGotify.findRefs(refs.map(_.id)) + case (ChannelType.Http, refs) => + RNotificationChannelHttp.findRefs(refs.map(_.id)) + } + union(queries).build.query[ChannelRef].to[List] + } + def getByHook(hook: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = { def opt(id: Option[Ident]): OptionT[ConnectionIO, Ident] = OptionT.fromOption(id) diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala index b0da9e79..40bb9528 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala @@ -9,6 +9,7 @@ package docspell.store.records import cats.data.NonEmptyList import docspell.common._ +import docspell.notification.api.ChannelType import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -97,4 +98,11 @@ object RNotificationChannelGotify { T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account))) ) } + + def findRefs(ids: NonEmptyList[Ident]): Select = + Select( + select(T.id.s, const(ChannelType.Gotify.name), T.name.s), + from(T), + T.id.in(ids) + ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala index a70eb8d8..b06eaae7 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala @@ -9,6 +9,7 @@ package docspell.store.records import cats.data.NonEmptyList import docspell.common._ +import docspell.notification.api.ChannelType import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -80,4 +81,11 @@ object RNotificationChannelHttp { T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account))) ) } + + def findRefs(ids: NonEmptyList[Ident]): Select = + Select( + select(T.id.s, const(ChannelType.Http.name), T.name.s), + from(T), + T.id.in(ids) + ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala index afa2d34c..d73fe0c8 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala @@ -9,6 +9,7 @@ package docspell.store.records import cats.data.NonEmptyList import docspell.common._ +import docspell.notification.api.ChannelType import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -90,4 +91,11 @@ object RNotificationChannelMail { T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account))) ) } + + def findRefs(ids: NonEmptyList[Ident]): Select = + Select( + select(T.id.s, const(ChannelType.Mail.name), T.name.s), + from(T), + T.id.in(ids) + ) } diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala index d6981906..f6965b88 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala @@ -9,6 +9,7 @@ package docspell.store.records import cats.data.NonEmptyList import docspell.common._ +import docspell.notification.api.ChannelType import docspell.store.qb.DSL._ import docspell.store.qb._ @@ -106,4 +107,11 @@ object RNotificationChannelMatrix { T.id === id && T.uid.in(Select(select(u.uid), from(u), u.isAccount(account))) ) } + + def findRefs(ids: NonEmptyList[Ident]): Select = + Select( + select(T.id.s, const(ChannelType.Matrix.name), T.name.s), + from(T), + T.id.in(ids) + ) } diff --git a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala index 60ce80f3..13685aca 100644 --- a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala +++ b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala @@ -29,6 +29,11 @@ case class UserTask[A]( def encode(implicit E: Encoder[A]): UserTask[String] = copy(args = E(args).noSpaces) + def withArgs[B](newArgs: B): UserTask[B] = + copy(args = newArgs) + + def mapArgs[B](f: A => B): UserTask[B] = + withArgs(f(args)) } object UserTask { diff --git a/modules/webapp/src/main/elm/App/Update.elm b/modules/webapp/src/main/elm/App/Update.elm index 48e2621d..6da2ac12 100644 --- a/modules/webapp/src/main/elm/App/Update.elm +++ b/modules/webapp/src/main/elm/App/Update.elm @@ -499,8 +499,11 @@ updateRegister lmsg model = updateQueue : Page.Queue.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) updateQueue lmsg model = let + notQueuePage = + model.page /= QueuePage + ( lm, lc ) = - Page.Queue.Update.update model.flags lmsg model.queueModel + Page.Queue.Update.update model.flags notQueuePage lmsg model.queueModel in ( { model | queueModel = lm } , Cmd.map QueueMsg lc @@ -509,7 +512,7 @@ updateQueue lmsg model = updateUserSettings : Messages -> Page.UserSettings.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) -updateUserSettings texts lmsg model = +updateUserSettings _ lmsg model = let result = Page.UserSettings.Update.update model.flags model.uiSettings lmsg model.userSettingsModel @@ -626,54 +629,50 @@ initPage model_ page = texts = Messages.get <| App.Data.getUiLanguage model + + noop = + ( model, Cmd.none, Sub.none ) in case page of SearchPage _ -> Util.Update.andThen2 [ updateSearch texts Page.Search.Data.Init - , updateQueue Page.Queue.Data.StopRefresh ] model LoginPage _ -> - updateQueue Page.Queue.Data.StopRefresh model + noop ManageDataPage -> - updateQueue Page.Queue.Data.StopRefresh model + noop CollectiveSettingPage -> Util.Update.andThen2 - [ updateQueue Page.Queue.Data.StopRefresh - , updateCollSettings texts Page.CollectiveSettings.Data.Init + [ updateCollSettings texts Page.CollectiveSettings.Data.Init ] model UserSettingPage -> - Util.Update.andThen2 - [ updateQueue Page.Queue.Data.StopRefresh - ] - model + noop QueuePage -> updateQueue Page.Queue.Data.Init model RegisterPage -> - updateQueue Page.Queue.Data.StopRefresh model + noop UploadPage _ -> Util.Update.andThen2 - [ updateQueue Page.Queue.Data.StopRefresh - , updateUpload Page.Upload.Data.reset + [ updateUpload Page.Upload.Data.reset ] model NewInvitePage -> - updateQueue Page.Queue.Data.StopRefresh model + noop ItemDetailPage id -> Util.Update.andThen2 [ updateItemDetail texts (Page.ItemDetail.Data.Init id) - , updateQueue Page.Queue.Data.StopRefresh ] model diff --git a/modules/webapp/src/main/elm/Page/Queue/Data.elm b/modules/webapp/src/main/elm/Page/Queue/Data.elm index 6683f2cd..a8a30116 100644 --- a/modules/webapp/src/main/elm/Page/Queue/Data.elm +++ b/modules/webapp/src/main/elm/Page/Queue/Data.elm @@ -31,7 +31,6 @@ type alias Model = , formState : FormState , pollingInterval : Float , init : Bool - , stopRefresh : Bool , currentMillis : Int , showLog : Maybe JobDetail , deleteConfirm : Comp.YesNoDimmer.Model @@ -60,7 +59,6 @@ emptyModel = , formState = InitialForm , pollingInterval = 1200 , init = False - , stopRefresh = False , currentMillis = 0 , showLog = Nothing , deleteConfirm = Comp.YesNoDimmer.emptyModel @@ -72,7 +70,6 @@ emptyModel = type Msg = Init | StateResp (Result Http.Error JobQueueState) - | StopRefresh | NewTime Time.Posix | ShowLog JobDetail | QuitShowLog diff --git a/modules/webapp/src/main/elm/Page/Queue/Update.elm b/modules/webapp/src/main/elm/Page/Queue/Update.elm index 124fa363..3407f74c 100644 --- a/modules/webapp/src/main/elm/Page/Queue/Update.elm +++ b/modules/webapp/src/main/elm/Page/Queue/Update.elm @@ -15,8 +15,8 @@ import Task import Time -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) -update flags msg model = +update : Flags -> Bool -> Msg -> Model -> ( Model, Cmd Msg ) +update flags stopRefresh msg model = case msg of Init -> let @@ -30,12 +30,15 @@ update flags msg model = , getNewTime ] in - ( { model | init = True, stopRefresh = False }, start ) + ( { model | init = True }, start ) StateResp (Ok s) -> let + stop = + model.pollingInterval <= 0 || stopRefresh + refresh = - if model.pollingInterval <= 0 || model.stopRefresh then + if stop then Cmd.none else @@ -44,14 +47,11 @@ update flags msg model = , getNewTime ] in - ( { model | state = s, stopRefresh = False }, refresh ) + ( { model | state = s, init = False }, refresh ) StateResp (Err err) -> ( { model | formState = HttpError err }, Cmd.none ) - StopRefresh -> - ( { model | stopRefresh = True, init = False }, Cmd.none ) - NewTime t -> ( { model | currentMillis = Time.posixToMillis t }, Cmd.none ) @@ -66,7 +66,7 @@ update flags msg model = newModel = { model | cancelJobRequest = Just job.id } in - update flags (DimmerMsg job Comp.YesNoDimmer.Activate) newModel + update flags stopRefresh (DimmerMsg job Comp.YesNoDimmer.Activate) newModel DimmerMsg job m -> let