Merge pull request #1301 from eikek/notification-improvements

Notification improvements
This commit is contained in:
mergify[bot]
2022-01-19 21:11:36 +00:00
committed by GitHub
82 changed files with 2702 additions and 1441 deletions

View File

@ -11,7 +11,6 @@ import cats.implicits._
import docspell.backend.MailAddressCodec import docspell.backend.MailAddressCodec
import docspell.common._ import docspell.common._
import docspell.notification.api.ChannelOrRef._
import docspell.notification.api.PeriodicQueryArgs import docspell.notification.api.PeriodicQueryArgs
import docspell.store.records.RJob import docspell.store.records.RJob
@ -25,7 +24,7 @@ object JobFactory extends MailAddressCodec {
PeriodicQueryArgs.taskName, PeriodicQueryArgs.taskName,
submitter.collective, submitter.collective,
args, args,
s"Running periodic query, notify via ${args.channel.channelType}", s"Running periodic query, notify via ${args.channels.map(_.channelType)}",
now, now,
submitter.user, submitter.user,
Priority.Low, Priority.Low,

View File

@ -39,7 +39,10 @@ trait ONotification[F[_]] {
userId: Ident userId: Ident
): F[Vector[NotificationChannel]] ): 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]] def listChannels(account: AccountId): F[Vector[Channel]]
@ -65,7 +68,7 @@ trait ONotification[F[_]] {
def sendSampleEvent( def sendSampleEvent(
evt: EventType, evt: EventType,
channel: Channel, channel: Nel[ChannelRef],
account: AccountId, account: AccountId,
baseUrl: Option[LenientUri] baseUrl: Option[LenientUri]
): F[ONotification.SendTestResult] ): F[ONotification.SendTestResult]
@ -89,7 +92,7 @@ object ONotification {
.getOrElse(UpdateResult.notFound) .getOrElse(UpdateResult.notFound)
def offerEvents(ev: Iterable[Event]): F[Unit] = def offerEvents(ev: Iterable[Event]): F[Unit] =
ev.toList.traverse(notMod.offer(_)).as(()) ev.toList.traverse(notMod.offer).as(())
def sendMessage( def sendMessage(
logger: Logger[F], logger: Logger[F],
@ -109,33 +112,27 @@ object ONotification {
def sendSampleEvent( def sendSampleEvent(
evt: EventType, evt: EventType,
channel: Channel, channels: Nel[ChannelRef],
account: AccountId, account: AccountId,
baseUrl: Option[LenientUri] baseUrl: Option[LenientUri]
): F[SendTestResult] = { ): F[SendTestResult] =
def doCreate(userId: Ident) = (for {
(for { ev <- sampleEvent(evt, account, baseUrl)
ev <- sampleEvent(evt, account, baseUrl) logbuf <- Logger.buffer()
logbuf <- Logger.buffer() ch <- channels.toList.toVector.flatTraverse(
ch <- mkNotificationChannel(channel, userId) 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"))
}
OptionT(store.transact(RUser.findIdByAccount(account)))
.semiflatMap(doCreate)
.getOrElse(
SendTestResult(false, Vector(s"No user found in db for: ${account.asString}"))
) )
} _ <- 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]] = def listChannels(account: AccountId): F[Vector[Channel]] =
store store
@ -153,7 +150,7 @@ object ONotification {
(for { (for {
newId <- OptionT.liftF(Ident.randomId[F]) newId <- OptionT.liftF(Ident.randomId[F])
userId <- OptionT(store.transact(RUser.findIdByAccount(account))) 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(store.transact(RNotificationChannel.insert(r)))
_ <- OptionT.liftF(log.debug(s"Created channel $r for $account")) _ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
} yield AddResult.Success) } yield AddResult.Success)
@ -162,7 +159,7 @@ object ONotification {
def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] = def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] =
(for { (for {
userId <- OptionT(store.transact(RUser.findIdByAccount(account))) 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))) n <- OptionT.liftF(store.transact(RNotificationChannel.update(r)))
} yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound) } yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound)
@ -179,16 +176,14 @@ object ONotification {
def createHook(hook: Hook, account: AccountId): F[AddResult] = def createHook(hook: Hook, account: AccountId): F[AddResult] =
(for { (for {
_ <- OptionT.liftF(log.debug(s"Creating new notification hook: $hook")) _ <- OptionT.liftF(log.debug(s"Creating new notification hook: $hook"))
channelId <- OptionT.liftF(Ident.randomId[F])
userId <- OptionT(store.transact(RUser.findIdByAccount(account))) 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( _ <- OptionT.liftF(
if (channelId == r.id) store.transact(RNotificationChannel.insert(r)) store.transact(
else ().pure[F] 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( _ <- OptionT.liftF(
store.transact(RNotificationHookEvent.insertAll(hr.id, hook.events)) store.transact(RNotificationHookEvent.insertAll(hr.id, hook.events))
) )
@ -203,31 +198,25 @@ object ONotification {
.getOrElse(UpdateResult.notFound) .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] = def doUpdate(r: RNotificationHook): F[UpdateResult] =
withChannel(r) { ch => UpdateResult.fromUpdate(store.transact(for {
UpdateResult.fromUpdate(store.transact(for { ne <- RNotificationHookEvent.updateAll(
nc <- RNotificationChannel.update(ch) r.id,
ne <- RNotificationHookEvent.updateAll( if (hook.allEvents) Nil else hook.events
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, } yield nc + ne + nr))
allEvents = hook.allEvents,
eventFilter = hook.eventFilter
)
)
} yield nc + ne + nr))
}
withHook(doUpdate) withHook(doUpdate)
} }
@ -238,13 +227,17 @@ object ONotification {
): F[Vector[NotificationChannel]] = ): F[Vector[NotificationChannel]] =
(for { (for {
rec <- ChannelConv rec <- ChannelConv
.makeRecord(store, log, Right(channel), channel.id, userId) .makeRecord(store, channel, channel.id, userId)
ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec))) ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
} yield ch).getOrElse(Vector.empty) } yield ch).getOrElse(Vector.empty)
def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] = def findNotificationChannel(
ref: ChannelRef,
accountId: AccountId
): F[Vector[NotificationChannel]] =
(for { (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))) ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
} yield ch).getOrElse(Vector.empty) } yield ch).getOrElse(Vector.empty)
}) })
@ -254,75 +247,40 @@ object ONotification {
private[ops] def makeChannel(r: RNotificationChannel): Channel = private[ops] def makeChannel(r: RNotificationChannel): Channel =
r.fold( r.fold(
mail => mail =>
Channel.Mail(mail.id, mail.connection, Nel.fromListUnsafe(mail.recipients)), Channel.Mail(
gotify => Channel.Gotify(r.id, gotify.url, gotify.appKey, gotify.priority), mail.id,
mail.name,
mail.connection,
Nel.fromListUnsafe(mail.recipients)
),
gotify =>
Channel.Gotify(r.id, gotify.name, gotify.url, gotify.appKey, gotify.priority),
matrix => matrix =>
Channel.Matrix(r.id, matrix.homeServer, matrix.roomId, matrix.accessToken), Channel
http => Channel.Http(r.id, http.url) .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], store: Store[F],
logger: Logger[F], channel: Channel,
channelIn: Either[ChannelRef, Channel],
id: Ident, id: Ident,
userId: Ident userId: Ident
): OptionT[F, RNotificationChannel] = ): OptionT[F, RNotificationChannel] =
channelIn match { RNotificationChannel.fromChannel(channel, id, userId).mapK(store.transform)
case Left(ref) =>
OptionT.liftF(logger.debug(s"Loading channel for ref: ${ref}")) *>
OptionT(store.transact(RNotificationChannel.getByRef(ref)))
case Right(channel) =>
for {
time <- OptionT.liftF(Timestamp.current[F])
r <-
channel match {
case Channel.Mail(_, 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,
mailConn.id,
recipients.toList,
time
).vary
} yield rec
case Channel.Gotify(_, url, appKey, prio) =>
OptionT.pure[F](
RNotificationChannelGotify(id, userId, url, appKey, prio, time).vary
)
case Channel.Matrix(_, homeServer, roomId, accessToken) =>
OptionT.pure[F](
RNotificationChannelMatrix(
id,
userId,
homeServer,
roomId,
accessToken,
"m.text",
time
).vary
)
case Channel.Http(_, url) =>
OptionT.pure[F](RNotificationChannelHttp(id, userId, url, time).vary)
}
} yield r
}
} }
final case class Hook( final case class Hook(
id: Ident, id: Ident,
enabled: Boolean, enabled: Boolean,
channel: Either[ChannelRef, Channel], channels: List[ChannelRef],
allEvents: Boolean, allEvents: Boolean,
eventFilter: Option[JsonMiniQuery], eventFilter: Option[JsonMiniQuery],
events: List[EventType] events: List[EventType]
@ -335,14 +293,12 @@ object ONotification {
r: RNotificationHook, r: RNotificationHook,
events: List[EventType] events: List[EventType]
): ConnectionIO[Hook] = ): ConnectionIO[Hook] =
RNotificationChannel RNotificationHookChannel
.getByHook(r) .allOfNel(r.id)
.map(_.head) .flatMap(rhcs => RNotificationHookChannel.resolveRefs(rhcs))
.map(ChannelConv.makeChannel) .map(refs => Hook(r.id, r.enabled, refs, r.allEvents, r.eventFilter, events))
.map(ch => Hook(r.id, r.enabled, Right(ch), r.allEvents, r.eventFilter, events))
private[ops] def makeRecord[F[_]: Sync]( private[ops] def makeRecord[F[_]: Sync](
ch: RNotificationChannel,
userId: Ident, userId: Ident,
hook: Hook hook: Hook
): F[RNotificationHook] = ): F[RNotificationHook] =
@ -353,10 +309,6 @@ object ONotification {
id, id,
userId, userId,
hook.enabled, 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.allEvents,
hook.eventFilter, hook.eventFilter,
time time

View File

@ -11,7 +11,6 @@ import cats.effect._
import cats.implicits._ import cats.implicits._
import fs2.Stream import fs2.Stream
import docspell.backend.MailAddressCodec._
import docspell.common._ import docspell.common._
import docspell.notification.api.PeriodicDueItemsArgs import docspell.notification.api.PeriodicDueItemsArgs
import docspell.notification.api.PeriodicQueryArgs import docspell.notification.api.PeriodicQueryArgs

View File

@ -7,7 +7,6 @@
package docspell.joex.notify package docspell.joex.notify
import cats.data.NonEmptyList import cats.data.NonEmptyList
import cats.data.OptionT
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
@ -24,7 +23,6 @@ import docspell.query.ItemQueryDsl._
import docspell.store.qb.Batch import docspell.store.qb.Batch
import docspell.store.queries.ListItem import docspell.store.queries.ListItem
import docspell.store.queries.{QItem, Query} import docspell.store.queries.{QItem, Query}
import docspell.store.records.RUser
object PeriodicDueItemsTask { object PeriodicDueItemsTask {
val taskName = PeriodicDueItemsArgs.taskName val taskName = PeriodicDueItemsArgs.taskName
@ -51,11 +49,7 @@ object PeriodicDueItemsTask {
def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])( def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])(
cont: Vector[NotificationChannel] => F[Unit] cont: Vector[NotificationChannel] => F[Unit]
): F[Unit] = ): F[Unit] =
OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account))) TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont)
.semiflatMap(userId =>
TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont)
)
.getOrElse(())
def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)( def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)(
cont: Vector[ListItem] => F[Unit] cont: Vector[ListItem] => F[Unit]

View File

@ -26,7 +26,6 @@ import docspell.store.queries.ListItem
import docspell.store.queries.{QItem, Query} import docspell.store.queries.{QItem, Query}
import docspell.store.records.RQueryBookmark import docspell.store.records.RQueryBookmark
import docspell.store.records.RShare import docspell.store.records.RShare
import docspell.store.records.RUser
object PeriodicQueryTask { object PeriodicQueryTask {
val taskName = PeriodicQueryArgs.taskName val taskName = PeriodicQueryArgs.taskName
@ -53,11 +52,7 @@ object PeriodicQueryTask {
def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])( def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])(
cont: Vector[NotificationChannel] => F[Unit] cont: Vector[NotificationChannel] => F[Unit]
): F[Unit] = ): F[Unit] =
OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account))) TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont)
.semiflatMap(userId =>
TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont)
)
.getOrElse(())
private def queryString(q: ItemQuery.Expr) = private def queryString(q: ItemQuery.Expr) =
ItemQueryParser.asString(q) ItemQueryParser.asString(q)

View File

@ -12,7 +12,7 @@ import cats.implicits._
import docspell.backend.ops.ONotification import docspell.backend.ops.ONotification
import docspell.common._ import docspell.common._
import docspell.notification.api.ChannelOrRef import docspell.notification.api.ChannelRef
import docspell.notification.api.Event import docspell.notification.api.Event
import docspell.notification.api.EventContext import docspell.notification.api.EventContext
import docspell.notification.api.NotificationChannel import docspell.notification.api.NotificationChannel
@ -23,19 +23,18 @@ trait TaskOperations {
def withChannel[F[_]: Sync]( def withChannel[F[_]: Sync](
logger: Logger[F], logger: Logger[F],
channel: ChannelOrRef, channelsIn: NonEmptyList[ChannelRef],
userId: Ident, accountId: AccountId,
ops: ONotification[F] ops: ONotification[F]
)( )(
cont: Vector[NotificationChannel] => F[Unit] cont: Vector[NotificationChannel] => F[Unit]
): F[Unit] = { ): F[Unit] = {
val channels = channel match { val channels =
case Right(ch) => ops.mkNotificationChannel(ch, userId) channelsIn.toList.toVector.flatTraverse(ops.findNotificationChannel(_, accountId))
case Left(ref) => ops.findNotificationChannel(ref)
}
channels.flatMap { ch => channels.flatMap { ch =>
if (ch.isEmpty) 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) else cont(ch)
} }
} }

View File

@ -19,13 +19,14 @@ import io.circe.{Decoder, Encoder}
sealed trait Channel { sealed trait Channel {
def id: Ident def id: Ident
def channelType: ChannelType def channelType: ChannelType
def name: Option[String]
def fold[A]( def fold[A](
f1: Channel.Mail => A, f1: Channel.Mail => A,
f2: Channel.Gotify => A, f2: Channel.Gotify => A,
f3: Channel.Matrix => A, f3: Channel.Matrix => A,
f4: Channel.Http => A f4: Channel.Http => A
): A ): A
def asRef: ChannelRef = ChannelRef(id, channelType) def asRef: ChannelRef = ChannelRef(id, channelType, name)
} }
object Channel { object Channel {
@ -33,6 +34,7 @@ object Channel {
final case class Mail( final case class Mail(
id: Ident, id: Ident,
name: Option[String],
connection: Ident, connection: Ident,
recipients: Nel[MailAddress] recipients: Nel[MailAddress]
) extends Channel { ) extends Channel {
@ -55,6 +57,7 @@ object Channel {
final case class Gotify( final case class Gotify(
id: Ident, id: Ident,
name: Option[String],
url: LenientUri, url: LenientUri,
appKey: Password, appKey: Password,
priority: Option[Int] priority: Option[Int]
@ -77,6 +80,7 @@ object Channel {
final case class Matrix( final case class Matrix(
id: Ident, id: Ident,
name: Option[String],
homeServer: LenientUri, homeServer: LenientUri,
roomId: String, roomId: String,
accessToken: Password accessToken: Password
@ -95,7 +99,8 @@ object Channel {
implicit val jsonEncoder: Encoder[Matrix] = deriveConfiguredEncoder implicit val jsonEncoder: Encoder[Matrix] = deriveConfiguredEncoder
} }
final case class Http(id: Ident, url: LenientUri) extends Channel { final case class Http(id: Ident, name: Option[String], url: LenientUri)
extends Channel {
val channelType = ChannelType.Http val channelType = ChannelType.Http
def fold[A]( def fold[A](
f1: Mail => A, f1: Mail => A,

View File

@ -12,7 +12,7 @@ import io.circe.Decoder
import io.circe.Encoder import io.circe.Encoder
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} 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 { object ChannelRef {

View File

@ -6,9 +6,10 @@
package docspell.notification.api package docspell.notification.api
import cats.data.NonEmptyList
import docspell.common._ import docspell.common._
import emil.MailAddress
import io.circe.generic.semiauto import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder}
@ -21,7 +22,7 @@ import io.circe.{Decoder, Encoder}
*/ */
final case class PeriodicDueItemsArgs( final case class PeriodicDueItemsArgs(
account: AccountId, account: AccountId,
channel: ChannelOrRef, channels: NonEmptyList[ChannelRef],
remindDays: Int, remindDays: Int,
daysBack: Option[Int], daysBack: Option[Int],
tagsInclude: List[Ident], tagsInclude: List[Ident],
@ -30,19 +31,11 @@ final case class PeriodicDueItemsArgs(
) )
object PeriodicDueItemsArgs { object PeriodicDueItemsArgs {
val taskName = Ident.unsafe("periodic-due-items-notify") val taskName = Ident.unsafe("periodic-due-items-notify2")
implicit def jsonDecoder(implicit implicit val jsonDecoder: Decoder[PeriodicDueItemsArgs] =
mc: Decoder[MailAddress]
): Decoder[PeriodicDueItemsArgs] = {
implicit val x = ChannelOrRef.jsonDecoder
semiauto.deriveDecoder semiauto.deriveDecoder
}
implicit def jsonEncoder(implicit implicit val jsonEncoder: Encoder[PeriodicDueItemsArgs] =
mc: Encoder[MailAddress]
): Encoder[PeriodicDueItemsArgs] = {
implicit val x = ChannelOrRef.jsonEncoder
semiauto.deriveEncoder semiauto.deriveEncoder
}
} }

View File

@ -6,15 +6,16 @@
package docspell.notification.api package docspell.notification.api
import cats.data.NonEmptyList
import docspell.common._ import docspell.common._
import emil.MailAddress
import io.circe.generic.semiauto import io.circe.generic.semiauto
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder}
final case class PeriodicQueryArgs( final case class PeriodicQueryArgs(
account: AccountId, account: AccountId,
channel: ChannelOrRef, channels: NonEmptyList[ChannelRef],
query: Option[ItemQueryString], query: Option[ItemQueryString],
bookmark: Option[String], bookmark: Option[String],
baseUrl: Option[LenientUri], baseUrl: Option[LenientUri],
@ -22,19 +23,11 @@ final case class PeriodicQueryArgs(
) )
object PeriodicQueryArgs { object PeriodicQueryArgs {
val taskName = Ident.unsafe("periodic-query-notify") val taskName = Ident.unsafe("periodic-query-notify2")
implicit def jsonDecoder(implicit implicit val jsonDecoder: Decoder[PeriodicQueryArgs] =
mc: Decoder[MailAddress]
): Decoder[PeriodicQueryArgs] = {
implicit val x = ChannelOrRef.jsonDecoder
semiauto.deriveDecoder semiauto.deriveDecoder
}
implicit def jsonEncoder(implicit implicit def jsonEncoder: Encoder[PeriodicQueryArgs] =
mc: Encoder[MailAddress]
): Encoder[PeriodicQueryArgs] = {
implicit val x = ChannelOrRef.jsonEncoder
semiauto.deriveEncoder semiauto.deriveEncoder
}
} }

View File

@ -1739,7 +1739,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/extraSchemas/NotificationHook" $ref: "#/components/schemas/NotificationHook"
post: post:
operationId: "sec-notification-hook-post" operationId: "sec-notification-hook-post"
tags: [ Notification ] tags: [ Notification ]
@ -1753,7 +1753,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/NotificationHook" $ref: "#/components/schemas/NotificationHook"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -1775,7 +1775,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/NotificationHook" $ref: "#/components/schemas/NotificationHook"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -1821,7 +1821,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/NotificationHook" $ref: "#/components/schemas/NotificationHook"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -4917,7 +4917,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/extraSchemas/PeriodicDueItemsSettings" $ref: "#/components/schemas/PeriodicDueItemsSettings"
post: post:
operationId: "sec-usertask-notify-new" operationId: "sec-usertask-notify-new"
tags: [ User Tasks ] tags: [ User Tasks ]
@ -4931,7 +4931,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicDueItemsSettings" $ref: "#/components/schemas/PeriodicDueItemsSettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -4954,7 +4954,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicDueItemsSettings" $ref: "#/components/schemas/PeriodicDueItemsSettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -4984,7 +4984,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicDueItemsSettings" $ref: "#/components/schemas/PeriodicDueItemsSettings"
delete: delete:
operationId: "sec-usertask-notify-delete" operationId: "sec-usertask-notify-delete"
tags: [ User Tasks ] tags: [ User Tasks ]
@ -5018,7 +5018,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicDueItemsSettings" $ref: "#/components/schemas/PeriodicDueItemsSettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -5048,7 +5048,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: "#/extraSchemas/PeriodicQuerySettings" $ref: "#/components/schemas/PeriodicQuerySettings"
post: post:
operationId: "sec-usertask-periodic-query-new" operationId: "sec-usertask-periodic-query-new"
tags: [ User Tasks ] tags: [ User Tasks ]
@ -5062,7 +5062,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicQuerySettings" $ref: "#/components/schemas/PeriodicQuerySettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -5085,7 +5085,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicQuerySettings" $ref: "#/components/schemas/PeriodicQuerySettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -5115,7 +5115,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicQuerySettings" $ref: "#/components/schemas/PeriodicQuerySettings"
delete: delete:
operationId: "sec-usertask-periodic-query-delete" operationId: "sec-usertask-periodic-query-delete"
tags: [ User Tasks ] tags: [ User Tasks ]
@ -5149,7 +5149,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: "#/extraSchemas/PeriodicQuerySettings" $ref: "#/components/schemas/PeriodicQuerySettings"
responses: responses:
422: 422:
description: BadRequest description: BadRequest
@ -5467,7 +5467,10 @@ components:
type: string type: string
NotificationChannelRef: NotificationChannelRef:
description: | 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: required:
- id - id
- channelType - channelType
@ -5478,6 +5481,8 @@ components:
channelType: channelType:
type: string type: string
format: channeltype format: channeltype
name:
type: string
NotificationMatrix: NotificationMatrix:
description: | description: |
A notification channel for matrix. A notification channel for matrix.
@ -5491,6 +5496,8 @@ components:
id: id:
type: string type: string
format: ident format: ident
name:
type: string
channelType: channelType:
type: string type: string
format: channeltype format: channeltype
@ -5514,6 +5521,8 @@ components:
id: id:
type: string type: string
format: ident format: ident
name:
type: string
channelType: channelType:
type: string type: string
format: channeltype format: channeltype
@ -5539,6 +5548,8 @@ components:
id: id:
type: string type: string
format: ident format: ident
name:
type: string
channelType: channelType:
type: string type: string
format: channeltype format: channeltype
@ -5557,6 +5568,8 @@ components:
id: id:
type: string type: string
format: ident format: ident
name:
type: string
channelType: channelType:
type: string type: string
format: channeltype format: channeltype
@ -5568,6 +5581,136 @@ components:
items: items:
type: string 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: ShareSecret:
description: | description: |
The secret (the share id + optional password) to access a The secret (the share id + optional password) to access a
@ -8001,137 +8144,3 @@ components:
schema: schema:
type: string type: string
format: ident 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"

View File

@ -73,15 +73,24 @@ object NotificationChannel {
.map(NonEmptyList.fromList) .map(NonEmptyList.fromList)
.flatMap(_.toRight("No recipients given!")) .flatMap(_.toRight("No recipients given!"))
.leftMap(new IllegalArgumentException(_)) .leftMap(new IllegalArgumentException(_))
.map(rec => Channel.Mail(mail.id, mail.connection, rec)), .map(rec => Channel.Mail(mail.id, mail.name, mail.connection, rec)),
gotify => gotify =>
Right(Channel.Gotify(gotify.id, gotify.url, gotify.appKey, gotify.priority)), Right(
Channel
.Gotify(gotify.id, gotify.name, gotify.url, gotify.appKey, gotify.priority)
),
matrix => matrix =>
Right( Right(
Channel Channel
.Matrix(matrix.id, matrix.homeServer, matrix.roomId, matrix.accessToken) .Matrix(
matrix.id,
matrix.name,
matrix.homeServer,
matrix.roomId,
matrix.accessToken
)
), ),
http => Right(Channel.Http(http.id, http.url)) http => Right(Channel.Http(http.id, http.name, http.url))
) )
def convert(c: Channel): NotificationChannel = def convert(c: Channel): NotificationChannel =
@ -90,24 +99,35 @@ object NotificationChannel {
mail { mail {
NotificationMail( NotificationMail(
m.id, m.id,
m.name,
ChannelType.Mail, ChannelType.Mail,
m.connection, m.connection,
m.recipients.toList.map(_.displayString) m.recipients.toList.map(_.displayString)
) )
}, },
g => g =>
gotify(NotificationGotify(g.id, ChannelType.Gotify, g.url, g.appKey, g.priority)), gotify(
NotificationGotify(
g.id,
g.name,
ChannelType.Gotify,
g.url,
g.appKey,
g.priority
)
),
m => m =>
matrix( matrix(
NotificationMatrix( NotificationMatrix(
m.id, m.id,
m.name,
ChannelType.Matrix, ChannelType.Matrix,
m.homeServer, m.homeServer,
m.roomId, m.roomId,
m.accessToken m.accessToken
) )
), ),
h => http(NotificationHttp(h.id, ChannelType.Http, h.url)) h => http(NotificationHttp(h.id, h.name, ChannelType.Http, h.url))
) )
implicit val jsonDecoder: Decoder[NotificationChannel] = implicit val jsonDecoder: Decoder[NotificationChannel] =

View File

@ -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
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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)
)
)
)
)
}
}

View File

@ -6,6 +6,7 @@
package docspell.restserver.routes package docspell.restserver.routes
import cats.data.NonEmptyList
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
@ -14,10 +15,10 @@ import docspell.backend.auth.AuthToken
import docspell.common._ import docspell.common._
import docspell.joexapi.model.BasicResult import docspell.joexapi.model.BasicResult
import docspell.jsonminiq.JsonMiniQuery import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.EventType import docspell.notification.api.{ChannelRef, EventType}
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.Config import docspell.restserver.Config
import docspell.restserver.conv.Conversions import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
import docspell.restserver.http4s.ClientRequestInfo import docspell.restserver.http4s.ClientRequestInfo
import org.http4s._ import org.http4s._
@ -26,7 +27,7 @@ import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
import org.http4s.server.Router import org.http4s.server.Router
object NotificationRoutes { object NotificationRoutes extends NonEmptyListSupport {
def apply[F[_]: Async]( def apply[F[_]: Async](
cfg: Config, cfg: Config,
@ -126,17 +127,11 @@ object NotificationRoutes {
case req @ POST -> Root / "sendTestEvent" => case req @ POST -> Root / "sendTestEvent" =>
for { for {
input <- req.as[NotificationHook] input <- req.as[NotificationHook]
ch <- Sync[F] ch <- requireNonEmpty(input.channels)
.pure(
input.channel.left
.map(_ => new Exception(s"ChannelRefs not allowed for testing"))
.flatMap(NotificationChannel.convert)
)
.rethrow
baseUrl = ClientRequestInfo.getBaseUrl(cfg, req) baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
res <- backend.notification.sendSampleEvent( res <- backend.notification.sendSampleEvent(
input.events.headOption.getOrElse(EventType.all.head), input.events.headOption.getOrElse(EventType.all.head),
ch, ch.map(r => ChannelRef(r.id, r.channelType, r.name)),
user.account, user.account,
baseUrl.some baseUrl.some
) )
@ -179,33 +174,26 @@ object NotificationRoutes {
NotificationHook( NotificationHook(
h.id, h.id,
h.enabled, h.enabled,
h.channel.map(NotificationChannel.convert), h.channels.map(c => NotificationChannelRef(c.id, c.channelType, c.name)).toList,
h.allEvents, h.allEvents,
h.eventFilter, h.eventFilter,
h.events h.events
) )
def convertHook(h: NotificationHook): Either[Throwable, ONotification.Hook] = def convertHook(h: NotificationHook): Either[Throwable, ONotification.Hook] =
h.channel match { NonEmptyList
case Left(cref) => .fromList(h.channels)
Right( .toRight(new IllegalArgumentException(s"Empty channels not allowed!"))
ONotification.Hook( .map(_ =>
h.id, ONotification.Hook(
h.enabled, h.id,
Left(cref), h.enabled,
h.allEvents, h.channels.map(c => ChannelRef(c.id, c.channelType, c.name)),
h.eventFilter, h.allEvents,
h.events 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)
)
}
} }
} }

View File

@ -14,10 +14,10 @@ import docspell.backend.BackendApp
import docspell.backend.MailAddressCodec import docspell.backend.MailAddressCodec
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.common._ import docspell.common._
import docspell.notification.api.PeriodicDueItemsArgs import docspell.notification.api.{ChannelRef, PeriodicDueItemsArgs}
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.Config import docspell.restserver.Config
import docspell.restserver.conv.Conversions import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
import docspell.restserver.http4s.ClientRequestInfo import docspell.restserver.http4s.ClientRequestInfo
import docspell.store.usertask._ import docspell.store.usertask._
@ -26,7 +26,7 @@ import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._ import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
object NotifyDueItemsRoutes extends MailAddressCodec { object NotifyDueItemsRoutes extends MailAddressCodec with NonEmptyListSupport {
def apply[F[_]: Async]( def apply[F[_]: Async](
cfg: Config, cfg: Config,
@ -113,7 +113,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
user: AccountId, user: AccountId,
settings: PeriodicDueItemsSettings settings: PeriodicDueItemsSettings
): F[UserTask[PeriodicDueItemsArgs]] = ): F[UserTask[PeriodicDueItemsArgs]] =
Sync[F].pure(NotificationChannel.convert(settings.channel)).rethrow.map { channel => requireNonEmpty(settings.channels).map { ch =>
UserTask( UserTask(
id, id,
PeriodicDueItemsArgs.taskName, PeriodicDueItemsArgs.taskName,
@ -122,7 +122,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
settings.summary, settings.summary,
PeriodicDueItemsArgs( PeriodicDueItemsArgs(
user, user,
Right(channel), ch.map(c => ChannelRef(c.id, c.channelType, c.name)),
settings.remindDays, settings.remindDays,
if (settings.capOverdue) Some(settings.remindDays) if (settings.capOverdue) Some(settings.remindDays)
else None, else None,
@ -140,20 +140,13 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
for { for {
tinc <- backend.tag.loadAll(task.args.tagsInclude) tinc <- backend.tag.loadAll(task.args.tagsInclude)
texc <- backend.tag.loadAll(task.args.tagsExclude) 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( } yield PeriodicDueItemsSettings(
task.id, task.id,
task.enabled, task.enabled,
task.summary, task.summary,
ch, task.args.channels
.map(c => NotificationChannelRef(c.id, c.channelType, c.name))
.toList,
task.timer, task.timer,
task.args.remindDays, task.args.remindDays,
task.args.daysBack.isDefined, task.args.daysBack.isDefined,

View File

@ -6,7 +6,7 @@
package docspell.restserver.routes package docspell.restserver.routes
import cats.data.OptionT import cats.data.{NonEmptyList, OptionT}
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
@ -14,11 +14,11 @@ import docspell.backend.BackendApp
import docspell.backend.MailAddressCodec import docspell.backend.MailAddressCodec
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.common._ import docspell.common._
import docspell.notification.api.PeriodicQueryArgs import docspell.notification.api.{ChannelRef, PeriodicQueryArgs}
import docspell.query.ItemQueryParser import docspell.query.ItemQueryParser
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.Config import docspell.restserver.Config
import docspell.restserver.conv.Conversions import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
import docspell.restserver.http4s.ClientRequestInfo import docspell.restserver.http4s.ClientRequestInfo
import docspell.store.usertask._ import docspell.store.usertask._
@ -27,7 +27,7 @@ import org.http4s.circe.CirceEntityDecoder._
import org.http4s.circe.CirceEntityEncoder._ import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
object PeriodicQueryRoutes extends MailAddressCodec { object PeriodicQueryRoutes extends MailAddressCodec with NonEmptyListSupport {
def apply[F[_]: Async]( def apply[F[_]: Async](
cfg: Config, cfg: Config,
@ -116,7 +116,9 @@ object PeriodicQueryRoutes extends MailAddressCodec {
): F[UserTask[PeriodicQueryArgs]] = ): F[UserTask[PeriodicQueryArgs]] =
Sync[F] Sync[F]
.pure(for { .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 { qstr <- settings.query match {
case Some(q) => case Some(q) =>
ItemQueryParser ItemQueryParser
@ -132,7 +134,7 @@ object PeriodicQueryRoutes extends MailAddressCodec {
else Left(new IllegalArgumentException("No query or bookmark provided")) else Left(new IllegalArgumentException("No query or bookmark provided"))
} yield (ch, qstr.map(ItemQueryString.apply))) } yield (ch, qstr.map(ItemQueryString.apply)))
.rethrow .rethrow
.map { case (channel, qstr) => .map { case (channels, qstr) =>
UserTask( UserTask(
id, id,
PeriodicQueryArgs.taskName, PeriodicQueryArgs.taskName,
@ -141,7 +143,7 @@ object PeriodicQueryRoutes extends MailAddressCodec {
settings.summary, settings.summary,
PeriodicQueryArgs( PeriodicQueryArgs(
user, user,
Right(channel), channels.map(r => ChannelRef(r.id, r.channelType, r.name)),
qstr, qstr,
settings.bookmark, settings.bookmark,
Some(baseUrl / "app" / "item"), Some(baseUrl / "app" / "item"),
@ -153,22 +155,18 @@ object PeriodicQueryRoutes extends MailAddressCodec {
def taskToSettings[F[_]: Sync]( def taskToSettings[F[_]: Sync](
task: UserTask[PeriodicQueryArgs] task: UserTask[PeriodicQueryArgs]
): F[PeriodicQuerySettings] = ): F[PeriodicQuerySettings] =
for { Sync[F].pure(
ch <- task.args.channel match { PeriodicQuerySettings(
case Right(c) => NotificationChannel.convert(c).pure[F] task.id,
case Left(ref) => task.enabled,
Sync[F].raiseError( task.summary,
new IllegalStateException(s"ChannelRefs are not supported: $ref") task.args.channels
) .map(c => NotificationChannelRef(c.id, c.channelType, c.name))
} .toList,
} yield PeriodicQuerySettings( task.timer,
task.id, task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe),
task.summary, task.args.bookmark,
task.enabled, task.args.contentStart
ch, )
task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe),
task.args.bookmark,
task.args.contentStart,
task.timer
) )
} }

View File

@ -0,0 +1,11 @@
alter table "notification_channel_mail"
add column "name" varchar(254);
alter table "notification_channel_gotify"
add column "name" varchar(254);
alter table "notification_channel_matrix"
add column "name" varchar(254);
alter table "notification_channel_http"
add column "name" varchar(254);

View File

@ -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";

View File

@ -0,0 +1,11 @@
alter table `notification_channel_mail`
add column `name` varchar(254);
alter table `notification_channel_gotify`
add column `name` varchar(254);
alter table `notification_channel_matrix`
add column `name` varchar(254);
alter table `notification_channel_http`
add column `name` varchar(254);

View File

@ -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`;

View File

@ -0,0 +1,11 @@
alter table "notification_channel_mail"
add column "name" varchar(254);
alter table "notification_channel_gotify"
add column "name" varchar(254);
alter table "notification_channel_matrix"
add column "name" varchar(254);
alter table "notification_channel_http"
add column "name" varchar(254);

View File

@ -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";

View File

@ -6,23 +6,23 @@
package db.migration package db.migration
import cats.data.NonEmptyList import cats.data.{NonEmptyList, OptionT}
import cats.effect.{IO, Sync} import cats.effect.{IO, Sync}
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.common.syntax.StringSyntax._ import docspell.common.syntax.StringSyntax._
import docspell.notification.api.Channel import docspell.notification.api._
import docspell.notification.api.PeriodicDueItemsArgs import docspell.store.records._
import docspell.store.records.RPeriodicTask
import db.migration.data.{PeriodicDueItemsArgsOld, PeriodicQueryArgsOld}
import doobie._ import doobie._
import doobie.implicits._ import doobie.implicits._
import doobie.util.transactor.Strategy import doobie.util.transactor.Strategy
import emil.MailAddress import emil.MailAddress
import emil.javamail.syntax._ import emil.javamail.syntax._
import io.circe.Encoder
import io.circe.syntax._ import io.circe.syntax._
import io.circe.{Decoder, Encoder}
import org.flywaydb.core.api.migration.Context import org.flywaydb.core.api.migration.Context
trait MigrationTasks { trait MigrationTasks {
@ -31,6 +31,8 @@ trait MigrationTasks {
implicit val jsonEncoder: Encoder[MailAddress] = implicit val jsonEncoder: Encoder[MailAddress] =
Encoder.encodeString.contramap(_.asUnicodeString) Encoder.encodeString.contramap(_.asUnicodeString)
implicit val jsonDecoder: Decoder[MailAddress] =
Decoder.decodeString.emap(MailAddress.parse)
def migrateDueItemTasks: ConnectionIO[Unit] = def migrateDueItemTasks: ConnectionIO[Unit] =
for { for {
@ -42,20 +44,114 @@ trait MigrationTasks {
_ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false) _ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false)
} yield () } yield ()
def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = { def migratePeriodicItemTasks: ConnectionIO[Unit] =
val converted = old.args for {
.parseJsonAs[NotifyDueItemsArgs] tasks2 <- RPeriodicTask.findByTask(PeriodicDueItemsArgsOld.taskName)
.leftMap(_.getMessage()) tasks3 <- RPeriodicTask.findByTask(PeriodicQueryArgsOld.taskName)
.flatMap(convertArgs) 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 { private def migratePeriodicQueryTask(old: RPeriodicTask): ConnectionIO[Int] =
case Right(args) => old.args
Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *> .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( RPeriodicTask.updateTask(
old.id, old.id,
PeriodicDueItemsArgs.taskName, 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) => case Left(err) =>
logger.error(s"Error converting user task: $old. $err") logger.error(s"Error converting user task: $old. $err")
@ -63,22 +159,44 @@ trait MigrationTasks {
} }
} }
def convertArgs(old: NotifyDueItemsArgs): Either[String, PeriodicDueItemsArgs] = private def convertArgs(
old.recipients old: NotifyDueItemsArgs
.traverse(MailAddress.parse) ): OptionT[ConnectionIO, PeriodicDueItemsArgs] = {
.flatMap(l => NonEmptyList.fromList(l).toRight("No recipients provided")) val recs = old.recipients
.map { rec => .map(MailAddress.parse)
PeriodicDueItemsArgs( .flatMap {
old.account, case Right(m) => Some(m)
Right(Channel.Mail(Ident.unsafe(""), old.smtpConnection, rec)), case Left(err) =>
old.remindDays, logger.warn(s"Cannot read mail address: $err. Skip this while migrating.")
old.daysBack, None
old.tagsInclude,
old.tagsExclude,
old.itemDetailUrl
)
} }
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] = { def mkTransactor(ctx: Context): Transactor[IO] = {
val xa = Transactor.fromConnection[IO](ctx.getConnection()) val xa = Transactor.fromConnection[IO](ctx.getConnection())
Transactor.strategy.set(xa, Strategy.void) // transactions are handled by flyway Transactor.strategy.set(xa, Strategy.void) // transactions are handled by flyway

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -4,13 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
package docspell.notification package db.migration
import docspell.notification.api._
import emil.MailAddress import emil.MailAddress
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder}
package object api { package object data {
type ChannelOrRef = Either[ChannelRef, Channel] type ChannelOrRef = Either[ChannelRef, Channel]
object ChannelOrRef { object ChannelOrRef {
@ -25,5 +26,4 @@ package object api {
cr.fold(_.channelType, _.channelType) cr.fold(_.channelType, _.channelType)
} }
} }
} }

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -12,7 +12,7 @@ import java.time.{Instant, LocalDate}
import docspell.common._ import docspell.common._
import docspell.common.syntax.all._ import docspell.common.syntax.all._
import docspell.jsonminiq.JsonMiniQuery import docspell.jsonminiq.JsonMiniQuery
import docspell.notification.api.EventType import docspell.notification.api.{ChannelType, EventType}
import docspell.query.{ItemQuery, ItemQueryParser} import docspell.query.{ItemQuery, ItemQueryParser}
import docspell.totp.Key import docspell.totp.Key
@ -156,6 +156,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] = implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] =
Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString) Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString)
implicit val channelTypeRead: Read[ChannelType] =
Read[String].map(ChannelType.unsafeFromString)
} }
object DoobieMeta extends DoobieMeta { object DoobieMeta extends DoobieMeta {

View File

@ -27,7 +27,11 @@ object QNotification {
def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] = def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] =
for { for {
hooks <- listHooks(event.account.collective, event.eventType) 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 } yield chs
// -- // --
@ -50,21 +54,27 @@ object QNotification {
) )
).query[RNotificationHook].to[Vector] ).query[RNotificationHook].to[Vector]
def listChannels(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] =
RNotificationHookChannel.allOf(hookId)
def readHookChannel( def readHookChannel(
hook: RNotificationHook userId: Ident,
): ConnectionIO[HookChannel] = hook: RNotificationHookChannel
): ConnectionIO[Vector[NotificationChannel]] =
for { for {
c1 <- read(hook.channelMail)(RNotificationChannelMail.getById)( c1 <- read(hook.channelMail)(RNotificationChannelMail.getById(userId))(
ChannelMap.readMail ChannelMap.readMail
) )
c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById)( c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById(userId))(
ChannelMap.readGotify ChannelMap.readGotify
) )
c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById)( c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById(userId))(
ChannelMap.readMatrix ChannelMap.readMatrix
) )
c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById)(ChannelMap.readHttp) c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById(userId))(
} yield HookChannel(hook, c1 ++ c2 ++ c3 ++ c4) ChannelMap.readHttp
)
} yield c1 ++ c2 ++ c3 ++ c4
def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] = def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] =
ch.fold( ch.fold(

View File

@ -7,16 +7,29 @@
package docspell.store.records package docspell.store.records
import cats.data.OptionT import cats.data.OptionT
import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.notification.api.ChannelRef import docspell.notification.api.{Channel, ChannelRef, ChannelType}
import docspell.notification.api.ChannelType
import doobie._ import doobie._
sealed trait RNotificationChannel { sealed trait RNotificationChannel {
def id: Ident def id: Ident = fold(_.id, _.id, _.id, _.id)
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]( def fold[A](
f1: RNotificationChannelMail => A, f1: RNotificationChannelMail => A,
@ -24,7 +37,6 @@ sealed trait RNotificationChannel {
f3: RNotificationChannelMatrix => A, f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A f4: RNotificationChannelHttp => A
): A ): A
} }
object RNotificationChannel { object RNotificationChannel {
@ -37,8 +49,6 @@ object RNotificationChannel {
f3: RNotificationChannelMatrix => A, f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A f4: RNotificationChannelHttp => A
): A = f1(r) ): A = f1(r)
val id = r.id
} }
final case class Gotify(r: RNotificationChannelGotify) extends RNotificationChannel { final case class Gotify(r: RNotificationChannelGotify) extends RNotificationChannel {
@ -48,8 +58,6 @@ object RNotificationChannel {
f3: RNotificationChannelMatrix => A, f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A f4: RNotificationChannelHttp => A
): A = f2(r) ): A = f2(r)
val id = r.id
} }
final case class Matrix(r: RNotificationChannelMatrix) extends RNotificationChannel { final case class Matrix(r: RNotificationChannelMatrix) extends RNotificationChannel {
@ -59,8 +67,6 @@ object RNotificationChannel {
f3: RNotificationChannelMatrix => A, f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A f4: RNotificationChannelHttp => A
): A = f3(r) ): A = f3(r)
val id = r.id
} }
final case class Http(r: RNotificationChannelHttp) extends RNotificationChannel { final case class Http(r: RNotificationChannelHttp) extends RNotificationChannel {
@ -70,8 +76,6 @@ object RNotificationChannel {
f3: RNotificationChannelMatrix => A, f3: RNotificationChannelMatrix => A,
f4: RNotificationChannelHttp => A f4: RNotificationChannelHttp => A
): A = f4(r) ): A = f4(r)
val id = r.id
} }
def insert(r: RNotificationChannel): ConnectionIO[Int] = def insert(r: RNotificationChannel): ConnectionIO[Int] =
@ -100,42 +104,60 @@ object RNotificationChannel {
Matrix.apply Matrix.apply
) ++ http.map(Http.apply) ) ++ http.map(Http.apply)
def getById(id: Ident): ConnectionIO[Vector[RNotificationChannel]] = def getById(id: Ident, userId: Ident): ConnectionIO[Vector[RNotificationChannel]] =
for { for {
mail <- RNotificationChannelMail.getById(id) mail <- RNotificationChannelMail.getById(userId)(id)
gotify <- RNotificationChannelGotify.getById(id) gotify <- RNotificationChannelGotify.getById(userId)(id)
matrix <- RNotificationChannelMatrix.getById(id) matrix <- RNotificationChannelMatrix.getById(userId)(id)
http <- RNotificationChannelHttp.getById(id) http <- RNotificationChannelHttp.getById(userId)(id)
} yield mail.map(Email.apply).toVector ++ } yield mail.map(Email.apply).toVector ++
gotify.map(Gotify.apply).toVector ++ gotify.map(Gotify.apply).toVector ++
matrix.map(Matrix.apply).toVector ++ matrix.map(Matrix.apply).toVector ++
http.map(Http.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 { ref.channelType match {
case ChannelType.Mail => case ChannelType.Mail =>
RNotificationChannelMail.getById(ref.id).map(_.map(Email.apply)) RNotificationChannelMail.getById(userId)(ref.id).map(_.map(Email.apply))
case ChannelType.Matrix => case ChannelType.Matrix =>
RNotificationChannelMatrix.getById(ref.id).map(_.map(Matrix.apply)) RNotificationChannelMatrix.getById(userId)(ref.id).map(_.map(Matrix.apply))
case ChannelType.Gotify => case ChannelType.Gotify =>
RNotificationChannelGotify.getById(ref.id).map(_.map(Gotify.apply)) RNotificationChannelGotify.getById(userId)(ref.id).map(_.map(Gotify.apply))
case ChannelType.Http => 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] = def opt(id: Option[Ident]): OptionT[ConnectionIO, Ident] =
OptionT.fromOption(id) OptionT.fromOption(id)
for { def find(
mail <- opt(r.channelMail).flatMapF(RNotificationChannelMail.getById).value r: RNotificationHookChannel
gotify <- opt(r.channelGotify).flatMapF(RNotificationChannelGotify.getById).value ): ConnectionIO[Vector[RNotificationChannel]] =
matrix <- opt(r.channelMatrix).flatMapF(RNotificationChannelMatrix.getById).value for {
http <- opt(r.channelHttp).flatMapF(RNotificationChannelHttp.getById).value mail <- opt(r.channelMail)
} yield mail.map(Email.apply).toVector ++ .flatMapF(RNotificationChannelMail.getById(hook.uid))
gotify.map(Gotify.apply).toVector ++ .value
matrix.map(Matrix.apply).toVector ++ gotify <- opt(r.channelGotify)
http.map(Http.apply).toVector .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] = def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] =
@ -145,4 +167,63 @@ object RNotificationChannel {
n3 <- RNotificationChannelMatrix.deleteByAccount(id, account) n3 <- RNotificationChannelMatrix.deleteByAccount(id, account)
n4 <- RNotificationChannelHttp.deleteByAccount(id, account) n4 <- RNotificationChannelHttp.deleteByAccount(id, account)
} yield n1 + n2 + n3 + n4 } 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
} }

View File

@ -18,6 +18,7 @@ import doobie.implicits._
final case class RNotificationChannelGotify( final case class RNotificationChannelGotify(
id: Ident, id: Ident,
uid: Ident, uid: Ident,
name: Option[String],
url: LenientUri, url: LenientUri,
appKey: Password, appKey: Password,
priority: Option[Int], priority: Option[Int],
@ -34,27 +35,32 @@ object RNotificationChannelGotify {
val id = Column[Ident]("id", this) val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this) val uid = Column[Ident]("uid", this)
val name = Column[String]("name", this)
val url = Column[LenientUri]("url", this) val url = Column[LenientUri]("url", this)
val appKey = Column[Password]("app_key", this) val appKey = Column[Password]("app_key", this)
val priority = Column[Int]("priority", this) val priority = Column[Int]("priority", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] = val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, url, appKey, priority, created) NonEmptyList.of(id, uid, name, url, appKey, priority, created)
} }
val T: Table = Table(None) val T: Table = Table(None)
def as(alias: String): Table = def as(alias: String): Table =
Table(Some(alias)) Table(Some(alias))
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] = def getById(
run(select(T.all), from(T), T.id === id).query[RNotificationChannelGotify].option 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] = def insert(r: RNotificationChannelGotify): ConnectionIO[Int] =
DML.insert( DML.insert(
T, T,
T.all, T.all,
sql"${r.id},${r.uid},${r.url},${r.appKey},${r.priority},${r.created}" sql"${r.id},${r.uid},${r.name},${r.url},${r.appKey},${r.priority},${r.created}"
) )
def update(r: RNotificationChannelGotify): ConnectionIO[Int] = def update(r: RNotificationChannelGotify): ConnectionIO[Int] =
@ -64,7 +70,8 @@ object RNotificationChannelGotify {
DML.set( DML.set(
T.url.setTo(r.url), T.url.setTo(r.url),
T.appKey.setTo(r.appKey), T.appKey.setTo(r.appKey),
T.priority.setTo(r.priority) T.priority.setTo(r.priority),
T.name.setTo(r.name)
) )
) )

View File

@ -18,6 +18,7 @@ import doobie.implicits._
final case class RNotificationChannelHttp( final case class RNotificationChannelHttp(
id: Ident, id: Ident,
uid: Ident, uid: Ident,
name: Option[String],
url: LenientUri, url: LenientUri,
created: Timestamp created: Timestamp
) { ) {
@ -32,25 +33,32 @@ object RNotificationChannelHttp {
val id = Column[Ident]("id", this) val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this) val uid = Column[Ident]("uid", this)
val name = Column[String]("name", this)
val url = Column[LenientUri]("url", this) val url = Column[LenientUri]("url", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] = val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, url, created) NonEmptyList.of(id, uid, name, url, created)
} }
val T: Table = Table(None) val T: Table = Table(None)
def as(alias: String): Table = def as(alias: String): Table =
Table(Some(alias)) Table(Some(alias))
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] = def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelHttp].option run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelHttp]
.option
def insert(r: RNotificationChannelHttp): ConnectionIO[Int] = def insert(r: RNotificationChannelHttp): ConnectionIO[Int] =
DML.insert(T, T.all, sql"${r.id},${r.uid},${r.url},${r.created}") DML.insert(T, T.all, sql"${r.id},${r.uid},${r.name},${r.url},${r.created}")
def update(r: RNotificationChannelHttp): ConnectionIO[Int] = def update(r: RNotificationChannelHttp): ConnectionIO[Int] =
DML.update(T, T.id === r.id && T.uid === r.uid, DML.set(T.url.setTo(r.url))) DML.update(
T,
T.id === r.id && T.uid === r.uid,
DML.set(T.url.setTo(r.url), T.name.setTo(r.name))
)
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelHttp]] = { def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelHttp]] = {
val user = RUser.as("u") val user = RUser.as("u")

View File

@ -19,6 +19,7 @@ import emil.MailAddress
final case class RNotificationChannelMail( final case class RNotificationChannelMail(
id: Ident, id: Ident,
uid: Ident, uid: Ident,
name: Option[String],
connection: Ident, connection: Ident,
recipients: List[MailAddress], recipients: List[MailAddress],
created: Timestamp created: Timestamp
@ -34,12 +35,13 @@ object RNotificationChannelMail {
val id = Column[Ident]("id", this) val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this) val uid = Column[Ident]("uid", this)
val name = Column[String]("name", this)
val connection = Column[Ident]("conn_id", this) val connection = Column[Ident]("conn_id", this)
val recipients = Column[List[MailAddress]]("recipients", this) val recipients = Column[List[MailAddress]]("recipients", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] = val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, connection, recipients, created) NonEmptyList.of(id, uid, name, connection, recipients, created)
} }
val T: Table = Table(None) val T: Table = Table(None)
@ -49,7 +51,7 @@ object RNotificationChannelMail {
DML.insert( DML.insert(
T, T,
T.all, T.all,
sql"${r.id},${r.uid},${r.connection},${r.recipients},${r.created}" sql"${r.id},${r.uid},${r.name},${r.connection},${r.recipients},${r.created}"
) )
def update(r: RNotificationChannelMail): ConnectionIO[Int] = def update(r: RNotificationChannelMail): ConnectionIO[Int] =
@ -58,12 +60,15 @@ object RNotificationChannelMail {
T.id === r.id && T.uid === r.uid, T.id === r.id && T.uid === r.uid,
DML.set( DML.set(
T.connection.setTo(r.connection), T.connection.setTo(r.connection),
T.recipients.setTo(r.recipients.toList) T.recipients.setTo(r.recipients.toList),
T.name.setTo(r.name)
) )
) )
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] = def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMail].option run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelMail]
.option
def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = { def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = {
val user = RUser.as("u") val user = RUser.as("u")

View File

@ -18,6 +18,7 @@ import doobie.implicits._
final case class RNotificationChannelMatrix( final case class RNotificationChannelMatrix(
id: Ident, id: Ident,
uid: Ident, uid: Ident,
name: Option[String],
homeServer: LenientUri, homeServer: LenientUri,
roomId: String, roomId: String,
accessToken: Password, accessToken: Password,
@ -34,6 +35,7 @@ object RNotificationChannelMatrix {
val id = Column[Ident]("id", this) val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this) val uid = Column[Ident]("uid", this)
val name = Column[String]("name", this)
val homeServer = Column[LenientUri]("home_server", this) val homeServer = Column[LenientUri]("home_server", this)
val roomId = Column[String]("room_id", this) val roomId = Column[String]("room_id", this)
val accessToken = Column[Password]("access_token", this) val accessToken = Column[Password]("access_token", this)
@ -41,7 +43,16 @@ object RNotificationChannelMatrix {
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val all: NonEmptyList[Column[_]] = val all: NonEmptyList[Column[_]] =
NonEmptyList.of(id, uid, homeServer, roomId, accessToken, messageType, created) NonEmptyList.of(
id,
uid,
name,
homeServer,
roomId,
accessToken,
messageType,
created
)
} }
val T: Table = Table(None) val T: Table = Table(None)
def as(alias: String): Table = Table(Some(alias)) def as(alias: String): Table = Table(Some(alias))
@ -50,7 +61,7 @@ object RNotificationChannelMatrix {
DML.insert( DML.insert(
T, T,
T.all, T.all,
sql"${r.id},${r.uid},${r.homeServer},${r.roomId},${r.accessToken},${r.messageType},${r.created}" sql"${r.id},${r.uid},${r.name},${r.homeServer},${r.roomId},${r.accessToken},${r.messageType},${r.created}"
) )
def update(r: RNotificationChannelMatrix): ConnectionIO[Int] = def update(r: RNotificationChannelMatrix): ConnectionIO[Int] =
@ -61,12 +72,17 @@ object RNotificationChannelMatrix {
T.homeServer.setTo(r.homeServer), T.homeServer.setTo(r.homeServer),
T.roomId.setTo(r.roomId), T.roomId.setTo(r.roomId),
T.accessToken.setTo(r.accessToken), T.accessToken.setTo(r.accessToken),
T.messageType.setTo(r.messageType) T.messageType.setTo(r.messageType),
T.name.setTo(r.name)
) )
) )
def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMatrix]] = def getById(userId: Ident)(
run(select(T.all), from(T), T.id === id).query[RNotificationChannelMatrix].option id: Ident
): ConnectionIO[Option[RNotificationChannelMatrix]] =
run(select(T.all), from(T), T.id === id && T.uid === userId)
.query[RNotificationChannelMatrix]
.option
def getByAccount( def getByAccount(
account: AccountId account: AccountId

View File

@ -7,7 +7,6 @@
package docspell.store.records package docspell.store.records
import cats.data.NonEmptyList import cats.data.NonEmptyList
import cats.implicits._
import docspell.common._ import docspell.common._
import docspell.jsonminiq.JsonMiniQuery import docspell.jsonminiq.JsonMiniQuery
@ -22,115 +21,18 @@ final case class RNotificationHook(
id: Ident, id: Ident,
uid: Ident, uid: Ident,
enabled: Boolean, enabled: Boolean,
channelMail: Option[Ident],
channelGotify: Option[Ident],
channelMatrix: Option[Ident],
channelHttp: Option[Ident],
allEvents: Boolean, allEvents: Boolean,
eventFilter: Option[JsonMiniQuery], eventFilter: Option[JsonMiniQuery],
created: Timestamp 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 { 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 { final case class Table(alias: Option[String]) extends TableDef {
val tableName = "notification_hook" val tableName = "notification_hook"
val id = Column[Ident]("id", this) val id = Column[Ident]("id", this)
val uid = Column[Ident]("uid", this) val uid = Column[Ident]("uid", this)
val enabled = Column[Boolean]("enabled", 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 allEvents = Column[Boolean]("all_events", this)
val eventFilter = Column[JsonMiniQuery]("event_filter", this) val eventFilter = Column[JsonMiniQuery]("event_filter", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
@ -140,10 +42,6 @@ object RNotificationHook {
id, id,
uid, uid,
enabled, enabled,
channelMail,
channelGotify,
channelMatrix,
channelHttp,
allEvents, allEvents,
eventFilter, eventFilter,
created created
@ -157,7 +55,7 @@ object RNotificationHook {
DML.insert( DML.insert(
T, T,
T.all, 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] = { def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
@ -174,10 +72,6 @@ object RNotificationHook {
T.id === r.id && T.uid === r.uid, T.id === r.id && T.uid === r.uid,
DML.set( DML.set(
T.enabled.setTo(r.enabled), 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.allEvents.setTo(r.allEvents),
T.eventFilter.setTo(r.eventFilter) T.eventFilter.setTo(r.eventFilter)
) )

View File

@ -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]
}
}
}

View File

@ -23,6 +23,7 @@ module Api exposing
, checkCalEvent , checkCalEvent
, confirmMultiple , confirmMultiple
, confirmOtp , confirmOtp
, createChannel
, createHook , createHook
, createImapSettings , createImapSettings
, createMailSettings , createMailSettings
@ -34,6 +35,7 @@ module Api exposing
, deleteAttachment , deleteAttachment
, deleteAttachments , deleteAttachments
, deleteBookmark , deleteBookmark
, deleteChannel
, deleteCustomField , deleteCustomField
, deleteCustomValue , deleteCustomValue
, deleteCustomValueMultiple , deleteCustomValueMultiple
@ -56,6 +58,8 @@ module Api exposing
, fileURL , fileURL
, getAttachmentMeta , getAttachmentMeta
, getBookmarks , getBookmarks
, getChannels
, getChannelsIgnoreError
, getClientSettings , getClientSettings
, getCollective , getCollective
, getCollectiveSettings , getCollectiveSettings
@ -172,6 +176,7 @@ module Api exposing
, twoFactor , twoFactor
, unconfirmMultiple , unconfirmMultiple
, updateBookmark , updateBookmark
, updateChannel
, updateHook , updateHook
, updateNotifyDueItems , updateNotifyDueItems
, updatePeriodicQuery , updatePeriodicQuery
@ -229,6 +234,7 @@ import Api.Model.MoveAttachment exposing (MoveAttachment)
import Api.Model.NewCustomField exposing (NewCustomField) import Api.Model.NewCustomField exposing (NewCustomField)
import Api.Model.NewFolder exposing (NewFolder) import Api.Model.NewFolder exposing (NewFolder)
import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult) import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult)
import Api.Model.NotificationHook exposing (NotificationHook)
import Api.Model.NotificationSampleEventReq exposing (NotificationSampleEventReq) import Api.Model.NotificationSampleEventReq exposing (NotificationSampleEventReq)
import Api.Model.OptionalDate exposing (OptionalDate) import Api.Model.OptionalDate exposing (OptionalDate)
import Api.Model.OptionalId exposing (OptionalId) 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.OtpResult exposing (OtpResult)
import Api.Model.OtpState exposing (OtpState) import Api.Model.OtpState exposing (OtpState)
import Api.Model.PasswordChange exposing (PasswordChange) 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.Person exposing (Person)
import Api.Model.PersonList exposing (PersonList) import Api.Model.PersonList exposing (PersonList)
import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.ReferenceList exposing (ReferenceList)
@ -274,10 +282,8 @@ import Data.EquipmentOrder exposing (EquipmentOrder)
import Data.EventType exposing (EventType) import Data.EventType exposing (EventType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder exposing (FolderOrder) import Data.FolderOrder exposing (FolderOrder)
import Data.NotificationHook exposing (NotificationHook) import Data.NotificationChannel exposing (NotificationChannel)
import Data.OrganizationOrder exposing (OrganizationOrder) import Data.OrganizationOrder exposing (OrganizationOrder)
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Data.PersonOrder exposing (PersonOrder) import Data.PersonOrder exposing (PersonOrder)
import Data.Priority exposing (Priority) import Data.Priority exposing (Priority)
import Data.TagOrder exposing (TagOrder) import Data.TagOrder exposing (TagOrder)
@ -604,7 +610,7 @@ startOnceNotifyDueItems flags settings receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems/startonce" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems/startonce"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -618,7 +624,7 @@ updateNotifyDueItems flags settings receive =
Http2.authPut Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -632,7 +638,7 @@ createNotifyDueItems flags settings receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -645,7 +651,7 @@ getNotifyDueItems flags receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
, account = getAccount flags , 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 Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -689,7 +695,7 @@ startOncePeriodicQuery flags settings receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery/startonce" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery/startonce"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -703,7 +709,7 @@ updatePeriodicQuery flags settings receive =
Http2.authPut Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -717,7 +723,7 @@ createPeriodicQuery flags settings receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -730,7 +736,7 @@ getPeriodicQuery flags receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
, account = getAccount flags , 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 Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery" { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
, account = getAccount flags , 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 , 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 --- NotificationHook
@ -2584,7 +2647,7 @@ getHooks flags receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
, account = getAccount flags , 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 Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -2612,7 +2675,7 @@ updateHook flags hook receive =
Http2.authPut Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook" { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }
@ -2642,7 +2705,7 @@ testHook flags hook receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook/sendTestEvent" { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook/sendTestEvent"
, account = getAccount flags , 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 , expect = Http.expectJson receive Api.Model.NotificationChannelTestResult.decoder
} }

View File

@ -48,17 +48,11 @@ type alias HttpModel =
} }
type alias RefModel =
{ channelType : ChannelType
}
type Model type Model
= Matrix MatrixModel = Matrix MatrixModel
| Gotify GotifyModel | Gotify GotifyModel
| Mail MailModel | Mail MailModel
| Http HttpModel | Http HttpModel
| Ref RefModel
type Msg type Msg
@ -147,11 +141,6 @@ initWith flags channel =
, Cmd.none , Cmd.none
) )
Data.NotificationChannel.Ref m ->
( Ref { channelType = m.channelType }
, Cmd.none
)
channelType : Model -> ChannelType channelType : Model -> ChannelType
channelType model = channelType model =
@ -168,9 +157,6 @@ channelType model =
Http _ -> Http _ ->
Data.ChannelType.Http Data.ChannelType.Http
Ref ref ->
ref.channelType
getChannel : Model -> Maybe NotificationChannel getChannel : Model -> Maybe NotificationChannel
getChannel model = getChannel model =
@ -187,9 +173,6 @@ getChannel model =
Http mm -> Http mm ->
Maybe.map Data.NotificationChannel.Http mm.value Maybe.map Data.NotificationChannel.Http mm.value
Ref _ ->
Nothing
--- Update --- Update
@ -269,12 +252,3 @@ view texts settings model =
Http m -> Http m ->
Html.map HttpMsg Html.map HttpMsg
(Comp.NotificationHttpForm.view texts.httpForm m.form) (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" ] []

View File

@ -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 }

View File

@ -16,22 +16,18 @@ module Comp.DueItemsTaskForm exposing
) )
import Api import Api
import Api.Model.EmailSettingsList exposing (EmailSettingsList) import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Api.Model.Tag exposing (Tag)
import Api.Model.TagList exposing (TagList) import Api.Model.TagList exposing (TagList)
import Comp.Basic as B import Comp.Basic as B
import Comp.CalEventInput import Comp.CalEventInput
import Comp.ChannelForm import Comp.ChannelRefInput
import Comp.IntField import Comp.IntField
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.TagDropdown import Comp.TagDropdown
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.CalEvent exposing (CalEvent) import Data.CalEvent exposing (CalEvent)
import Data.ChannelType exposing (ChannelType)
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.NotificationChannel
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Data.TagOrder import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..)) import Data.Validated exposing (Validated(..))
@ -43,13 +39,11 @@ import Markdown
import Messages.Comp.DueItemsTaskForm exposing (Texts) import Messages.Comp.DueItemsTaskForm exposing (Texts)
import Styles as S import Styles as S
import Util.Maybe import Util.Maybe
import Util.Tag
import Util.Update
type alias Model = type alias Model =
{ settings : PeriodicDueItemsSettings { settings : PeriodicDueItemsSettings
, channelModel : Comp.ChannelForm.Model , channelModel : Comp.ChannelRefInput.Model
, tagInclModel : Comp.TagDropdown.Model , tagInclModel : Comp.TagDropdown.Model
, tagExclModel : Comp.TagDropdown.Model , tagExclModel : Comp.TagDropdown.Model
, remindDays : Maybe Int , remindDays : Maybe Int
@ -99,18 +93,14 @@ type Msg
| RequestDelete | RequestDelete
| YesNoDeleteMsg Comp.YesNoDimmer.Msg | YesNoDeleteMsg Comp.YesNoDimmer.Msg
| SetSummary String | SetSummary String
| ChannelMsg Comp.ChannelForm.Msg | ChannelMsg Comp.ChannelRefInput.Msg
initWith : Flags -> PeriodicDueItemsSettings -> ( Model, Cmd Msg ) initWith : Flags -> PeriodicDueItemsSettings -> ( Model, Cmd Msg )
initWith flags s = initWith flags s =
let let
ct =
Data.NotificationChannel.channelType s.channel
|> Maybe.withDefault Data.ChannelType.Matrix
( im, ic ) = ( im, ic ) =
init flags ct init flags
newSchedule = newSchedule =
Data.CalEvent.fromEvent s.schedule Data.CalEvent.fromEvent s.schedule
@ -120,7 +110,7 @@ initWith flags s =
Comp.CalEventInput.init flags newSchedule Comp.CalEventInput.init flags newSchedule
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.initWith flags s.channel Comp.ChannelRefInput.initSelected flags s.channels
in in
( { im ( { im
| settings = s | settings = s
@ -145,8 +135,8 @@ initWith flags s =
) )
init : Flags -> ChannelType -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags ct = init flags =
let let
initialSchedule = initialSchedule =
Data.CalEvent.everyMonth Data.CalEvent.everyMonth
@ -155,9 +145,9 @@ init flags ct =
Comp.CalEventInput.init flags initialSchedule Comp.CalEventInput.init flags initialSchedule
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.init flags ct Comp.ChannelRefInput.init flags
in in
( { settings = Data.PeriodicDueItemsSettings.empty ct ( { settings = Api.Model.PeriodicDueItemsSettings.empty
, channelModel = cfm , channelModel = cfm
, tagInclModel = Comp.TagDropdown.initWith [] [] , tagInclModel = Comp.TagDropdown.initWith [] []
, tagExclModel = Comp.TagDropdown.initWith [] [] , tagExclModel = Comp.TagDropdown.initWith [] []
@ -203,11 +193,17 @@ makeSettings model =
Err ValidateCalEventInvalid Err ValidateCalEventInvalid
channelM = channelM =
Result.fromMaybe let
ValidateChannelRequired list =
(Comp.ChannelForm.getChannel model.channelModel) Comp.ChannelRefInput.getSelected model.channelModel
in
if list == [] then
Err ValidateChannelRequired
make days timer channel = else
Ok list
make days timer channels =
{ prev { prev
| tagsInclude = Comp.TagDropdown.getSelected model.tagInclModel | tagsInclude = Comp.TagDropdown.getSelected model.tagInclModel
, tagsExclude = Comp.TagDropdown.getSelected model.tagExclModel , tagsExclude = Comp.TagDropdown.getSelected model.tagExclModel
@ -216,7 +212,7 @@ makeSettings model =
, enabled = model.enabled , enabled = model.enabled
, schedule = Data.CalEvent.makeEvent timer , schedule = Data.CalEvent.makeEvent timer
, summary = model.summary , summary = model.summary
, channel = channel , channels = channels
} }
in in
Result.map3 make Result.map3 make
@ -247,7 +243,7 @@ update flags msg model =
ChannelMsg lm -> ChannelMsg lm ->
let let
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.update flags lm model.channelModel Comp.ChannelRefInput.update lm model.channelModel
in in
( { model | channelModel = cfm } ( { model | channelModel = cfm }
, NoAction , NoAction
@ -538,9 +534,9 @@ view2 texts extraClasses settings model =
] ]
] ]
, div [ class "mb-4" ] , div [ class "mb-4" ]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) [ formHeader texts.channelHeader
, Html.map ChannelMsg , Html.map ChannelMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel) (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
] ]
, formHeader texts.queryLabel , formHeader texts.queryLabel
, div [ class "mb-4" ] , div [ class "mb-4" ]

View File

@ -14,15 +14,16 @@ module Comp.DueItemsTaskList exposing
, view2 , view2
) )
import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Comp.Basic as B import Comp.Basic as B
import Data.ChannelRef
import Data.ChannelType import Data.ChannelType
import Data.NotificationChannel
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Messages.Comp.DueItemsTaskList exposing (Texts) import Messages.Comp.DueItemsTaskList exposing (Texts)
import Styles as S import Styles as S
import Util.Html import Util.Html
import Util.List
type alias Model = type alias Model =
@ -94,9 +95,7 @@ viewItem2 texts item =
] ]
] ]
, td [ class "text-left mr-2" ] , td [ class "text-left mr-2" ]
[ Data.NotificationChannel.channelType item.channel [ div [ class " space-x-1" ]
|> Maybe.map Data.ChannelType.asString (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels)
|> Maybe.withDefault "-"
|> text
] ]
] ]

View File

@ -15,13 +15,11 @@ module Comp.DueItemsTaskManage exposing
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
import Comp.ChannelMenu import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Comp.DueItemsTaskForm import Comp.DueItemsTaskForm
import Comp.DueItemsTaskList import Comp.DueItemsTaskList
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -35,7 +33,6 @@ type alias Model =
, detailModel : Maybe Comp.DueItemsTaskForm.Model , detailModel : Maybe Comp.DueItemsTaskForm.Model
, items : List PeriodicDueItemsSettings , items : List PeriodicDueItemsSettings
, formState : FormState , formState : FormState
, channelMenuOpen : Bool
} }
@ -57,9 +54,8 @@ type Msg
= ListMsg Comp.DueItemsTaskList.Msg = ListMsg Comp.DueItemsTaskList.Msg
| DetailMsg Comp.DueItemsTaskForm.Msg | DetailMsg Comp.DueItemsTaskForm.Msg
| GetDataResp (Result Http.Error (List PeriodicDueItemsSettings)) | GetDataResp (Result Http.Error (List PeriodicDueItemsSettings))
| NewTaskInit ChannelType | NewTaskInit
| SubmitResp SubmitType (Result Http.Error BasicResult) | SubmitResp SubmitType (Result Http.Error BasicResult)
| ToggleChannelMenu
initModel : Model initModel : Model
@ -68,7 +64,6 @@ initModel =
, detailModel = Nothing , detailModel = Nothing
, items = [] , items = []
, formState = FormStateInitial , formState = FormStateInitial
, channelMenuOpen = False
} }
@ -89,11 +84,6 @@ init flags =
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model = update flags msg model =
case msg of case msg of
ToggleChannelMenu ->
( { model | channelMenuOpen = not model.channelMenuOpen }
, Cmd.none
)
GetDataResp (Ok items) -> GetDataResp (Ok items) ->
( { model ( { model
| items = items | items = items
@ -194,12 +184,12 @@ update flags msg model =
Nothing -> Nothing ->
( model, Cmd.none ) ( model, Cmd.none )
NewTaskInit ct -> NewTaskInit ->
let let
( mm, mc ) = ( mm, mc ) =
Comp.DueItemsTaskForm.init flags ct Comp.DueItemsTaskForm.init flags
in in
( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc ) ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc )
SubmitResp submitType (Ok res) -> SubmitResp submitType (Ok res) ->
( { model ( { model
@ -295,18 +285,15 @@ viewForm2 texts settings model =
viewList2 : Texts -> Model -> List (Html Msg) viewList2 : Texts -> Model -> List (Html Msg)
viewList2 texts model = viewList2 texts model =
let
menuModel =
{ menuOpen = model.channelMenuOpen
, toggleMenu = ToggleChannelMenu
, menuLabel = texts.newTask
, onItem = NewTaskInit
}
in
[ MB.view [ MB.view
{ start = [] { start = []
, end = , 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" , rootClasses = "mb-4"
} }

View File

@ -115,8 +115,8 @@ dropdownCfg texts =
} }
viewJson : Texts -> Model -> Html Msg viewJson : Texts -> Bool -> Model -> Html Msg
viewJson texts model = viewJson texts enableEventChooser model =
let let
json = json =
Result.withDefault "" Result.withDefault ""
@ -125,7 +125,10 @@ viewJson texts model =
div div
[ class "flex flex-col w-full relative" [ 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 [ Html.map EventTypeMsg
(Comp.FixedDropdown.viewStyled2 (dropdownCfg texts) (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
False False
@ -144,8 +147,8 @@ viewJson texts model =
] ]
viewMessage : Texts -> Model -> Html Msg viewMessage : Texts -> Bool -> Model -> Html Msg
viewMessage texts model = viewMessage texts enableEventChooser model =
let let
titleDecoder = titleDecoder =
D.at [ "message", "title" ] D.string D.at [ "message", "title" ] D.string
@ -162,7 +165,10 @@ viewMessage texts model =
div div
[ class "flex flex-col w-full relative" [ 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 [ Html.map EventTypeMsg
(Comp.FixedDropdown.viewStyled2 (dropdownCfg texts) (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
False False

View File

@ -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
)
]

View File

@ -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
]
]

View File

@ -17,24 +17,25 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onInput) import Html.Events exposing (onInput)
import Messages.Comp.NotificationGotifyForm exposing (Texts) import Messages.Comp.NotificationGotifyForm exposing (Texts)
import Styles as S import Styles as S
import Util.Maybe
type alias Model = type alias Model =
{ hook : NotificationGotify { channel : NotificationGotify
, prioModel : Comp.FixedDropdown.Model Int , prioModel : Comp.FixedDropdown.Model Int
} }
init : Model init : Model
init = 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) , prioModel = Comp.FixedDropdown.init (List.range 0 10)
} }
initWith : NotificationGotify -> Model initWith : NotificationGotify -> Model
initWith hook = initWith channel =
{ hook = Data.NotificationChannel.setTypeGotify hook { channel = Data.NotificationChannel.setTypeGotify channel
, prioModel = Comp.FixedDropdown.init (List.range 0 10) , prioModel = Comp.FixedDropdown.init (List.range 0 10)
} }
@ -42,6 +43,7 @@ initWith hook =
type Msg type Msg
= SetUrl String = SetUrl String
| SetAppKey String | SetAppKey String
| SetName String
| PrioMsg (Comp.FixedDropdown.Msg Int) | PrioMsg (Comp.FixedDropdown.Msg Int)
@ -52,30 +54,33 @@ type Msg
update : Msg -> Model -> ( Model, Maybe NotificationGotify ) update : Msg -> Model -> ( Model, Maybe NotificationGotify )
update msg model = update msg model =
let let
hook = channel =
model.hook model.channel
newModel = newModel =
case msg of case msg of
SetUrl s -> SetUrl s ->
{ model | hook = { hook | url = s } } { model | channel = { channel | url = s } }
SetAppKey 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 -> PrioMsg lm ->
let let
( m, sel ) = ( m, sel ) =
Comp.FixedDropdown.update lm model.prioModel Comp.FixedDropdown.update lm model.prioModel
in in
{ model | hook = { hook | priority = sel }, prioModel = m } { model | channel = { channel | priority = sel }, prioModel = m }
in in
( newModel, check newModel.hook ) ( newModel, check newModel.channel )
check : NotificationGotify -> Maybe NotificationGotify check : NotificationGotify -> Maybe NotificationGotify
check hook = check channel =
Just hook Just channel
@ -94,6 +99,25 @@ view texts model =
in in
div [] div []
[ 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" [ class "mb-2"
] ]
[ label [ label
@ -107,7 +131,7 @@ view texts model =
[ type_ "text" [ type_ "text"
, onInput SetUrl , onInput SetUrl
, placeholder texts.gotifyUrl , placeholder texts.gotifyUrl
, value model.hook.url , value model.channel.url
, name "gotifyurl" , name "gotifyurl"
, class S.textInput , class S.textInput
] ]
@ -127,7 +151,7 @@ view texts model =
[ type_ "text" [ type_ "text"
, onInput SetAppKey , onInput SetAppKey
, placeholder texts.appKey , placeholder texts.appKey
, value model.hook.appKey , value model.channel.appKey
, name "appkey" , name "appkey"
, class S.textInput , class S.textInput
] ]
@ -142,7 +166,7 @@ view texts model =
] ]
[ text texts.priority [ 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" ] , span [ class "text-sm opacity-75" ]
[ text texts.priorityInfo [ text texts.priorityInfo
] ]

View File

@ -8,7 +8,6 @@
module Comp.NotificationHookForm exposing module Comp.NotificationHookForm exposing
( Model ( Model
, Msg(..) , Msg(..)
, channelType
, getHook , getHook
, init , init
, initWith , initWith
@ -16,17 +15,16 @@ module Comp.NotificationHookForm exposing
, view , view
) )
import Api.Model.NotificationHook exposing (NotificationHook)
import Comp.Basic as B import Comp.Basic as B
import Comp.ChannelForm import Comp.ChannelRefInput
import Comp.Dropdown import Comp.Dropdown
import Comp.EventSample import Comp.EventSample
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.NotificationTest import Comp.NotificationTest
import Data.ChannelType exposing (ChannelType)
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.EventType exposing (EventType) import Data.EventType exposing (EventType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -39,7 +37,7 @@ import Util.Maybe
type alias Model = type alias Model =
{ hook : NotificationHook { hook : NotificationHook
, enabled : Bool , enabled : Bool
, channelModel : Comp.ChannelForm.Model , channelModel : Comp.ChannelRefInput.Model
, eventsDropdown : Comp.Dropdown.Model EventType , eventsDropdown : Comp.Dropdown.Model EventType
, eventSampleModel : Comp.EventSample.Model , eventSampleModel : Comp.EventSample.Model
, testDeliveryModel : Comp.NotificationTest.Model , testDeliveryModel : Comp.NotificationTest.Model
@ -48,16 +46,16 @@ type alias Model =
} }
init : Flags -> ChannelType -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags ct = init flags =
let let
( cm, cc ) = ( cm, cc ) =
Comp.ChannelForm.init flags ct Comp.ChannelRefInput.init flags
( esm, esc ) = ( esm, esc ) =
Comp.EventSample.initWith flags Data.EventType.TagsChanged Comp.EventSample.initWith flags Data.EventType.TagsChanged
in in
( { hook = Data.NotificationHook.empty ct ( { hook = Api.Model.NotificationHook.empty
, enabled = True , enabled = True
, channelModel = cm , channelModel = cm
, eventsDropdown = , eventsDropdown =
@ -81,7 +79,7 @@ initWith : Flags -> NotificationHook -> ( Model, Cmd Msg )
initWith flags h = initWith flags h =
let let
( cm, cc ) = ( cm, cc ) =
Comp.ChannelForm.initWith flags h.channel Comp.ChannelRefInput.initSelected flags h.channels
( esm, esc ) = ( esm, esc ) =
Comp.EventSample.initWith flags Data.EventType.TagsChanged Comp.EventSample.initWith flags Data.EventType.TagsChanged
@ -92,7 +90,7 @@ initWith flags h =
, eventsDropdown = , eventsDropdown =
Comp.Dropdown.makeMultipleList Comp.Dropdown.makeMultipleList
{ options = Data.EventType.all { options = Data.EventType.all
, selected = h.events , selected = List.filterMap Data.EventType.fromString h.events
} }
, eventSampleModel = esm , eventSampleModel = esm
, testDeliveryModel = Comp.NotificationTest.init , 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 -> Maybe NotificationHook
getHook model = getHook model =
let let
@ -123,20 +116,28 @@ getHook model =
Nothing Nothing
else else
Just ev Just (List.map Data.EventType.asString ev)
channel = channels =
Comp.ChannelForm.getChannel model.channelModel let
list =
Comp.ChannelRefInput.getSelected model.channelModel
in
if list == [] then
Nothing
else
Just list
mkHook ev ch = mkHook ev ch =
NotificationHook model.hook.id model.enabled ch model.allEvents model.eventFilter ev NotificationHook model.hook.id model.enabled ch model.allEvents model.eventFilter ev
in in
Maybe.map2 mkHook events channel Maybe.map2 mkHook events channels
type Msg type Msg
= ToggleEnabled = ToggleEnabled
| ChannelFormMsg Comp.ChannelForm.Msg | ChannelFormMsg Comp.ChannelRefInput.Msg
| EventMsg (Comp.Dropdown.Msg EventType) | EventMsg (Comp.Dropdown.Msg EventType)
| EventSampleMsg Comp.EventSample.Msg | EventSampleMsg Comp.EventSample.Msg
| DeliveryTestMsg Comp.NotificationTest.Msg | DeliveryTestMsg Comp.NotificationTest.Msg
@ -163,7 +164,7 @@ update flags msg model =
ChannelFormMsg lm -> ChannelFormMsg lm ->
let let
( cm, cc ) = ( cm, cc ) =
Comp.ChannelForm.update flags lm model.channelModel Comp.ChannelRefInput.update lm model.channelModel
in in
( { model | channelModel = cm }, Cmd.map ChannelFormMsg cc ) ( { model | channelModel = cm }, Cmd.map ChannelFormMsg cc )
@ -229,9 +230,9 @@ view texts settings model =
} }
] ]
, div [ class "mb-4" ] , div [ class "mb-4" ]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) [ formHeader texts.channelHeader
, Html.map ChannelFormMsg , Html.map ChannelFormMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel) (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
] ]
, div [ class "mb-4" ] , div [ class "mb-4" ]
[ formHeader texts.events [ formHeader texts.events
@ -290,21 +291,21 @@ view texts settings model =
] ]
, div , div
[ class "mt-4" [ 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 [ formHeader texts.samplePayload
, div [ class "opacity-80 mb-1" ]
[ text texts.payloadInfo
]
, Html.map EventSampleMsg , 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" ] , div [ class "mt-4" ]
[ formHeader "Test Delviery" [ formHeader "Test Delviery"

View File

@ -15,14 +15,12 @@ module Comp.NotificationHookManage exposing
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.NotificationHook exposing (NotificationHook)
import Comp.Basic as B import Comp.Basic as B
import Comp.ChannelMenu
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.NotificationHookForm import Comp.NotificationHookForm
import Comp.NotificationHookTable import Comp.NotificationHookTable
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -39,7 +37,6 @@ type alias Model =
, deleteConfirm : DeleteConfirm , deleteConfirm : DeleteConfirm
, loading : Bool , loading : Bool
, formState : FormState , formState : FormState
, newHookMenuOpen : Bool
, jsonFilterError : Maybe String , jsonFilterError : Maybe String
} }
@ -67,9 +64,8 @@ type Msg
= TableMsg Comp.NotificationHookTable.Msg = TableMsg Comp.NotificationHookTable.Msg
| DetailMsg Comp.NotificationHookForm.Msg | DetailMsg Comp.NotificationHookForm.Msg
| GetDataResp (Result Http.Error (List NotificationHook)) | GetDataResp (Result Http.Error (List NotificationHook))
| ToggleNewHookMenu
| SubmitResp SubmitType (Result Http.Error BasicResult) | SubmitResp SubmitType (Result Http.Error BasicResult)
| NewHookInit ChannelType | NewHookInit
| BackToTable | BackToTable
| Submit | Submit
| RequestDelete | RequestDelete
@ -85,7 +81,6 @@ initModel =
, items = [] , items = []
, loading = False , loading = False
, formState = FormStateInitial , formState = FormStateInitial
, newHookMenuOpen = False
, deleteConfirm = DeleteConfirmOff , deleteConfirm = DeleteConfirmOff
, jsonFilterError = Nothing , jsonFilterError = Nothing
} }
@ -177,9 +172,6 @@ update flags msg model =
Nothing -> Nothing ->
( model, Cmd.none ) ( model, Cmd.none )
ToggleNewHookMenu ->
( { model | newHookMenuOpen = not model.newHookMenuOpen }, Cmd.none )
SubmitResp submitType (Ok res) -> SubmitResp submitType (Ok res) ->
( { model ( { model
| formState = | formState =
@ -208,12 +200,12 @@ update flags msg model =
, Cmd.none , Cmd.none
) )
NewHookInit ct -> NewHookInit ->
let let
( mm, mc ) = ( mm, mc ) =
Comp.NotificationHookForm.init flags ct Comp.NotificationHookForm.init flags
in in
( { model | detailModel = Just mm, newHookMenuOpen = False }, Cmd.map DetailMsg mc ) ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc )
BackToTable -> BackToTable ->
( { model | detailModel = Nothing }, initCmd flags ) ( { model | detailModel = Nothing }, initCmd flags )
@ -327,60 +319,14 @@ viewForm texts settings outerModel model =
let let
newHook = newHook =
model.hook.id == "" 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 in
[ h1 [ class S.header2 ] [ 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 text texts.addWebhook
else else
text texts.updateWebhook text texts.updateWebhook
] ]
, div [ class "pt-2 pb-4 font-medium" ]
[ headline
]
, MB.view , MB.view
{ start = { start =
[ MB.CustomElement <| [ MB.CustomElement <|
@ -452,18 +398,15 @@ viewForm texts settings outerModel model =
viewList : Texts -> Model -> List (Html Msg) viewList : Texts -> Model -> List (Html Msg)
viewList texts model = viewList texts model =
let
menuModel =
{ menuOpen = model.newHookMenuOpen
, toggleMenu = ToggleNewHookMenu
, menuLabel = texts.newHook
, onItem = NewHookInit
}
in
[ MB.view [ MB.view
{ start = [] { start = []
, end = , 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" , rootClasses = "mb-4"
} }

View File

@ -14,15 +14,13 @@ module Comp.NotificationHookTable exposing
, view , view
) )
import Api.Model.NotificationHook exposing (NotificationHook)
import Comp.Basic as B import Comp.Basic as B
import Data.ChannelType import Data.ChannelRef
import Data.EventType import Data.EventType
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.NotificationChannel
import Data.NotificationHook exposing (NotificationHook)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.NotificationHookTable exposing (Texts) import Messages.Comp.NotificationHookTable exposing (Texts)
import Styles as S import Styles as S
import Util.Html import Util.Html
@ -80,7 +78,7 @@ view texts model hooks =
renderNotificationHookLine : Texts -> Model -> NotificationHook -> Html Msg renderNotificationHookLine : Texts -> Model -> NotificationHook -> Html Msg
renderNotificationHookLine texts model hook = renderNotificationHookLine texts _ hook =
let let
eventName = eventName =
texts.eventType >> .name texts.eventType >> .name
@ -93,14 +91,17 @@ renderNotificationHookLine texts model hook =
[ Util.Html.checkbox2 hook.enabled [ Util.Html.checkbox2 hook.enabled
] ]
, td [ class "text-left py-4 md:py-2" ] , td [ class "text-left py-4 md:py-2" ]
[ Data.NotificationChannel.channelType hook.channel [ div [ class "space-x-1" ]
|> Maybe.map Data.ChannelType.asString (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] hook.channels)
|> Maybe.withDefault "-"
|> text
] ]
, td [ class "text-left hidden sm:table-cell" ] , td [ class "text-left hidden sm:table-cell" ]
[ List.map eventName hook.events [ if hook.allEvents then
|> String.join ", " text texts.allEvents
|> text
else
List.filterMap Data.EventType.fromString hook.events
|> List.map eventName
|> String.join ", "
|> text
] ]
] ]

View File

@ -15,29 +15,31 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onInput) import Html.Events exposing (onInput)
import Messages.Comp.NotificationHttpForm exposing (Texts) import Messages.Comp.NotificationHttpForm exposing (Texts)
import Styles as S import Styles as S
import Util.Maybe
type alias Model = type alias Model =
{ hook : NotificationHttp { channel : NotificationHttp
} }
init : Model init : Model
init = init =
{ hook = { channel =
Data.NotificationChannel.setTypeHttp Data.NotificationChannel.setTypeHttp
Api.Model.NotificationHttp.empty Api.Model.NotificationHttp.empty
} }
initWith : NotificationHttp -> Model initWith : NotificationHttp -> Model
initWith hook = initWith channel =
{ hook = Data.NotificationChannel.setTypeHttp hook { channel = Data.NotificationChannel.setTypeHttp channel
} }
type Msg type Msg
= SetUrl String = SetUrl String
| SetName String
@ -47,26 +49,29 @@ type Msg
update : Msg -> Model -> ( Model, Maybe NotificationHttp ) update : Msg -> Model -> ( Model, Maybe NotificationHttp )
update msg model = update msg model =
let let
newHook = newChannel =
updateHook msg model.hook updateChannel msg model.channel
in in
( { model | hook = newHook }, check newHook ) ( { model | channel = newChannel }, check newChannel )
check : NotificationHttp -> Maybe NotificationHttp check : NotificationHttp -> Maybe NotificationHttp
check hook = check channel =
if hook.url == "" then if channel.url == "" then
Nothing Nothing
else else
Just hook Just channel
updateHook : Msg -> NotificationHttp -> NotificationHttp updateChannel : Msg -> NotificationHttp -> NotificationHttp
updateHook msg hook = updateChannel msg channel =
case msg of case msg of
SetUrl s -> 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 = view texts model =
div [] div []
[ 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" [ class "mb-2"
] ]
[ label [ label
@ -90,7 +114,7 @@ view texts model =
[ type_ "text" [ type_ "text"
, onInput SetUrl , onInput SetUrl
, placeholder texts.httpUrl , placeholder texts.httpUrl
, value model.hook.url , value model.channel.url
, name "httpurl" , name "httpurl"
, class S.textInput , class S.textInput
] ]

View File

@ -23,13 +23,15 @@ import Html.Events exposing (onInput)
import Http import Http
import Messages.Comp.NotificationMailForm exposing (Texts) import Messages.Comp.NotificationMailForm exposing (Texts)
import Styles as S import Styles as S
import Util.Maybe
type alias Model = type alias Model =
{ hook : NotificationMail { channel : NotificationMail
, connectionModel : Comp.Dropdown.Model String , connectionModel : Comp.Dropdown.Model String
, recipients : List String , recipients : List String
, recipientsModel : Comp.EmailInput.Model , recipientsModel : Comp.EmailInput.Model
, name : Maybe String
, formState : FormState , formState : FormState
} }
@ -46,10 +48,11 @@ type ValidateError
init : Flags -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags = init flags =
( { hook = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty ( { channel = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty
, connectionModel = Comp.Dropdown.makeSingle , connectionModel = Comp.Dropdown.makeSingle
, recipients = [] , recipients = []
, recipientsModel = Comp.EmailInput.init , recipientsModel = Comp.EmailInput.init
, name = Nothing
, formState = FormStateInitial , formState = FormStateInitial
} }
, Cmd.batch , Cmd.batch
@ -59,17 +62,17 @@ init flags =
initWith : Flags -> NotificationMail -> ( Model, Cmd Msg ) initWith : Flags -> NotificationMail -> ( Model, Cmd Msg )
initWith flags hook = initWith flags channel =
let let
( mm, mc ) = ( mm, mc ) =
init flags init flags
( cm, _ ) = ( cm, _ ) =
Comp.Dropdown.update (Comp.Dropdown.SetSelection [ hook.connection ]) mm.connectionModel Comp.Dropdown.update (Comp.Dropdown.SetSelection [ channel.connection ]) mm.connectionModel
in in
( { mm ( { mm
| hook = Data.NotificationChannel.setTypeMail hook | channel = Data.NotificationChannel.setTypeMail channel
, recipients = hook.recipients , recipients = channel.recipients
, connectionModel = cm , connectionModel = cm
} }
, mc , mc
@ -80,6 +83,7 @@ type Msg
= ConnResp (Result Http.Error EmailSettingsList) = ConnResp (Result Http.Error EmailSettingsList)
| ConnMsg (Comp.Dropdown.Msg String) | ConnMsg (Comp.Dropdown.Msg String)
| RecipientMsg Comp.EmailInput.Msg | RecipientMsg Comp.EmailInput.Msg
| SetName String
@ -108,12 +112,12 @@ check model =
|> List.head |> List.head
h = h =
model.hook model.channel
makeHook _ rec conn = makeChannel _ rec conn =
{ h | connection = conn, recipients = rec } { h | connection = conn, recipients = rec, name = model.name }
in in
Maybe.map3 makeHook formState recipients connection Maybe.map3 makeChannel formState recipients connection
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe NotificationMail ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe NotificationMail )
@ -152,6 +156,16 @@ update flags msg model =
, Nothing , Nothing
) )
SetName s ->
let
model_ =
{ model | name = Util.Maybe.fromString s }
in
( model_
, Cmd.none
, check model_
)
ConnMsg lm -> ConnMsg lm ->
let let
( cm, cc ) = ( cm, cc ) =
@ -201,7 +215,26 @@ view texts settings model =
} }
in in
div [] 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 ] [ label [ class S.inputLabel ]
[ text texts.sendVia [ text texts.sendVia
, B.inputRequired , B.inputRequired

View File

@ -15,22 +15,23 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onInput) import Html.Events exposing (onInput)
import Messages.Comp.NotificationMatrixForm exposing (Texts) import Messages.Comp.NotificationMatrixForm exposing (Texts)
import Styles as S import Styles as S
import Util.Maybe
type alias Model = type alias Model =
{ hook : NotificationMatrix { channel : NotificationMatrix
} }
init : Model init : Model
init = init =
{ hook = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty { channel = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty
} }
initWith : NotificationMatrix -> Model initWith : NotificationMatrix -> Model
initWith hook = initWith channel =
{ hook = Data.NotificationChannel.setTypeMatrix hook { channel = Data.NotificationChannel.setTypeMatrix channel
} }
@ -38,6 +39,7 @@ type Msg
= SetHomeServer String = SetHomeServer String
| SetRoomId String | SetRoomId String
| SetAccessKey String | SetAccessKey String
| SetName String
@ -47,28 +49,31 @@ type Msg
update : Msg -> Model -> ( Model, Maybe NotificationMatrix ) update : Msg -> Model -> ( Model, Maybe NotificationMatrix )
update msg model = update msg model =
let let
newHook = newChannel =
updateHook msg model.hook updateChannel msg model.channel
in in
( { model | hook = newHook }, check newHook ) ( { model | channel = newChannel }, check newChannel )
check : NotificationMatrix -> Maybe NotificationMatrix check : NotificationMatrix -> Maybe NotificationMatrix
check hook = check channel =
Just hook Just channel
updateHook : Msg -> NotificationMatrix -> NotificationMatrix updateChannel : Msg -> NotificationMatrix -> NotificationMatrix
updateHook msg hook = updateChannel msg channel =
case msg of case msg of
SetHomeServer s -> SetHomeServer s ->
{ hook | homeServer = s } { channel | homeServer = s }
SetRoomId s -> SetRoomId s ->
{ hook | roomId = s } { channel | roomId = s }
SetAccessKey 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 = view texts model =
div [] div []
[ 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" [ class "mb-2"
] ]
[ label [ label
@ -92,7 +116,7 @@ view texts model =
[ type_ "text" [ type_ "text"
, onInput SetHomeServer , onInput SetHomeServer
, placeholder texts.homeServer , placeholder texts.homeServer
, value model.hook.homeServer , value model.channel.homeServer
, name "homeserver" , name "homeserver"
, class S.textInput , class S.textInput
] ]
@ -112,7 +136,7 @@ view texts model =
[ type_ "text" [ type_ "text"
, onInput SetRoomId , onInput SetRoomId
, placeholder texts.roomId , placeholder texts.roomId
, value model.hook.roomId , value model.channel.roomId
, name "roomid" , name "roomid"
, class S.textInput , class S.textInput
] ]
@ -131,7 +155,7 @@ view texts model =
, textarea , textarea
[ onInput SetAccessKey [ onInput SetAccessKey
, placeholder texts.accessKey , placeholder texts.accessKey
, value model.hook.accessToken , value model.channel.accessToken
, name "accesskey" , name "accesskey"
, class S.textAreaInput , class S.textAreaInput
] ]

View File

@ -9,10 +9,10 @@ module Comp.NotificationTest exposing (Model, Msg, ViewConfig, init, update, vie
import Api import Api
import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult) import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult)
import Api.Model.NotificationHook exposing (NotificationHook)
import Comp.Basic as B import Comp.Basic as B
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.NotificationHook exposing (NotificationHook)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick) import Html.Events exposing (onClick)

View File

@ -16,16 +16,15 @@ module Comp.PeriodicQueryTaskForm exposing
, view , view
) )
import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Comp.Basic as B import Comp.Basic as B
import Comp.BookmarkDropdown import Comp.BookmarkDropdown
import Comp.CalEventInput import Comp.CalEventInput
import Comp.ChannelForm import Comp.ChannelRefInput
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.PowerSearchInput import Comp.PowerSearchInput
import Data.CalEvent exposing (CalEvent) import Data.CalEvent exposing (CalEvent)
import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..)) import Data.Validated exposing (Validated(..))
import Html exposing (..) import Html exposing (..)
@ -44,7 +43,7 @@ type alias Model =
, schedule : Maybe CalEvent , schedule : Maybe CalEvent
, scheduleModel : Comp.CalEventInput.Model , scheduleModel : Comp.CalEventInput.Model
, queryModel : Comp.PowerSearchInput.Model , queryModel : Comp.PowerSearchInput.Model
, channelModel : Comp.ChannelForm.Model , channelModel : Comp.ChannelRefInput.Model
, bookmarkDropdown : Comp.BookmarkDropdown.Model , bookmarkDropdown : Comp.BookmarkDropdown.Model
, contentStart : Maybe String , contentStart : Maybe String
, formState : FormState , formState : FormState
@ -78,7 +77,7 @@ type Msg
| ToggleEnabled | ToggleEnabled
| CalEventMsg Comp.CalEventInput.Msg | CalEventMsg Comp.CalEventInput.Msg
| QueryMsg Comp.PowerSearchInput.Msg | QueryMsg Comp.PowerSearchInput.Msg
| ChannelMsg Comp.ChannelForm.Msg | ChannelMsg Comp.ChannelRefInput.Msg
| BookmarkMsg Comp.BookmarkDropdown.Msg | BookmarkMsg Comp.BookmarkDropdown.Msg
| SetContentStart String | SetContentStart String
| StartOnce | StartOnce
@ -105,7 +104,7 @@ initWith flags s =
Comp.PowerSearchInput.init Comp.PowerSearchInput.init
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.initWith flags s.channel Comp.ChannelRefInput.initSelected flags s.channels
( bm, bc ) = ( bm, bc ) =
Comp.BookmarkDropdown.init flags s.bookmark Comp.BookmarkDropdown.init flags s.bookmark
@ -132,8 +131,8 @@ initWith flags s =
) )
init : Flags -> ChannelType -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags ct = init flags =
let let
initialSchedule = initialSchedule =
Data.CalEvent.everyMonth Data.CalEvent.everyMonth
@ -142,12 +141,12 @@ init flags ct =
Comp.CalEventInput.init flags initialSchedule Comp.CalEventInput.init flags initialSchedule
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.init flags ct Comp.ChannelRefInput.init flags
( bm, bc ) = ( bm, bc ) =
Comp.BookmarkDropdown.init flags Nothing Comp.BookmarkDropdown.init flags Nothing
in in
( { settings = Data.PeriodicQuerySettings.empty ct ( { settings = Api.Model.PeriodicQuerySettings.empty
, enabled = False , enabled = False
, schedule = Just initialSchedule , schedule = Just initialSchedule
, scheduleModel = sm , scheduleModel = sm
@ -210,16 +209,22 @@ makeSettings model =
Result.Ok ( qstr, bm ) Result.Ok ( qstr, bm )
channelM = channelM =
Result.fromMaybe let
ValidateChannelRequired list =
(Comp.ChannelForm.getChannel model.channelModel) Comp.ChannelRefInput.getSelected model.channelModel
in
if list == [] then
Err ValidateChannelRequired
make timer channel q = else
Ok list
make timer channels q =
{ prev { prev
| enabled = model.enabled | enabled = model.enabled
, schedule = Data.CalEvent.makeEvent timer , schedule = Data.CalEvent.makeEvent timer
, summary = model.summary , summary = model.summary
, channel = channel , channels = channels
, query = Tuple.first q , query = Tuple.first q
, bookmark = Tuple.second q , bookmark = Tuple.second q
, contentStart = model.contentStart , contentStart = model.contentStart
@ -285,7 +290,7 @@ update flags msg model =
ChannelMsg lm -> ChannelMsg lm ->
let let
( cfm, cfc ) = ( cfm, cfc ) =
Comp.ChannelForm.update flags lm model.channelModel Comp.ChannelRefInput.update lm model.channelModel
in in
{ model = { model | channelModel = cfm } { model = { model | channelModel = cfm }
, action = NoAction , action = NoAction
@ -536,9 +541,9 @@ view texts extraClasses settings model =
] ]
] ]
, div [ class "mb-4" ] , div [ class "mb-4" ]
[ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) False [ formHeader texts.channelHeader True
, Html.map ChannelMsg , Html.map ChannelMsg
(Comp.ChannelForm.view texts.channelForm settings model.channelModel) (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
] ]
, div [ class "mb-4" ] , div [ class "mb-4" ]
[ formHeader texts.queryLabel True [ formHeader texts.queryLabel True

View File

@ -14,15 +14,17 @@ module Comp.PeriodicQueryTaskList exposing
, view2 , view2
) )
import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Comp.Basic as B import Comp.Basic as B
import Data.ChannelRef
import Data.ChannelType import Data.ChannelType
import Data.NotificationChannel import Data.NotificationChannel
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Messages.Comp.PeriodicQueryTaskList exposing (Texts) import Messages.Comp.PeriodicQueryTaskList exposing (Texts)
import Styles as S import Styles as S
import Util.Html import Util.Html
import Util.List
type alias Model = type alias Model =
@ -94,9 +96,7 @@ viewItem2 texts item =
] ]
] ]
, td [ class "text-left py-4 md:py-2" ] , td [ class "text-left py-4 md:py-2" ]
[ Data.NotificationChannel.channelType item.channel [ div [ class " space-x-1" ]
|> Maybe.map Data.ChannelType.asString (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels)
|> Maybe.withDefault "-"
|> text
] ]
] ]

View File

@ -15,17 +15,16 @@ module Comp.PeriodicQueryTaskManage exposing
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Comp.ChannelMenu import Comp.ChannelMenu
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.PeriodicQueryTaskForm import Comp.PeriodicQueryTaskForm
import Comp.PeriodicQueryTaskList import Comp.PeriodicQueryTaskList
import Data.ChannelType exposing (ChannelType) import Data.ChannelType exposing (ChannelType)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Http import Http
import Messages.Comp.PeriodicQueryTaskManage exposing (Texts) import Messages.Comp.PeriodicQueryTaskManage exposing (Texts)
import Styles as S import Styles as S
@ -36,7 +35,6 @@ type alias Model =
, detailModel : Maybe Comp.PeriodicQueryTaskForm.Model , detailModel : Maybe Comp.PeriodicQueryTaskForm.Model
, items : List PeriodicQuerySettings , items : List PeriodicQuerySettings
, formState : FormState , formState : FormState
, channelMenuOpen : Bool
} }
@ -58,9 +56,8 @@ type Msg
= ListMsg Comp.PeriodicQueryTaskList.Msg = ListMsg Comp.PeriodicQueryTaskList.Msg
| DetailMsg Comp.PeriodicQueryTaskForm.Msg | DetailMsg Comp.PeriodicQueryTaskForm.Msg
| GetDataResp (Result Http.Error (List PeriodicQuerySettings)) | GetDataResp (Result Http.Error (List PeriodicQuerySettings))
| NewTaskInit ChannelType | NewTaskInit
| SubmitResp SubmitType (Result Http.Error BasicResult) | SubmitResp SubmitType (Result Http.Error BasicResult)
| ToggleChannelMenu
initModel : Model initModel : Model
@ -69,7 +66,6 @@ initModel =
, detailModel = Nothing , detailModel = Nothing
, items = [] , items = []
, formState = FormStateInitial , formState = FormStateInitial
, channelMenuOpen = False
} }
@ -195,12 +191,12 @@ update flags msg model =
Nothing -> Nothing ->
( model, Cmd.none, Sub.none ) ( model, Cmd.none, Sub.none )
NewTaskInit ct -> NewTaskInit ->
let let
( mm, mc ) = ( mm, mc ) =
Comp.PeriodicQueryTaskForm.init flags ct Comp.PeriodicQueryTaskForm.init flags
in 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) -> SubmitResp submitType (Ok res) ->
( { model ( { model
@ -231,9 +227,6 @@ update flags msg model =
, Sub.none , Sub.none
) )
ToggleChannelMenu ->
( { model | channelMenuOpen = not model.channelMenuOpen }, Cmd.none, Sub.none )
--- View2 --- View2
@ -301,18 +294,15 @@ viewForm2 texts settings model =
viewList2 : Texts -> Model -> List (Html Msg) viewList2 : Texts -> Model -> List (Html Msg)
viewList2 texts model = viewList2 texts model =
let
menuModel =
{ menuOpen = model.channelMenuOpen
, toggleMenu = ToggleChannelMenu
, menuLabel = texts.newTask
, onItem = NewTaskInit
}
in
[ MB.view [ MB.view
{ start = [] { start = []
, end = , 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" , rootClasses = "mb-4"
} }

View File

@ -7,27 +7,63 @@
module Data.ChannelRef exposing (..) module Data.ChannelRef exposing (..)
import Api.Model.NotificationChannelRef exposing (NotificationChannelRef)
import Data.ChannelType exposing (ChannelType) import Data.ChannelType exposing (ChannelType)
import Json.Decode as D import Html exposing (Attribute, Html, div, span, text)
import Json.Encode as E import Html.Attributes exposing (class)
import Messages.Data.ChannelType as M
import Util.List
type alias ChannelRef = channelType : NotificationChannelRef -> Maybe ChannelType
{ id : String channelType ref =
, channelType : ChannelType Data.ChannelType.fromString ref.channelType
}
decoder : D.Decoder ChannelRef split : M.Texts -> NotificationChannelRef -> ( String, String )
decoder = split texts ref =
D.map2 ChannelRef let
(D.field "id" D.string) chStr =
(D.field "channelType" Data.ChannelType.decoder) 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 asString : M.Texts -> NotificationChannelRef -> String
encode cref = asString texts ref =
E.object let
[ ( "id", E.string cref.id ) ( chStr, name ) =
, ( "channelType", Data.ChannelType.encode cref.channelType ) 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

View File

@ -12,17 +12,18 @@ module Data.NotificationChannel exposing
, decoder , decoder
, empty , empty
, encode , encode
, getRef
, setTypeGotify , setTypeGotify
, setTypeHttp , setTypeHttp
, setTypeMail , setTypeMail
, setTypeMatrix , setTypeMatrix
) )
import Api.Model.NotificationChannelRef exposing (NotificationChannelRef)
import Api.Model.NotificationGotify exposing (NotificationGotify) import Api.Model.NotificationGotify exposing (NotificationGotify)
import Api.Model.NotificationHttp exposing (NotificationHttp) import Api.Model.NotificationHttp exposing (NotificationHttp)
import Api.Model.NotificationMail exposing (NotificationMail) import Api.Model.NotificationMail exposing (NotificationMail)
import Api.Model.NotificationMatrix exposing (NotificationMatrix) import Api.Model.NotificationMatrix exposing (NotificationMatrix)
import Data.ChannelRef exposing (ChannelRef)
import Data.ChannelType exposing (ChannelType) import Data.ChannelType exposing (ChannelType)
import Json.Decode as D import Json.Decode as D
import Json.Encode as E import Json.Encode as E
@ -33,7 +34,6 @@ type NotificationChannel
| Mail NotificationMail | Mail NotificationMail
| Gotify NotificationGotify | Gotify NotificationGotify
| Http NotificationHttp | Http NotificationHttp
| Ref ChannelRef
empty : ChannelType -> NotificationChannel empty : ChannelType -> NotificationChannel
@ -87,46 +87,49 @@ decoder =
, D.map Mail Api.Model.NotificationMail.decoder , D.map Mail Api.Model.NotificationMail.decoder
, D.map Matrix Api.Model.NotificationMatrix.decoder , D.map Matrix Api.Model.NotificationMatrix.decoder
, D.map Http Api.Model.NotificationHttp.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 : NotificationChannel -> E.Value
encode channel = encode channel =
case channel of fold
Matrix ch -> Api.Model.NotificationMail.encode
Api.Model.NotificationMatrix.encode ch Api.Model.NotificationGotify.encode
Api.Model.NotificationMatrix.encode
Mail ch -> Api.Model.NotificationHttp.encode
Api.Model.NotificationMail.encode ch channel
Gotify ch ->
Api.Model.NotificationGotify.encode ch
Http ch ->
Api.Model.NotificationHttp.encode ch
Ref ch ->
Data.ChannelRef.encode ch
channelType : NotificationChannel -> Maybe ChannelType channelType : NotificationChannel -> Maybe ChannelType
channelType ch = channelType ch =
case ch of fold
Matrix m -> (.channelType >> Data.ChannelType.fromString)
Data.ChannelType.fromString m.channelType (.channelType >> Data.ChannelType.fromString)
(.channelType >> Data.ChannelType.fromString)
Mail m -> (.channelType >> Data.ChannelType.fromString)
Data.ChannelType.fromString m.channelType ch
Gotify m ->
Data.ChannelType.fromString m.channelType
Http m ->
Data.ChannelType.fromString m.channelType
Ref m ->
Just m.channelType
asString : NotificationChannel -> String asString : NotificationChannel -> String
@ -144,5 +147,12 @@ asString channel =
Http ch -> Http ch ->
"Http @ " ++ ch.url "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

View File

@ -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

View File

@ -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 )
]

View File

@ -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 )
]

View File

@ -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"
}

View File

@ -15,6 +15,7 @@ import Http
import Messages.Basics import Messages.Basics
import Messages.Comp.CalEventInput import Messages.Comp.CalEventInput
import Messages.Comp.ChannelForm import Messages.Comp.ChannelForm
import Messages.Comp.ChannelRefInput
import Messages.Comp.HttpError import Messages.Comp.HttpError
import Messages.Comp.TagDropdown import Messages.Comp.TagDropdown
import Messages.Data.ChannelType import Messages.Data.ChannelType
@ -26,6 +27,8 @@ type alias Texts =
, httpError : Http.Error -> String , httpError : Http.Error -> String
, channelForm : Messages.Comp.ChannelForm.Texts , channelForm : Messages.Comp.ChannelForm.Texts
, tagDropdown : Messages.Comp.TagDropdown.Texts , tagDropdown : Messages.Comp.TagDropdown.Texts
, channelType : Messages.Data.ChannelType.Texts
, channelRef : Messages.Comp.ChannelRefInput.Texts
, reallyDeleteTask : String , reallyDeleteTask : String
, startOnce : String , startOnce : String
, startTaskNow : String , startTaskNow : String
@ -50,7 +53,7 @@ type alias Texts =
, recipientsRequired : String , recipientsRequired : String
, queryLabel : String , queryLabel : String
, channelRequired : String , channelRequired : String
, channelHeader : Messages.Data.ChannelType.Texts , channelHeader : String
} }
@ -61,6 +64,8 @@ gb =
, httpError = Messages.Comp.HttpError.gb , httpError = Messages.Comp.HttpError.gb
, channelForm = Messages.Comp.ChannelForm.gb , channelForm = Messages.Comp.ChannelForm.gb
, tagDropdown = Messages.Comp.TagDropdown.gb , tagDropdown = Messages.Comp.TagDropdown.gb
, channelType = Messages.Data.ChannelType.gb
, channelRef = Messages.Comp.ChannelRefInput.gb
, reallyDeleteTask = "Really delete this notification task?" , reallyDeleteTask = "Really delete this notification task?"
, startOnce = "Start Once" , startOnce = "Start Once"
, startTaskNow = "Start this task now" , startTaskNow = "Start this task now"
@ -89,7 +94,7 @@ gb =
, recipientsRequired = "At least one recipient is required." , recipientsRequired = "At least one recipient is required."
, queryLabel = "Query" , queryLabel = "Query"
, channelRequired = "A valid channel must be given." , 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 { basics = Messages.Basics.de
, calEventInput = Messages.Comp.CalEventInput.de , calEventInput = Messages.Comp.CalEventInput.de
, httpError = Messages.Comp.HttpError.de , httpError = Messages.Comp.HttpError.de
, channelForm = Messages.Comp.ChannelForm.gb , channelForm = Messages.Comp.ChannelForm.de
, tagDropdown = Messages.Comp.TagDropdown.gb , tagDropdown = Messages.Comp.TagDropdown.de
, channelType = Messages.Data.ChannelType.de
, channelRef = Messages.Comp.ChannelRefInput.de
, reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?"
, startOnce = "Jetzt starten" , startOnce = "Jetzt starten"
, startTaskNow = "Starte den Auftrag sofort" , startTaskNow = "Starte den Auftrag sofort"
@ -128,5 +135,5 @@ de =
, recipientsRequired = "Mindestens ein Empfänger muss angegeben werden." , recipientsRequired = "Mindestens ein Empfänger muss angegeben werden."
, queryLabel = "Abfrage" , queryLabel = "Abfrage"
, channelRequired = "Ein Versandkanal muss angegeben werden." , channelRequired = "Ein Versandkanal muss angegeben werden."
, channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct , channelHeader = "Kanäle"
} }

View File

@ -12,32 +12,33 @@ module Messages.Comp.DueItemsTaskList exposing
) )
import Messages.Basics import Messages.Basics
import Messages.Data.ChannelType
type alias Texts = type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, channelType : Messages.Data.ChannelType.Texts
, summary : String , summary : String
, schedule : String , schedule : String
, connection : String , connection : String
, recipients : String
} }
gb : Texts gb : Texts
gb = gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, channelType = Messages.Data.ChannelType.gb
, summary = "Summary" , summary = "Summary"
, schedule = "Schedule" , schedule = "Schedule"
, connection = "Connection" , connection = "Channel"
, recipients = "Recipients"
} }
de : Texts de : Texts
de = de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, channelType = Messages.Data.ChannelType.de
, summary = "Kurzbeschreibung" , summary = "Kurzbeschreibung"
, schedule = "Zeitplan" , schedule = "Zeitplan"
, connection = "Verbindung" , connection = "Kanal"
, recipients = "Empfänger"
} }

View File

@ -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?"
}

View File

@ -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"
}

View File

@ -13,18 +13,17 @@ module Messages.Comp.NotificationHookForm exposing
import Data.EventType exposing (EventType) import Data.EventType exposing (EventType)
import Messages.Basics import Messages.Basics
import Messages.Comp.ChannelForm import Messages.Comp.ChannelRefInput
import Messages.Comp.EventSample import Messages.Comp.EventSample
import Messages.Data.ChannelType
import Messages.Data.EventType import Messages.Data.EventType
type alias Texts = type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, channelForm : Messages.Comp.ChannelForm.Texts , channelRef : Messages.Comp.ChannelRefInput.Texts
, eventType : EventType -> Messages.Data.EventType.Texts , eventType : EventType -> Messages.Data.EventType.Texts
, eventSample : Messages.Comp.EventSample.Texts , eventSample : Messages.Comp.EventSample.Texts
, channelHeader : Messages.Data.ChannelType.Texts , channelHeader : String
, enableDisable : String , enableDisable : String
, eventsInfo : String , eventsInfo : String
, selectEvents : String , selectEvents : String
@ -34,16 +33,19 @@ type alias Texts =
, eventFilter : String , eventFilter : String
, eventFilterInfo : String , eventFilterInfo : String
, eventFilterClickForHelp : String , eventFilterClickForHelp : String
, jsonPayload : String
, messagePayload : String
, payloadInfo : String
} }
gb : Texts gb : Texts
gb = gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, channelForm = Messages.Comp.ChannelForm.gb , channelRef = Messages.Comp.ChannelRefInput.gb
, eventType = Messages.Data.EventType.gb , eventType = Messages.Data.EventType.gb
, eventSample = Messages.Comp.EventSample.gb , eventSample = Messages.Comp.EventSample.gb
, channelHeader = Messages.Data.ChannelType.gb , channelHeader = "Select channels"
, enableDisable = "Enabled / Disabled" , enableDisable = "Enabled / Disabled"
, eventsInfo = "Select events that trigger this webhook" , eventsInfo = "Select events that trigger this webhook"
, selectEvents = "Select" , selectEvents = "Select"
@ -53,16 +55,19 @@ gb =
, eventFilter = "Event Filter Expression" , eventFilter = "Event Filter Expression"
, eventFilterInfo = "Optional specify an expression to filter events based on their JSON structure." , eventFilterInfo = "Optional specify an expression to filter events based on their JSON structure."
, eventFilterClickForHelp = "Click here for help" , 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 : Texts
de = de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, channelForm = Messages.Comp.ChannelForm.de , channelRef = Messages.Comp.ChannelRefInput.de
, eventType = Messages.Data.EventType.de , eventType = Messages.Data.EventType.de
, eventSample = Messages.Comp.EventSample.de , eventSample = Messages.Comp.EventSample.de
, channelHeader = Messages.Data.ChannelType.de , channelHeader = "Kanäle"
, enableDisable = "Aktiviert / Deaktivert" , enableDisable = "Aktiviert / Deaktivert"
, eventsInfo = "Wähle die Ereignisse, die diesen webhook auslösen" , eventsInfo = "Wähle die Ereignisse, die diesen webhook auslösen"
, selectEvents = "Wähle" , selectEvents = "Wähle"
@ -72,4 +77,7 @@ de =
, eventFilter = "Ereignisfilter" , eventFilter = "Ereignisfilter"
, eventFilterInfo = "Optionaler Ausdruck zum filtern von Ereignissen auf Basis ihrer JSON Struktur." , eventFilterInfo = "Optionaler Ausdruck zum filtern von Ereignissen auf Basis ihrer JSON Struktur."
, eventFilterClickForHelp = "Klicke für Hilfe" , 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."
} }

View File

@ -11,7 +11,6 @@ module Messages.Comp.NotificationHookManage exposing
, gb , gb
) )
import Html exposing (Html, text)
import Http import Http
import Messages.Basics import Messages.Basics
import Messages.Comp.HttpError import Messages.Comp.HttpError
@ -27,9 +26,6 @@ type alias Texts =
, httpError : Http.Error -> String , httpError : Http.Error -> String
, channelType : Messages.Data.ChannelType.Texts , channelType : Messages.Data.ChannelType.Texts
, newHook : String , newHook : String
, matrix : String
, gotify : String
, email : String
, httpRequest : String , httpRequest : String
, hookCreated : String , hookCreated : String
, hookUpdated : String , hookUpdated : String
@ -39,12 +35,8 @@ type alias Texts =
, reallyDeleteHook : String , reallyDeleteHook : String
, formInvalid : String , formInvalid : String
, invalidJsonFilter : String -> String , invalidJsonFilter : String -> String
, integrate : String
, intoDocspell : String
, postRequestInfo : String
, updateWebhook : String , updateWebhook : String
, addWebhook : String , addWebhook : String
, notifyEmailInfo : String
} }
@ -56,9 +48,6 @@ gb =
, httpError = Messages.Comp.HttpError.gb , httpError = Messages.Comp.HttpError.gb
, channelType = Messages.Data.ChannelType.gb , channelType = Messages.Data.ChannelType.gb
, newHook = "New Webhook" , newHook = "New Webhook"
, matrix = "Matrix"
, gotify = "Gotify"
, email = "E-Mail"
, httpRequest = "HTTP Request" , httpRequest = "HTTP Request"
, hookCreated = "Webhook created" , hookCreated = "Webhook created"
, hookUpdated = "Webhook updated" , hookUpdated = "Webhook updated"
@ -68,12 +57,8 @@ gb =
, reallyDeleteHook = "Really delete this webhook?" , reallyDeleteHook = "Really delete this webhook?"
, formInvalid = "Please fill in all required fields" , formInvalid = "Please fill in all required fields"
, invalidJsonFilter = \m -> "Event filter invalid: " ++ m , invalidJsonFilter = \m -> "Event filter invalid: " ++ m
, integrate = "Integrate"
, intoDocspell = "into Docspell"
, postRequestInfo = "Docspell will send POST requests with JSON payload."
, updateWebhook = "Update webhook" , updateWebhook = "Update webhook"
, addWebhook = "Add new webhook" , addWebhook = "Add new webhook"
, notifyEmailInfo = "Get notified via e-mail."
} }
@ -85,9 +70,6 @@ de =
, httpError = Messages.Comp.HttpError.de , httpError = Messages.Comp.HttpError.de
, channelType = Messages.Data.ChannelType.de , channelType = Messages.Data.ChannelType.de
, newHook = "Neuer Webhook" , newHook = "Neuer Webhook"
, matrix = "Matrix"
, gotify = "Gotify"
, email = "E-Mail"
, httpRequest = "HTTP Request" , httpRequest = "HTTP Request"
, hookCreated = "Webhook erstellt" , hookCreated = "Webhook erstellt"
, hookUpdated = "Webhook aktualisiert" , hookUpdated = "Webhook aktualisiert"
@ -97,10 +79,6 @@ de =
, reallyDeleteHook = "Den webhook wirklich löschen?" , reallyDeleteHook = "Den webhook wirklich löschen?"
, formInvalid = "Bitte alle erforderlichen Felder ausfüllen" , formInvalid = "Bitte alle erforderlichen Felder ausfüllen"
, invalidJsonFilter = \m -> "Ereignisfilter ist falsch: " ++ m , invalidJsonFilter = \m -> "Ereignisfilter ist falsch: " ++ m
, integrate = "Integriere"
, intoDocspell = "in Docspell"
, postRequestInfo = "Docspell wird JSON POST requests senden."
, updateWebhook = "Webhook aktualisieren" , updateWebhook = "Webhook aktualisieren"
, addWebhook = "Neuen Webhook hinzufügen" , addWebhook = "Neuen Webhook hinzufügen"
, notifyEmailInfo = "Werde per E-Mail benachrichtigt."
} }

View File

@ -13,15 +13,18 @@ module Messages.Comp.NotificationHookTable exposing
import Data.EventType exposing (EventType) import Data.EventType exposing (EventType)
import Messages.Basics import Messages.Basics
import Messages.Data.ChannelType
import Messages.Data.EventType import Messages.Data.EventType
type alias Texts = type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, eventType : EventType -> Messages.Data.EventType.Texts , eventType : EventType -> Messages.Data.EventType.Texts
, channelType : Messages.Data.ChannelType.Texts
, enabled : String , enabled : String
, channel : String , channel : String
, events : String , events : String
, allEvents : String
} }
@ -29,9 +32,11 @@ gb : Texts
gb = gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, eventType = Messages.Data.EventType.gb , eventType = Messages.Data.EventType.gb
, channelType = Messages.Data.ChannelType.gb
, enabled = "Enabled" , enabled = "Enabled"
, channel = "Channel" , channel = "Channel"
, events = "Events" , events = "Events"
, allEvents = "All"
} }
@ -39,7 +44,9 @@ de : Texts
de = de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, eventType = Messages.Data.EventType.de , eventType = Messages.Data.EventType.de
, channelType = Messages.Data.ChannelType.de
, enabled = "Aktiv" , enabled = "Aktiv"
, channel = "Kanal" , channel = "Kanal"
, events = "Ereignisse" , events = "Ereignisse"
, allEvents = "Alle"
} }

View File

@ -11,14 +11,13 @@ module Messages.Comp.PeriodicQueryTaskForm exposing
, gb , gb
) )
import Data.ChannelType exposing (ChannelType)
import Http import Http
import Messages.Basics import Messages.Basics
import Messages.Comp.BookmarkDropdown import Messages.Comp.BookmarkDropdown
import Messages.Comp.CalEventInput import Messages.Comp.CalEventInput
import Messages.Comp.ChannelForm import Messages.Comp.ChannelForm
import Messages.Comp.ChannelRefInput
import Messages.Comp.HttpError import Messages.Comp.HttpError
import Messages.Data.ChannelType
type alias Texts = type alias Texts =
@ -26,6 +25,7 @@ type alias Texts =
, calEventInput : Messages.Comp.CalEventInput.Texts , calEventInput : Messages.Comp.CalEventInput.Texts
, channelForm : Messages.Comp.ChannelForm.Texts , channelForm : Messages.Comp.ChannelForm.Texts
, bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts , bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts
, channelRef : Messages.Comp.ChannelRefInput.Texts
, httpError : Http.Error -> String , httpError : Http.Error -> String
, reallyDeleteTask : String , reallyDeleteTask : String
, startOnce : String , startOnce : String
@ -41,7 +41,7 @@ type alias Texts =
, invalidCalEvent : String , invalidCalEvent : String
, channelRequired : String , channelRequired : String
, queryStringRequired : String , queryStringRequired : String
, channelHeader : ChannelType -> String , channelHeader : String
, messageContentTitle : String , messageContentTitle : String
, messageContentLabel : String , messageContentLabel : String
, messageContentInfo : String , messageContentInfo : String
@ -56,6 +56,7 @@ gb =
, channelForm = Messages.Comp.ChannelForm.gb , channelForm = Messages.Comp.ChannelForm.gb
, httpError = Messages.Comp.HttpError.gb , httpError = Messages.Comp.HttpError.gb
, bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb , bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb
, channelRef = Messages.Comp.ChannelRefInput.gb
, reallyDeleteTask = "Really delete this notification task?" , reallyDeleteTask = "Really delete this notification task?"
, startOnce = "Start Once" , startOnce = "Start Once"
, startTaskNow = "Start this task now" , startTaskNow = "Start this task now"
@ -74,7 +75,7 @@ gb =
, queryLabel = "Query" , queryLabel = "Query"
, channelRequired = "A valid channel must be given." , channelRequired = "A valid channel must be given."
, queryStringRequired = "A query string and/or bookmark must be supplied" , 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" , messageContentTitle = "Customize message"
, messageContentLabel = "Beginning of message" , messageContentLabel = "Beginning of message"
, messageContentInfo = "Insert text that is prependend to the generated message." , messageContentInfo = "Insert text that is prependend to the generated message."
@ -89,6 +90,7 @@ de =
, channelForm = Messages.Comp.ChannelForm.de , channelForm = Messages.Comp.ChannelForm.de
, httpError = Messages.Comp.HttpError.de , httpError = Messages.Comp.HttpError.de
, bookmarkDropdown = Messages.Comp.BookmarkDropdown.de , bookmarkDropdown = Messages.Comp.BookmarkDropdown.de
, channelRef = Messages.Comp.ChannelRefInput.de
, reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?"
, startOnce = "Jetzt starten" , startOnce = "Jetzt starten"
, startTaskNow = "Starte den Auftrag sofort" , startTaskNow = "Starte den Auftrag sofort"
@ -107,7 +109,7 @@ de =
, queryLabel = "Abfrage" , queryLabel = "Abfrage"
, channelRequired = "Ein Versandkanal muss angegeben werden." , channelRequired = "Ein Versandkanal muss angegeben werden."
, queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark 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" , messageContentTitle = "Nachricht anpassen"
, messageContentLabel = "Anfang der Nachricht" , messageContentLabel = "Anfang der Nachricht"
, messageContentInfo = "Dieser Text wird an den Anfang der generierten Nachricht angefügt." , messageContentInfo = "Dieser Text wird an den Anfang der generierten Nachricht angefügt."

View File

@ -12,32 +12,33 @@ module Messages.Comp.PeriodicQueryTaskList exposing
) )
import Messages.Basics import Messages.Basics
import Messages.Data.ChannelType
type alias Texts = type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, channelType : Messages.Data.ChannelType.Texts
, summary : String , summary : String
, schedule : String , schedule : String
, connection : String , connection : String
, recipients : String
} }
gb : Texts gb : Texts
gb = gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, channelType = Messages.Data.ChannelType.gb
, summary = "Summary" , summary = "Summary"
, schedule = "Schedule" , schedule = "Schedule"
, connection = "Connection" , connection = "Channel"
, recipients = "Recipients"
} }
de : Texts de : Texts
de = de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, channelType = Messages.Data.ChannelType.de
, summary = "Kurzbeschreibung" , summary = "Kurzbeschreibung"
, schedule = "Zeitplan" , schedule = "Zeitplan"
, connection = "Verbindung" , connection = "Kanal"
, recipients = "Empfänger"
} }

View File

@ -27,7 +27,7 @@ gb ct =
"E-Mail" "E-Mail"
Data.ChannelType.Http -> Data.ChannelType.Http ->
"HTTP request" "JSON"
de : Texts de : Texts
@ -43,4 +43,4 @@ de ct =
"E-Mail" "E-Mail"
Data.ChannelType.Http -> Data.ChannelType.Http ->
"HTTP Request" "JSON"

View File

@ -15,6 +15,7 @@ import Messages.Comp.ChangePasswordForm
import Messages.Comp.DueItemsTaskManage import Messages.Comp.DueItemsTaskManage
import Messages.Comp.EmailSettingsManage import Messages.Comp.EmailSettingsManage
import Messages.Comp.ImapSettingsManage import Messages.Comp.ImapSettingsManage
import Messages.Comp.NotificationChannelManage
import Messages.Comp.NotificationHookManage import Messages.Comp.NotificationHookManage
import Messages.Comp.OtpSetup import Messages.Comp.OtpSetup
import Messages.Comp.PeriodicQueryTaskManage import Messages.Comp.PeriodicQueryTaskManage
@ -31,6 +32,7 @@ type alias Texts =
, scanMailboxManage : Messages.Comp.ScanMailboxManage.Texts , scanMailboxManage : Messages.Comp.ScanMailboxManage.Texts
, notificationHookManage : Messages.Comp.NotificationHookManage.Texts , notificationHookManage : Messages.Comp.NotificationHookManage.Texts
, periodicQueryTask : Messages.Comp.PeriodicQueryTaskManage.Texts , periodicQueryTask : Messages.Comp.PeriodicQueryTaskManage.Texts
, channelManage : Messages.Comp.NotificationChannelManage.Texts
, otpSetup : Messages.Comp.OtpSetup.Texts , otpSetup : Messages.Comp.OtpSetup.Texts
, userSettings : String , userSettings : String
, uiSettings : String , uiSettings : String
@ -38,6 +40,7 @@ type alias Texts =
, scanMailbox : String , scanMailbox : String
, emailSettingSmtp : String , emailSettingSmtp : String
, emailSettingImap : String , emailSettingImap : String
, channelSettings : String
, changePassword : String , changePassword : String
, uiSettingsInfo : String , uiSettingsInfo : String
, scanMailboxInfo1 : String , scanMailboxInfo1 : String
@ -50,6 +53,8 @@ type alias Texts =
, webhookInfoText : String , webhookInfoText : String
, dueItemsInfoText : String , dueItemsInfoText : String
, periodicQueryInfoText : String , periodicQueryInfoText : String
, channels : String
, channelInfoText : String
} }
@ -63,6 +68,7 @@ gb =
, scanMailboxManage = Messages.Comp.ScanMailboxManage.gb , scanMailboxManage = Messages.Comp.ScanMailboxManage.gb
, notificationHookManage = Messages.Comp.NotificationHookManage.gb , notificationHookManage = Messages.Comp.NotificationHookManage.gb
, periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.gb , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.gb
, channelManage = Messages.Comp.NotificationChannelManage.gb
, otpSetup = Messages.Comp.OtpSetup.gb , otpSetup = Messages.Comp.OtpSetup.gb
, userSettings = "User Settings" , userSettings = "User Settings"
, uiSettings = "UI Settings" , uiSettings = "UI Settings"
@ -71,6 +77,7 @@ gb =
, emailSettingSmtp = "E-Mail Settings (SMTP)" , emailSettingSmtp = "E-Mail Settings (SMTP)"
, emailSettingImap = "E-Mail Settings (IMAP)" , emailSettingImap = "E-Mail Settings (IMAP)"
, changePassword = "Change Password" , changePassword = "Change Password"
, channelSettings = "Notification Channels"
, uiSettingsInfo = , uiSettingsInfo =
"These settings only affect the web ui. They are stored in the browser, " "These settings only affect the web ui. They are stored in the browser, "
++ "so they are separated between browsers and devices." ++ "so they are separated between browsers and devices."
@ -103,14 +110,16 @@ its payload.
Additionally, you can setup queries that are executed periodically. Additionally, you can setup queries that are executed periodically.
The results are send as a notification message. The results are send as a notification message.
When creating a new notification task, choose first the communication A notification setting needs at least one communication channel, which
channel. must be created before.
""" """
, webhookInfoText = """Webhooks execute http request upon certain events in docspell. , webhookInfoText = """Webhooks execute http request upon certain events in docspell.
""" """
, dueItemsInfoText = """Docspell can notify you once the due dates of your items come closer. """ , 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." , 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 , scanMailboxManage = Messages.Comp.ScanMailboxManage.de
, notificationHookManage = Messages.Comp.NotificationHookManage.de , notificationHookManage = Messages.Comp.NotificationHookManage.de
, periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.de , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.de
, channelManage = Messages.Comp.NotificationChannelManage.de
, otpSetup = Messages.Comp.OtpSetup.de , otpSetup = Messages.Comp.OtpSetup.de
, userSettings = "Benutzereinstellung" , userSettings = "Benutzereinstellung"
, uiSettings = "Oberfläche" , uiSettings = "Oberfläche"
@ -131,6 +141,7 @@ de =
, scanMailbox = "E-Mail-Import" , scanMailbox = "E-Mail-Import"
, emailSettingSmtp = "E-Mail-Einstellungen (SMTP)" , emailSettingSmtp = "E-Mail-Einstellungen (SMTP)"
, emailSettingImap = "E-Mail-Einstellungen (IMAP)" , emailSettingImap = "E-Mail-Einstellungen (IMAP)"
, channelSettings = "Benachrichtigungskanäle"
, changePassword = "Passwort ändern" , changePassword = "Passwort ändern"
, uiSettingsInfo = , uiSettingsInfo =
"Diese Einstellungen sind für die Web-Oberfläche." "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 E-Mail. Zusätzlich kann das HTTP request direkt empfangen werden, was
alle Details zu einem Ereignis enthält. alle Details zu einem Ereignis enthält.
Ausserdem können periodische Suchabfragen erstellt werden, dessen Ausserdem können periodische Suchabfragen erstellt werden, dessen
Ergebnis dann als Benachrichtigung versendet wird. Ergebnis dann als Benachrichtigung versendet wird.
Beim Erstellen eines neuen Auftrags muss zunächst der gewünschte Für eine Notifikation ist ein Kommunikationskanal notwendig, der zuvor
Versandkanal gewählt werden. erstellt werden muss.
""" """
, webhookInfoText = """Webhooks versenden HTTP Requests wenn bestimmte Ereignisse in Docspell auftreten.""" , 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. """ , 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." , periodicQueryInfoText = "Hier können beliebige Abfragen definiert werden, welche regelmäßig ausgeführt werden."
, channels = "Benachrichtigungskanäle"
, channelInfoText = "Über Kanäle werden Notifizierungen versendet."
} }

View File

@ -16,6 +16,7 @@ import Comp.ChangePasswordForm
import Comp.DueItemsTaskManage import Comp.DueItemsTaskManage
import Comp.EmailSettingsManage import Comp.EmailSettingsManage
import Comp.ImapSettingsManage import Comp.ImapSettingsManage
import Comp.NotificationChannelManage
import Comp.NotificationHookManage import Comp.NotificationHookManage
import Comp.OtpSetup import Comp.OtpSetup
import Comp.PeriodicQueryTaskManage import Comp.PeriodicQueryTaskManage
@ -35,6 +36,7 @@ type alias Model =
, uiSettingsModel : Comp.UiSettingsManage.Model , uiSettingsModel : Comp.UiSettingsManage.Model
, otpSetupModel : Comp.OtpSetup.Model , otpSetupModel : Comp.OtpSetup.Model
, notificationHookModel : Comp.NotificationHookManage.Model , notificationHookModel : Comp.NotificationHookManage.Model
, channelModel : Comp.NotificationChannelManage.Model
, periodicQueryModel : Comp.PeriodicQueryTaskManage.Model , periodicQueryModel : Comp.PeriodicQueryTaskManage.Model
} }
@ -53,6 +55,9 @@ init flags settings =
( pqm, pqc ) = ( pqm, pqc ) =
Comp.PeriodicQueryTaskManage.init flags Comp.PeriodicQueryTaskManage.init flags
( ncm, ncc ) =
Comp.NotificationChannelManage.init flags
in in
( { currentTab = Just UiSettingsTab ( { currentTab = Just UiSettingsTab
, changePassModel = Comp.ChangePasswordForm.emptyModel , changePassModel = Comp.ChangePasswordForm.emptyModel
@ -64,12 +69,14 @@ init flags settings =
, otpSetupModel = otpm , otpSetupModel = otpm
, notificationHookModel = nhm , notificationHookModel = nhm
, periodicQueryModel = pqm , periodicQueryModel = pqm
, channelModel = ncm
} }
, Cmd.batch , Cmd.batch
[ Cmd.map UiSettingsMsg uc [ Cmd.map UiSettingsMsg uc
, Cmd.map OtpSetupMsg otpc , Cmd.map OtpSetupMsg otpc
, Cmd.map NotificationHookMsg nhc , Cmd.map NotificationHookMsg nhc
, Cmd.map PeriodicQueryMsg pqc , Cmd.map PeriodicQueryMsg pqc
, Cmd.map ChannelMsg ncc
] ]
) )
@ -85,6 +92,7 @@ type Tab
| ScanMailboxTab | ScanMailboxTab
| UiSettingsTab | UiSettingsTab
| OtpTab | OtpTab
| ChannelTab
type Msg type Msg
@ -98,5 +106,6 @@ type Msg
| OtpSetupMsg Comp.OtpSetup.Msg | OtpSetupMsg Comp.OtpSetup.Msg
| NotificationHookMsg Comp.NotificationHookManage.Msg | NotificationHookMsg Comp.NotificationHookManage.Msg
| PeriodicQueryMsg Comp.PeriodicQueryTaskManage.Msg | PeriodicQueryMsg Comp.PeriodicQueryTaskManage.Msg
| ChannelMsg Comp.NotificationChannelManage.Msg
| UpdateSettings | UpdateSettings
| ReceiveBrowserSettings StoredUiSettings | ReceiveBrowserSettings StoredUiSettings

View File

@ -11,6 +11,7 @@ import Comp.ChangePasswordForm
import Comp.DueItemsTaskManage import Comp.DueItemsTaskManage
import Comp.EmailSettingsManage import Comp.EmailSettingsManage
import Comp.ImapSettingsManage import Comp.ImapSettingsManage
import Comp.NotificationChannelManage
import Comp.NotificationHookManage import Comp.NotificationHookManage
import Comp.OtpSetup import Comp.OtpSetup
import Comp.PeriodicQueryTaskManage import Comp.PeriodicQueryTaskManage
@ -71,8 +72,12 @@ update flags settings msg model =
} }
NotificationWebhookTab -> NotificationWebhookTab ->
let
( _, nc ) =
Comp.NotificationHookManage.init flags
in
{ model = m { model = m
, cmd = Cmd.none , cmd = Cmd.map NotificationHookMsg nc
, sub = Sub.none , sub = Sub.none
, newSettings = Nothing , newSettings = Nothing
} }
@ -107,6 +112,9 @@ update flags settings msg model =
OtpTab -> OtpTab ->
UpdateResult m Cmd.none Sub.none Nothing UpdateResult m Cmd.none Sub.none Nothing
ChannelTab ->
UpdateResult m Cmd.none Sub.none Nothing
ChangePassMsg m -> ChangePassMsg m ->
let let
( m2, c2 ) = ( m2, c2 ) =
@ -195,6 +203,17 @@ update flags settings msg model =
, newSettings = Nothing , 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 -> UpdateSettings ->
update flags update flags
settings settings

View File

@ -11,6 +11,7 @@ import Comp.ChangePasswordForm
import Comp.DueItemsTaskManage import Comp.DueItemsTaskManage
import Comp.EmailSettingsManage import Comp.EmailSettingsManage
import Comp.ImapSettingsManage import Comp.ImapSettingsManage
import Comp.NotificationChannelManage
import Comp.NotificationHookManage import Comp.NotificationHookManage
import Comp.OtpSetup import Comp.OtpSetup
import Comp.PeriodicQueryTaskManage import Comp.PeriodicQueryTaskManage
@ -77,7 +78,7 @@ viewSidebar texts visible _ _ model =
, menuEntryActive model NotificationTab , menuEntryActive model NotificationTab
, class S.sidebarLink , class S.sidebarLink
] ]
[ i [ class "fa fa-bullhorn" ] [] [ i [ class "fa fa-comment font-thin" ] []
, span , span
[ class "ml-3" ] [ class "ml-3" ]
[ text texts.notifications ] [ 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 , a
[ href "#" [ href "#"
, onClick (SetTab ScanMailboxTab) , onClick (SetTab ScanMailboxTab)
@ -217,6 +229,9 @@ viewContent texts flags settings model =
Just OtpTab -> Just OtpTab ->
viewOtpSetup texts settings model viewOtpSetup texts settings model
Just ChannelTab ->
viewChannels texts settings model
Nothing -> Nothing ->
[] []
) )
@ -235,6 +250,26 @@ menuEntryActive model tab =
class "" 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 -> UiSettings -> Model -> List (Html Msg)
viewOtpSetup texts _ model = viewOtpSetup texts _ model =
[ h2 [ h2