diff --git a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala index 128dc097..05320b31 100644 --- a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala +++ b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala @@ -11,7 +11,6 @@ import cats.implicits._ import docspell.backend.MailAddressCodec import docspell.common._ -import docspell.notification.api.ChannelOrRef._ import docspell.notification.api.PeriodicQueryArgs import docspell.store.records.RJob @@ -25,7 +24,7 @@ object JobFactory extends MailAddressCodec { PeriodicQueryArgs.taskName, submitter.collective, args, - s"Running periodic query, notify via ${args.channel.channelType}", + s"Running periodic query, notify via ${args.channels.map(_.channelType)}", now, submitter.user, Priority.Low, diff --git a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala index efa54b71..1ed08014 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala @@ -39,7 +39,10 @@ trait ONotification[F[_]] { userId: Ident ): F[Vector[NotificationChannel]] - def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] + def findNotificationChannel( + ref: ChannelRef, + account: AccountId + ): F[Vector[NotificationChannel]] def listChannels(account: AccountId): F[Vector[Channel]] @@ -65,7 +68,7 @@ trait ONotification[F[_]] { def sendSampleEvent( evt: EventType, - channel: Channel, + channel: Nel[ChannelRef], account: AccountId, baseUrl: Option[LenientUri] ): F[ONotification.SendTestResult] @@ -89,7 +92,7 @@ object ONotification { .getOrElse(UpdateResult.notFound) def offerEvents(ev: Iterable[Event]): F[Unit] = - ev.toList.traverse(notMod.offer(_)).as(()) + ev.toList.traverse(notMod.offer).as(()) def sendMessage( logger: Logger[F], @@ -109,33 +112,27 @@ object ONotification { def sendSampleEvent( evt: EventType, - channel: Channel, + channels: Nel[ChannelRef], account: AccountId, baseUrl: Option[LenientUri] - ): F[SendTestResult] = { - def doCreate(userId: Ident) = - (for { - ev <- sampleEvent(evt, account, baseUrl) - logbuf <- Logger.buffer() - ch <- mkNotificationChannel(channel, userId) - _ <- notMod.send(logbuf._2.andThen(log), ev, ch) - logs <- logbuf._1.get - res = SendTestResult(true, logs) - } yield res).attempt - .map { - case Right(res) => res - case Left(ex) => - val ps = new StringWriter() - ex.printStackTrace(new PrintWriter(ps)) - SendTestResult(false, Vector(s"${ex.getMessage}\n$ps")) - } - - OptionT(store.transact(RUser.findIdByAccount(account))) - .semiflatMap(doCreate) - .getOrElse( - SendTestResult(false, Vector(s"No user found in db for: ${account.asString}")) + ): F[SendTestResult] = + (for { + ev <- sampleEvent(evt, account, baseUrl) + logbuf <- Logger.buffer() + ch <- channels.toList.toVector.flatTraverse( + findNotificationChannel(_, account) ) - } + _ <- notMod.send(logbuf._2.andThen(log), ev, ch) + logs <- logbuf._1.get + res = SendTestResult(true, logs) + } yield res).attempt + .map { + case Right(res) => res + case Left(ex) => + val ps = new StringWriter() + ex.printStackTrace(new PrintWriter(ps)) + SendTestResult(false, Vector(s"${ex.getMessage}\n$ps")) + } def listChannels(account: AccountId): F[Vector[Channel]] = store @@ -153,7 +150,7 @@ object ONotification { (for { newId <- OptionT.liftF(Ident.randomId[F]) userId <- OptionT(store.transact(RUser.findIdByAccount(account))) - r <- ChannelConv.makeRecord[F](store, log, Right(channel), newId, userId) + r <- ChannelConv.makeRecord[F](store, channel, newId, userId) _ <- OptionT.liftF(store.transact(RNotificationChannel.insert(r))) _ <- OptionT.liftF(log.debug(s"Created channel $r for $account")) } yield AddResult.Success) @@ -162,7 +159,7 @@ object ONotification { def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] = (for { userId <- OptionT(store.transact(RUser.findIdByAccount(account))) - r <- ChannelConv.makeRecord[F](store, log, Right(channel), channel.id, userId) + r <- ChannelConv.makeRecord[F](store, channel, channel.id, userId) n <- OptionT.liftF(store.transact(RNotificationChannel.update(r))) } yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound) @@ -179,16 +176,14 @@ object ONotification { def createHook(hook: Hook, account: AccountId): F[AddResult] = (for { _ <- OptionT.liftF(log.debug(s"Creating new notification hook: $hook")) - channelId <- OptionT.liftF(Ident.randomId[F]) userId <- OptionT(store.transact(RUser.findIdByAccount(account))) - r <- ChannelConv.makeRecord[F](store, log, hook.channel, channelId, userId) + hr <- OptionT.liftF(Hook.makeRecord(userId, hook)) _ <- OptionT.liftF( - if (channelId == r.id) store.transact(RNotificationChannel.insert(r)) - else ().pure[F] + store.transact( + RNotificationHook.insert(hr) *> RNotificationHookChannel + .updateAll(hr.id, hook.channels.toList) + ) ) - _ <- OptionT.liftF(log.debug(s"Created channel $r for $account")) - hr <- OptionT.liftF(Hook.makeRecord(r, userId, hook)) - _ <- OptionT.liftF(store.transact(RNotificationHook.insert(hr))) _ <- OptionT.liftF( store.transact(RNotificationHookEvent.insertAll(hr.id, hook.events)) ) @@ -203,31 +198,25 @@ object ONotification { .getOrElse(UpdateResult.notFound) ) - def withChannel( - r: RNotificationHook - )(f: RNotificationChannel => F[UpdateResult]): F[UpdateResult] = - ChannelConv - .makeRecord(store, log, hook.channel, r.channelId, r.uid) - .semiflatMap(f) - .getOrElse(UpdateResult.notFound) - def doUpdate(r: RNotificationHook): F[UpdateResult] = - withChannel(r) { ch => - UpdateResult.fromUpdate(store.transact(for { - nc <- RNotificationChannel.update(ch) - ne <- RNotificationHookEvent.updateAll( - r.id, - if (hook.allEvents) Nil else hook.events + UpdateResult.fromUpdate(store.transact(for { + ne <- RNotificationHookEvent.updateAll( + r.id, + if (hook.allEvents) Nil else hook.events + ) + nc <- RNotificationHookChannel.updateAll( + r.id, + hook.channels.toList + ) + nr <- RNotificationHook.update( + r.copy( + enabled = hook.enabled, + allEvents = hook.allEvents, + eventFilter = hook.eventFilter ) - nr <- RNotificationHook.update( - r.copy( - enabled = hook.enabled, - allEvents = hook.allEvents, - eventFilter = hook.eventFilter - ) - ) - } yield nc + ne + nr)) - } + ) + + } yield nc + ne + nr)) withHook(doUpdate) } @@ -238,13 +227,17 @@ object ONotification { ): F[Vector[NotificationChannel]] = (for { rec <- ChannelConv - .makeRecord(store, log, Right(channel), channel.id, userId) + .makeRecord(store, channel, channel.id, userId) ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec))) } yield ch).getOrElse(Vector.empty) - def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] = + def findNotificationChannel( + ref: ChannelRef, + accountId: AccountId + ): F[Vector[NotificationChannel]] = (for { - rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref))) + userId <- OptionT(store.transact(RUser.findIdByAccount(accountId))) + rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref, userId))) ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec))) } yield ch).getOrElse(Vector.empty) }) @@ -264,84 +257,30 @@ object ONotification { Channel.Gotify(r.id, gotify.name, gotify.url, gotify.appKey, gotify.priority), matrix => Channel - .Matrix(r.id, matrix.name, matrix.homeServer, matrix.roomId, matrix.accessToken), + .Matrix( + r.id, + matrix.name, + matrix.homeServer, + matrix.roomId, + matrix.accessToken + ), http => Channel.Http(r.id, http.name, http.url) ) - private[ops] def makeRecord[F[_]: Sync]( + private[ops] def makeRecord[F[_]]( store: Store[F], - logger: Logger[F], - channelIn: Either[ChannelRef, Channel], + channel: Channel, id: Ident, userId: Ident ): OptionT[F, RNotificationChannel] = - channelIn match { - case Left(ref) => - OptionT.liftF(logger.debug(s"Loading channel for ref: ${ref}")) *> - OptionT(store.transact(RNotificationChannel.getByRef(ref))) + RNotificationChannel.fromChannel(channel, id, userId).mapK(store.transform) - case Right(channel) => - for { - time <- OptionT.liftF(Timestamp.current[F]) - r <- - channel match { - case Channel.Mail(_, name, conn, recipients) => - for { - _ <- OptionT.liftF( - logger.debug( - s"Looking up user smtp for ${userId.id} and ${conn.id}" - ) - ) - mailConn <- OptionT( - store.transact(RUserEmail.getByUser(userId, conn)) - ) - rec = RNotificationChannelMail( - id, - userId, - name, - mailConn.id, - recipients.toList, - time - ).vary - } yield rec - case Channel.Gotify(_, name, url, appKey, prio) => - OptionT.pure[F]( - RNotificationChannelGotify( - id, - userId, - name, - url, - appKey, - prio, - time - ).vary - ) - case Channel.Matrix(_, name, homeServer, roomId, accessToken) => - OptionT.pure[F]( - RNotificationChannelMatrix( - id, - userId, - name, - homeServer, - roomId, - accessToken, - "m.text", - time - ).vary - ) - case Channel.Http(_, name, url) => - OptionT.pure[F]( - RNotificationChannelHttp(id, userId, name, url, time).vary - ) - } - } yield r - } } final case class Hook( id: Ident, enabled: Boolean, - channel: Either[ChannelRef, Channel], + channels: List[ChannelRef], allEvents: Boolean, eventFilter: Option[JsonMiniQuery], events: List[EventType] @@ -354,14 +293,12 @@ object ONotification { r: RNotificationHook, events: List[EventType] ): ConnectionIO[Hook] = - RNotificationChannel - .getByHook(r) - .map(_.head) - .map(ChannelConv.makeChannel) - .map(ch => Hook(r.id, r.enabled, Right(ch), r.allEvents, r.eventFilter, events)) + RNotificationHookChannel + .allOfNel(r.id) + .flatMap(rhcs => RNotificationHookChannel.resolveRefs(rhcs)) + .map(refs => Hook(r.id, r.enabled, refs, r.allEvents, r.eventFilter, events)) private[ops] def makeRecord[F[_]: Sync]( - ch: RNotificationChannel, userId: Ident, hook: Hook ): F[RNotificationHook] = @@ -372,10 +309,6 @@ object ONotification { id, userId, hook.enabled, - ch.fold(_.id.some, _ => None, _ => None, _ => None), - ch.fold(_ => None, _.id.some, _ => None, _ => None), - ch.fold(_ => None, _ => None, _.id.some, _ => None), - ch.fold(_ => None, _ => None, _ => None, _.id.some), hook.allEvents, hook.eventFilter, time 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 dfc77491..9485e4e0 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala @@ -11,7 +11,6 @@ import cats.effect._ import cats.implicits._ import fs2.Stream -import docspell.backend.MailAddressCodec._ import docspell.common._ import docspell.notification.api.PeriodicDueItemsArgs import docspell.notification.api.PeriodicQueryArgs diff --git a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala index ebaa60b5..8ef5bc33 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala @@ -7,7 +7,6 @@ package docspell.joex.notify import cats.data.NonEmptyList -import cats.data.OptionT import cats.effect._ import cats.implicits._ @@ -24,7 +23,6 @@ import docspell.query.ItemQueryDsl._ import docspell.store.qb.Batch import docspell.store.queries.ListItem import docspell.store.queries.{QItem, Query} -import docspell.store.records.RUser object PeriodicDueItemsTask { val taskName = PeriodicDueItemsArgs.taskName @@ -51,11 +49,7 @@ object PeriodicDueItemsTask { def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])( cont: Vector[NotificationChannel] => F[Unit] ): F[Unit] = - OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account))) - .semiflatMap(userId => - TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont) - ) - .getOrElse(()) + TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont) def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)( cont: Vector[ListItem] => F[Unit] 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 ad16f2b9..51f06b20 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala @@ -26,7 +26,6 @@ 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 { val taskName = PeriodicQueryArgs.taskName @@ -53,11 +52,7 @@ object PeriodicQueryTask { def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])( cont: Vector[NotificationChannel] => F[Unit] ): F[Unit] = - OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account))) - .semiflatMap(userId => - TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont) - ) - .getOrElse(()) + TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont) private def queryString(q: ItemQuery.Expr) = ItemQueryParser.asString(q) diff --git a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala index a1152c13..d57ba05e 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala @@ -12,7 +12,7 @@ import cats.implicits._ import docspell.backend.ops.ONotification import docspell.common._ -import docspell.notification.api.ChannelOrRef +import docspell.notification.api.ChannelRef import docspell.notification.api.Event import docspell.notification.api.EventContext import docspell.notification.api.NotificationChannel @@ -23,19 +23,18 @@ trait TaskOperations { def withChannel[F[_]: Sync]( logger: Logger[F], - channel: ChannelOrRef, - userId: Ident, + channelsIn: NonEmptyList[ChannelRef], + accountId: AccountId, ops: ONotification[F] )( cont: Vector[NotificationChannel] => F[Unit] ): F[Unit] = { - val channels = channel match { - case Right(ch) => ops.mkNotificationChannel(ch, userId) - case Left(ref) => ops.findNotificationChannel(ref) - } + val channels = + channelsIn.toList.toVector.flatTraverse(ops.findNotificationChannel(_, accountId)) + channels.flatMap { ch => if (ch.isEmpty) - logger.error(s"No channels found for the given data: ${channel}") + logger.error(s"No channels found for the given data: ${channelsIn}") else cont(ch) } } diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala b/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala index 1735b610..fa9f785f 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala @@ -19,13 +19,14 @@ import io.circe.{Decoder, Encoder} sealed trait Channel { def id: Ident def channelType: ChannelType + def name: Option[String] def fold[A]( f1: Channel.Mail => A, f2: Channel.Gotify => A, f3: Channel.Matrix => A, f4: Channel.Http => A ): A - def asRef: ChannelRef = ChannelRef(id, channelType) + def asRef: ChannelRef = ChannelRef(id, channelType, name) } object Channel { @@ -98,7 +99,8 @@ object Channel { implicit val jsonEncoder: Encoder[Matrix] = deriveConfiguredEncoder } - final case class Http(id: Ident, name: Option[String], url: LenientUri) extends Channel { + final case class Http(id: Ident, name: Option[String], url: LenientUri) + extends Channel { val channelType = ChannelType.Http def fold[A]( f1: Mail => A, diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala index cfd21d48..1cc3d191 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala @@ -12,7 +12,7 @@ import io.circe.Decoder import io.circe.Encoder import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -final case class ChannelRef(id: Ident, channelType: ChannelType) +final case class ChannelRef(id: Ident, channelType: ChannelType, name: Option[String]) object ChannelRef { diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala index fc10767f..7de991cf 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala +++ b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala @@ -6,9 +6,10 @@ package docspell.notification.api +import cats.data.NonEmptyList + import docspell.common._ -import emil.MailAddress import io.circe.generic.semiauto import io.circe.{Decoder, Encoder} @@ -21,7 +22,7 @@ import io.circe.{Decoder, Encoder} */ final case class PeriodicDueItemsArgs( account: AccountId, - channel: ChannelOrRef, + channels: NonEmptyList[ChannelRef], remindDays: Int, daysBack: Option[Int], tagsInclude: List[Ident], @@ -30,19 +31,11 @@ final case class PeriodicDueItemsArgs( ) object PeriodicDueItemsArgs { - val taskName = Ident.unsafe("periodic-due-items-notify") + val taskName = Ident.unsafe("periodic-due-items-notify2") - implicit def jsonDecoder(implicit - mc: Decoder[MailAddress] - ): Decoder[PeriodicDueItemsArgs] = { - implicit val x = ChannelOrRef.jsonDecoder + implicit val jsonDecoder: Decoder[PeriodicDueItemsArgs] = semiauto.deriveDecoder - } - implicit def jsonEncoder(implicit - mc: Encoder[MailAddress] - ): Encoder[PeriodicDueItemsArgs] = { - implicit val x = ChannelOrRef.jsonEncoder + implicit val jsonEncoder: Encoder[PeriodicDueItemsArgs] = semiauto.deriveEncoder - } } 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 cf1c16a1..8e0dd405 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 @@ -6,15 +6,16 @@ package docspell.notification.api +import cats.data.NonEmptyList + import docspell.common._ -import emil.MailAddress import io.circe.generic.semiauto import io.circe.{Decoder, Encoder} final case class PeriodicQueryArgs( account: AccountId, - channel: ChannelOrRef, + channels: NonEmptyList[ChannelRef], query: Option[ItemQueryString], bookmark: Option[String], baseUrl: Option[LenientUri], @@ -22,19 +23,11 @@ final case class PeriodicQueryArgs( ) object PeriodicQueryArgs { - val taskName = Ident.unsafe("periodic-query-notify") + val taskName = Ident.unsafe("periodic-query-notify2") - implicit def jsonDecoder(implicit - mc: Decoder[MailAddress] - ): Decoder[PeriodicQueryArgs] = { - implicit val x = ChannelOrRef.jsonDecoder + implicit val jsonDecoder: Decoder[PeriodicQueryArgs] = semiauto.deriveDecoder - } - implicit def jsonEncoder(implicit - mc: Encoder[MailAddress] - ): Encoder[PeriodicQueryArgs] = { - implicit val x = ChannelOrRef.jsonEncoder + implicit def jsonEncoder: Encoder[PeriodicQueryArgs] = semiauto.deriveEncoder - } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 4e9a169e..52180b9f 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1739,7 +1739,7 @@ paths: schema: type: array items: - $ref: "#/extraSchemas/NotificationHook" + $ref: "#/components/schemas/NotificationHook" post: operationId: "sec-notification-hook-post" tags: [ Notification ] @@ -1753,7 +1753,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/NotificationHook" + $ref: "#/components/schemas/NotificationHook" responses: 422: description: BadRequest @@ -1775,7 +1775,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/NotificationHook" + $ref: "#/components/schemas/NotificationHook" responses: 422: description: BadRequest @@ -1821,7 +1821,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/NotificationHook" + $ref: "#/components/schemas/NotificationHook" responses: 422: description: BadRequest @@ -4917,7 +4917,7 @@ paths: schema: type: array items: - $ref: "#/extraSchemas/PeriodicDueItemsSettings" + $ref: "#/components/schemas/PeriodicDueItemsSettings" post: operationId: "sec-usertask-notify-new" tags: [ User Tasks ] @@ -4931,7 +4931,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicDueItemsSettings" + $ref: "#/components/schemas/PeriodicDueItemsSettings" responses: 422: description: BadRequest @@ -4954,7 +4954,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicDueItemsSettings" + $ref: "#/components/schemas/PeriodicDueItemsSettings" responses: 422: description: BadRequest @@ -4984,7 +4984,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicDueItemsSettings" + $ref: "#/components/schemas/PeriodicDueItemsSettings" delete: operationId: "sec-usertask-notify-delete" tags: [ User Tasks ] @@ -5018,7 +5018,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicDueItemsSettings" + $ref: "#/components/schemas/PeriodicDueItemsSettings" responses: 422: description: BadRequest @@ -5048,7 +5048,7 @@ paths: schema: type: array items: - $ref: "#/extraSchemas/PeriodicQuerySettings" + $ref: "#/components/schemas/PeriodicQuerySettings" post: operationId: "sec-usertask-periodic-query-new" tags: [ User Tasks ] @@ -5062,7 +5062,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicQuerySettings" + $ref: "#/components/schemas/PeriodicQuerySettings" responses: 422: description: BadRequest @@ -5085,7 +5085,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicQuerySettings" + $ref: "#/components/schemas/PeriodicQuerySettings" responses: 422: description: BadRequest @@ -5115,7 +5115,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicQuerySettings" + $ref: "#/components/schemas/PeriodicQuerySettings" delete: operationId: "sec-usertask-periodic-query-delete" tags: [ User Tasks ] @@ -5149,7 +5149,7 @@ paths: content: application/json: schema: - $ref: "#/extraSchemas/PeriodicQuerySettings" + $ref: "#/components/schemas/PeriodicQuerySettings" responses: 422: description: BadRequest @@ -5467,7 +5467,10 @@ components: type: string NotificationChannelRef: description: | - A reference to a channel. + A reference to a channel. The `id` and `channelType` are + required to identify a channel. The `name` attribute is as a + descriptive name and is returned by the server if it is + specified for the corresponding channel. required: - id - channelType @@ -5478,6 +5481,8 @@ components: channelType: type: string format: channeltype + name: + type: string NotificationMatrix: description: | A notification channel for matrix. @@ -5576,6 +5581,136 @@ components: items: type: string + NotificationHook: + description: | + Describes a notifcation hook. There must be at least one + channel specified. When creating hooks, the channels must + provide the `ìd` and the `channelType` while their `name` + attribute is optional. + required: + - id + - enabled + - channel + - events + - allEvents + properties: + id: + type: string + format: ident + enabled: + type: boolean + channels: + type: array + items: + $ref: "#/components/schemas/NotificationChannelRef" + allEvents: + type: boolean + eventFilter: + type: string + format: jsonminiq + description: | + A filter expression that is applied to the event to be able + to ignore a subset of them. See its + [documentation](https://docspell.org/docs/jsonminiquery/). + events: + type: array + items: + type: string + format: eventtype + enum: + - tagsAdded + - tagsSet + + PeriodicQuerySettings: + description: | + Settings for the periodc-query task. At least one of `query` + and `bookmark` is required! There must be at least one channel + specified when creating settings. A channel must provide its + `id` and `channelType`, while its `name` is optional. + required: + - id + - enabled + - channel + - schedule + properties: + id: + type: string + format: ident + enabled: + type: boolean + summary: + type: string + channels: + type: array + items: + $ref: "#/components/schemas/NotificationChannelRef" + schedule: + type: string + format: calevent + query: + type: string + format: itemquery + bookmark: + type: string + description: | + Name or ID of bookmark to use. + contentStart: + type: string + + PeriodicDueItemsSettings: + description: | + Settings for notifying about due items. At least one of + `query` and `bookmark` is required! There must be at least one + channel specified when creating settings. A channel must + provide its `id` and `channelType`, while its `name` is + optional. + required: + - id + - enabled + - channel + - schedule + - remindDays + - capOverdue + - tagsInclude + - tagsExclude + properties: + id: + type: string + format: ident + enabled: + type: boolean + summary: + type: string + channels: + type: array + items: + $ref: "#/components/schemas/NotificationChannelRef" + schedule: + type: string + format: calevent + remindDays: + type: integer + format: int32 + description: | + Used to restrict items by their due dates. All items with + a due date lower than (now + remindDays) are searched. + capOverdue: + type: boolean + description: | + If this is true, the search is also restricted to due + dates greater than `now - remindDays'. Otherwise, due date + are not restricted in that direction (only lower than `now + + remindDays' applies) and it is expected to restrict it + more using custom tags. + tagsInclude: + type: array + items: + $ref: "#/components/schemas/Tag" + tagsExclude: + type: array + items: + $ref: "#/components/schemas/Tag" + ShareSecret: description: | The secret (the share id + optional password) to access a @@ -8009,137 +8144,3 @@ components: schema: type: string format: ident - -# sadly no generator support for these. -# Changes here requires corresponding changes in: -# - NotificationHook.elm -# - routes.model.* -extraSchemas: - NotificationHook: - description: | - Describes a notifcation hook. There must be exactly one channel - specified, so either use a `channelRef` or one `channel`. - required: - - id - - enabled - - channel - - events - - allEvents - properties: - id: - type: string - format: ident - enabled: - type: boolean - channel: - oneOf: - - $ref: "#/components/schemas/NotificationMail" - - $ref: "#/components/schemas/NotificationGotify" - - $ref: "#/components/schemas/NotificationMatrix" - - $ref: "#/components/schemas/NotificationHttp" - - $ref: "#/components/schemas/NotificationChannelRef" - allEvents: - type: boolean - eventFilter: - type: string - format: jsonminiq - description: | - A filter expression that is applied to the event to be able - to ignore a subset of them. See its - [documentation](https://docspell.org/docs/jsonminiquery/). - events: - type: array - items: - type: string - format: eventtype - enum: - - tagsAdded - - tagsSet - - PeriodicQuerySettings: - description: | - Settings for the periodc-query task. At least one of `query` and - `bookmark` is required! - required: - - id - - enabled - - channel - - schedule - properties: - id: - type: string - format: ident - enabled: - type: boolean - summary: - type: string - channel: - oneOf: - - $ref: "#/components/schemas/NotificationMail" - - $ref: "#/components/schemas/NotificationGotify" - - $ref: "#/components/schemas/NotificationMatrix" - - $ref: "#/components/schemas/NotificationHttp" - - $ref: "#/components/schemas/NotificationChannelRef" - schedule: - type: string - format: calevent - query: - type: string - format: itemquery - bookmark: - type: string - description: | - Name or ID of bookmark to use. - - PeriodicDueItemsSettings: - description: | - Settings for notifying about due items. - required: - - id - - enabled - - channel - - schedule - - remindDays - - capOverdue - - tagsInclude - - tagsExclude - properties: - id: - type: string - format: ident - enabled: - type: boolean - summary: - type: string - channel: - oneOf: - - $ref: "#/components/schemas/NotificationMail" - - $ref: "#/components/schemas/NotificationGotify" - - $ref: "#/components/schemas/NotificationMatrix" - - $ref: "#/components/schemas/NotificationHttp" - - $ref: "#/components/schemas/NotificationChannelRef" - schedule: - type: string - format: calevent - remindDays: - type: integer - format: int32 - description: | - Used to restrict items by their due dates. All items with - a due date lower than (now + remindDays) are searched. - capOverdue: - type: boolean - description: | - If this is true, the search is also restricted to due - dates greater than `now - remindDays'. Otherwise, due date - are not restricted in that direction (only lower than `now - + remindDays' applies) and it is expected to restrict it - more using custom tags. - tagsInclude: - type: array - items: - $ref: "#/components/schemas/Tag" - tagsExclude: - type: array - items: - $ref: "#/components/schemas/Tag" diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala b/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala deleted file mode 100644 index 172a2c5f..00000000 --- a/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.restapi.model - -import docspell.common._ -import docspell.jsonminiq.JsonMiniQuery -import docspell.notification.api.{ChannelRef, EventType} -import docspell.restapi.codec.ChannelEitherCodec - -import io.circe.{Decoder, Encoder} - -// this must comply to the definition in openapi.yml in `extraSchemas` -final case class NotificationHook( - id: Ident, - enabled: Boolean, - channel: Either[ChannelRef, NotificationChannel], - allEvents: Boolean, - eventFilter: Option[JsonMiniQuery], - events: List[EventType] -) - -object NotificationHook { - import ChannelEitherCodec._ - - implicit val jsonDecoder: Decoder[NotificationHook] = - io.circe.generic.semiauto.deriveDecoder - implicit val jsonEncoder: Encoder[NotificationHook] = - io.circe.generic.semiauto.deriveEncoder -} diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala deleted file mode 100644 index 5f4b7fac..00000000 --- a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.restapi.model - -import docspell.common._ -import docspell.restapi.model._ - -import com.github.eikek.calev.CalEvent -import com.github.eikek.calev.circe.CalevCirceCodec._ -import io.circe.generic.semiauto -import io.circe.{Decoder, Encoder} - -// this must comply to the definition in openapi.yml in `extraSchemas` -final case class PeriodicDueItemsSettings( - id: Ident, - enabled: Boolean, - summary: Option[String], - channel: NotificationChannel, - schedule: CalEvent, - remindDays: Int, - capOverdue: Boolean, - tagsInclude: List[Tag], - tagsExclude: List[Tag] -) -object PeriodicDueItemsSettings { - - implicit val jsonDecoder: Decoder[PeriodicDueItemsSettings] = - semiauto.deriveDecoder[PeriodicDueItemsSettings] - implicit val jsonEncoder: Encoder[PeriodicDueItemsSettings] = - semiauto.deriveEncoder[PeriodicDueItemsSettings] -} diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala deleted file mode 100644 index a3cc89ab..00000000 --- a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.restapi.model - -import docspell.common._ -import docspell.query.ItemQuery -import docspell.restapi.codec.ItemQueryJson._ - -import com.github.eikek.calev.CalEvent -import com.github.eikek.calev.circe.CalevCirceCodec._ -import io.circe.generic.semiauto -import io.circe.{Decoder, Encoder} - -// this must comply to the definition in openapi.yml in `extraSchemas` -final case class PeriodicQuerySettings( - id: Ident, - summary: Option[String], - enabled: Boolean, - channel: NotificationChannel, - query: Option[ItemQuery], - bookmark: Option[String], - contentStart: Option[String], - schedule: CalEvent -) {} - -object PeriodicQuerySettings { - - implicit val jsonDecoder: Decoder[PeriodicQuerySettings] = - semiauto.deriveDecoder - - implicit val jsonEncoder: Encoder[PeriodicQuerySettings] = - semiauto.deriveEncoder -} diff --git a/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala b/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala deleted file mode 100644 index 32052466..00000000 --- a/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 Eike K. & Contributors - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package docspell.restapi.model - -import docspell.common._ -import docspell.notification.api.ChannelRef -import docspell.notification.api.ChannelType - -import io.circe.Decoder -import io.circe.parser -import munit._ - -class NotificationCodecTest extends FunSuite { - - def parse[A: Decoder](str: String): A = - parser.parse(str).fold(throw _, identity).as[A].fold(throw _, identity) - - def id(str: String): Ident = - Ident.unsafe(str) - - test("decode with channelref") { - val json = """{"id":"", - "enabled": true, - "channel": {"id":"abcde", "channelType":"matrix"}, - "allEvents": false, - "events": ["TagsChanged", "SetFieldValue"] -}""" - - val hook = parse[NotificationHook](json) - assertEquals(hook.enabled, true) - assertEquals(hook.channel, Left(ChannelRef(id("abcde"), ChannelType.Matrix))) - } - - test("decode with gotify data") { - val json = """{"id":"", - "enabled": true, - "channel": {"id":"", "channelType":"gotify", "url":"http://test.gotify.com", "appKey": "abcde"}, - "allEvents": false, - "eventFilter": null, - "events": ["TagsChanged", "SetFieldValue"] -}""" - val hook = parse[NotificationHook](json) - assertEquals(hook.enabled, true) - assertEquals( - hook.channel, - Right( - NotificationChannel.Gotify( - NotificationGotify( - id(""), - ChannelType.Gotify, - LenientUri.unsafe("http://test.gotify.com"), - Password("abcde"), - None - ) - ) - ) - ) - } - - test("decode with gotify data with prio") { - val json = """{"id":"", - "enabled": true, - "channel": {"id":"", "channelType":"gotify", "url":"http://test.gotify.com", "appKey": "abcde", "priority":9}, - "allEvents": false, - "eventFilter": null, - "events": ["TagsChanged", "SetFieldValue"] -}""" - val hook = parse[NotificationHook](json) - assertEquals(hook.enabled, true) - assertEquals( - hook.channel, - Right( - NotificationChannel.Gotify( - NotificationGotify( - id(""), - ChannelType.Gotify, - LenientUri.unsafe("http://test.gotify.com"), - Password("abcde"), - Some(9) - ) - ) - ) - ) - } -} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala index 22d6ec87..cdd4370c 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala @@ -6,6 +6,7 @@ package docspell.restserver.routes +import cats.data.NonEmptyList import cats.effect._ import cats.implicits._ @@ -14,10 +15,10 @@ import docspell.backend.auth.AuthToken import docspell.common._ import docspell.joexapi.model.BasicResult import docspell.jsonminiq.JsonMiniQuery -import docspell.notification.api.EventType +import docspell.notification.api.{ChannelRef, EventType} import docspell.restapi.model._ import docspell.restserver.Config -import docspell.restserver.conv.Conversions +import docspell.restserver.conv.{Conversions, NonEmptyListSupport} import docspell.restserver.http4s.ClientRequestInfo import org.http4s._ @@ -26,7 +27,7 @@ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl import org.http4s.server.Router -object NotificationRoutes { +object NotificationRoutes extends NonEmptyListSupport { def apply[F[_]: Async]( cfg: Config, @@ -126,17 +127,11 @@ object NotificationRoutes { case req @ POST -> Root / "sendTestEvent" => for { input <- req.as[NotificationHook] - ch <- Sync[F] - .pure( - input.channel.left - .map(_ => new Exception(s"ChannelRefs not allowed for testing")) - .flatMap(NotificationChannel.convert) - ) - .rethrow + ch <- requireNonEmpty(input.channels) baseUrl = ClientRequestInfo.getBaseUrl(cfg, req) res <- backend.notification.sendSampleEvent( input.events.headOption.getOrElse(EventType.all.head), - ch, + ch.map(r => ChannelRef(r.id, r.channelType, r.name)), user.account, baseUrl.some ) @@ -179,33 +174,26 @@ object NotificationRoutes { NotificationHook( h.id, h.enabled, - h.channel.map(NotificationChannel.convert), + h.channels.map(c => NotificationChannelRef(c.id, c.channelType, c.name)).toList, h.allEvents, h.eventFilter, h.events ) def convertHook(h: NotificationHook): Either[Throwable, ONotification.Hook] = - h.channel match { - case Left(cref) => - Right( - ONotification.Hook( - h.id, - h.enabled, - Left(cref), - h.allEvents, - h.eventFilter, - h.events - ) + NonEmptyList + .fromList(h.channels) + .toRight(new IllegalArgumentException(s"Empty channels not allowed!")) + .map(_ => + ONotification.Hook( + h.id, + h.enabled, + h.channels.map(c => ChannelRef(c.id, c.channelType, c.name)), + h.allEvents, + h.eventFilter, + h.events ) - case Right(channel) => - NotificationChannel - .convert(channel) - .map(ch => - ONotification - .Hook(h.id, h.enabled, Right(ch), h.allEvents, h.eventFilter, h.events) - ) - } + ) } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala index 7ae72706..e53bef82 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala @@ -14,10 +14,10 @@ import docspell.backend.BackendApp import docspell.backend.MailAddressCodec import docspell.backend.auth.AuthToken import docspell.common._ -import docspell.notification.api.PeriodicDueItemsArgs +import docspell.notification.api.{ChannelRef, PeriodicDueItemsArgs} import docspell.restapi.model._ import docspell.restserver.Config -import docspell.restserver.conv.Conversions +import docspell.restserver.conv.{Conversions, NonEmptyListSupport} import docspell.restserver.http4s.ClientRequestInfo import docspell.store.usertask._ @@ -26,7 +26,7 @@ import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -object NotifyDueItemsRoutes extends MailAddressCodec { +object NotifyDueItemsRoutes extends MailAddressCodec with NonEmptyListSupport { def apply[F[_]: Async]( cfg: Config, @@ -113,7 +113,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec { user: AccountId, settings: PeriodicDueItemsSettings ): F[UserTask[PeriodicDueItemsArgs]] = - Sync[F].pure(NotificationChannel.convert(settings.channel)).rethrow.map { channel => + requireNonEmpty(settings.channels).map { ch => UserTask( id, PeriodicDueItemsArgs.taskName, @@ -122,7 +122,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec { settings.summary, PeriodicDueItemsArgs( user, - Right(channel), + ch.map(c => ChannelRef(c.id, c.channelType, c.name)), settings.remindDays, if (settings.capOverdue) Some(settings.remindDays) else None, @@ -140,20 +140,13 @@ object NotifyDueItemsRoutes extends MailAddressCodec { for { tinc <- backend.tag.loadAll(task.args.tagsInclude) texc <- backend.tag.loadAll(task.args.tagsExclude) - - ch <- task.args.channel match { - case Right(c) => NotificationChannel.convert(c).pure[F] - case Left(ref) => - Sync[F].raiseError( - new IllegalStateException(s"ChannelRefs are not supported: $ref") - ) - } - } yield PeriodicDueItemsSettings( task.id, task.enabled, task.summary, - ch, + task.args.channels + .map(c => NotificationChannelRef(c.id, c.channelType, c.name)) + .toList, task.timer, task.args.remindDays, task.args.daysBack.isDefined, 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 32e767b8..0efd2661 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala @@ -6,7 +6,7 @@ package docspell.restserver.routes -import cats.data.OptionT +import cats.data.{NonEmptyList, OptionT} import cats.effect._ import cats.implicits._ @@ -14,11 +14,11 @@ import docspell.backend.BackendApp import docspell.backend.MailAddressCodec import docspell.backend.auth.AuthToken import docspell.common._ -import docspell.notification.api.PeriodicQueryArgs +import docspell.notification.api.{ChannelRef, PeriodicQueryArgs} import docspell.query.ItemQueryParser import docspell.restapi.model._ import docspell.restserver.Config -import docspell.restserver.conv.Conversions +import docspell.restserver.conv.{Conversions, NonEmptyListSupport} import docspell.restserver.http4s.ClientRequestInfo import docspell.store.usertask._ @@ -27,7 +27,7 @@ import org.http4s.circe.CirceEntityDecoder._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -object PeriodicQueryRoutes extends MailAddressCodec { +object PeriodicQueryRoutes extends MailAddressCodec with NonEmptyListSupport { def apply[F[_]: Async]( cfg: Config, @@ -116,7 +116,9 @@ object PeriodicQueryRoutes extends MailAddressCodec { ): F[UserTask[PeriodicQueryArgs]] = Sync[F] .pure(for { - ch <- NotificationChannel.convert(settings.channel) + ch <- NonEmptyList + .fromList(settings.channels) + .toRight(new Exception(s"No channels found for: ${settings.channels}")) qstr <- settings.query match { case Some(q) => ItemQueryParser @@ -132,7 +134,7 @@ object PeriodicQueryRoutes extends MailAddressCodec { else Left(new IllegalArgumentException("No query or bookmark provided")) } yield (ch, qstr.map(ItemQueryString.apply))) .rethrow - .map { case (channel, qstr) => + .map { case (channels, qstr) => UserTask( id, PeriodicQueryArgs.taskName, @@ -141,7 +143,7 @@ object PeriodicQueryRoutes extends MailAddressCodec { settings.summary, PeriodicQueryArgs( user, - Right(channel), + channels.map(r => ChannelRef(r.id, r.channelType, r.name)), qstr, settings.bookmark, Some(baseUrl / "app" / "item"), @@ -153,22 +155,18 @@ object PeriodicQueryRoutes extends MailAddressCodec { def taskToSettings[F[_]: Sync]( task: UserTask[PeriodicQueryArgs] ): F[PeriodicQuerySettings] = - for { - ch <- task.args.channel match { - case Right(c) => NotificationChannel.convert(c).pure[F] - case Left(ref) => - Sync[F].raiseError( - new IllegalStateException(s"ChannelRefs are not supported: $ref") - ) - } - } yield PeriodicQuerySettings( - task.id, - task.summary, - task.enabled, - ch, - task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe), - task.args.bookmark, - task.args.contentStart, - task.timer + Sync[F].pure( + PeriodicQuerySettings( + task.id, + task.enabled, + task.summary, + task.args.channels + .map(c => NotificationChannelRef(c.id, c.channelType, c.name)) + .toList, + task.timer, + task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe), + task.args.bookmark, + task.args.contentStart + ) ) } diff --git a/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql new file mode 100644 index 00000000..9ba8788e --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql @@ -0,0 +1,33 @@ +CREATE TABLE "notification_hook_channel" ( + "id" varchar(254) not null primary key, + "hook_id" varchar(254) not null, + "channel_mail" varchar(254), + "channel_gotify" varchar(254), + "channel_matrix" varchar(254), + "channel_http" varchar(254), + foreign key ("hook_id") references "notification_hook"("id") on delete cascade, + foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade, + foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade, + foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade, + foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade, + unique("hook_id", "channel_mail"), + unique("hook_id", "channel_gotify"), + unique("hook_id", "channel_matrix"), + unique("hook_id", "channel_http") +); + +insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http") +select random_uuid(), id, channel_mail, channel_gotify, channel_matrix, channel_http +from "notification_hook"; + +alter table "notification_hook" +drop column "channel_mail"; + +alter table "notification_hook" +drop column "channel_gotify"; + +alter table "notification_hook" +drop column "channel_matrix"; + +alter table "notification_hook" +drop column "channel_http"; diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql new file mode 100644 index 00000000..be6198f8 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql @@ -0,0 +1,42 @@ +CREATE TABLE `notification_hook_channel` ( + `id` varchar(254) not null primary key, + `hook_id` varchar(254) not null, + `channel_mail` varchar(254), + `channel_gotify` varchar(254), + `channel_matrix` varchar(254), + `channel_http` varchar(254), + foreign key (`hook_id`) references `notification_hook`(`id`) on delete cascade, + foreign key (`channel_mail`) references `notification_channel_mail`(`id`) on delete cascade, + foreign key (`channel_gotify`) references `notification_channel_gotify`(`id`) on delete cascade, + foreign key (`channel_matrix`) references `notification_channel_matrix`(`id`) on delete cascade, + foreign key (`channel_http`) references `notification_channel_http`(`id`) on delete cascade, + unique(`hook_id`, `channel_mail`), + unique(`hook_id`, `channel_gotify`), + unique(`hook_id`, `channel_matrix`), + unique(`hook_id`, `channel_http`) +); + +insert into `notification_hook_channel` +select md5(rand()), id, channel_mail, channel_gotify, channel_matrix, channel_http +from `notification_hook`; + +alter table `notification_hook` +drop constraint `notification_hook_ibfk_2`; +alter table `notification_hook` +drop constraint `notification_hook_ibfk_3`; +alter table `notification_hook` +drop constraint `notification_hook_ibfk_4`; +alter table `notification_hook` +drop constraint `notification_hook_ibfk_5`; + +alter table `notification_hook` +drop column `channel_mail`; + +alter table `notification_hook` +drop column `channel_gotify`; + +alter table `notification_hook` +drop column `channel_matrix`; + +alter table `notification_hook` +drop column `channel_http`; diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql new file mode 100644 index 00000000..2f116f99 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql @@ -0,0 +1,33 @@ +CREATE TABLE "notification_hook_channel" ( + "id" varchar(254) not null primary key, + "hook_id" varchar(254) not null, + "channel_mail" varchar(254), + "channel_gotify" varchar(254), + "channel_matrix" varchar(254), + "channel_http" varchar(254), + foreign key ("hook_id") references "notification_hook"("id") on delete cascade, + foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade, + foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade, + foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade, + foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade, + unique("hook_id", "channel_mail"), + unique("hook_id", "channel_gotify"), + unique("hook_id", "channel_matrix"), + unique("hook_id", "channel_http") +); + +insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http") +select md5(random()::text), id, channel_mail, channel_gotify, channel_matrix, channel_http +from "notification_hook"; + +alter table "notification_hook" +drop column "channel_mail"; + +alter table "notification_hook" +drop column "channel_gotify"; + +alter table "notification_hook" +drop column "channel_matrix"; + +alter table "notification_hook" +drop column "channel_http"; diff --git a/modules/store/src/main/scala/db/migration/MigrationTasks.scala b/modules/store/src/main/scala/db/migration/MigrationTasks.scala index 47c814cd..f8e2bf55 100644 --- a/modules/store/src/main/scala/db/migration/MigrationTasks.scala +++ b/modules/store/src/main/scala/db/migration/MigrationTasks.scala @@ -6,23 +6,23 @@ package db.migration -import cats.data.NonEmptyList +import cats.data.{NonEmptyList, OptionT} import cats.effect.{IO, Sync} import cats.implicits._ import docspell.common._ import docspell.common.syntax.StringSyntax._ -import docspell.notification.api.Channel -import docspell.notification.api.PeriodicDueItemsArgs -import docspell.store.records.RPeriodicTask +import docspell.notification.api._ +import docspell.store.records._ +import db.migration.data.{PeriodicDueItemsArgsOld, PeriodicQueryArgsOld} import doobie._ import doobie.implicits._ import doobie.util.transactor.Strategy import emil.MailAddress import emil.javamail.syntax._ -import io.circe.Encoder import io.circe.syntax._ +import io.circe.{Decoder, Encoder} import org.flywaydb.core.api.migration.Context trait MigrationTasks { @@ -31,6 +31,8 @@ trait MigrationTasks { implicit val jsonEncoder: Encoder[MailAddress] = Encoder.encodeString.contramap(_.asUnicodeString) + implicit val jsonDecoder: Decoder[MailAddress] = + Decoder.decodeString.emap(MailAddress.parse) def migrateDueItemTasks: ConnectionIO[Unit] = for { @@ -42,20 +44,114 @@ trait MigrationTasks { _ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false) } yield () - def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = { - val converted = old.args - .parseJsonAs[NotifyDueItemsArgs] - .leftMap(_.getMessage()) - .flatMap(convertArgs) + def migratePeriodicItemTasks: ConnectionIO[Unit] = + for { + tasks2 <- RPeriodicTask.findByTask(PeriodicDueItemsArgsOld.taskName) + tasks3 <- RPeriodicTask.findByTask(PeriodicQueryArgsOld.taskName) + size = tasks2.size + tasks3.size + _ <- Sync[ConnectionIO].delay( + logger.info(s"Starting to migrate $size user tasks") + ) + _ <- tasks2.traverse(migratePeriodicDueItemsTask) + _ <- tasks3.traverse(migratePeriodicQueryTask) + _ <- RPeriodicTask.setEnabledByTask(PeriodicQueryArgsOld.taskName, false) + _ <- RPeriodicTask.setEnabledByTask(PeriodicDueItemsArgsOld.taskName, false) + } yield () - converted match { - case Right(args) => - Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *> + private def migratePeriodicQueryTask(old: RPeriodicTask): ConnectionIO[Int] = + old.args + .parseJsonAs[PeriodicQueryArgsOld] + .leftMap { ex => + logger.error(ex)(s"Error migrating tasks") + 0.pure[ConnectionIO] + } + .map { oldArgs => + val ref = oldArgs.channel match { + case Right(c) => saveChannel(c, oldArgs.account) + case Left(ref) => ref.pure[ConnectionIO] + } + + ref.flatMap(channelRef => + RPeriodicTask.updateTask( + old.id, + PeriodicQueryArgs.taskName, + PeriodicQueryArgs( + oldArgs.account, + NonEmptyList.of(channelRef), + oldArgs.query, + oldArgs.bookmark, + oldArgs.baseUrl, + oldArgs.contentStart + ).asJson.noSpaces + ) + ) + } + .fold(identity, identity) + + private def migratePeriodicDueItemsTask(old: RPeriodicTask): ConnectionIO[Int] = + old.args + .parseJsonAs[PeriodicDueItemsArgsOld] + .leftMap { ex => + logger.error(ex)(s"Error migrating tasks") + 0.pure[ConnectionIO] + } + .map { oldArgs => + val ref = oldArgs.channel match { + case Right(c) => saveChannel(c, oldArgs.account) + case Left(ref) => ref.pure[ConnectionIO] + } + + ref.flatMap(channelRef => RPeriodicTask.updateTask( old.id, PeriodicDueItemsArgs.taskName, - args.asJson.noSpaces + PeriodicDueItemsArgs( + oldArgs.account, + NonEmptyList.of(channelRef), + oldArgs.remindDays, + oldArgs.daysBack, + oldArgs.tagsInclude, + oldArgs.tagsExclude, + oldArgs.baseUrl + ).asJson.noSpaces ) + ) + } + .fold(identity, identity) + + private def saveChannel(ch: Channel, account: AccountId): ConnectionIO[ChannelRef] = + (for { + newId <- OptionT.liftF(Ident.randomId[ConnectionIO]) + userId <- OptionT(RUser.findIdByAccount(account)) + r <- RNotificationChannel.fromChannel(ch, newId, userId) + _ <- OptionT.liftF(RNotificationChannel.insert(r)) + _ <- OptionT.liftF( + Sync[ConnectionIO].delay(logger.debug(s"Created channel $r for $account")) + ) + ref = r.asRef + } yield ref) + .getOrElseF(Sync[ConnectionIO].raiseError(new Exception("User not found!"))) + + private def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = { + val converted = old.args + .parseJsonAs[NotifyDueItemsArgs] + .leftMap(_.getMessage()) + .map(convertArgs) + + converted match { + case Right(args) => + val task = args + .semiflatMap(a => + RPeriodicTask + .updateTask( + old.id, + PeriodicDueItemsArgs.taskName, + a.asJson.noSpaces + ) + ) + .getOrElse(0) + + Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *> task case Left(err) => logger.error(s"Error converting user task: $old. $err") @@ -63,22 +159,44 @@ trait MigrationTasks { } } - def convertArgs(old: NotifyDueItemsArgs): Either[String, PeriodicDueItemsArgs] = - old.recipients - .traverse(MailAddress.parse) - .flatMap(l => NonEmptyList.fromList(l).toRight("No recipients provided")) - .map { rec => - PeriodicDueItemsArgs( - old.account, - Right(Channel.Mail(Ident.unsafe(""), None, old.smtpConnection, rec)), - old.remindDays, - old.daysBack, - old.tagsInclude, - old.tagsExclude, - old.itemDetailUrl - ) + private def convertArgs( + old: NotifyDueItemsArgs + ): OptionT[ConnectionIO, PeriodicDueItemsArgs] = { + val recs = old.recipients + .map(MailAddress.parse) + .flatMap { + case Right(m) => Some(m) + case Left(err) => + logger.warn(s"Cannot read mail address: $err. Skip this while migrating.") + None } + for { + userId <- OptionT(RUser.findIdByAccount(old.account)) + id <- OptionT.liftF(Ident.randomId[ConnectionIO]) + now <- OptionT.liftF(Timestamp.current[ConnectionIO]) + chName = Some("migrate notify items") + ch = RNotificationChannelMail( + id, + userId, + chName, + old.smtpConnection, + recs, + now + ) + _ <- OptionT.liftF(RNotificationChannelMail.insert(ch)) + args = PeriodicDueItemsArgs( + old.account, + NonEmptyList.of(ChannelRef(ch.id, ChannelType.Mail, chName)), + old.remindDays, + old.daysBack, + old.tagsInclude, + old.tagsExclude, + old.itemDetailUrl + ) + } yield args + } + def mkTransactor(ctx: Context): Transactor[IO] = { val xa = Transactor.fromConnection[IO](ctx.getConnection()) Transactor.strategy.set(xa, Strategy.void) // transactions are handled by flyway diff --git a/modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala b/modules/store/src/main/scala/db/migration/data/NotifyDueItemsArgs.scala similarity index 100% rename from modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala rename to modules/store/src/main/scala/db/migration/data/NotifyDueItemsArgs.scala diff --git a/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala b/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala new file mode 100644 index 00000000..c10630d4 --- /dev/null +++ b/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package db.migration.data + +import docspell.common._ + +import emil.MailAddress +import io.circe.generic.semiauto +import io.circe.{Decoder, Encoder} + +/** Arguments to the notification task. + * + * This tasks queries items with a due date and informs the user via mail. + * + * If the structure changes, there must be some database migration to update or remove + * the json data of the corresponding task. + */ +final case class PeriodicDueItemsArgsOld( + account: AccountId, + channel: ChannelOrRef, + remindDays: Int, + daysBack: Option[Int], + tagsInclude: List[Ident], + tagsExclude: List[Ident], + baseUrl: Option[LenientUri] +) + +object PeriodicDueItemsArgsOld { + val taskName = Ident.unsafe("periodic-due-items-notify") + + implicit def jsonDecoder(implicit + mc: Decoder[MailAddress] + ): Decoder[PeriodicDueItemsArgsOld] = { + implicit val x = ChannelOrRef.jsonDecoder + semiauto.deriveDecoder + } + + implicit def jsonEncoder(implicit + mc: Encoder[MailAddress] + ): Encoder[PeriodicDueItemsArgsOld] = { + implicit val x = ChannelOrRef.jsonEncoder + semiauto.deriveEncoder + } +} diff --git a/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala b/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala new file mode 100644 index 00000000..403d4567 --- /dev/null +++ b/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package db.migration.data + +import docspell.common._ + +import emil.MailAddress +import io.circe.generic.semiauto +import io.circe.{Decoder, Encoder} + +final case class PeriodicQueryArgsOld( + account: AccountId, + channel: ChannelOrRef, + query: Option[ItemQueryString], + bookmark: Option[String], + baseUrl: Option[LenientUri], + contentStart: Option[String] +) + +object PeriodicQueryArgsOld { + val taskName = Ident.unsafe("periodic-query-notify") + + implicit def jsonDecoder(implicit + mc: Decoder[MailAddress] + ): Decoder[PeriodicQueryArgsOld] = { + implicit val x = ChannelOrRef.jsonDecoder + semiauto.deriveDecoder + } + + implicit def jsonEncoder(implicit + mc: Encoder[MailAddress] + ): Encoder[PeriodicQueryArgsOld] = { + implicit val x = ChannelOrRef.jsonEncoder + semiauto.deriveEncoder + } +} diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/package.scala b/modules/store/src/main/scala/db/migration/data/package.scala similarity index 90% rename from modules/notification/api/src/main/scala/docspell/notification/api/package.scala rename to modules/store/src/main/scala/db/migration/data/package.scala index 74f2e41e..246b460d 100644 --- a/modules/notification/api/src/main/scala/docspell/notification/api/package.scala +++ b/modules/store/src/main/scala/db/migration/data/package.scala @@ -4,13 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package docspell.notification +package db.migration + +import docspell.notification.api._ import emil.MailAddress import io.circe.{Decoder, Encoder} -package object api { - +package object data { type ChannelOrRef = Either[ChannelRef, Channel] object ChannelOrRef { @@ -25,5 +26,4 @@ package object api { cr.fold(_.channelType, _.channelType) } } - } diff --git a/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala new file mode 100644 index 00000000..83983d4a --- /dev/null +++ b/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package db.migration.h2 + +import cats.effect.unsafe.implicits._ + +import db.migration.MigrationTasks +import doobie.implicits._ +import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} + +class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks { + val logger = org.log4s.getLogger + + override def migrate(ctx: Context): Unit = { + val xa = mkTransactor(ctx) + migratePeriodicItemTasks.transact(xa).unsafeRunSync() + } +} diff --git a/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala new file mode 100644 index 00000000..ee524572 --- /dev/null +++ b/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package db.migration.mariadb + +import cats.effect.unsafe.implicits._ + +import db.migration.MigrationTasks +import doobie.implicits._ +import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} + +class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks { + val logger = org.log4s.getLogger + + override def migrate(ctx: Context): Unit = { + val xa = mkTransactor(ctx) + migratePeriodicItemTasks.transact(xa).unsafeRunSync() + } +} diff --git a/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala new file mode 100644 index 00000000..e887f510 --- /dev/null +++ b/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package db.migration.postgresql + +import cats.effect.unsafe.implicits._ + +import db.migration.MigrationTasks +import doobie.implicits._ +import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} + +class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks { + val logger = org.log4s.getLogger + + override def migrate(ctx: Context): Unit = { + val xa = mkTransactor(ctx) + migratePeriodicItemTasks.transact(xa).unsafeRunSync() + } +} diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala index 2b76e98a..dee20f76 100644 --- a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala +++ b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala @@ -12,7 +12,7 @@ import java.time.{Instant, LocalDate} import docspell.common._ import docspell.common.syntax.all._ import docspell.jsonminiq.JsonMiniQuery -import docspell.notification.api.EventType +import docspell.notification.api.{ChannelType, EventType} import docspell.query.{ItemQuery, ItemQueryParser} import docspell.totp.Key @@ -156,6 +156,9 @@ trait DoobieMeta extends EmilDoobieMeta { implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] = Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString) + + implicit val channelTypeRead: Read[ChannelType] = + Read[String].map(ChannelType.unsafeFromString) } object DoobieMeta extends DoobieMeta { diff --git a/modules/store/src/main/scala/docspell/store/queries/QNotification.scala b/modules/store/src/main/scala/docspell/store/queries/QNotification.scala index b76dbde7..e7f0175b 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QNotification.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QNotification.scala @@ -27,7 +27,11 @@ object QNotification { def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] = for { hooks <- listHooks(event.account.collective, event.eventType) - chs <- hooks.traverse(readHookChannel) + chs <- hooks.traverse(h => + listChannels(h.id) + .flatMap(_.flatTraverse(hc => readHookChannel(h.uid, hc))) + .map(c => HookChannel(h, c)) + ) } yield chs // -- @@ -50,21 +54,27 @@ object QNotification { ) ).query[RNotificationHook].to[Vector] + def listChannels(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] = + RNotificationHookChannel.allOf(hookId) + def readHookChannel( - hook: RNotificationHook - ): ConnectionIO[HookChannel] = + userId: Ident, + hook: RNotificationHookChannel + ): ConnectionIO[Vector[NotificationChannel]] = for { - c1 <- read(hook.channelMail)(RNotificationChannelMail.getById)( + c1 <- read(hook.channelMail)(RNotificationChannelMail.getById(userId))( ChannelMap.readMail ) - c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById)( + c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById(userId))( ChannelMap.readGotify ) - c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById)( + c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById(userId))( ChannelMap.readMatrix ) - c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById)(ChannelMap.readHttp) - } yield HookChannel(hook, c1 ++ c2 ++ c3 ++ c4) + c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById(userId))( + ChannelMap.readHttp + ) + } yield c1 ++ c2 ++ c3 ++ c4 def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] = ch.fold( 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 4de2675e..b51e366c 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala @@ -7,10 +7,10 @@ package docspell.store.records import cats.data.OptionT +import cats.implicits._ import docspell.common._ -import docspell.notification.api.ChannelRef -import docspell.notification.api.ChannelType +import docspell.notification.api.{Channel, ChannelRef, ChannelType} import doobie._ @@ -20,6 +20,17 @@ sealed trait RNotificationChannel { def name: Option[String] = fold(_.name, _.name, _.name, _.name) + def channelType: ChannelType = + fold( + _ => ChannelType.Mail, + _ => ChannelType.Gotify, + _ => ChannelType.Matrix, + _ => ChannelType.Http + ) + + def asRef: ChannelRef = + ChannelRef(id, channelType, name) + def fold[A]( f1: RNotificationChannelMail => A, f2: RNotificationChannelGotify => A, @@ -93,42 +104,60 @@ object RNotificationChannel { Matrix.apply ) ++ http.map(Http.apply) - def getById(id: Ident): ConnectionIO[Vector[RNotificationChannel]] = + def getById(id: Ident, userId: Ident): ConnectionIO[Vector[RNotificationChannel]] = for { - mail <- RNotificationChannelMail.getById(id) - gotify <- RNotificationChannelGotify.getById(id) - matrix <- RNotificationChannelMatrix.getById(id) - http <- RNotificationChannelHttp.getById(id) + mail <- RNotificationChannelMail.getById(userId)(id) + gotify <- RNotificationChannelGotify.getById(userId)(id) + matrix <- RNotificationChannelMatrix.getById(userId)(id) + http <- RNotificationChannelHttp.getById(userId)(id) } yield mail.map(Email.apply).toVector ++ gotify.map(Gotify.apply).toVector ++ matrix.map(Matrix.apply).toVector ++ http.map(Http.apply).toVector - def getByRef(ref: ChannelRef): ConnectionIO[Option[RNotificationChannel]] = + def getByRef( + ref: ChannelRef, + userId: Ident + ): ConnectionIO[Option[RNotificationChannel]] = ref.channelType match { case ChannelType.Mail => - RNotificationChannelMail.getById(ref.id).map(_.map(Email.apply)) + RNotificationChannelMail.getById(userId)(ref.id).map(_.map(Email.apply)) case ChannelType.Matrix => - RNotificationChannelMatrix.getById(ref.id).map(_.map(Matrix.apply)) + RNotificationChannelMatrix.getById(userId)(ref.id).map(_.map(Matrix.apply)) case ChannelType.Gotify => - RNotificationChannelGotify.getById(ref.id).map(_.map(Gotify.apply)) + RNotificationChannelGotify.getById(userId)(ref.id).map(_.map(Gotify.apply)) case ChannelType.Http => - RNotificationChannelHttp.getById(ref.id).map(_.map(Http.apply)) + RNotificationChannelHttp.getById(userId)(ref.id).map(_.map(Http.apply)) } - def getByHook(r: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = { + def getByHook(hook: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = { def opt(id: Option[Ident]): OptionT[ConnectionIO, Ident] = OptionT.fromOption(id) - for { - mail <- opt(r.channelMail).flatMapF(RNotificationChannelMail.getById).value - gotify <- opt(r.channelGotify).flatMapF(RNotificationChannelGotify.getById).value - matrix <- opt(r.channelMatrix).flatMapF(RNotificationChannelMatrix.getById).value - http <- opt(r.channelHttp).flatMapF(RNotificationChannelHttp.getById).value - } yield mail.map(Email.apply).toVector ++ - gotify.map(Gotify.apply).toVector ++ - matrix.map(Matrix.apply).toVector ++ - http.map(Http.apply).toVector + def find( + r: RNotificationHookChannel + ): ConnectionIO[Vector[RNotificationChannel]] = + for { + mail <- opt(r.channelMail) + .flatMapF(RNotificationChannelMail.getById(hook.uid)) + .value + gotify <- opt(r.channelGotify) + .flatMapF(RNotificationChannelGotify.getById(hook.uid)) + .value + matrix <- opt(r.channelMatrix) + .flatMapF(RNotificationChannelMatrix.getById(hook.uid)) + .value + http <- opt(r.channelHttp) + .flatMapF(RNotificationChannelHttp.getById(hook.uid)) + .value + } yield mail.map(Email.apply).toVector ++ + gotify.map(Gotify.apply).toVector ++ + matrix.map(Matrix.apply).toVector ++ + http.map(Http.apply).toVector + + RNotificationHookChannel + .allOf(hook.id) + .flatMap(_.flatTraverse(find)) } def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = @@ -138,4 +167,63 @@ object RNotificationChannel { n3 <- RNotificationChannelMatrix.deleteByAccount(id, account) n4 <- RNotificationChannelHttp.deleteByAccount(id, account) } yield n1 + n2 + n3 + n4 + + def fromChannel( + channel: Channel, + id: Ident, + userId: Ident + ): OptionT[ConnectionIO, RNotificationChannel] = + for { + time <- OptionT.liftF(Timestamp.current[ConnectionIO]) + logger = Logger.log4s[ConnectionIO](org.log4s.getLogger) + r <- + channel match { + case Channel.Mail(_, name, conn, recipients) => + for { + _ <- OptionT.liftF( + logger.debug( + s"Looking up user smtp for ${userId.id} and ${conn.id}" + ) + ) + mailConn <- OptionT(RUserEmail.getByUser(userId, conn)) + rec = RNotificationChannelMail( + id, + userId, + name, + mailConn.id, + recipients.toList, + time + ).vary + } yield rec + case Channel.Gotify(_, name, url, appKey, prio) => + OptionT.pure[ConnectionIO]( + RNotificationChannelGotify( + id, + userId, + name, + url, + appKey, + prio, + time + ).vary + ) + case Channel.Matrix(_, name, homeServer, roomId, accessToken) => + OptionT.pure[ConnectionIO]( + RNotificationChannelMatrix( + id, + userId, + name, + homeServer, + roomId, + accessToken, + "m.text", + time + ).vary + ) + case Channel.Http(_, name, url) => + OptionT.pure[ConnectionIO]( + RNotificationChannelHttp(id, userId, name, url, time).vary + ) + } + } yield r } 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 a5c93a61..b0da9e79 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala @@ -49,8 +49,12 @@ object RNotificationChannelGotify { def as(alias: String): Table = Table(Some(alias)) - def getById(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] = - run(select(T.all), from(T), T.id === id).query[RNotificationChannelGotify].option + def getById( + userId: Ident + )(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] = + run(select(T.all), from(T), T.id === id && T.uid === userId) + .query[RNotificationChannelGotify] + .option def insert(r: RNotificationChannelGotify): ConnectionIO[Int] = DML.insert( 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 9c3d6ad0..a70eb8d8 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala @@ -45,8 +45,10 @@ object RNotificationChannelHttp { def as(alias: String): Table = Table(Some(alias)) - def getById(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] = - run(select(T.all), from(T), T.id === id).query[RNotificationChannelHttp].option + def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] = + run(select(T.all), from(T), T.id === id && T.uid === userId) + .query[RNotificationChannelHttp] + .option def insert(r: RNotificationChannelHttp): ConnectionIO[Int] = DML.insert(T, T.all, sql"${r.id},${r.uid},${r.name},${r.url},${r.created}") 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 6c11eb0c..afa2d34c 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala @@ -65,8 +65,10 @@ object RNotificationChannelMail { ) ) - def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] = - run(select(T.all), from(T), T.id === id).query[RNotificationChannelMail].option + def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] = + run(select(T.all), from(T), T.id === id && T.uid === userId) + .query[RNotificationChannelMail] + .option def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = { val user = RUser.as("u") 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 386f9910..d6981906 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala @@ -77,8 +77,12 @@ object RNotificationChannelMatrix { ) ) - def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMatrix]] = - run(select(T.all), from(T), T.id === id).query[RNotificationChannelMatrix].option + def getById(userId: Ident)( + id: Ident + ): ConnectionIO[Option[RNotificationChannelMatrix]] = + run(select(T.all), from(T), T.id === id && T.uid === userId) + .query[RNotificationChannelMatrix] + .option def getByAccount( account: AccountId diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala index 50b21f1e..75c22d85 100644 --- a/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala @@ -7,7 +7,6 @@ package docspell.store.records import cats.data.NonEmptyList -import cats.implicits._ import docspell.common._ import docspell.jsonminiq.JsonMiniQuery @@ -22,115 +21,18 @@ final case class RNotificationHook( id: Ident, uid: Ident, enabled: Boolean, - channelMail: Option[Ident], - channelGotify: Option[Ident], - channelMatrix: Option[Ident], - channelHttp: Option[Ident], allEvents: Boolean, eventFilter: Option[JsonMiniQuery], created: Timestamp -) { - def channelId: Ident = - channelMail - .orElse(channelGotify) - .orElse(channelMatrix) - .orElse(channelHttp) - .getOrElse( - sys.error(s"Illegal internal state: notification hook has no channel: ${id.id}") - ) -} +) {} object RNotificationHook { - def mail( - id: Ident, - uid: Ident, - enabled: Boolean, - channelMail: Ident, - created: Timestamp - ): RNotificationHook = - RNotificationHook( - id, - uid, - enabled, - channelMail.some, - None, - None, - None, - false, - None, - created - ) - - def gotify( - id: Ident, - uid: Ident, - enabled: Boolean, - channelGotify: Ident, - created: Timestamp - ): RNotificationHook = - RNotificationHook( - id, - uid, - enabled, - None, - channelGotify.some, - None, - None, - false, - None, - created - ) - - def matrix( - id: Ident, - uid: Ident, - enabled: Boolean, - channelMatrix: Ident, - created: Timestamp - ): RNotificationHook = - RNotificationHook( - id, - uid, - enabled, - None, - None, - channelMatrix.some, - None, - false, - None, - created - ) - - def http( - id: Ident, - uid: Ident, - enabled: Boolean, - channelHttp: Ident, - created: Timestamp - ): RNotificationHook = - RNotificationHook( - id, - uid, - enabled, - None, - None, - None, - channelHttp.some, - false, - None, - created - ) - final case class Table(alias: Option[String]) extends TableDef { val tableName = "notification_hook" val id = Column[Ident]("id", this) val uid = Column[Ident]("uid", this) val enabled = Column[Boolean]("enabled", this) - val channelMail = Column[Ident]("channel_mail", this) - val channelGotify = Column[Ident]("channel_gotify", this) - val channelMatrix = Column[Ident]("channel_matrix", this) - val channelHttp = Column[Ident]("channel_http", this) val allEvents = Column[Boolean]("all_events", this) val eventFilter = Column[JsonMiniQuery]("event_filter", this) val created = Column[Timestamp]("created", this) @@ -140,10 +42,6 @@ object RNotificationHook { id, uid, enabled, - channelMail, - channelGotify, - channelMatrix, - channelHttp, allEvents, eventFilter, created @@ -157,7 +55,7 @@ object RNotificationHook { DML.insert( T, T.all, - sql"${r.id},${r.uid},${r.enabled},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp},${r.allEvents},${r.eventFilter},${r.created}" + sql"${r.id},${r.uid},${r.enabled},${r.allEvents},${r.eventFilter},${r.created}" ) def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = { @@ -174,10 +72,6 @@ object RNotificationHook { T.id === r.id && T.uid === r.uid, DML.set( T.enabled.setTo(r.enabled), - T.channelMail.setTo(r.channelMail), - T.channelGotify.setTo(r.channelGotify), - T.channelMatrix.setTo(r.channelMatrix), - T.channelHttp.setTo(r.channelHttp), T.allEvents.setTo(r.allEvents), T.eventFilter.setTo(r.eventFilter) ) diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala new file mode 100644 index 00000000..d499c593 --- /dev/null +++ b/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala @@ -0,0 +1,236 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.store.records + +import cats.data.{NonEmptyList => Nel} +import cats.effect.Sync +import cats.implicits._ + +import docspell.common._ +import docspell.notification.api.{ChannelRef, ChannelType} +import docspell.store.qb.DSL._ +import docspell.store.qb._ + +import doobie._ +import doobie.implicits._ + +final case class RNotificationHookChannel( + id: Ident, + hookId: Ident, + channelMail: Option[Ident], + channelGotify: Option[Ident], + channelMatrix: Option[Ident], + channelHttp: Option[Ident] +) { + + def channelId: Ident = + channelMail + .orElse(channelGotify) + .orElse(channelMatrix) + .orElse(channelHttp) + .getOrElse( + sys.error(s"Illegal internal state: notification hook has no channel: $this") + ) + + def channelType: ChannelType = + channelMail + .map(_ => ChannelType.Mail) + .orElse(channelGotify.map(_ => ChannelType.Gotify)) + .orElse(channelMatrix.map(_ => ChannelType.Matrix)) + .orElse(channelHttp.map(_ => ChannelType.Http)) + .getOrElse( + sys.error(s"Illegal internal state: notification hook has no channel: $this") + ) +} +object RNotificationHookChannel { + def fromRef(id: Ident, hookId: Ident, ref: ChannelRef): RNotificationHookChannel = + ref.channelType match { + case ChannelType.Mail => mail(id, hookId, ref.id) + case ChannelType.Gotify => gotify(id, hookId, ref.id) + case ChannelType.Matrix => matrix(id, hookId, ref.id) + case ChannelType.Http => http(id, hookId, ref.id) + } + + def mail( + id: Ident, + hookId: Ident, + channelMail: Ident + ): RNotificationHookChannel = + RNotificationHookChannel( + id, + hookId, + channelMail.some, + None, + None, + None + ) + + def gotify( + id: Ident, + hookId: Ident, + channelGotify: Ident + ): RNotificationHookChannel = + RNotificationHookChannel( + id, + hookId, + None, + channelGotify.some, + None, + None + ) + + def matrix( + id: Ident, + hookId: Ident, + channelMatrix: Ident + ): RNotificationHookChannel = + RNotificationHookChannel( + id, + hookId, + None, + None, + channelMatrix.some, + None + ) + + def http( + id: Ident, + hookId: Ident, + channelHttp: Ident + ): RNotificationHookChannel = + RNotificationHookChannel( + id, + hookId, + None, + None, + None, + channelHttp.some + ) + + final case class Table(alias: Option[String]) extends TableDef { + val tableName = "notification_hook_channel" + + val id = Column[Ident]("id", this) + val hookId = Column[Ident]("hook_id", this) + val channelMail = Column[Ident]("channel_mail", this) + val channelGotify = Column[Ident]("channel_gotify", this) + val channelMatrix = Column[Ident]("channel_matrix", this) + val channelHttp = Column[Ident]("channel_http", this) + + val all: Nel[Column[_]] = + Nel.of(id, hookId, channelMail, channelGotify, channelMatrix, channelHttp) + } + + def as(alias: String): Table = + Table(Some(alias)) + + val T: Table = Table(None) + + def insert(r: RNotificationHookChannel): ConnectionIO[Int] = + DML.insert( + T, + T.all, + sql"${r.id},${r.hookId},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp}" + ) + + def update(r: RNotificationHookChannel): ConnectionIO[Int] = + DML.update( + T, + T.id === r.id && T.hookId === r.hookId, + DML.set( + T.channelMail.setTo(r.channelMail), + T.channelGotify.setTo(r.channelGotify), + T.channelMatrix.setTo(r.channelMatrix), + T.channelHttp.setTo(r.channelHttp) + ) + ) + + def deleteByHook(hookId: Ident): ConnectionIO[Int] = + DML.delete(T, T.hookId === hookId) + + def insertAll(rs: List[RNotificationHookChannel]): ConnectionIO[Int] = + rs.traverse(insert).map(_.sum) + + def updateAll(hookId: Ident, channels: List[ChannelRef]): ConnectionIO[Int] = + channels + .traverse(ref => Ident.randomId[ConnectionIO].map(id => fromRef(id, hookId, ref))) + .flatMap(all => deleteByHook(hookId) *> insertAll(all)) + + def allOf(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] = + Select(select(T.all), from(T), T.hookId === hookId).build + .query[RNotificationHookChannel] + .to[Vector] + + def allOfNel(hookId: Ident): ConnectionIO[Nel[RNotificationHookChannel]] = + allOf(hookId) + .map(Nel.fromFoldable[Vector, RNotificationHookChannel]) + .flatMap( + _.map(_.pure[ConnectionIO]).getOrElse( + Sync[ConnectionIO] + .raiseError(new Exception(s"Hook '${hookId.id}' has no associated channels!")) + ) + ) + + def resolveRefs(rs: Nel[RNotificationHookChannel]): ConnectionIO[List[ChannelRef]] = { + val cmail = RNotificationChannelMail.as("cmail") + val cgotify = RNotificationChannelGotify.as("cgotify") + val cmatrix = RNotificationChannelMatrix.as("cmatrix") + val chttp = RNotificationChannelHttp.as("chttp") + + def selectRef( + idList: List[Ident], + idCol: Column[Ident], + nameCol: Column[String], + ctype: ChannelType, + table: TableDef + ) = + Nel + .fromList(idList) + .map(ids => + Select( + select(idCol.s, const(ctype.name), nameCol.s), + from(table), + idCol.in(ids) + ) + ) + + val mailRefs = selectRef( + rs.toList.flatMap(_.channelMail), + cmail.id, + cmail.name, + ChannelType.Mail, + cmail + ) + val gotifyRefs = selectRef( + rs.toList.flatMap(_.channelGotify), + cgotify.id, + cgotify.name, + ChannelType.Gotify, + cgotify + ) + val matrixRefs = selectRef( + rs.toList.flatMap(_.channelMatrix), + cmatrix.id, + cmatrix.name, + ChannelType.Matrix, + cmatrix + ) + val httpRefs = selectRef( + rs.toList.flatMap(_.channelHttp), + chttp.id, + chttp.name, + ChannelType.Http, + chttp + ) + + val queries = List(mailRefs, gotifyRefs, matrixRefs, httpRefs).flatten + Nel.fromList(queries) match { + case Some(nel) => union(nel.head, nel.tail: _*).build.query[ChannelRef].to[List] + case None => List.empty[ChannelRef].pure[ConnectionIO] + } + } +} diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 5e7f3c2c..3b9f63d4 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -23,6 +23,7 @@ module Api exposing , checkCalEvent , confirmMultiple , confirmOtp + , createChannel , createHook , createImapSettings , createMailSettings @@ -34,6 +35,7 @@ module Api exposing , deleteAttachment , deleteAttachments , deleteBookmark + , deleteChannel , deleteCustomField , deleteCustomValue , deleteCustomValueMultiple @@ -56,6 +58,8 @@ module Api exposing , fileURL , getAttachmentMeta , getBookmarks + , getChannels + , getChannelsIgnoreError , getClientSettings , getCollective , getCollectiveSettings @@ -172,6 +176,7 @@ module Api exposing , twoFactor , unconfirmMultiple , updateBookmark + , updateChannel , updateHook , updateNotifyDueItems , updatePeriodicQuery @@ -229,6 +234,7 @@ import Api.Model.MoveAttachment exposing (MoveAttachment) import Api.Model.NewCustomField exposing (NewCustomField) import Api.Model.NewFolder exposing (NewFolder) import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult) +import Api.Model.NotificationHook exposing (NotificationHook) import Api.Model.NotificationSampleEventReq exposing (NotificationSampleEventReq) import Api.Model.OptionalDate exposing (OptionalDate) import Api.Model.OptionalId exposing (OptionalId) @@ -239,6 +245,8 @@ import Api.Model.OtpConfirm exposing (OtpConfirm) import Api.Model.OtpResult exposing (OtpResult) import Api.Model.OtpState exposing (OtpState) import Api.Model.PasswordChange exposing (PasswordChange) +import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) +import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Api.Model.Person exposing (Person) import Api.Model.PersonList exposing (PersonList) import Api.Model.ReferenceList exposing (ReferenceList) @@ -274,10 +282,8 @@ import Data.EquipmentOrder exposing (EquipmentOrder) import Data.EventType exposing (EventType) import Data.Flags exposing (Flags) import Data.FolderOrder exposing (FolderOrder) -import Data.NotificationHook exposing (NotificationHook) +import Data.NotificationChannel exposing (NotificationChannel) import Data.OrganizationOrder exposing (OrganizationOrder) -import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) -import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Data.PersonOrder exposing (PersonOrder) import Data.Priority exposing (Priority) import Data.TagOrder exposing (TagOrder) @@ -604,7 +610,7 @@ startOnceNotifyDueItems flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems/startonce" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -618,7 +624,7 @@ updateNotifyDueItems flags settings receive = Http2.authPut { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -632,7 +638,7 @@ createNotifyDueItems flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -645,7 +651,7 @@ getNotifyDueItems flags receive = Http2.authGet { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" , account = getAccount flags - , expect = Http.expectJson receive (JsonDecode.list Data.PeriodicDueItemsSettings.decoder) + , expect = Http.expectJson receive (JsonDecode.list Api.Model.PeriodicDueItemsSettings.decoder) } @@ -658,7 +664,7 @@ submitNotifyDueItems flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -689,7 +695,7 @@ startOncePeriodicQuery flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery/startonce" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -703,7 +709,7 @@ updatePeriodicQuery flags settings receive = Http2.authPut { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -717,7 +723,7 @@ createPeriodicQuery flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -730,7 +736,7 @@ getPeriodicQuery flags receive = Http2.authGet { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" , account = getAccount flags - , expect = Http.expectJson receive (JsonDecode.list Data.PeriodicQuerySettings.decoder) + , expect = Http.expectJson receive (JsonDecode.list Api.Model.PeriodicQuerySettings.decoder) } @@ -743,7 +749,7 @@ submitPeriodicQuery flags settings receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" , account = getAccount flags - , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings) + , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -2576,6 +2582,63 @@ shareFileURL attachId = +--- NotificationChannel + + +getChannelsTask : Flags -> Task.Task Http.Error (List NotificationChannel) +getChannelsTask flags = + Http2.authTask + { method = "GET" + , url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel" + , account = getAccount flags + , body = Http.emptyBody + , resolver = Http2.jsonResolver (JsonDecode.list Data.NotificationChannel.decoder) + , headers = [] + , timeout = Nothing + } + + +getChannelsIgnoreError : Flags -> (List NotificationChannel -> msg) -> Cmd msg +getChannelsIgnoreError flags tagger = + getChannelsTask flags + |> Task.attempt (Result.map tagger >> Result.withDefault (tagger [])) + + +getChannels : Flags -> (Result Http.Error (List NotificationChannel) -> msg) -> Cmd msg +getChannels flags receive = + getChannelsTask flags |> Task.attempt receive + + +deleteChannel : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg +deleteChannel flags id receive = + Http2.authDelete + { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel/" ++ id + , account = getAccount flags + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +createChannel : Flags -> NotificationChannel -> (Result Http.Error BasicResult -> msg) -> Cmd msg +createChannel flags hook receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel" + , account = getAccount flags + , body = Http.jsonBody (Data.NotificationChannel.encode hook) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + +updateChannel : Flags -> NotificationChannel -> (Result Http.Error BasicResult -> msg) -> Cmd msg +updateChannel flags hook receive = + Http2.authPut + { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel" + , account = getAccount flags + , body = Http.jsonBody (Data.NotificationChannel.encode hook) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + + --- NotificationHook @@ -2584,7 +2647,7 @@ getHooks flags receive = Http2.authGet { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" , account = getAccount flags - , expect = Http.expectJson receive (JsonDecode.list Data.NotificationHook.decoder) + , expect = Http.expectJson receive (JsonDecode.list Api.Model.NotificationHook.decoder) } @@ -2602,7 +2665,7 @@ createHook flags hook receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" , account = getAccount flags - , body = Http.jsonBody (Data.NotificationHook.encode hook) + , body = Http.jsonBody (Api.Model.NotificationHook.encode hook) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -2612,7 +2675,7 @@ updateHook flags hook receive = Http2.authPut { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" , account = getAccount flags - , body = Http.jsonBody (Data.NotificationHook.encode hook) + , body = Http.jsonBody (Api.Model.NotificationHook.encode hook) , expect = Http.expectJson receive Api.Model.BasicResult.decoder } @@ -2642,7 +2705,7 @@ testHook flags hook receive = Http2.authPost { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook/sendTestEvent" , account = getAccount flags - , body = Http.jsonBody (Data.NotificationHook.encode hook) + , body = Http.jsonBody (Api.Model.NotificationHook.encode hook) , expect = Http.expectJson receive Api.Model.NotificationChannelTestResult.decoder } diff --git a/modules/webapp/src/main/elm/Comp/ChannelForm.elm b/modules/webapp/src/main/elm/Comp/ChannelForm.elm index 203fca16..83bcfee7 100644 --- a/modules/webapp/src/main/elm/Comp/ChannelForm.elm +++ b/modules/webapp/src/main/elm/Comp/ChannelForm.elm @@ -48,17 +48,11 @@ type alias HttpModel = } -type alias RefModel = - { channelType : ChannelType - } - - type Model = Matrix MatrixModel | Gotify GotifyModel | Mail MailModel | Http HttpModel - | Ref RefModel type Msg @@ -147,11 +141,6 @@ initWith flags channel = , Cmd.none ) - Data.NotificationChannel.Ref m -> - ( Ref { channelType = m.channelType } - , Cmd.none - ) - channelType : Model -> ChannelType channelType model = @@ -168,9 +157,6 @@ channelType model = Http _ -> Data.ChannelType.Http - Ref ref -> - ref.channelType - getChannel : Model -> Maybe NotificationChannel getChannel model = @@ -187,9 +173,6 @@ getChannel model = Http mm -> Maybe.map Data.NotificationChannel.Http mm.value - Ref _ -> - Nothing - --- Update @@ -269,12 +252,3 @@ view texts settings model = Http m -> Html.map HttpMsg (Comp.NotificationHttpForm.view texts.httpForm m.form) - - -- Note: currently when retrieving hooks, this is not - -- send from the server. The server always sends - -- concrete channel details. However, it is possible - -- to create hooks with a reference to an existing - -- channel, but this is not supported in this client. - -- So this channel is ignored here. - Ref _ -> - span [ class "hidden" ] [] diff --git a/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm b/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm new file mode 100644 index 00000000..e7d61ab9 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm @@ -0,0 +1,154 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.ChannelRefInput exposing (Model, Msg, getSelected, init, initSelected, initWith, setOptions, setSelected, update, view) + +import Api +import Api.Model.NotificationChannelRef exposing (NotificationChannelRef) +import Comp.Dropdown exposing (Option) +import Data.ChannelType +import Data.DropdownStyle +import Data.Flags exposing (Flags) +import Data.NotificationChannel +import Data.UiSettings exposing (UiSettings) +import Html exposing (Html) +import Messages.Comp.ChannelRefInput exposing (Texts) +import Util.String + + +type alias Model = + { ddm : Comp.Dropdown.Model NotificationChannelRef + , all : List NotificationChannelRef + } + + +type Msg + = DropdownMsg (Comp.Dropdown.Msg NotificationChannelRef) + | LoadChannelsResp (List NotificationChannelRef) + + +emptyModel : Model +emptyModel = + { ddm = makeDropdownModel + , all = [] + } + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( emptyModel, getOptions flags ) + + +getOptions : Flags -> Cmd Msg +getOptions flags = + Api.getChannelsIgnoreError flags (List.map Data.NotificationChannel.getRef >> LoadChannelsResp) + + +setOptions : List NotificationChannelRef -> Msg +setOptions refs = + LoadChannelsResp refs + + +initSelected : Flags -> List NotificationChannelRef -> ( Model, Cmd Msg ) +initSelected flags selected = + ( update (setSelected selected) emptyModel + |> Tuple.first + , getOptions flags + ) + + +initWith : List NotificationChannelRef -> List NotificationChannelRef -> Model +initWith options selected = + update (setSelected selected) emptyModel + |> Tuple.first + |> update (setOptions options) + |> Tuple.first + + +getSelected : Model -> List NotificationChannelRef +getSelected model = + Comp.Dropdown.getSelected model.ddm + + +setSelected : List NotificationChannelRef -> Msg +setSelected refs = + DropdownMsg (Comp.Dropdown.SetSelection refs) + + + +--- Update + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + DropdownMsg lm -> + let + ( dm, dc ) = + Comp.Dropdown.update lm model.ddm + in + ( { model | ddm = dm } + , Cmd.map DropdownMsg dc + ) + + LoadChannelsResp refs -> + let + ( dm, dc ) = + Comp.Dropdown.update (Comp.Dropdown.SetOptions refs) model.ddm + in + ( { model + | all = refs + , ddm = dm + } + , Cmd.map DropdownMsg dc + ) + + + +--- View + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + let + idShort id = + String.slice 0 6 id + + joinName name ct = + Option (ct ++ " (" ++ name ++ ")") "" + + mkName ref = + Data.ChannelType.fromString ref.channelType + |> Maybe.map texts.channelType + |> Maybe.withDefault ref.channelType + |> joinName (Maybe.withDefault (idShort ref.id) ref.name) + + viewCfg = + { makeOption = mkName + , placeholder = texts.placeholder + , labelColor = \_ -> \_ -> "" + , style = Data.DropdownStyle.mainStyle + } + in + Html.map DropdownMsg + (Comp.Dropdown.view2 viewCfg settings model.ddm) + + + +--- Helpers + + +makeDropdownModel : Comp.Dropdown.Model NotificationChannelRef +makeDropdownModel = + let + m = + Comp.Dropdown.makeModel + { multiple = True + , searchable = \n -> n > 0 + } + in + { m | searchWithAdditional = True } diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm index aea8a484..da412623 100644 --- a/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm +++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm @@ -16,22 +16,18 @@ module Comp.DueItemsTaskForm exposing ) import Api -import Api.Model.EmailSettingsList exposing (EmailSettingsList) -import Api.Model.Tag exposing (Tag) +import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Api.Model.TagList exposing (TagList) import Comp.Basic as B import Comp.CalEventInput -import Comp.ChannelForm +import Comp.ChannelRefInput import Comp.IntField import Comp.MenuBar as MB import Comp.TagDropdown import Comp.YesNoDimmer import Data.CalEvent exposing (CalEvent) -import Data.ChannelType exposing (ChannelType) import Data.DropdownStyle as DS import Data.Flags exposing (Flags) -import Data.NotificationChannel -import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Data.TagOrder import Data.UiSettings exposing (UiSettings) import Data.Validated exposing (Validated(..)) @@ -43,13 +39,11 @@ import Markdown import Messages.Comp.DueItemsTaskForm exposing (Texts) import Styles as S import Util.Maybe -import Util.Tag -import Util.Update type alias Model = { settings : PeriodicDueItemsSettings - , channelModel : Comp.ChannelForm.Model + , channelModel : Comp.ChannelRefInput.Model , tagInclModel : Comp.TagDropdown.Model , tagExclModel : Comp.TagDropdown.Model , remindDays : Maybe Int @@ -99,18 +93,14 @@ type Msg | RequestDelete | YesNoDeleteMsg Comp.YesNoDimmer.Msg | SetSummary String - | ChannelMsg Comp.ChannelForm.Msg + | ChannelMsg Comp.ChannelRefInput.Msg initWith : Flags -> PeriodicDueItemsSettings -> ( Model, Cmd Msg ) initWith flags s = let - ct = - Data.NotificationChannel.channelType s.channel - |> Maybe.withDefault Data.ChannelType.Matrix - ( im, ic ) = - init flags ct + init flags newSchedule = Data.CalEvent.fromEvent s.schedule @@ -120,7 +110,7 @@ initWith flags s = Comp.CalEventInput.init flags newSchedule ( cfm, cfc ) = - Comp.ChannelForm.initWith flags s.channel + Comp.ChannelRefInput.initSelected flags s.channels in ( { im | settings = s @@ -145,8 +135,8 @@ initWith flags s = ) -init : Flags -> ChannelType -> ( Model, Cmd Msg ) -init flags ct = +init : Flags -> ( Model, Cmd Msg ) +init flags = let initialSchedule = Data.CalEvent.everyMonth @@ -155,9 +145,9 @@ init flags ct = Comp.CalEventInput.init flags initialSchedule ( cfm, cfc ) = - Comp.ChannelForm.init flags ct + Comp.ChannelRefInput.init flags in - ( { settings = Data.PeriodicDueItemsSettings.empty ct + ( { settings = Api.Model.PeriodicDueItemsSettings.empty , channelModel = cfm , tagInclModel = Comp.TagDropdown.initWith [] [] , tagExclModel = Comp.TagDropdown.initWith [] [] @@ -203,11 +193,17 @@ makeSettings model = Err ValidateCalEventInvalid channelM = - Result.fromMaybe - ValidateChannelRequired - (Comp.ChannelForm.getChannel model.channelModel) + let + list = + Comp.ChannelRefInput.getSelected model.channelModel + in + if list == [] then + Err ValidateChannelRequired - make days timer channel = + else + Ok list + + make days timer channels = { prev | tagsInclude = Comp.TagDropdown.getSelected model.tagInclModel , tagsExclude = Comp.TagDropdown.getSelected model.tagExclModel @@ -216,7 +212,7 @@ makeSettings model = , enabled = model.enabled , schedule = Data.CalEvent.makeEvent timer , summary = model.summary - , channel = channel + , channels = channels } in Result.map3 make @@ -247,7 +243,7 @@ update flags msg model = ChannelMsg lm -> let ( cfm, cfc ) = - Comp.ChannelForm.update flags lm model.channelModel + Comp.ChannelRefInput.update lm model.channelModel in ( { model | channelModel = cfm } , NoAction @@ -538,9 +534,9 @@ view2 texts extraClasses settings model = ] ] , div [ class "mb-4" ] - [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) + [ formHeader texts.channelHeader , Html.map ChannelMsg - (Comp.ChannelForm.view texts.channelForm settings model.channelModel) + (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel) ] , formHeader texts.queryLabel , div [ class "mb-4" ] diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm index 565d00ba..95f17cdb 100644 --- a/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm +++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm @@ -14,15 +14,16 @@ module Comp.DueItemsTaskList exposing , view2 ) +import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Comp.Basic as B +import Data.ChannelRef import Data.ChannelType -import Data.NotificationChannel -import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Html exposing (..) import Html.Attributes exposing (..) import Messages.Comp.DueItemsTaskList exposing (Texts) import Styles as S import Util.Html +import Util.List type alias Model = @@ -94,9 +95,7 @@ viewItem2 texts item = ] ] , td [ class "text-left mr-2" ] - [ Data.NotificationChannel.channelType item.channel - |> Maybe.map Data.ChannelType.asString - |> Maybe.withDefault "-" - |> text + [ div [ class " space-x-1" ] + (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels) ] ] diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm index 6baa28d5..3846b9b4 100644 --- a/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm +++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm @@ -15,13 +15,11 @@ module Comp.DueItemsTaskManage exposing import Api import Api.Model.BasicResult exposing (BasicResult) -import Comp.ChannelMenu +import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Comp.DueItemsTaskForm import Comp.DueItemsTaskList import Comp.MenuBar as MB -import Data.ChannelType exposing (ChannelType) import Data.Flags exposing (Flags) -import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -35,7 +33,6 @@ type alias Model = , detailModel : Maybe Comp.DueItemsTaskForm.Model , items : List PeriodicDueItemsSettings , formState : FormState - , channelMenuOpen : Bool } @@ -57,9 +54,8 @@ type Msg = ListMsg Comp.DueItemsTaskList.Msg | DetailMsg Comp.DueItemsTaskForm.Msg | GetDataResp (Result Http.Error (List PeriodicDueItemsSettings)) - | NewTaskInit ChannelType + | NewTaskInit | SubmitResp SubmitType (Result Http.Error BasicResult) - | ToggleChannelMenu initModel : Model @@ -68,7 +64,6 @@ initModel = , detailModel = Nothing , items = [] , formState = FormStateInitial - , channelMenuOpen = False } @@ -89,11 +84,6 @@ init flags = update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update flags msg model = case msg of - ToggleChannelMenu -> - ( { model | channelMenuOpen = not model.channelMenuOpen } - , Cmd.none - ) - GetDataResp (Ok items) -> ( { model | items = items @@ -194,12 +184,12 @@ update flags msg model = Nothing -> ( model, Cmd.none ) - NewTaskInit ct -> + NewTaskInit -> let ( mm, mc ) = - Comp.DueItemsTaskForm.init flags ct + Comp.DueItemsTaskForm.init flags in - ( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc ) + ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc ) SubmitResp submitType (Ok res) -> ( { model @@ -295,18 +285,15 @@ viewForm2 texts settings model = viewList2 : Texts -> Model -> List (Html Msg) viewList2 texts model = - let - menuModel = - { menuOpen = model.channelMenuOpen - , toggleMenu = ToggleChannelMenu - , menuLabel = texts.newTask - , onItem = NewTaskInit - } - in [ MB.view { start = [] , end = - [ Comp.ChannelMenu.channelMenu texts.channelType menuModel + [ MB.PrimaryButton + { tagger = NewTaskInit + , title = texts.newTask + , icon = Just "fa fa-plus" + , label = texts.newTask + } ] , rootClasses = "mb-4" } diff --git a/modules/webapp/src/main/elm/Comp/EventSample.elm b/modules/webapp/src/main/elm/Comp/EventSample.elm index 0516e1b0..556e847b 100644 --- a/modules/webapp/src/main/elm/Comp/EventSample.elm +++ b/modules/webapp/src/main/elm/Comp/EventSample.elm @@ -115,8 +115,8 @@ dropdownCfg texts = } -viewJson : Texts -> Model -> Html Msg -viewJson texts model = +viewJson : Texts -> Bool -> Model -> Html Msg +viewJson texts enableEventChooser model = let json = Result.withDefault "" @@ -125,7 +125,10 @@ viewJson texts model = div [ class "flex flex-col w-full relative" ] - [ div [ class "flex inline-flex items-center absolute top-2 right-4" ] + [ div + [ class "flex inline-flex items-center absolute top-2 right-4" + , classList [ ( "hidden", not enableEventChooser ) ] + ] [ Html.map EventTypeMsg (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts) False @@ -144,8 +147,8 @@ viewJson texts model = ] -viewMessage : Texts -> Model -> Html Msg -viewMessage texts model = +viewMessage : Texts -> Bool -> Model -> Html Msg +viewMessage texts enableEventChooser model = let titleDecoder = D.at [ "message", "title" ] D.string @@ -162,7 +165,10 @@ viewMessage texts model = div [ class "flex flex-col w-full relative" ] - [ div [ class "flex inline-flex items-center absolute top-2 right-4" ] + [ div + [ class "flex inline-flex items-center absolute top-2 right-4" + , classList [ ( "hidden", not enableEventChooser ) ] + ] [ Html.map EventTypeMsg (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts) False diff --git a/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm b/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm new file mode 100644 index 00000000..27af5a51 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm @@ -0,0 +1,454 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.NotificationChannelManage exposing + ( Model + , Msg + , init + , update + , view + ) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Comp.Basic as B +import Comp.ChannelForm +import Comp.ChannelMenu +import Comp.MenuBar as MB +import Comp.NotificationChannelTable +import Data.ChannelType exposing (ChannelType) +import Data.Flags exposing (Flags) +import Data.NotificationChannel exposing (NotificationChannel) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Http +import Messages.Comp.NotificationChannelManage exposing (Texts) +import Styles as S + + +type alias Model = + { listModel : Comp.NotificationChannelTable.Model + , detailModel : Maybe Comp.ChannelForm.Model + , items : List NotificationChannel + , deleteConfirm : DeleteConfirm + , loading : Bool + , formState : FormState + , newChannelMenuOpen : Bool + , jsonFilterError : Maybe String + } + + +type DeleteConfirm + = DeleteConfirmOff + | DeleteConfirmOn + + +type SubmitType + = SubmitDelete + | SubmitUpdate + | SubmitCreate + + +type FormState + = FormStateInitial + | FormErrorHttp Http.Error + | FormSubmitSuccessful SubmitType + | FormErrorSubmit String + | FormErrorInvalid + + +type Msg + = TableMsg Comp.NotificationChannelTable.Msg + | DetailMsg Comp.ChannelForm.Msg + | GetDataResp (Result Http.Error (List NotificationChannel)) + | ToggleNewChannelMenu + | SubmitResp SubmitType (Result Http.Error BasicResult) + | NewChannelInit ChannelType + | BackToTable + | Submit + | RequestDelete + | CancelDelete + | DeleteChannelNow String + + +initModel : Model +initModel = + { listModel = Comp.NotificationChannelTable.init + , detailModel = Nothing + , items = [] + , loading = False + , formState = FormStateInitial + , newChannelMenuOpen = False + , deleteConfirm = DeleteConfirmOff + , jsonFilterError = Nothing + } + + +initCmd : Flags -> Cmd Msg +initCmd flags = + Api.getChannels flags GetDataResp + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( initModel, initCmd flags ) + + + +--- Update + + +update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update flags msg model = + case msg of + GetDataResp (Ok res) -> + ( { model + | items = res + , formState = FormStateInitial + } + , Cmd.none + ) + + GetDataResp (Err err) -> + ( { model | formState = FormErrorHttp err } + , Cmd.none + ) + + TableMsg lm -> + let + ( mm, action ) = + Comp.NotificationChannelTable.update flags lm model.listModel + + ( detail, cmd ) = + case action of + Comp.NotificationChannelTable.NoAction -> + ( Nothing, Cmd.none ) + + Comp.NotificationChannelTable.EditAction channel -> + let + ( dm, dc ) = + Comp.ChannelForm.initWith flags channel + in + ( Just dm, Cmd.map DetailMsg dc ) + in + ( { model + | listModel = mm + , detailModel = detail + } + , cmd + ) + + DetailMsg lm -> + case model.detailModel of + Just dm -> + let + ( mm, mc ) = + Comp.ChannelForm.update flags lm dm + in + ( { model | detailModel = Just mm } + , Cmd.map DetailMsg mc + ) + + Nothing -> + ( model, Cmd.none ) + + ToggleNewChannelMenu -> + ( { model | newChannelMenuOpen = not model.newChannelMenuOpen }, Cmd.none ) + + SubmitResp submitType (Ok res) -> + ( { model + | formState = + if res.success then + FormSubmitSuccessful submitType + + else + FormErrorSubmit res.message + , detailModel = + if submitType == SubmitDelete then + Nothing + + else + model.detailModel + , loading = False + } + , if submitType == SubmitDelete then + initCmd flags + + else + Cmd.none + ) + + SubmitResp _ (Err err) -> + ( { model | formState = FormErrorHttp err, loading = False } + , Cmd.none + ) + + NewChannelInit ct -> + let + ( mm, mc ) = + Comp.ChannelForm.init flags ct + in + ( { model | detailModel = Just mm, newChannelMenuOpen = False }, Cmd.map DetailMsg mc ) + + BackToTable -> + ( { model | detailModel = Nothing }, initCmd flags ) + + Submit -> + case model.detailModel of + Just dm -> + case Comp.ChannelForm.getChannel dm of + Just data -> + postChannel flags data model + + Nothing -> + ( { model | formState = FormErrorInvalid }, Cmd.none ) + + Nothing -> + ( model, Cmd.none ) + + RequestDelete -> + ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none ) + + CancelDelete -> + ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none ) + + DeleteChannelNow id -> + ( { model | deleteConfirm = DeleteConfirmOff, loading = True } + , Api.deleteChannel flags id (SubmitResp SubmitDelete) + ) + + +postChannel : Flags -> NotificationChannel -> Model -> ( Model, Cmd Msg ) +postChannel flags channel model = + if (Data.NotificationChannel.getRef channel |> .id) == "" then + ( { model | loading = True }, Api.createChannel flags channel (SubmitResp SubmitCreate) ) + + else + ( { model | loading = True }, Api.updateChannel flags channel (SubmitResp SubmitUpdate) ) + + + +--- View2 + + +view : Texts -> UiSettings -> Model -> Html Msg +view texts settings model = + div [ class "flex flex-col" ] + (case model.detailModel of + Just msett -> + viewForm texts settings model msett + + Nothing -> + viewList texts model + ) + + +viewState : Texts -> Model -> Html Msg +viewState texts model = + div + [ classList + [ ( S.errorMessage, not (isSuccess model.formState) ) + , ( S.successMessage, isSuccess model.formState ) + , ( "hidden", model.formState == FormStateInitial ) + ] + , class "mb-2" + ] + [ case model.formState of + FormStateInitial -> + text "" + + FormSubmitSuccessful SubmitCreate -> + text texts.channelCreated + + FormSubmitSuccessful SubmitUpdate -> + text texts.channelUpdated + + FormSubmitSuccessful SubmitDelete -> + text texts.channelDeleted + + FormErrorSubmit m -> + text m + + FormErrorHttp err -> + text (texts.httpError err) + + FormErrorInvalid -> + text texts.formInvalid + ] + + +isSuccess : FormState -> Bool +isSuccess state = + case state of + FormSubmitSuccessful _ -> + True + + _ -> + False + + +viewForm : Texts -> UiSettings -> Model -> Comp.ChannelForm.Model -> List (Html Msg) +viewForm texts settings outerModel model = + let + channelId = + Comp.ChannelForm.getChannel model + |> Maybe.map Data.NotificationChannel.getRef + |> Maybe.map .id + + newChannel = + channelId |> (==) (Just "") + + headline = + case Comp.ChannelForm.channelType model of + Data.ChannelType.Matrix -> + span [] + [ text texts.integrate + , a + [ href "https://matrix.org" + , target "_blank" + , class S.link + , class "mx-3" + ] + [ i [ class "fa fa-external-link-alt mr-1" ] [] + , text "Matrix" + ] + , text texts.intoDocspell + ] + + Data.ChannelType.Mail -> + span [] + [ text texts.notifyEmailInfo + ] + + Data.ChannelType.Gotify -> + span [] + [ text texts.integrate + , a + [ href "https://gotify.net" + , target "_blank" + , class S.link + , class "mx-3" + ] + [ i [ class "fa fa-external-link-alt mr-1" ] [] + , text "Gotify" + ] + , text texts.intoDocspell + ] + + Data.ChannelType.Http -> + span [] + [ text texts.postRequestInfo + ] + in + [ h1 [ class S.header2 ] + [ Data.ChannelType.icon (Comp.ChannelForm.channelType model) "w-8 h-8 inline-block mr-2" + , if newChannel then + text texts.addChannel + + else + text texts.updateChannel + , div [ class "text-xs opacity-50 font-mono" ] + [ Maybe.withDefault "" channelId |> text + ] + ] + , div [ class "pt-2 pb-4 font-medium" ] + [ headline + ] + , MB.view + { start = + [ MB.CustomElement <| + B.primaryButton + { handler = onClick Submit + , title = texts.basics.submitThisForm + , icon = "fa fa-save" + , label = texts.basics.submit + , disabled = False + , attrs = [ href "#" ] + } + , MB.SecondaryButton + { tagger = BackToTable + , title = texts.basics.backToList + , icon = Just "fa fa-arrow-left" + , label = texts.basics.backToList + } + ] + , end = + if not newChannel then + [ MB.DeleteButton + { tagger = RequestDelete + , title = texts.deleteThisChannel + , icon = Just "fa fa-trash" + , label = texts.basics.delete + } + ] + + else + [] + , rootClasses = "mb-4" + } + , div [ class "mt-2" ] + [ viewState texts outerModel + ] + , Html.map DetailMsg + (Comp.ChannelForm.view texts.notificationForm settings model) + , B.loadingDimmer + { active = outerModel.loading + , label = texts.basics.loading + } + , B.contentDimmer + (outerModel.deleteConfirm == DeleteConfirmOn) + (div [ class "flex flex-col" ] + [ div [ class "text-lg" ] + [ i [ class "fa fa-info-circle mr-2" ] [] + , text texts.reallyDeleteChannel + ] + , div [ class "mt-4 flex flex-row items-center" ] + [ B.deleteButton + { label = texts.basics.yes + , icon = "fa fa-check" + , disabled = False + , handler = onClick (DeleteChannelNow (Maybe.withDefault "" channelId)) + , attrs = [ href "#" ] + } + , B.secondaryButton + { label = texts.basics.no + , icon = "fa fa-times" + , disabled = False + , handler = onClick CancelDelete + , attrs = [ href "#", class "ml-2" ] + } + ] + ] + ) + ] + + +viewList : Texts -> Model -> List (Html Msg) +viewList texts model = + let + menuModel = + { menuOpen = model.newChannelMenuOpen + , toggleMenu = ToggleNewChannelMenu + , menuLabel = texts.newChannel + , onItem = NewChannelInit + } + in + [ MB.view + { start = [] + , end = + [ Comp.ChannelMenu.channelMenu texts.channelType menuModel + ] + , rootClasses = "mb-4" + } + , Html.map TableMsg + (Comp.NotificationChannelTable.view texts.notificationTable + model.listModel + model.items + ) + ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm b/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm new file mode 100644 index 00000000..31c9e061 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm @@ -0,0 +1,87 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Comp.NotificationChannelTable exposing (..) + +import Comp.Basic as B +import Data.ChannelType +import Data.Flags exposing (Flags) +import Data.NotificationChannel exposing (NotificationChannel) +import Html exposing (..) +import Html.Attributes exposing (..) +import Messages.Comp.NotificationChannelTable exposing (Texts) +import Styles as S + + +type alias Model = + {} + + +type Action + = NoAction + | EditAction NotificationChannel + + +init : Model +init = + {} + + +type Msg + = Select NotificationChannel + + +update : Flags -> Msg -> Model -> ( Model, Action ) +update _ msg model = + case msg of + Select channel -> + ( model, EditAction channel ) + + + +--- View + + +view : Texts -> Model -> List NotificationChannel -> Html Msg +view texts model channels = + table [ class S.tableMain ] + [ thead [] + [ tr [] + [ th [ class "" ] [] + , th [ class "text-left" ] + [ text texts.basics.name + ] + , th [ class "text-left" ] + [ text texts.channelType + ] + ] + ] + , tbody [] + (List.map (renderNotificationChannelLine texts model) channels) + ] + + +renderNotificationChannelLine : Texts -> Model -> NotificationChannel -> Html Msg +renderNotificationChannelLine texts _ channel = + let + ref = + Data.NotificationChannel.getRef channel + in + tr + [ class S.tableRow + ] + [ B.editLinkTableCell texts.basics.edit (Select channel) + , td + [ class "text-left " + , classList [ ( "font-mono", ref.name == Nothing ) ] + ] + [ Maybe.withDefault (String.slice 0 10 ref.id) ref.name |> text + ] + , td [ class "text-left py-4 md:py-2" ] + [ text ref.channelType + ] + ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm b/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm index 83454297..a94eda54 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm @@ -17,24 +17,25 @@ import Html.Attributes exposing (..) import Html.Events exposing (onInput) import Messages.Comp.NotificationGotifyForm exposing (Texts) import Styles as S +import Util.Maybe type alias Model = - { hook : NotificationGotify + { channel : NotificationGotify , prioModel : Comp.FixedDropdown.Model Int } init : Model init = - { hook = Data.NotificationChannel.setTypeGotify Api.Model.NotificationGotify.empty + { channel = Data.NotificationChannel.setTypeGotify Api.Model.NotificationGotify.empty , prioModel = Comp.FixedDropdown.init (List.range 0 10) } initWith : NotificationGotify -> Model -initWith hook = - { hook = Data.NotificationChannel.setTypeGotify hook +initWith channel = + { channel = Data.NotificationChannel.setTypeGotify channel , prioModel = Comp.FixedDropdown.init (List.range 0 10) } @@ -42,6 +43,7 @@ initWith hook = type Msg = SetUrl String | SetAppKey String + | SetName String | PrioMsg (Comp.FixedDropdown.Msg Int) @@ -52,30 +54,33 @@ type Msg update : Msg -> Model -> ( Model, Maybe NotificationGotify ) update msg model = let - hook = - model.hook + channel = + model.channel newModel = case msg of SetUrl s -> - { model | hook = { hook | url = s } } + { model | channel = { channel | url = s } } SetAppKey s -> - { model | hook = { hook | appKey = s } } + { model | channel = { channel | appKey = s } } + + SetName s -> + { model | channel = { channel | name = Util.Maybe.fromString s } } PrioMsg lm -> let ( m, sel ) = Comp.FixedDropdown.update lm model.prioModel in - { model | hook = { hook | priority = sel }, prioModel = m } + { model | channel = { channel | priority = sel }, prioModel = m } in - ( newModel, check newModel.hook ) + ( newModel, check newModel.channel ) check : NotificationGotify -> Maybe NotificationGotify -check hook = - Just hook +check channel = + Just channel @@ -94,6 +99,25 @@ view texts model = in div [] [ div + [ class "mb-2" + ] + [ label + [ for "name" + , class S.inputLabel + ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , onInput SetName + , placeholder texts.basics.name + , value (Maybe.withDefault "" model.channel.name) + , name "name" + , class S.textInput + ] + [] + ] + , div [ class "mb-2" ] [ label @@ -107,7 +131,7 @@ view texts model = [ type_ "text" , onInput SetUrl , placeholder texts.gotifyUrl - , value model.hook.url + , value model.channel.url , name "gotifyurl" , class S.textInput ] @@ -127,7 +151,7 @@ view texts model = [ type_ "text" , onInput SetAppKey , placeholder texts.appKey - , value model.hook.appKey + , value model.channel.appKey , name "appkey" , class S.textInput ] @@ -142,7 +166,7 @@ view texts model = ] [ text texts.priority ] - , Html.map PrioMsg (Comp.FixedDropdown.viewStyled2 cfg False model.hook.priority model.prioModel) + , Html.map PrioMsg (Comp.FixedDropdown.viewStyled2 cfg False model.channel.priority model.prioModel) , span [ class "text-sm opacity-75" ] [ text texts.priorityInfo ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm b/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm index f1067a25..bfaa19ae 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm @@ -8,7 +8,6 @@ module Comp.NotificationHookForm exposing ( Model , Msg(..) - , channelType , getHook , init , initWith @@ -16,17 +15,16 @@ module Comp.NotificationHookForm exposing , view ) +import Api.Model.NotificationHook exposing (NotificationHook) import Comp.Basic as B -import Comp.ChannelForm +import Comp.ChannelRefInput import Comp.Dropdown import Comp.EventSample import Comp.MenuBar as MB import Comp.NotificationTest -import Data.ChannelType exposing (ChannelType) import Data.DropdownStyle as DS import Data.EventType exposing (EventType) import Data.Flags exposing (Flags) -import Data.NotificationHook exposing (NotificationHook) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -39,7 +37,7 @@ import Util.Maybe type alias Model = { hook : NotificationHook , enabled : Bool - , channelModel : Comp.ChannelForm.Model + , channelModel : Comp.ChannelRefInput.Model , eventsDropdown : Comp.Dropdown.Model EventType , eventSampleModel : Comp.EventSample.Model , testDeliveryModel : Comp.NotificationTest.Model @@ -48,16 +46,16 @@ type alias Model = } -init : Flags -> ChannelType -> ( Model, Cmd Msg ) -init flags ct = +init : Flags -> ( Model, Cmd Msg ) +init flags = let ( cm, cc ) = - Comp.ChannelForm.init flags ct + Comp.ChannelRefInput.init flags ( esm, esc ) = Comp.EventSample.initWith flags Data.EventType.TagsChanged in - ( { hook = Data.NotificationHook.empty ct + ( { hook = Api.Model.NotificationHook.empty , enabled = True , channelModel = cm , eventsDropdown = @@ -81,7 +79,7 @@ initWith : Flags -> NotificationHook -> ( Model, Cmd Msg ) initWith flags h = let ( cm, cc ) = - Comp.ChannelForm.initWith flags h.channel + Comp.ChannelRefInput.initSelected flags h.channels ( esm, esc ) = Comp.EventSample.initWith flags Data.EventType.TagsChanged @@ -92,7 +90,7 @@ initWith flags h = , eventsDropdown = Comp.Dropdown.makeMultipleList { options = Data.EventType.all - , selected = h.events + , selected = List.filterMap Data.EventType.fromString h.events } , eventSampleModel = esm , testDeliveryModel = Comp.NotificationTest.init @@ -106,11 +104,6 @@ initWith flags h = ) -channelType : Model -> ChannelType -channelType model = - Comp.ChannelForm.channelType model.channelModel - - getHook : Model -> Maybe NotificationHook getHook model = let @@ -123,20 +116,28 @@ getHook model = Nothing else - Just ev + Just (List.map Data.EventType.asString ev) - channel = - Comp.ChannelForm.getChannel model.channelModel + channels = + let + list = + Comp.ChannelRefInput.getSelected model.channelModel + in + if list == [] then + Nothing + + else + Just list mkHook ev ch = NotificationHook model.hook.id model.enabled ch model.allEvents model.eventFilter ev in - Maybe.map2 mkHook events channel + Maybe.map2 mkHook events channels type Msg = ToggleEnabled - | ChannelFormMsg Comp.ChannelForm.Msg + | ChannelFormMsg Comp.ChannelRefInput.Msg | EventMsg (Comp.Dropdown.Msg EventType) | EventSampleMsg Comp.EventSample.Msg | DeliveryTestMsg Comp.NotificationTest.Msg @@ -163,7 +164,7 @@ update flags msg model = ChannelFormMsg lm -> let ( cm, cc ) = - Comp.ChannelForm.update flags lm model.channelModel + Comp.ChannelRefInput.update lm model.channelModel in ( { model | channelModel = cm }, Cmd.map ChannelFormMsg cc ) @@ -229,9 +230,9 @@ view texts settings model = } ] , div [ class "mb-4" ] - [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) + [ formHeader texts.channelHeader , Html.map ChannelFormMsg - (Comp.ChannelForm.view texts.channelForm settings model.channelModel) + (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel) ] , div [ class "mb-4" ] [ formHeader texts.events @@ -290,21 +291,21 @@ view texts settings model = ] , div [ class "mt-4" - , classList [ ( "hidden", channelType model /= Data.ChannelType.Http ) ] - ] - [ h3 [ class S.header3 ] - [ text texts.samplePayload - ] - , Html.map EventSampleMsg - (Comp.EventSample.viewJson texts.eventSample model.eventSampleModel) - ] - , div - [ class "mt-4" - , classList [ ( "hidden", channelType model == Data.ChannelType.Http ) ] ] [ formHeader texts.samplePayload + , div [ class "opacity-80 mb-1" ] + [ text texts.payloadInfo + ] , Html.map EventSampleMsg - (Comp.EventSample.viewMessage texts.eventSample model.eventSampleModel) + (Comp.EventSample.viewMessage texts.eventSample True model.eventSampleModel) + , div [ class "py-2 text-center text-sm" ] + [ text texts.jsonPayload + , i [ class "fa fa-arrow-down ml-1 mr-3" ] [] + , i [ class "fa fa-arrow-up mr-1" ] [] + , text texts.messagePayload + ] + , Html.map EventSampleMsg + (Comp.EventSample.viewJson texts.eventSample False model.eventSampleModel) ] , div [ class "mt-4" ] [ formHeader "Test Delviery" diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm b/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm index d15d4e7f..921d5f2f 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm @@ -15,14 +15,12 @@ module Comp.NotificationHookManage exposing import Api import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.NotificationHook exposing (NotificationHook) import Comp.Basic as B -import Comp.ChannelMenu import Comp.MenuBar as MB import Comp.NotificationHookForm import Comp.NotificationHookTable -import Data.ChannelType exposing (ChannelType) import Data.Flags exposing (Flags) -import Data.NotificationHook exposing (NotificationHook) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -39,7 +37,6 @@ type alias Model = , deleteConfirm : DeleteConfirm , loading : Bool , formState : FormState - , newHookMenuOpen : Bool , jsonFilterError : Maybe String } @@ -67,9 +64,8 @@ type Msg = TableMsg Comp.NotificationHookTable.Msg | DetailMsg Comp.NotificationHookForm.Msg | GetDataResp (Result Http.Error (List NotificationHook)) - | ToggleNewHookMenu | SubmitResp SubmitType (Result Http.Error BasicResult) - | NewHookInit ChannelType + | NewHookInit | BackToTable | Submit | RequestDelete @@ -85,7 +81,6 @@ initModel = , items = [] , loading = False , formState = FormStateInitial - , newHookMenuOpen = False , deleteConfirm = DeleteConfirmOff , jsonFilterError = Nothing } @@ -177,9 +172,6 @@ update flags msg model = Nothing -> ( model, Cmd.none ) - ToggleNewHookMenu -> - ( { model | newHookMenuOpen = not model.newHookMenuOpen }, Cmd.none ) - SubmitResp submitType (Ok res) -> ( { model | formState = @@ -208,12 +200,12 @@ update flags msg model = , Cmd.none ) - NewHookInit ct -> + NewHookInit -> let ( mm, mc ) = - Comp.NotificationHookForm.init flags ct + Comp.NotificationHookForm.init flags in - ( { model | detailModel = Just mm, newHookMenuOpen = False }, Cmd.map DetailMsg mc ) + ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc ) BackToTable -> ( { model | detailModel = Nothing }, initCmd flags ) @@ -327,60 +319,14 @@ viewForm texts settings outerModel model = let newHook = model.hook.id == "" - - headline = - case Comp.NotificationHookForm.channelType model of - Data.ChannelType.Matrix -> - span [] - [ text texts.integrate - , a - [ href "https://matrix.org" - , target "_blank" - , class S.link - , class "mx-3" - ] - [ i [ class "fa fa-external-link-alt mr-1" ] [] - , text "Matrix" - ] - , text texts.intoDocspell - ] - - Data.ChannelType.Mail -> - span [] - [ text texts.notifyEmailInfo - ] - - Data.ChannelType.Gotify -> - span [] - [ text texts.integrate - , a - [ href "https://gotify.net" - , target "_blank" - , class S.link - , class "mx-3" - ] - [ i [ class "fa fa-external-link-alt mr-1" ] [] - , text "Gotify" - ] - , text texts.intoDocspell - ] - - Data.ChannelType.Http -> - span [] - [ text texts.postRequestInfo - ] in [ h1 [ class S.header2 ] - [ Data.ChannelType.icon (Comp.NotificationHookForm.channelType model) "w-8 h-8 inline-block mr-4" - , if newHook then + [ if newHook then text texts.addWebhook else text texts.updateWebhook ] - , div [ class "pt-2 pb-4 font-medium" ] - [ headline - ] , MB.view { start = [ MB.CustomElement <| @@ -452,18 +398,15 @@ viewForm texts settings outerModel model = viewList : Texts -> Model -> List (Html Msg) viewList texts model = - let - menuModel = - { menuOpen = model.newHookMenuOpen - , toggleMenu = ToggleNewHookMenu - , menuLabel = texts.newHook - , onItem = NewHookInit - } - in [ MB.view { start = [] , end = - [ Comp.ChannelMenu.channelMenu texts.channelType menuModel + [ MB.PrimaryButton + { tagger = NewHookInit + , title = texts.newHook + , icon = Just "fa fa-plus" + , label = texts.newHook + } ] , rootClasses = "mb-4" } diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm b/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm index 8188a1df..8091e7f3 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm @@ -14,15 +14,13 @@ module Comp.NotificationHookTable exposing , view ) +import Api.Model.NotificationHook exposing (NotificationHook) import Comp.Basic as B -import Data.ChannelType +import Data.ChannelRef import Data.EventType import Data.Flags exposing (Flags) -import Data.NotificationChannel -import Data.NotificationHook exposing (NotificationHook) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick) import Messages.Comp.NotificationHookTable exposing (Texts) import Styles as S import Util.Html @@ -80,7 +78,7 @@ view texts model hooks = renderNotificationHookLine : Texts -> Model -> NotificationHook -> Html Msg -renderNotificationHookLine texts model hook = +renderNotificationHookLine texts _ hook = let eventName = texts.eventType >> .name @@ -93,14 +91,17 @@ renderNotificationHookLine texts model hook = [ Util.Html.checkbox2 hook.enabled ] , td [ class "text-left py-4 md:py-2" ] - [ Data.NotificationChannel.channelType hook.channel - |> Maybe.map Data.ChannelType.asString - |> Maybe.withDefault "-" - |> text + [ div [ class "space-x-1" ] + (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] hook.channels) ] , td [ class "text-left hidden sm:table-cell" ] - [ List.map eventName hook.events - |> String.join ", " - |> text + [ if hook.allEvents then + text texts.allEvents + + else + List.filterMap Data.EventType.fromString hook.events + |> List.map eventName + |> String.join ", " + |> text ] ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm b/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm index 1e637796..4f898ad9 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm @@ -15,29 +15,31 @@ import Html.Attributes exposing (..) import Html.Events exposing (onInput) import Messages.Comp.NotificationHttpForm exposing (Texts) import Styles as S +import Util.Maybe type alias Model = - { hook : NotificationHttp + { channel : NotificationHttp } init : Model init = - { hook = + { channel = Data.NotificationChannel.setTypeHttp Api.Model.NotificationHttp.empty } initWith : NotificationHttp -> Model -initWith hook = - { hook = Data.NotificationChannel.setTypeHttp hook +initWith channel = + { channel = Data.NotificationChannel.setTypeHttp channel } type Msg = SetUrl String + | SetName String @@ -47,26 +49,29 @@ type Msg update : Msg -> Model -> ( Model, Maybe NotificationHttp ) update msg model = let - newHook = - updateHook msg model.hook + newChannel = + updateChannel msg model.channel in - ( { model | hook = newHook }, check newHook ) + ( { model | channel = newChannel }, check newChannel ) check : NotificationHttp -> Maybe NotificationHttp -check hook = - if hook.url == "" then +check channel = + if channel.url == "" then Nothing else - Just hook + Just channel -updateHook : Msg -> NotificationHttp -> NotificationHttp -updateHook msg hook = +updateChannel : Msg -> NotificationHttp -> NotificationHttp +updateChannel msg channel = case msg of SetUrl s -> - { hook | url = s } + { channel | url = s } + + SetName s -> + { channel | name = Util.Maybe.fromString s } @@ -77,6 +82,25 @@ view : Texts -> Model -> Html Msg view texts model = div [] [ div + [ class "mb-2" + ] + [ label + [ for "name" + , class S.inputLabel + ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , onInput SetName + , placeholder texts.basics.name + , value (Maybe.withDefault "" model.channel.name) + , name "name" + , class S.textInput + ] + [] + ] + , div [ class "mb-2" ] [ label @@ -90,7 +114,7 @@ view texts model = [ type_ "text" , onInput SetUrl , placeholder texts.httpUrl - , value model.hook.url + , value model.channel.url , name "httpurl" , class S.textInput ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm b/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm index ce756826..0faa0563 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm @@ -23,13 +23,15 @@ import Html.Events exposing (onInput) import Http import Messages.Comp.NotificationMailForm exposing (Texts) import Styles as S +import Util.Maybe type alias Model = - { hook : NotificationMail + { channel : NotificationMail , connectionModel : Comp.Dropdown.Model String , recipients : List String , recipientsModel : Comp.EmailInput.Model + , name : Maybe String , formState : FormState } @@ -46,10 +48,11 @@ type ValidateError init : Flags -> ( Model, Cmd Msg ) init flags = - ( { hook = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty + ( { channel = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty , connectionModel = Comp.Dropdown.makeSingle , recipients = [] , recipientsModel = Comp.EmailInput.init + , name = Nothing , formState = FormStateInitial } , Cmd.batch @@ -59,17 +62,17 @@ init flags = initWith : Flags -> NotificationMail -> ( Model, Cmd Msg ) -initWith flags hook = +initWith flags channel = let ( mm, mc ) = init flags ( cm, _ ) = - Comp.Dropdown.update (Comp.Dropdown.SetSelection [ hook.connection ]) mm.connectionModel + Comp.Dropdown.update (Comp.Dropdown.SetSelection [ channel.connection ]) mm.connectionModel in ( { mm - | hook = Data.NotificationChannel.setTypeMail hook - , recipients = hook.recipients + | channel = Data.NotificationChannel.setTypeMail channel + , recipients = channel.recipients , connectionModel = cm } , mc @@ -80,6 +83,7 @@ type Msg = ConnResp (Result Http.Error EmailSettingsList) | ConnMsg (Comp.Dropdown.Msg String) | RecipientMsg Comp.EmailInput.Msg + | SetName String @@ -108,12 +112,12 @@ check model = |> List.head h = - model.hook + model.channel - makeHook _ rec conn = - { h | connection = conn, recipients = rec } + makeChannel _ rec conn = + { h | connection = conn, recipients = rec, name = model.name } in - Maybe.map3 makeHook formState recipients connection + Maybe.map3 makeChannel formState recipients connection update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe NotificationMail ) @@ -152,6 +156,16 @@ update flags msg model = , Nothing ) + SetName s -> + let + model_ = + { model | name = Util.Maybe.fromString s } + in + ( model_ + , Cmd.none + , check model_ + ) + ConnMsg lm -> let ( cm, cc ) = @@ -201,7 +215,26 @@ view texts settings model = } in div [] - [ div [ class "mb-4" ] + [ div + [ class "mb-2" + ] + [ label + [ for "name" + , class S.inputLabel + ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , onInput SetName + , placeholder texts.basics.name + , value (Maybe.withDefault "" model.name) + , name "name" + , class S.textInput + ] + [] + ] + , div [ class "mb-4" ] [ label [ class S.inputLabel ] [ text texts.sendVia , B.inputRequired diff --git a/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm b/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm index 8f4104b0..76969267 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm @@ -15,22 +15,23 @@ import Html.Attributes exposing (..) import Html.Events exposing (onInput) import Messages.Comp.NotificationMatrixForm exposing (Texts) import Styles as S +import Util.Maybe type alias Model = - { hook : NotificationMatrix + { channel : NotificationMatrix } init : Model init = - { hook = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty + { channel = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty } initWith : NotificationMatrix -> Model -initWith hook = - { hook = Data.NotificationChannel.setTypeMatrix hook +initWith channel = + { channel = Data.NotificationChannel.setTypeMatrix channel } @@ -38,6 +39,7 @@ type Msg = SetHomeServer String | SetRoomId String | SetAccessKey String + | SetName String @@ -47,28 +49,31 @@ type Msg update : Msg -> Model -> ( Model, Maybe NotificationMatrix ) update msg model = let - newHook = - updateHook msg model.hook + newChannel = + updateChannel msg model.channel in - ( { model | hook = newHook }, check newHook ) + ( { model | channel = newChannel }, check newChannel ) check : NotificationMatrix -> Maybe NotificationMatrix -check hook = - Just hook +check channel = + Just channel -updateHook : Msg -> NotificationMatrix -> NotificationMatrix -updateHook msg hook = +updateChannel : Msg -> NotificationMatrix -> NotificationMatrix +updateChannel msg channel = case msg of SetHomeServer s -> - { hook | homeServer = s } + { channel | homeServer = s } SetRoomId s -> - { hook | roomId = s } + { channel | roomId = s } SetAccessKey s -> - { hook | accessToken = s } + { channel | accessToken = s } + + SetName s -> + { channel | name = Util.Maybe.fromString s } @@ -79,6 +84,25 @@ view : Texts -> Model -> Html Msg view texts model = div [] [ div + [ class "mb-2" + ] + [ label + [ for "name" + , class S.inputLabel + ] + [ text texts.basics.name + ] + , input + [ type_ "text" + , onInput SetName + , placeholder texts.basics.name + , value (Maybe.withDefault "" model.channel.name) + , name "name" + , class S.textInput + ] + [] + ] + , div [ class "mb-2" ] [ label @@ -92,7 +116,7 @@ view texts model = [ type_ "text" , onInput SetHomeServer , placeholder texts.homeServer - , value model.hook.homeServer + , value model.channel.homeServer , name "homeserver" , class S.textInput ] @@ -112,7 +136,7 @@ view texts model = [ type_ "text" , onInput SetRoomId , placeholder texts.roomId - , value model.hook.roomId + , value model.channel.roomId , name "roomid" , class S.textInput ] @@ -131,7 +155,7 @@ view texts model = , textarea [ onInput SetAccessKey , placeholder texts.accessKey - , value model.hook.accessToken + , value model.channel.accessToken , name "accesskey" , class S.textAreaInput ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationTest.elm b/modules/webapp/src/main/elm/Comp/NotificationTest.elm index 81003721..5af2c2a9 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationTest.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationTest.elm @@ -9,10 +9,10 @@ module Comp.NotificationTest exposing (Model, Msg, ViewConfig, init, update, vie import Api import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult) +import Api.Model.NotificationHook exposing (NotificationHook) import Comp.Basic as B import Comp.MenuBar as MB import Data.Flags exposing (Flags) -import Data.NotificationHook exposing (NotificationHook) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm index 154c6ab5..8560b331 100644 --- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm +++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm @@ -16,16 +16,15 @@ module Comp.PeriodicQueryTaskForm exposing , view ) +import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Comp.Basic as B import Comp.BookmarkDropdown import Comp.CalEventInput -import Comp.ChannelForm +import Comp.ChannelRefInput import Comp.MenuBar as MB import Comp.PowerSearchInput import Data.CalEvent exposing (CalEvent) -import Data.ChannelType exposing (ChannelType) import Data.Flags exposing (Flags) -import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Data.UiSettings exposing (UiSettings) import Data.Validated exposing (Validated(..)) import Html exposing (..) @@ -44,7 +43,7 @@ type alias Model = , schedule : Maybe CalEvent , scheduleModel : Comp.CalEventInput.Model , queryModel : Comp.PowerSearchInput.Model - , channelModel : Comp.ChannelForm.Model + , channelModel : Comp.ChannelRefInput.Model , bookmarkDropdown : Comp.BookmarkDropdown.Model , contentStart : Maybe String , formState : FormState @@ -78,7 +77,7 @@ type Msg | ToggleEnabled | CalEventMsg Comp.CalEventInput.Msg | QueryMsg Comp.PowerSearchInput.Msg - | ChannelMsg Comp.ChannelForm.Msg + | ChannelMsg Comp.ChannelRefInput.Msg | BookmarkMsg Comp.BookmarkDropdown.Msg | SetContentStart String | StartOnce @@ -105,7 +104,7 @@ initWith flags s = Comp.PowerSearchInput.init ( cfm, cfc ) = - Comp.ChannelForm.initWith flags s.channel + Comp.ChannelRefInput.initSelected flags s.channels ( bm, bc ) = Comp.BookmarkDropdown.init flags s.bookmark @@ -132,8 +131,8 @@ initWith flags s = ) -init : Flags -> ChannelType -> ( Model, Cmd Msg ) -init flags ct = +init : Flags -> ( Model, Cmd Msg ) +init flags = let initialSchedule = Data.CalEvent.everyMonth @@ -142,12 +141,12 @@ init flags ct = Comp.CalEventInput.init flags initialSchedule ( cfm, cfc ) = - Comp.ChannelForm.init flags ct + Comp.ChannelRefInput.init flags ( bm, bc ) = Comp.BookmarkDropdown.init flags Nothing in - ( { settings = Data.PeriodicQuerySettings.empty ct + ( { settings = Api.Model.PeriodicQuerySettings.empty , enabled = False , schedule = Just initialSchedule , scheduleModel = sm @@ -210,16 +209,22 @@ makeSettings model = Result.Ok ( qstr, bm ) channelM = - Result.fromMaybe - ValidateChannelRequired - (Comp.ChannelForm.getChannel model.channelModel) + let + list = + Comp.ChannelRefInput.getSelected model.channelModel + in + if list == [] then + Err ValidateChannelRequired - make timer channel q = + else + Ok list + + make timer channels q = { prev | enabled = model.enabled , schedule = Data.CalEvent.makeEvent timer , summary = model.summary - , channel = channel + , channels = channels , query = Tuple.first q , bookmark = Tuple.second q , contentStart = model.contentStart @@ -285,7 +290,7 @@ update flags msg model = ChannelMsg lm -> let ( cfm, cfc ) = - Comp.ChannelForm.update flags lm model.channelModel + Comp.ChannelRefInput.update lm model.channelModel in { model = { model | channelModel = cfm } , action = NoAction @@ -536,9 +541,9 @@ view texts extraClasses settings model = ] ] , div [ class "mb-4" ] - [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) False + [ formHeader texts.channelHeader True , Html.map ChannelMsg - (Comp.ChannelForm.view texts.channelForm settings model.channelModel) + (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel) ] , div [ class "mb-4" ] [ formHeader texts.queryLabel True diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm index f654d11e..c1a83a07 100644 --- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm +++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm @@ -14,15 +14,17 @@ module Comp.PeriodicQueryTaskList exposing , view2 ) +import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Comp.Basic as B +import Data.ChannelRef import Data.ChannelType import Data.NotificationChannel -import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Html exposing (..) import Html.Attributes exposing (..) import Messages.Comp.PeriodicQueryTaskList exposing (Texts) import Styles as S import Util.Html +import Util.List type alias Model = @@ -94,9 +96,7 @@ viewItem2 texts item = ] ] , td [ class "text-left py-4 md:py-2" ] - [ Data.NotificationChannel.channelType item.channel - |> Maybe.map Data.ChannelType.asString - |> Maybe.withDefault "-" - |> text + [ div [ class " space-x-1" ] + (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels) ] ] diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm index 000bc7db..03647749 100644 --- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm +++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm @@ -15,17 +15,16 @@ module Comp.PeriodicQueryTaskManage exposing import Api import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Comp.ChannelMenu import Comp.MenuBar as MB import Comp.PeriodicQueryTaskForm import Comp.PeriodicQueryTaskList import Data.ChannelType exposing (ChannelType) import Data.Flags exposing (Flags) -import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick) import Http import Messages.Comp.PeriodicQueryTaskManage exposing (Texts) import Styles as S @@ -36,7 +35,6 @@ type alias Model = , detailModel : Maybe Comp.PeriodicQueryTaskForm.Model , items : List PeriodicQuerySettings , formState : FormState - , channelMenuOpen : Bool } @@ -58,9 +56,8 @@ type Msg = ListMsg Comp.PeriodicQueryTaskList.Msg | DetailMsg Comp.PeriodicQueryTaskForm.Msg | GetDataResp (Result Http.Error (List PeriodicQuerySettings)) - | NewTaskInit ChannelType + | NewTaskInit | SubmitResp SubmitType (Result Http.Error BasicResult) - | ToggleChannelMenu initModel : Model @@ -69,7 +66,6 @@ initModel = , detailModel = Nothing , items = [] , formState = FormStateInitial - , channelMenuOpen = False } @@ -195,12 +191,12 @@ update flags msg model = Nothing -> ( model, Cmd.none, Sub.none ) - NewTaskInit ct -> + NewTaskInit -> let ( mm, mc ) = - Comp.PeriodicQueryTaskForm.init flags ct + Comp.PeriodicQueryTaskForm.init flags in - ( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc, Sub.none ) + ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc, Sub.none ) SubmitResp submitType (Ok res) -> ( { model @@ -231,9 +227,6 @@ update flags msg model = , Sub.none ) - ToggleChannelMenu -> - ( { model | channelMenuOpen = not model.channelMenuOpen }, Cmd.none, Sub.none ) - --- View2 @@ -301,18 +294,15 @@ viewForm2 texts settings model = viewList2 : Texts -> Model -> List (Html Msg) viewList2 texts model = - let - menuModel = - { menuOpen = model.channelMenuOpen - , toggleMenu = ToggleChannelMenu - , menuLabel = texts.newTask - , onItem = NewTaskInit - } - in [ MB.view { start = [] , end = - [ Comp.ChannelMenu.channelMenu texts.channelType menuModel + [ MB.PrimaryButton + { tagger = NewTaskInit + , title = texts.newTask + , icon = Just "fa fa-plus" + , label = texts.newTask + } ] , rootClasses = "mb-4" } diff --git a/modules/webapp/src/main/elm/Data/ChannelRef.elm b/modules/webapp/src/main/elm/Data/ChannelRef.elm index 4c69cb74..440e66ff 100644 --- a/modules/webapp/src/main/elm/Data/ChannelRef.elm +++ b/modules/webapp/src/main/elm/Data/ChannelRef.elm @@ -7,27 +7,63 @@ module Data.ChannelRef exposing (..) +import Api.Model.NotificationChannelRef exposing (NotificationChannelRef) import Data.ChannelType exposing (ChannelType) -import Json.Decode as D -import Json.Encode as E +import Html exposing (Attribute, Html, div, span, text) +import Html.Attributes exposing (class) +import Messages.Data.ChannelType as M +import Util.List -type alias ChannelRef = - { id : String - , channelType : ChannelType - } +channelType : NotificationChannelRef -> Maybe ChannelType +channelType ref = + Data.ChannelType.fromString ref.channelType -decoder : D.Decoder ChannelRef -decoder = - D.map2 ChannelRef - (D.field "id" D.string) - (D.field "channelType" Data.ChannelType.decoder) +split : M.Texts -> NotificationChannelRef -> ( String, String ) +split texts ref = + let + chStr = + channelType ref + |> Maybe.map texts + |> Maybe.withDefault ref.channelType + + name = + Maybe.withDefault (String.slice 0 6 ref.id) ref.name + in + ( chStr, name ) -encode : ChannelRef -> E.Value -encode cref = - E.object - [ ( "id", E.string cref.id ) - , ( "channelType", Data.ChannelType.encode cref.channelType ) +asString : M.Texts -> NotificationChannelRef -> String +asString texts ref = + let + ( chStr, name ) = + split texts ref + in + chStr ++ " (" ++ name ++ ")" + + +asDiv : List (Attribute msg) -> M.Texts -> NotificationChannelRef -> Html msg +asDiv attrs texts ref = + let + ( chStr, name ) = + split texts ref + in + div attrs + [ text chStr + , span [ class "ml-1 text-xs opacity-75" ] + [ text ("(" ++ name ++ ")") + ] ] + + +asStringJoined : M.Texts -> List NotificationChannelRef -> String +asStringJoined texts refs = + List.map (asString texts) refs + |> Util.List.distinct + |> String.join ", " + + +asDivs : M.Texts -> List (Attribute msg) -> List NotificationChannelRef -> List (Html msg) +asDivs texts inner refs = + List.map (asDiv inner texts) refs diff --git a/modules/webapp/src/main/elm/Data/NotificationChannel.elm b/modules/webapp/src/main/elm/Data/NotificationChannel.elm index 643cac06..63fe34f5 100644 --- a/modules/webapp/src/main/elm/Data/NotificationChannel.elm +++ b/modules/webapp/src/main/elm/Data/NotificationChannel.elm @@ -12,17 +12,18 @@ module Data.NotificationChannel exposing , decoder , empty , encode + , getRef , setTypeGotify , setTypeHttp , setTypeMail , setTypeMatrix ) +import Api.Model.NotificationChannelRef exposing (NotificationChannelRef) import Api.Model.NotificationGotify exposing (NotificationGotify) import Api.Model.NotificationHttp exposing (NotificationHttp) import Api.Model.NotificationMail exposing (NotificationMail) import Api.Model.NotificationMatrix exposing (NotificationMatrix) -import Data.ChannelRef exposing (ChannelRef) import Data.ChannelType exposing (ChannelType) import Json.Decode as D import Json.Encode as E @@ -33,7 +34,6 @@ type NotificationChannel | Mail NotificationMail | Gotify NotificationGotify | Http NotificationHttp - | Ref ChannelRef empty : ChannelType -> NotificationChannel @@ -87,46 +87,49 @@ decoder = , D.map Mail Api.Model.NotificationMail.decoder , D.map Matrix Api.Model.NotificationMatrix.decoder , D.map Http Api.Model.NotificationHttp.decoder - , D.map Ref Data.ChannelRef.decoder ] +fold : + (NotificationMail -> a) + -> (NotificationGotify -> a) + -> (NotificationMatrix -> a) + -> (NotificationHttp -> a) + -> NotificationChannel + -> a +fold fa fb fc fd channel = + case channel of + Mail ch -> + fa ch + + Gotify ch -> + fb ch + + Matrix ch -> + fc ch + + Http ch -> + fd ch + + encode : NotificationChannel -> E.Value encode channel = - case channel of - Matrix ch -> - Api.Model.NotificationMatrix.encode ch - - Mail ch -> - Api.Model.NotificationMail.encode ch - - Gotify ch -> - Api.Model.NotificationGotify.encode ch - - Http ch -> - Api.Model.NotificationHttp.encode ch - - Ref ch -> - Data.ChannelRef.encode ch + fold + Api.Model.NotificationMail.encode + Api.Model.NotificationGotify.encode + Api.Model.NotificationMatrix.encode + Api.Model.NotificationHttp.encode + channel channelType : NotificationChannel -> Maybe ChannelType channelType ch = - case ch of - Matrix m -> - Data.ChannelType.fromString m.channelType - - Mail m -> - Data.ChannelType.fromString m.channelType - - Gotify m -> - Data.ChannelType.fromString m.channelType - - Http m -> - Data.ChannelType.fromString m.channelType - - Ref m -> - Just m.channelType + fold + (.channelType >> Data.ChannelType.fromString) + (.channelType >> Data.ChannelType.fromString) + (.channelType >> Data.ChannelType.fromString) + (.channelType >> Data.ChannelType.fromString) + ch asString : NotificationChannel -> String @@ -144,5 +147,12 @@ asString channel = Http ch -> "Http @ " ++ ch.url - Ref ch -> - "Ref(" ++ Data.ChannelType.asString ch.channelType ++ "/" ++ ch.id ++ ")" + +getRef : NotificationChannel -> NotificationChannelRef +getRef channel = + fold + (\c -> NotificationChannelRef c.id c.channelType c.name) + (\c -> NotificationChannelRef c.id c.channelType c.name) + (\c -> NotificationChannelRef c.id c.channelType c.name) + (\c -> NotificationChannelRef c.id c.channelType c.name) + channel diff --git a/modules/webapp/src/main/elm/Data/NotificationHook.elm b/modules/webapp/src/main/elm/Data/NotificationHook.elm deleted file mode 100644 index 91c53b60..00000000 --- a/modules/webapp/src/main/elm/Data/NotificationHook.elm +++ /dev/null @@ -1,62 +0,0 @@ -{- - Copyright 2020 Eike K. & Contributors - - SPDX-License-Identifier: AGPL-3.0-or-later --} - - -module Data.NotificationHook exposing (NotificationHook, decoder, empty, encode) - -import Data.ChannelType exposing (ChannelType) -import Data.EventType exposing (EventType) -import Data.NotificationChannel exposing (NotificationChannel) -import Json.Decode as D -import Json.Encode as E - - -type alias NotificationHook = - { id : String - , enabled : Bool - , channel : NotificationChannel - , allEvents : Bool - , eventFilter : Maybe String - , events : List EventType - } - - -empty : ChannelType -> NotificationHook -empty ct = - { id = "" - , enabled = True - , channel = Data.NotificationChannel.empty ct - , allEvents = False - , eventFilter = Nothing - , events = [] - } - - -decoder : D.Decoder NotificationHook -decoder = - D.map6 NotificationHook - (D.field "id" D.string) - (D.field "enabled" D.bool) - (D.field "channel" Data.NotificationChannel.decoder) - (D.field "allEvents" D.bool) - (D.field "eventFilter" (D.maybe D.string)) - (D.field "events" (D.list Data.EventType.decoder)) - - -encode : NotificationHook -> E.Value -encode hook = - E.object - [ ( "id", E.string hook.id ) - , ( "enabled", E.bool hook.enabled ) - , ( "channel", Data.NotificationChannel.encode hook.channel ) - , ( "allEvents", E.bool hook.allEvents ) - , ( "eventFilter", Maybe.map E.string hook.eventFilter |> Maybe.withDefault E.null ) - , ( "events", E.list Data.EventType.encode hook.events ) - ] - - - ---- private diff --git a/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm b/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm deleted file mode 100644 index d1058d18..00000000 --- a/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm +++ /dev/null @@ -1,77 +0,0 @@ -{- - Copyright 2020 Eike K. & Contributors - - SPDX-License-Identifier: AGPL-3.0-or-later --} - - -module Data.PeriodicDueItemsSettings exposing (..) - -import Api.Model.Tag exposing (Tag) -import Data.ChannelType exposing (ChannelType) -import Data.NotificationChannel exposing (NotificationChannel) -import Json.Decode as Decode -import Json.Decode.Pipeline as P -import Json.Encode as Encode - - - -{-- - - Settings for notifying about due items. - --} - - -type alias PeriodicDueItemsSettings = - { id : String - , enabled : Bool - , summary : Maybe String - , channel : NotificationChannel - , schedule : String - , remindDays : Int - , capOverdue : Bool - , tagsInclude : List Tag - , tagsExclude : List Tag - } - - -empty : ChannelType -> PeriodicDueItemsSettings -empty ct = - { id = "" - , enabled = False - , summary = Nothing - , channel = Data.NotificationChannel.empty ct - , schedule = "" - , remindDays = 0 - , capOverdue = False - , tagsInclude = [] - , tagsExclude = [] - } - - -decoder : Decode.Decoder PeriodicDueItemsSettings -decoder = - Decode.succeed PeriodicDueItemsSettings - |> P.required "id" Decode.string - |> P.required "enabled" Decode.bool - |> P.optional "summary" (Decode.maybe Decode.string) Nothing - |> P.required "channel" Data.NotificationChannel.decoder - |> P.required "schedule" Decode.string - |> P.required "remindDays" Decode.int - |> P.required "capOverdue" Decode.bool - |> P.required "tagsInclude" (Decode.list Api.Model.Tag.decoder) - |> P.required "tagsExclude" (Decode.list Api.Model.Tag.decoder) - - -encode : PeriodicDueItemsSettings -> Encode.Value -encode value = - Encode.object - [ ( "id", Encode.string value.id ) - , ( "enabled", Encode.bool value.enabled ) - , ( "summary", (Maybe.map Encode.string >> Maybe.withDefault Encode.null) value.summary ) - , ( "channel", Data.NotificationChannel.encode value.channel ) - , ( "schedule", Encode.string value.schedule ) - , ( "remindDays", Encode.int value.remindDays ) - , ( "capOverdue", Encode.bool value.capOverdue ) - , ( "tagsInclude", Encode.list Api.Model.Tag.encode value.tagsInclude ) - , ( "tagsExclude", Encode.list Api.Model.Tag.encode value.tagsExclude ) - ] diff --git a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm b/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm deleted file mode 100644 index 73603fc1..00000000 --- a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm +++ /dev/null @@ -1,65 +0,0 @@ -{- - Copyright 2020 Eike K. & Contributors - - SPDX-License-Identifier: AGPL-3.0-or-later --} - - -module Data.PeriodicQuerySettings exposing (PeriodicQuerySettings, decoder, empty, encode) - -import Data.ChannelType exposing (ChannelType) -import Data.NotificationChannel exposing (NotificationChannel) -import Json.Decode as D -import Json.Encode as E - - -type alias PeriodicQuerySettings = - { id : String - , enabled : Bool - , summary : Maybe String - , channel : NotificationChannel - , query : Maybe String - , bookmark : Maybe String - , contentStart : Maybe String - , schedule : String - } - - -empty : ChannelType -> PeriodicQuerySettings -empty ct = - { id = "" - , enabled = False - , summary = Nothing - , channel = Data.NotificationChannel.empty ct - , query = Nothing - , bookmark = Nothing - , contentStart = Nothing - , schedule = "" - } - - -decoder : D.Decoder PeriodicQuerySettings -decoder = - D.map8 PeriodicQuerySettings - (D.field "id" D.string) - (D.field "enabled" D.bool) - (D.maybe (D.field "summary" D.string)) - (D.field "channel" Data.NotificationChannel.decoder) - (D.maybe (D.field "query" D.string)) - (D.maybe (D.field "bookmark" D.string)) - (D.maybe (D.field "contentStart" D.string)) - (D.field "schedule" D.string) - - -encode : PeriodicQuerySettings -> E.Value -encode s = - E.object - [ ( "id", E.string s.id ) - , ( "enabled", E.bool s.enabled ) - , ( "summary", Maybe.map E.string s.summary |> Maybe.withDefault E.null ) - , ( "channel", Data.NotificationChannel.encode s.channel ) - , ( "query", Maybe.map E.string s.query |> Maybe.withDefault E.null ) - , ( "bookmark", Maybe.map E.string s.bookmark |> Maybe.withDefault E.null ) - , ( "contentStart", Maybe.map E.string s.contentStart |> Maybe.withDefault E.null ) - , ( "schedule", E.string s.schedule ) - ] diff --git a/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm b/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm new file mode 100644 index 00000000..e5730b19 --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm @@ -0,0 +1,41 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.ChannelRefInput exposing + ( Texts + , de + , gb + ) + +import Messages.Basics +import Messages.Data.ChannelType + + +type alias Texts = + { basics : Messages.Basics.Texts + , channelType : Messages.Data.ChannelType.Texts + , placeholder : String + , noCategory : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , channelType = Messages.Data.ChannelType.gb + , placeholder = "Choose…" + , noCategory = "No channel" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , channelType = Messages.Data.ChannelType.de + , placeholder = "Wähle…" + , noCategory = "Kein Kanal" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm index c19c0152..33bd1533 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm @@ -15,6 +15,7 @@ import Http import Messages.Basics import Messages.Comp.CalEventInput import Messages.Comp.ChannelForm +import Messages.Comp.ChannelRefInput import Messages.Comp.HttpError import Messages.Comp.TagDropdown import Messages.Data.ChannelType @@ -26,6 +27,8 @@ type alias Texts = , httpError : Http.Error -> String , channelForm : Messages.Comp.ChannelForm.Texts , tagDropdown : Messages.Comp.TagDropdown.Texts + , channelType : Messages.Data.ChannelType.Texts + , channelRef : Messages.Comp.ChannelRefInput.Texts , reallyDeleteTask : String , startOnce : String , startTaskNow : String @@ -50,7 +53,7 @@ type alias Texts = , recipientsRequired : String , queryLabel : String , channelRequired : String - , channelHeader : Messages.Data.ChannelType.Texts + , channelHeader : String } @@ -61,6 +64,8 @@ gb = , httpError = Messages.Comp.HttpError.gb , channelForm = Messages.Comp.ChannelForm.gb , tagDropdown = Messages.Comp.TagDropdown.gb + , channelType = Messages.Data.ChannelType.gb + , channelRef = Messages.Comp.ChannelRefInput.gb , reallyDeleteTask = "Really delete this notification task?" , startOnce = "Start Once" , startTaskNow = "Start this task now" @@ -89,7 +94,7 @@ gb = , recipientsRequired = "At least one recipient is required." , queryLabel = "Query" , channelRequired = "A valid channel must be given." - , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct + , channelHeader = "Channels" } @@ -98,8 +103,10 @@ de = { basics = Messages.Basics.de , calEventInput = Messages.Comp.CalEventInput.de , httpError = Messages.Comp.HttpError.de - , channelForm = Messages.Comp.ChannelForm.gb - , tagDropdown = Messages.Comp.TagDropdown.gb + , channelForm = Messages.Comp.ChannelForm.de + , tagDropdown = Messages.Comp.TagDropdown.de + , channelType = Messages.Data.ChannelType.de + , channelRef = Messages.Comp.ChannelRefInput.de , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" , startOnce = "Jetzt starten" , startTaskNow = "Starte den Auftrag sofort" @@ -128,5 +135,5 @@ de = , recipientsRequired = "Mindestens ein Empfänger muss angegeben werden." , queryLabel = "Abfrage" , channelRequired = "Ein Versandkanal muss angegeben werden." - , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct + , channelHeader = "Kanäle" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm index 3efa4228..0da1b2d9 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm @@ -12,32 +12,33 @@ module Messages.Comp.DueItemsTaskList exposing ) import Messages.Basics +import Messages.Data.ChannelType type alias Texts = { basics : Messages.Basics.Texts + , channelType : Messages.Data.ChannelType.Texts , summary : String , schedule : String , connection : String - , recipients : String } gb : Texts gb = { basics = Messages.Basics.gb + , channelType = Messages.Data.ChannelType.gb , summary = "Summary" , schedule = "Schedule" - , connection = "Connection" - , recipients = "Recipients" + , connection = "Channel" } de : Texts de = { basics = Messages.Basics.de + , channelType = Messages.Data.ChannelType.de , summary = "Kurzbeschreibung" , schedule = "Zeitplan" - , connection = "Verbindung" - , recipients = "Empfänger" + , connection = "Kanal" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm new file mode 100644 index 00000000..fec7ee3a --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm @@ -0,0 +1,83 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.NotificationChannelManage exposing (Texts, de, gb) + +import Http +import Messages.Basics +import Messages.Comp.ChannelForm +import Messages.Comp.HttpError +import Messages.Comp.NotificationChannelTable +import Messages.Data.ChannelType + + +type alias Texts = + { basics : Messages.Basics.Texts + , notificationForm : Messages.Comp.ChannelForm.Texts + , notificationTable : Messages.Comp.NotificationChannelTable.Texts + , httpError : Http.Error -> String + , channelType : Messages.Data.ChannelType.Texts + , newChannel : String + , channelCreated : String + , channelUpdated : String + , channelDeleted : String + , formInvalid : String + , integrate : String + , intoDocspell : String + , postRequestInfo : String + , notifyEmailInfo : String + , addChannel : String + , updateChannel : String + , deleteThisChannel : String + , reallyDeleteChannel : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , notificationForm = Messages.Comp.ChannelForm.gb + , notificationTable = Messages.Comp.NotificationChannelTable.gb + , httpError = Messages.Comp.HttpError.gb + , channelType = Messages.Data.ChannelType.gb + , newChannel = "New Channel" + , channelCreated = "Channel created" + , channelUpdated = "Channel updated" + , channelDeleted = "Channel deleted" + , formInvalid = "Please fill in all required fields" + , integrate = "Integrate" + , intoDocspell = "into Docspell" + , postRequestInfo = "Docspell will send POST requests with JSON payload." + , notifyEmailInfo = "Get notified via e-mail." + , addChannel = "Add new channel" + , updateChannel = "Update channel" + , deleteThisChannel = "Kanal löschen" + , reallyDeleteChannel = "Really delete this channel?" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , notificationForm = Messages.Comp.ChannelForm.de + , notificationTable = Messages.Comp.NotificationChannelTable.de + , httpError = Messages.Comp.HttpError.de + , channelType = Messages.Data.ChannelType.de + , newChannel = "Neuer Kanal" + , channelCreated = "Kanal wurde angelegt." + , channelUpdated = "Kanal wurde aktualisiert." + , channelDeleted = "Kanal wurde entfernt." + , formInvalid = "Bitte alle erforderlichen Felder ausfüllen" + , integrate = "Integriere" + , intoDocspell = "in Docspell" + , postRequestInfo = "Docspell wird JSON POST requests senden." + , notifyEmailInfo = "Werde per E-Mail benachrichtigt." + , addChannel = "Neuen Kanal hinzufügen" + , updateChannel = "Kanal aktualisieren" + , deleteThisChannel = "Kanal löschen" + , reallyDeleteChannel = "Den Kanal wirklich löschen?" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm new file mode 100644 index 00000000..b511163a --- /dev/null +++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm @@ -0,0 +1,35 @@ +{- + Copyright 2020 Eike K. & Contributors + + SPDX-License-Identifier: AGPL-3.0-or-later +-} + + +module Messages.Comp.NotificationChannelTable exposing (..) + +import Data.EventType exposing (EventType) +import Messages.Basics +import Messages.Data.EventType + + +type alias Texts = + { basics : Messages.Basics.Texts + , eventType : EventType -> Messages.Data.EventType.Texts + , channelType : String + } + + +gb : Texts +gb = + { basics = Messages.Basics.gb + , eventType = Messages.Data.EventType.gb + , channelType = "Channel type" + } + + +de : Texts +de = + { basics = Messages.Basics.de + , eventType = Messages.Data.EventType.de + , channelType = "Kanaltyp" + } diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm index e2b30382..21189adc 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm @@ -13,18 +13,17 @@ module Messages.Comp.NotificationHookForm exposing import Data.EventType exposing (EventType) import Messages.Basics -import Messages.Comp.ChannelForm +import Messages.Comp.ChannelRefInput import Messages.Comp.EventSample -import Messages.Data.ChannelType import Messages.Data.EventType type alias Texts = { basics : Messages.Basics.Texts - , channelForm : Messages.Comp.ChannelForm.Texts + , channelRef : Messages.Comp.ChannelRefInput.Texts , eventType : EventType -> Messages.Data.EventType.Texts , eventSample : Messages.Comp.EventSample.Texts - , channelHeader : Messages.Data.ChannelType.Texts + , channelHeader : String , enableDisable : String , eventsInfo : String , selectEvents : String @@ -34,16 +33,19 @@ type alias Texts = , eventFilter : String , eventFilterInfo : String , eventFilterClickForHelp : String + , jsonPayload : String + , messagePayload : String + , payloadInfo : String } gb : Texts gb = { basics = Messages.Basics.gb - , channelForm = Messages.Comp.ChannelForm.gb + , channelRef = Messages.Comp.ChannelRefInput.gb , eventType = Messages.Data.EventType.gb , eventSample = Messages.Comp.EventSample.gb - , channelHeader = Messages.Data.ChannelType.gb + , channelHeader = "Select channels" , enableDisable = "Enabled / Disabled" , eventsInfo = "Select events that trigger this webhook" , selectEvents = "Select…" @@ -53,16 +55,19 @@ gb = , eventFilter = "Event Filter Expression" , eventFilterInfo = "Optional specify an expression to filter events based on their JSON structure." , eventFilterClickForHelp = "Click here for help" + , jsonPayload = "JSON" + , messagePayload = "Message" + , payloadInfo = "Message payloads are sent to gotify, email and matrix. The JSON is sent to http channel." } de : Texts de = { basics = Messages.Basics.de - , channelForm = Messages.Comp.ChannelForm.de + , channelRef = Messages.Comp.ChannelRefInput.de , eventType = Messages.Data.EventType.de , eventSample = Messages.Comp.EventSample.de - , channelHeader = Messages.Data.ChannelType.de + , channelHeader = "Kanäle" , enableDisable = "Aktiviert / Deaktivert" , eventsInfo = "Wähle die Ereignisse, die diesen webhook auslösen" , selectEvents = "Wähle…" @@ -72,4 +77,7 @@ de = , eventFilter = "Ereignisfilter" , eventFilterInfo = "Optionaler Ausdruck zum filtern von Ereignissen auf Basis ihrer JSON Struktur." , eventFilterClickForHelp = "Klicke für Hilfe" + , jsonPayload = "JSON" + , messagePayload = "Nachricht" + , payloadInfo = "Es werden abhängig vom Kanal JSON oder Nachricht-Formate versendet. Der HTTP Kanal empfängt nur JSON, an die anderen wird das Nachrichtformat gesendet." } diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm index d215ff32..6b4c7f2f 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm @@ -11,7 +11,6 @@ module Messages.Comp.NotificationHookManage exposing , gb ) -import Html exposing (Html, text) import Http import Messages.Basics import Messages.Comp.HttpError @@ -27,9 +26,6 @@ type alias Texts = , httpError : Http.Error -> String , channelType : Messages.Data.ChannelType.Texts , newHook : String - , matrix : String - , gotify : String - , email : String , httpRequest : String , hookCreated : String , hookUpdated : String @@ -39,12 +35,8 @@ type alias Texts = , reallyDeleteHook : String , formInvalid : String , invalidJsonFilter : String -> String - , integrate : String - , intoDocspell : String - , postRequestInfo : String , updateWebhook : String , addWebhook : String - , notifyEmailInfo : String } @@ -56,9 +48,6 @@ gb = , httpError = Messages.Comp.HttpError.gb , channelType = Messages.Data.ChannelType.gb , newHook = "New Webhook" - , matrix = "Matrix" - , gotify = "Gotify" - , email = "E-Mail" , httpRequest = "HTTP Request" , hookCreated = "Webhook created" , hookUpdated = "Webhook updated" @@ -68,12 +57,8 @@ gb = , reallyDeleteHook = "Really delete this webhook?" , formInvalid = "Please fill in all required fields" , invalidJsonFilter = \m -> "Event filter invalid: " ++ m - , integrate = "Integrate" - , intoDocspell = "into Docspell" - , postRequestInfo = "Docspell will send POST requests with JSON payload." , updateWebhook = "Update webhook" , addWebhook = "Add new webhook" - , notifyEmailInfo = "Get notified via e-mail." } @@ -85,9 +70,6 @@ de = , httpError = Messages.Comp.HttpError.de , channelType = Messages.Data.ChannelType.de , newHook = "Neuer Webhook" - , matrix = "Matrix" - , gotify = "Gotify" - , email = "E-Mail" , httpRequest = "HTTP Request" , hookCreated = "Webhook erstellt" , hookUpdated = "Webhook aktualisiert" @@ -97,10 +79,6 @@ de = , reallyDeleteHook = "Den webhook wirklich löschen?" , formInvalid = "Bitte alle erforderlichen Felder ausfüllen" , invalidJsonFilter = \m -> "Ereignisfilter ist falsch: " ++ m - , integrate = "Integriere" - , intoDocspell = "in Docspell" - , postRequestInfo = "Docspell wird JSON POST requests senden." , updateWebhook = "Webhook aktualisieren" , addWebhook = "Neuen Webhook hinzufügen" - , notifyEmailInfo = "Werde per E-Mail benachrichtigt." } diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm index ddf8c8ff..411a646b 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm @@ -13,15 +13,18 @@ module Messages.Comp.NotificationHookTable exposing import Data.EventType exposing (EventType) import Messages.Basics +import Messages.Data.ChannelType import Messages.Data.EventType type alias Texts = { basics : Messages.Basics.Texts , eventType : EventType -> Messages.Data.EventType.Texts + , channelType : Messages.Data.ChannelType.Texts , enabled : String , channel : String , events : String + , allEvents : String } @@ -29,9 +32,11 @@ gb : Texts gb = { basics = Messages.Basics.gb , eventType = Messages.Data.EventType.gb + , channelType = Messages.Data.ChannelType.gb , enabled = "Enabled" , channel = "Channel" , events = "Events" + , allEvents = "All" } @@ -39,7 +44,9 @@ de : Texts de = { basics = Messages.Basics.de , eventType = Messages.Data.EventType.de + , channelType = Messages.Data.ChannelType.de , enabled = "Aktiv" , channel = "Kanal" , events = "Ereignisse" + , allEvents = "Alle" } diff --git a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm index 49e717a7..7f9afbf1 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm @@ -11,14 +11,13 @@ module Messages.Comp.PeriodicQueryTaskForm exposing , gb ) -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.ChannelRefInput import Messages.Comp.HttpError -import Messages.Data.ChannelType type alias Texts = @@ -26,6 +25,7 @@ type alias Texts = , calEventInput : Messages.Comp.CalEventInput.Texts , channelForm : Messages.Comp.ChannelForm.Texts , bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts + , channelRef : Messages.Comp.ChannelRefInput.Texts , httpError : Http.Error -> String , reallyDeleteTask : String , startOnce : String @@ -41,7 +41,7 @@ type alias Texts = , invalidCalEvent : String , channelRequired : String , queryStringRequired : String - , channelHeader : ChannelType -> String + , channelHeader : String , messageContentTitle : String , messageContentLabel : String , messageContentInfo : String @@ -56,6 +56,7 @@ gb = , channelForm = Messages.Comp.ChannelForm.gb , httpError = Messages.Comp.HttpError.gb , bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb + , channelRef = Messages.Comp.ChannelRefInput.gb , reallyDeleteTask = "Really delete this notification task?" , startOnce = "Start Once" , startTaskNow = "Start this task now" @@ -74,7 +75,7 @@ gb = , queryLabel = "Query" , channelRequired = "A valid channel must be given." , queryStringRequired = "A query string and/or bookmark must be supplied" - , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct + , channelHeader = "Channels" , messageContentTitle = "Customize message" , messageContentLabel = "Beginning of message" , messageContentInfo = "Insert text that is prependend to the generated message." @@ -89,6 +90,7 @@ de = , channelForm = Messages.Comp.ChannelForm.de , httpError = Messages.Comp.HttpError.de , bookmarkDropdown = Messages.Comp.BookmarkDropdown.de + , channelRef = Messages.Comp.ChannelRefInput.de , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" , startOnce = "Jetzt starten" , startTaskNow = "Starte den Auftrag sofort" @@ -107,7 +109,7 @@ de = , queryLabel = "Abfrage" , channelRequired = "Ein Versandkanal muss angegeben werden." , queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark muss angegeben werden." - , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct + , channelHeader = "Kanäle" , messageContentTitle = "Nachricht anpassen" , messageContentLabel = "Anfang der Nachricht" , messageContentInfo = "Dieser Text wird an den Anfang der generierten Nachricht angefügt." diff --git a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm index 0920b6f5..57f8c287 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm @@ -12,32 +12,33 @@ module Messages.Comp.PeriodicQueryTaskList exposing ) import Messages.Basics +import Messages.Data.ChannelType type alias Texts = { basics : Messages.Basics.Texts + , channelType : Messages.Data.ChannelType.Texts , summary : String , schedule : String , connection : String - , recipients : String } gb : Texts gb = { basics = Messages.Basics.gb + , channelType = Messages.Data.ChannelType.gb , summary = "Summary" , schedule = "Schedule" - , connection = "Connection" - , recipients = "Recipients" + , connection = "Channel" } de : Texts de = { basics = Messages.Basics.de + , channelType = Messages.Data.ChannelType.de , summary = "Kurzbeschreibung" , schedule = "Zeitplan" - , connection = "Verbindung" - , recipients = "Empfänger" + , connection = "Kanal" } diff --git a/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm b/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm index 4522fa27..2003e6e1 100644 --- a/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm +++ b/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm @@ -27,7 +27,7 @@ gb ct = "E-Mail" Data.ChannelType.Http -> - "HTTP request" + "JSON" de : Texts @@ -43,4 +43,4 @@ de ct = "E-Mail" Data.ChannelType.Http -> - "HTTP Request" + "JSON" diff --git a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm index a1bb4e39..dcef8b6c 100644 --- a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm +++ b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm @@ -15,6 +15,7 @@ import Messages.Comp.ChangePasswordForm import Messages.Comp.DueItemsTaskManage import Messages.Comp.EmailSettingsManage import Messages.Comp.ImapSettingsManage +import Messages.Comp.NotificationChannelManage import Messages.Comp.NotificationHookManage import Messages.Comp.OtpSetup import Messages.Comp.PeriodicQueryTaskManage @@ -31,6 +32,7 @@ type alias Texts = , scanMailboxManage : Messages.Comp.ScanMailboxManage.Texts , notificationHookManage : Messages.Comp.NotificationHookManage.Texts , periodicQueryTask : Messages.Comp.PeriodicQueryTaskManage.Texts + , channelManage : Messages.Comp.NotificationChannelManage.Texts , otpSetup : Messages.Comp.OtpSetup.Texts , userSettings : String , uiSettings : String @@ -38,6 +40,7 @@ type alias Texts = , scanMailbox : String , emailSettingSmtp : String , emailSettingImap : String + , channelSettings : String , changePassword : String , uiSettingsInfo : String , scanMailboxInfo1 : String @@ -50,6 +53,8 @@ type alias Texts = , webhookInfoText : String , dueItemsInfoText : String , periodicQueryInfoText : String + , channels : String + , channelInfoText : String } @@ -63,6 +68,7 @@ gb = , scanMailboxManage = Messages.Comp.ScanMailboxManage.gb , notificationHookManage = Messages.Comp.NotificationHookManage.gb , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.gb + , channelManage = Messages.Comp.NotificationChannelManage.gb , otpSetup = Messages.Comp.OtpSetup.gb , userSettings = "User Settings" , uiSettings = "UI Settings" @@ -71,6 +77,7 @@ gb = , emailSettingSmtp = "E-Mail Settings (SMTP)" , emailSettingImap = "E-Mail Settings (IMAP)" , changePassword = "Change Password" + , channelSettings = "Notification Channels" , uiSettingsInfo = "These settings only affect the web ui. They are stored in the browser, " ++ "so they are separated between browsers and devices." @@ -103,14 +110,16 @@ its payload. Additionally, you can setup queries that are executed periodically. The results are send as a notification message. -When creating a new notification task, choose first the communication -channel. +A notification setting needs at least one communication channel, which +must be created before. """ , webhookInfoText = """Webhooks execute http request upon certain events in docspell. """ , dueItemsInfoText = """Docspell can notify you once the due dates of your items come closer. """ , periodicQueryInfoText = "You can define a custom query that gets executed periodically." + , channels = "Notification Channels" + , channelInfoText = "Channels are used to send notification messages." } @@ -124,6 +133,7 @@ de = , scanMailboxManage = Messages.Comp.ScanMailboxManage.de , notificationHookManage = Messages.Comp.NotificationHookManage.de , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.de + , channelManage = Messages.Comp.NotificationChannelManage.de , otpSetup = Messages.Comp.OtpSetup.de , userSettings = "Benutzereinstellung" , uiSettings = "Oberfläche" @@ -131,6 +141,7 @@ de = , scanMailbox = "E-Mail-Import" , emailSettingSmtp = "E-Mail-Einstellungen (SMTP)" , emailSettingImap = "E-Mail-Einstellungen (IMAP)" + , channelSettings = "Benachrichtigungskanäle" , changePassword = "Passwort ändern" , uiSettingsInfo = "Diese Einstellungen sind für die Web-Oberfläche." @@ -161,15 +172,16 @@ Es kann aus diesen Versandkanälen gewählt werden: E-Mail. Zusätzlich kann das HTTP request direkt empfangen werden, was alle Details zu einem Ereignis enthält. - Ausserdem können periodische Suchabfragen erstellt werden, dessen Ergebnis dann als Benachrichtigung versendet wird. -Beim Erstellen eines neuen Auftrags muss zunächst der gewünschte -Versandkanal gewählt werden. +Für eine Notifikation ist ein Kommunikationskanal notwendig, der zuvor +erstellt werden muss. """ , webhookInfoText = """Webhooks versenden HTTP Requests wenn bestimmte Ereignisse in Docspell auftreten.""" , dueItemsInfoText = """Docspell kann dich benachrichtigen, sobald das Fälligkeitsdatum von Dokumenten näher kommt. """ , periodicQueryInfoText = "Hier können beliebige Abfragen definiert werden, welche regelmäßig ausgeführt werden." + , channels = "Benachrichtigungskanäle" + , channelInfoText = "Über Kanäle werden Notifizierungen versendet." } diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm index 58b761e0..2c71dda6 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm @@ -16,6 +16,7 @@ import Comp.ChangePasswordForm import Comp.DueItemsTaskManage import Comp.EmailSettingsManage import Comp.ImapSettingsManage +import Comp.NotificationChannelManage import Comp.NotificationHookManage import Comp.OtpSetup import Comp.PeriodicQueryTaskManage @@ -35,6 +36,7 @@ type alias Model = , uiSettingsModel : Comp.UiSettingsManage.Model , otpSetupModel : Comp.OtpSetup.Model , notificationHookModel : Comp.NotificationHookManage.Model + , channelModel : Comp.NotificationChannelManage.Model , periodicQueryModel : Comp.PeriodicQueryTaskManage.Model } @@ -53,6 +55,9 @@ init flags settings = ( pqm, pqc ) = Comp.PeriodicQueryTaskManage.init flags + + ( ncm, ncc ) = + Comp.NotificationChannelManage.init flags in ( { currentTab = Just UiSettingsTab , changePassModel = Comp.ChangePasswordForm.emptyModel @@ -64,12 +69,14 @@ init flags settings = , otpSetupModel = otpm , notificationHookModel = nhm , periodicQueryModel = pqm + , channelModel = ncm } , Cmd.batch [ Cmd.map UiSettingsMsg uc , Cmd.map OtpSetupMsg otpc , Cmd.map NotificationHookMsg nhc , Cmd.map PeriodicQueryMsg pqc + , Cmd.map ChannelMsg ncc ] ) @@ -85,6 +92,7 @@ type Tab | ScanMailboxTab | UiSettingsTab | OtpTab + | ChannelTab type Msg @@ -98,5 +106,6 @@ type Msg | OtpSetupMsg Comp.OtpSetup.Msg | NotificationHookMsg Comp.NotificationHookManage.Msg | PeriodicQueryMsg Comp.PeriodicQueryTaskManage.Msg + | ChannelMsg Comp.NotificationChannelManage.Msg | UpdateSettings | ReceiveBrowserSettings StoredUiSettings diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm index 263288a2..656674ad 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm @@ -11,6 +11,7 @@ import Comp.ChangePasswordForm import Comp.DueItemsTaskManage import Comp.EmailSettingsManage import Comp.ImapSettingsManage +import Comp.NotificationChannelManage import Comp.NotificationHookManage import Comp.OtpSetup import Comp.PeriodicQueryTaskManage @@ -71,8 +72,12 @@ update flags settings msg model = } NotificationWebhookTab -> + let + ( _, nc ) = + Comp.NotificationHookManage.init flags + in { model = m - , cmd = Cmd.none + , cmd = Cmd.map NotificationHookMsg nc , sub = Sub.none , newSettings = Nothing } @@ -107,6 +112,9 @@ update flags settings msg model = OtpTab -> UpdateResult m Cmd.none Sub.none Nothing + ChannelTab -> + UpdateResult m Cmd.none Sub.none Nothing + ChangePassMsg m -> let ( m2, c2 ) = @@ -195,6 +203,17 @@ update flags settings msg model = , newSettings = Nothing } + ChannelMsg lm -> + let + ( cm, cc ) = + Comp.NotificationChannelManage.update flags lm model.channelModel + in + { model = { model | channelModel = cm } + , cmd = Cmd.map ChannelMsg cc + , sub = Sub.none + , newSettings = Nothing + } + UpdateSettings -> update flags settings diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm index b466f038..50193b85 100644 --- a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm +++ b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm @@ -11,6 +11,7 @@ import Comp.ChangePasswordForm import Comp.DueItemsTaskManage import Comp.EmailSettingsManage import Comp.ImapSettingsManage +import Comp.NotificationChannelManage import Comp.NotificationHookManage import Comp.OtpSetup import Comp.PeriodicQueryTaskManage @@ -77,7 +78,7 @@ viewSidebar texts visible _ _ model = , menuEntryActive model NotificationTab , class S.sidebarLink ] - [ i [ class "fa fa-bullhorn" ] [] + [ i [ class "fa fa-comment font-thin" ] [] , span [ class "ml-3" ] [ text texts.notifications ] @@ -121,6 +122,17 @@ viewSidebar texts visible _ _ model = ] ] ] + , a + [ href "#" + , onClick (SetTab ChannelTab) + , menuEntryActive model ChannelTab + , class S.sidebarLink + ] + [ i [ class "fa fa-bullhorn" ] [] + , span + [ class "ml-3" ] + [ text texts.channelSettings ] + ] , a [ href "#" , onClick (SetTab ScanMailboxTab) @@ -217,6 +229,9 @@ viewContent texts flags settings model = Just OtpTab -> viewOtpSetup texts settings model + Just ChannelTab -> + viewChannels texts settings model + Nothing -> [] ) @@ -235,6 +250,26 @@ menuEntryActive model tab = class "" +viewChannels : Texts -> UiSettings -> Model -> List (Html Msg) +viewChannels texts settings model = + [ h2 + [ class S.header1 + , class "inline-flex items-center" + ] + [ i [ class "fa fa-bell" ] [] + , div [ class "ml-3" ] + [ text texts.channels + ] + ] + , Markdown.toHtml [ class "opacity-80 text-lg mb-3 markdown-preview" ] texts.channelInfoText + , Html.map ChannelMsg + (Comp.NotificationChannelManage.view texts.channelManage + settings + model.channelModel + ) + ] + + viewOtpSetup : Texts -> UiSettings -> Model -> List (Html Msg) viewOtpSetup texts _ model = [ h2