Merge pull request #1264 from eikek/feature/query-bookmark
Feature/query bookmark
| @@ -2,7 +2,7 @@ version = "3.3.1" | ||||
|  | ||||
| preset = default | ||||
| align.preset = some | ||||
| runner.dialect = scala213 | ||||
| runner.dialect = scala213source3 | ||||
|  | ||||
| maxColumn = 90 | ||||
|  | ||||
|   | ||||
| @@ -49,6 +49,7 @@ trait BackendApp[F[_]] { | ||||
|   def pubSub: PubSubT[F] | ||||
|   def events: EventExchange[F] | ||||
|   def notification: ONotification[F] | ||||
|   def bookmarks: OQueryBookmarks[F] | ||||
| } | ||||
|  | ||||
| object BackendApp { | ||||
| @@ -89,6 +90,7 @@ object BackendApp { | ||||
|         OShare(store, itemSearchImpl, simpleSearchImpl, javaEmil) | ||||
|       ) | ||||
|       notifyImpl <- ONotification(store, notificationMod) | ||||
|       bookmarksImpl <- OQueryBookmarks(store) | ||||
|     } yield new BackendApp[F] { | ||||
|       val pubSub = pubSubT | ||||
|       val login = loginImpl | ||||
| @@ -115,5 +117,6 @@ object BackendApp { | ||||
|       val share = shareImpl | ||||
|       val events = notificationMod | ||||
|       val notification = notifyImpl | ||||
|       val bookmarks = bookmarksImpl | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,56 +12,78 @@ import cats.implicits._ | ||||
|  | ||||
| import docspell.common.AccountId | ||||
| import docspell.common._ | ||||
| import docspell.common.syntax.all._ | ||||
| import docspell.store.Store | ||||
| import docspell.store.records.RClientSettings | ||||
| import docspell.store.records.RClientSettingsCollective | ||||
| import docspell.store.records.RClientSettingsUser | ||||
| import docspell.store.records.RUser | ||||
|  | ||||
| import io.circe.Json | ||||
| import org.log4s._ | ||||
|  | ||||
| trait OClientSettings[F[_]] { | ||||
|  | ||||
|   def delete(clientId: Ident, account: AccountId): F[Boolean] | ||||
|   def save(clientId: Ident, account: AccountId, data: Json): F[Unit] | ||||
|   def load(clientId: Ident, account: AccountId): F[Option[RClientSettings]] | ||||
|   def deleteUser(clientId: Ident, account: AccountId): F[Boolean] | ||||
|   def saveUser(clientId: Ident, account: AccountId, data: Json): F[Unit] | ||||
|   def loadUser(clientId: Ident, account: AccountId): F[Option[RClientSettingsUser]] | ||||
|  | ||||
|   def deleteCollective(clientId: Ident, account: AccountId): F[Boolean] | ||||
|   def saveCollective(clientId: Ident, account: AccountId, data: Json): F[Unit] | ||||
|   def loadCollective( | ||||
|       clientId: Ident, | ||||
|       account: AccountId | ||||
|   ): F[Option[RClientSettingsCollective]] | ||||
|  | ||||
| } | ||||
|  | ||||
| object OClientSettings { | ||||
|   private[this] val logger = getLogger | ||||
|   private[this] val logger = org.log4s.getLogger | ||||
|  | ||||
|   def apply[F[_]: Async](store: Store[F]): Resource[F, OClientSettings[F]] = | ||||
|     Resource.pure[F, OClientSettings[F]](new OClientSettings[F] { | ||||
|       val log = Logger.log4s[F](logger) | ||||
|  | ||||
|       private def getUserId(account: AccountId): OptionT[F, Ident] = | ||||
|         OptionT(store.transact(RUser.findByAccount(account))).map(_.uid) | ||||
|  | ||||
|       def delete(clientId: Ident, account: AccountId): F[Boolean] = | ||||
|       def deleteCollective(clientId: Ident, account: AccountId): F[Boolean] = | ||||
|         store | ||||
|           .transact(RClientSettingsCollective.delete(clientId, account.collective)) | ||||
|           .map(_ > 0) | ||||
|  | ||||
|       def deleteUser(clientId: Ident, account: AccountId): F[Boolean] = | ||||
|         (for { | ||||
|           _ <- OptionT.liftF( | ||||
|             logger.fdebug( | ||||
|             log.debug( | ||||
|               s"Deleting client settings for client ${clientId.id} and account $account" | ||||
|             ) | ||||
|           ) | ||||
|           userId <- getUserId(account) | ||||
|           n <- OptionT.liftF( | ||||
|             store.transact( | ||||
|               RClientSettings.delete(clientId, userId) | ||||
|               RClientSettingsUser.delete(clientId, userId) | ||||
|             ) | ||||
|           ) | ||||
|         } yield n > 0).getOrElse(false) | ||||
|  | ||||
|       def save(clientId: Ident, account: AccountId, data: Json): F[Unit] = | ||||
|       def saveCollective(clientId: Ident, account: AccountId, data: Json): F[Unit] = | ||||
|         for { | ||||
|           n <- store.transact( | ||||
|             RClientSettingsCollective.upsert(clientId, account.collective, data) | ||||
|           ) | ||||
|           _ <- | ||||
|             if (n <= 0) Async[F].raiseError(new IllegalStateException("No rows updated!")) | ||||
|             else ().pure[F] | ||||
|         } yield () | ||||
|  | ||||
|       def saveUser(clientId: Ident, account: AccountId, data: Json): F[Unit] = | ||||
|         (for { | ||||
|           _ <- OptionT.liftF( | ||||
|             logger.fdebug( | ||||
|             log.debug( | ||||
|               s"Storing client settings for client ${clientId.id} and account $account" | ||||
|             ) | ||||
|           ) | ||||
|           userId <- getUserId(account) | ||||
|           n <- OptionT.liftF( | ||||
|             store.transact(RClientSettings.upsert(clientId, userId, data)) | ||||
|             store.transact(RClientSettingsUser.upsert(clientId, userId, data)) | ||||
|           ) | ||||
|           _ <- OptionT.liftF( | ||||
|             if (n <= 0) Async[F].raiseError(new Exception("No rows updated!")) | ||||
| @@ -69,15 +91,21 @@ object OClientSettings { | ||||
|           ) | ||||
|         } yield ()).getOrElse(()) | ||||
|  | ||||
|       def load(clientId: Ident, account: AccountId): F[Option[RClientSettings]] = | ||||
|       def loadCollective( | ||||
|           clientId: Ident, | ||||
|           account: AccountId | ||||
|       ): F[Option[RClientSettingsCollective]] = | ||||
|         store.transact(RClientSettingsCollective.find(clientId, account.collective)) | ||||
|  | ||||
|       def loadUser(clientId: Ident, account: AccountId): F[Option[RClientSettingsUser]] = | ||||
|         (for { | ||||
|           _ <- OptionT.liftF( | ||||
|             logger.fdebug( | ||||
|             log.debug( | ||||
|               s"Loading client settings for client ${clientId.id} and account $account" | ||||
|             ) | ||||
|           ) | ||||
|           userId <- getUserId(account) | ||||
|           data <- OptionT(store.transact(RClientSettings.find(clientId, userId))) | ||||
|           data <- OptionT(store.transact(RClientSettingsUser.find(clientId, userId))) | ||||
|         } yield data).value | ||||
|  | ||||
|     }) | ||||
|   | ||||
| @@ -0,0 +1,100 @@ | ||||
| /* | ||||
|  * Copyright 2020 Eike K. & Contributors | ||||
|  * | ||||
|  * SPDX-License-Identifier: AGPL-3.0-or-later | ||||
|  */ | ||||
|  | ||||
| package docspell.backend.ops | ||||
|  | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.common._ | ||||
| import docspell.query.ItemQuery | ||||
| import docspell.store.AddResult | ||||
| import docspell.store.Store | ||||
| import docspell.store.UpdateResult | ||||
| import docspell.store.records.RQueryBookmark | ||||
|  | ||||
| trait OQueryBookmarks[F[_]] { | ||||
|  | ||||
|   def getAll(account: AccountId): F[Vector[OQueryBookmarks.Bookmark]] | ||||
|  | ||||
|   def findOne(account: AccountId, nameOrId: String): F[Option[OQueryBookmarks.Bookmark]] | ||||
|  | ||||
|   def create(account: AccountId, bookmark: OQueryBookmarks.NewBookmark): F[AddResult] | ||||
|  | ||||
|   def update( | ||||
|       account: AccountId, | ||||
|       id: Ident, | ||||
|       bookmark: OQueryBookmarks.NewBookmark | ||||
|   ): F[UpdateResult] | ||||
|  | ||||
|   def delete(account: AccountId, bookmark: Ident): F[Unit] | ||||
| } | ||||
|  | ||||
| object OQueryBookmarks { | ||||
|   final case class NewBookmark( | ||||
|       name: String, | ||||
|       label: Option[String], | ||||
|       query: ItemQuery, | ||||
|       personal: Boolean | ||||
|   ) | ||||
|  | ||||
|   final case class Bookmark( | ||||
|       id: Ident, | ||||
|       name: String, | ||||
|       label: Option[String], | ||||
|       query: ItemQuery, | ||||
|       personal: Boolean, | ||||
|       created: Timestamp | ||||
|   ) | ||||
|  | ||||
|   def apply[F[_]: Sync](store: Store[F]): Resource[F, OQueryBookmarks[F]] = | ||||
|     Resource.pure(new OQueryBookmarks[F] { | ||||
|       def getAll(account: AccountId): F[Vector[Bookmark]] = | ||||
|         store | ||||
|           .transact(RQueryBookmark.allForUser(account)) | ||||
|           .map(_.map(convert.toModel)) | ||||
|  | ||||
|       def findOne( | ||||
|           account: AccountId, | ||||
|           nameOrId: String | ||||
|       ): F[Option[OQueryBookmarks.Bookmark]] = | ||||
|         store | ||||
|           .transact(RQueryBookmark.findByNameOrId(account, nameOrId)) | ||||
|           .map(_.map(convert.toModel)) | ||||
|  | ||||
|       def create(account: AccountId, b: NewBookmark): F[AddResult] = { | ||||
|         val record = | ||||
|           RQueryBookmark.createNew(account, b.name, b.label, b.query, b.personal) | ||||
|         store.transact(RQueryBookmark.insertIfNotExists(account, record)) | ||||
|       } | ||||
|  | ||||
|       def update(account: AccountId, id: Ident, b: NewBookmark): F[UpdateResult] = | ||||
|         UpdateResult.fromUpdate( | ||||
|           store.transact(RQueryBookmark.update(convert.toRecord(account, id, b))) | ||||
|         ) | ||||
|  | ||||
|       def delete(account: AccountId, bookmark: Ident): F[Unit] = | ||||
|         store.transact(RQueryBookmark.deleteById(account.collective, bookmark)).as(()) | ||||
|     }) | ||||
|  | ||||
|   private object convert { | ||||
|  | ||||
|     def toModel(r: RQueryBookmark): Bookmark = | ||||
|       Bookmark(r.id, r.name, r.label, r.query, r.isPersonal, r.created) | ||||
|  | ||||
|     def toRecord(account: AccountId, id: Ident, b: NewBookmark): RQueryBookmark = | ||||
|       RQueryBookmark( | ||||
|         id, | ||||
|         b.name, | ||||
|         b.label, | ||||
|         None, // userId and some other values are not used | ||||
|         account.collective, | ||||
|         b.query, | ||||
|         Timestamp.Epoch | ||||
|       ) | ||||
|  | ||||
|   } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ | ||||
| package docspell.joex.notify | ||||
|  | ||||
| import cats.data.OptionT | ||||
| import cats.data.{NonEmptyList => Nel} | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
|  | ||||
| @@ -17,10 +18,14 @@ import docspell.joex.scheduler.Task | ||||
| import docspell.notification.api.EventContext | ||||
| import docspell.notification.api.NotificationChannel | ||||
| import docspell.notification.api.PeriodicQueryArgs | ||||
| import docspell.query.ItemQuery | ||||
| import docspell.query.ItemQuery.Expr.AndExpr | ||||
| import docspell.query.ItemQueryParser | ||||
| import docspell.store.qb.Batch | ||||
| import docspell.store.queries.ListItem | ||||
| import docspell.store.queries.{QItem, Query} | ||||
| import docspell.store.records.RQueryBookmark | ||||
| import docspell.store.records.RShare | ||||
| import docspell.store.records.RUser | ||||
|  | ||||
| object PeriodicQueryTask { | ||||
| @@ -54,22 +59,77 @@ object PeriodicQueryTask { | ||||
|       ) | ||||
|       .getOrElse(()) | ||||
|  | ||||
|   private def queryString(q: ItemQuery.Expr) = | ||||
|     ItemQueryParser.asString(q) | ||||
|  | ||||
|   def withQuery[F[_]: Sync](ctx: Context[F, Args])(cont: Query => F[Unit]): F[Unit] = { | ||||
|     def fromBookmark(id: String) = | ||||
|       ctx.store | ||||
|         .transact(RQueryBookmark.findByNameOrId(ctx.args.account, id)) | ||||
|         .map(_.map(_.query)) | ||||
|         .flatTap(q => | ||||
|           ctx.logger.debug(s"Loaded bookmark '$id': ${q.map(_.expr).map(queryString)}") | ||||
|         ) | ||||
|  | ||||
|     def fromShare(id: String) = | ||||
|       ctx.store | ||||
|         .transact(RShare.findOneByCollective(ctx.args.account.collective, Some(true), id)) | ||||
|         .map(_.map(_.query)) | ||||
|         .flatTap(q => | ||||
|           ctx.logger.debug(s"Loaded share '$id': ${q.map(_.expr).map(queryString)}") | ||||
|         ) | ||||
|  | ||||
|     def fromBookmarkOrShare(id: String) = | ||||
|       OptionT(fromBookmark(id)).orElse(OptionT(fromShare(id))).value | ||||
|  | ||||
|     def runQuery(bm: Option[ItemQuery], str: String): F[Unit] = | ||||
|       ItemQueryParser.parse(str) match { | ||||
|         case Right(q) => | ||||
|           val expr = bm.map(b => AndExpr(Nel.of(b.expr, q.expr))).getOrElse(q.expr) | ||||
|           val query = Query(Query.Fix(ctx.args.account, Some(expr), None)) | ||||
|           ctx.logger.debug(s"Running query: ${queryString(expr)}") *> cont(query) | ||||
|  | ||||
|         case Left(err) => | ||||
|           ctx.logger.error( | ||||
|             s"Item query is invalid, stopping: ${ctx.args.query.map(_.query)} - ${err.render}" | ||||
|           ) | ||||
|       } | ||||
|  | ||||
|     (ctx.args.bookmark, ctx.args.query) match { | ||||
|       case (Some(bm), Some(qstr)) => | ||||
|         ctx.logger.debug(s"Using bookmark $bm and query $qstr") *> | ||||
|           fromBookmarkOrShare(bm).flatMap(bq => runQuery(bq, qstr.query)) | ||||
|  | ||||
|       case (Some(bm), None) => | ||||
|         fromBookmarkOrShare(bm).flatMap { | ||||
|           case Some(bq) => | ||||
|             val query = Query(Query.Fix(ctx.args.account, Some(bq.expr), None)) | ||||
|             ctx.logger.debug(s"Using bookmark: ${queryString(bq.expr)}") *> cont(query) | ||||
|  | ||||
|           case None => | ||||
|             ctx.logger.error( | ||||
|               s"No bookmark found for id: $bm. Can't continue. Please fix the task query." | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|       case (None, Some(qstr)) => | ||||
|         ctx.logger.debug(s"Using query: ${qstr.query}") *> runQuery(None, qstr.query) | ||||
|  | ||||
|       case (None, None) => | ||||
|         ctx.logger.error(s"No query provided for task $taskName!") | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)( | ||||
|       cont: Vector[ListItem] => F[Unit] | ||||
|   ): F[Unit] = | ||||
|     ItemQueryParser.parse(ctx.args.query.query) match { | ||||
|       case Right(q) => | ||||
|         val query = Query(Query.Fix(ctx.args.account, Some(q.expr), None)) | ||||
|         val items = ctx.store | ||||
|           .transact(QItem.findItems(query, now.toUtcDate, 0, Batch.limit(limit))) | ||||
|           .compile | ||||
|           .to(Vector) | ||||
|     withQuery(ctx) { query => | ||||
|       val items = ctx.store | ||||
|         .transact(QItem.findItems(query, now.toUtcDate, 0, Batch.limit(limit))) | ||||
|         .compile | ||||
|         .to(Vector) | ||||
|  | ||||
|         items.flatMap(cont) | ||||
|       case Left(err) => | ||||
|         ctx.logger.error( | ||||
|           s"Item query is invalid, stopping: ${ctx.args.query} - ${err.render}" | ||||
|         ) | ||||
|       items.flatMap(cont) | ||||
|     } | ||||
|  | ||||
|   def withEventContext[F[_]]( | ||||
|   | ||||
| @@ -15,7 +15,8 @@ import io.circe.{Decoder, Encoder} | ||||
| final case class PeriodicQueryArgs( | ||||
|     account: AccountId, | ||||
|     channel: ChannelOrRef, | ||||
|     query: ItemQueryString, | ||||
|     query: Option[ItemQueryString], | ||||
|     bookmark: Option[String], | ||||
|     baseUrl: Option[LenientUri] | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -1880,18 +1880,146 @@ paths: | ||||
|             application/json: | ||||
|               schema: {} | ||||
|  | ||||
|   /sec/querybookmark: | ||||
|     get: | ||||
|       operationId: "sec-querybookmark-get-all" | ||||
|       tags: [Query Bookmarks] | ||||
|       summary: Return all query bookmarks | ||||
|       description: | | ||||
|         Returns all query bookmarks of the current user. | ||||
|  | ||||
|         Bookmarks can be "global", where they belong to the whole | ||||
|         collective or personal, so they are only for the user. This | ||||
|         returns both. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 type: array | ||||
|                 items: | ||||
|                   $ref: "#/components/schemas/BookmarkedQuery" | ||||
|     post: | ||||
|       operationId: "sec-querybookmark-post" | ||||
|       tags: [Query Bookmarks] | ||||
|       summary: Create a new query bookmark | ||||
|       description: | | ||||
|         Creates a new query bookmark. | ||||
|  | ||||
|         A bookmark must have a unique name (within both collective and | ||||
|         personal scope). If a name already exists, a failure is | ||||
|         returned - use PUT instead for changing existing bookmarks. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/BookmarkedQuery" | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|     put: | ||||
|       operationId: "sec-querybookmark-put" | ||||
|       tags: [Query Bookmarks] | ||||
|       summary: Change a query bookmark | ||||
|       description: | | ||||
|         Changes an existing  query bookmark. | ||||
|  | ||||
|         A bookmark must have a unique name within the collective | ||||
|         (considering collective and personal scope). The bookmark is | ||||
|         identified by its id, which must exist. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: | ||||
|               $ref: "#/components/schemas/BookmarkedQuery" | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|   /sec/querybookmark/{bookmarkId}: | ||||
|     parameters: | ||||
|       - $ref: "#/components/parameters/bookmarkId" | ||||
|     delete: | ||||
|       operationId: "sec-querybookmark-delete" | ||||
|       tags: [Query Bookmark] | ||||
|       summary: Delete a bookmark. | ||||
|       description: | | ||||
|         Deletes a bookmarks by its id. | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest             | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/clientSettings/{clientId}: | ||||
|     parameters: | ||||
|       - $ref: "#/components/parameters/clientId" | ||||
|     get: | ||||
|       operationId: "sec-clientsettings-get" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Return the current user settings | ||||
|       summary: Return the client settings of current user | ||||
|       description: | | ||||
|         Returns the settings for the current user by merging the | ||||
|         client settings for the collective and the user's own client | ||||
|         settings into one json structure. | ||||
|  | ||||
|         Null, Array, Boolean, String and Number are treated as values, | ||||
|         and values from the users settings completely replace values | ||||
|         from the collective's settings. | ||||
|          | ||||
|         The `clientId` is an identifier to a client application. It | ||||
|         returns a JSON structure. The server doesn't care about the | ||||
|         actual data, since it is meant to be interpreted by clients. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: {} | ||||
|  | ||||
|   /sec/clientSettings/user/{clientId}: | ||||
|     parameters: | ||||
|       - $ref: "#/components/parameters/clientId" | ||||
|     get: | ||||
|       operationId: "sec-clientsettings-user-get" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Return the user's own client settings  | ||||
|       description: | | ||||
|         Returns the settings for the current user. The `clientId` is | ||||
|         an identifier to a client application. It returns a JSON | ||||
|         structure. The server doesn't care about the actual data, | ||||
|         since it is meant to be interpreted by clients. | ||||
|  | ||||
|         If there is nothing stored for the given `clientId` an empty | ||||
|         JSON object (`{}`) is returned! | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       responses: | ||||
| @@ -1903,9 +2031,9 @@ paths: | ||||
|             application/json: | ||||
|               schema: {} | ||||
|     put: | ||||
|       operationId: "sec-clientsettings-update" | ||||
|       operationId: "sec-clientsettings-user-update" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Update current user settings | ||||
|       summary: Update client settings of current user | ||||
|       description: | | ||||
|         Updates (replaces or creates) the current user's settings with | ||||
|         the given data. The `clientId` is an identifier to a client | ||||
| @@ -1933,9 +2061,9 @@ paths: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|     delete: | ||||
|       operationId: "sec-clientsettings-delete" | ||||
|       operationId: "sec-clientsettings-user-delete" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Clears the current user settings | ||||
|       summary: Clears client settings of current user | ||||
|       description: | | ||||
|         Removes all stored user settings for the client identified by | ||||
|         `clientId`. | ||||
| @@ -1951,6 +2079,80 @@ paths: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /sec/clientSettings/collective/{clientId}: | ||||
|     parameters: | ||||
|       - $ref: "#/components/parameters/clientId" | ||||
|     get: | ||||
|       operationId: "sec-clientsettings-collective-get" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Return collective client settings | ||||
|       description: | | ||||
|         Returns the settings for the current collective. The | ||||
|         `clientId` is an identifier to a client application. It | ||||
|         returns a JSON structure. The server doesn't care about the | ||||
|         actual data, since it is meant to be interpreted by clients. | ||||
|  | ||||
|         If there is nothing stored for the given `clientId` an empty | ||||
|         JSON object (`{}`) is returned! | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: {} | ||||
|     put: | ||||
|       operationId: "sec-clientsettings-collective-update" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Update collective client settings | ||||
|       description: | | ||||
|         Updates (replaces or creates) the current collective's | ||||
|         settings with the given data. The `clientId` is an identifier | ||||
|         to a client application. The request body is expected to be | ||||
|         JSON, the structure is not important to the server. | ||||
|  | ||||
|         The data is stored for the current user and given `clientId`. | ||||
|  | ||||
|         The data is only saved without being checked in any way | ||||
|         (besides being valid JSON). It is returned "as is" to the | ||||
|         client in the corresponding GET endpoint. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       requestBody: | ||||
|         content: | ||||
|           application/json: | ||||
|             schema: {} | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|     delete: | ||||
|       operationId: "sec-clientsettings-collective-delete" | ||||
|       tags: [ Client Settings ] | ||||
|       summary: Clears collective's client settings | ||||
|       description: | | ||||
|         Removes all stored client settings of id `clientId` for | ||||
|         collective. | ||||
|       security: | ||||
|         - authTokenHeader: [] | ||||
|       responses: | ||||
|         422: | ||||
|           description: BadRequest | ||||
|         200: | ||||
|           description: Ok | ||||
|           content: | ||||
|             application/json: | ||||
|               schema: | ||||
|                 $ref: "#/components/schemas/BasicResult" | ||||
|  | ||||
|   /share/search/query: | ||||
|     post: | ||||
|       operationId: "share-search-query" | ||||
| @@ -5206,6 +5408,32 @@ paths: | ||||
|  | ||||
| components: | ||||
|   schemas: | ||||
|     BookmarkedQuery: | ||||
|       description: | | ||||
|         A query bookmark. | ||||
|       required: | ||||
|         - id | ||||
|         - name | ||||
|         - query | ||||
|         - personal | ||||
|         - created | ||||
|       properties: | ||||
|         id: | ||||
|           type: string | ||||
|           format: ident | ||||
|         name: | ||||
|           type: string | ||||
|         label: | ||||
|           type: string | ||||
|         query: | ||||
|           type: string | ||||
|           format: itemquery | ||||
|         personal: | ||||
|           type: boolean | ||||
|         created: | ||||
|           type: integer | ||||
|           format: date-time | ||||
|  | ||||
|     StringValue: | ||||
|       description: | | ||||
|         A generic string value | ||||
| @@ -7755,6 +7983,14 @@ components: | ||||
|         some identifier for a client application | ||||
|       schema: | ||||
|         type: string | ||||
|     bookmarkId: | ||||
|       name: bookmarkId | ||||
|       in: path | ||||
|       required: true | ||||
|       description: | | ||||
|         An identifier for a bookmark | ||||
|       schema: | ||||
|         type: string | ||||
|     providerId: | ||||
|       name: providerId | ||||
|       in: path | ||||
| @@ -7811,12 +8047,12 @@ extraSchemas: | ||||
|  | ||||
|   PeriodicQuerySettings: | ||||
|     description: | | ||||
|       Settings for the periodc-query task. | ||||
|       Settings for the periodc-query task. At least one of `query` and | ||||
|       `bookmark` is required! | ||||
|     required: | ||||
|       - id | ||||
|       - enabled | ||||
|       - channel | ||||
|       - query | ||||
|       - schedule | ||||
|     properties: | ||||
|       id: | ||||
| @@ -7839,6 +8075,10 @@ extraSchemas: | ||||
|       query: | ||||
|         type: string | ||||
|         format: itemquery | ||||
|       bookmark: | ||||
|         type: string | ||||
|         description: | | ||||
|           Name or ID of bookmark to use.  | ||||
|  | ||||
|   PeriodicDueItemsSettings: | ||||
|     description: | | ||||
|   | ||||
| @@ -21,7 +21,8 @@ final case class PeriodicQuerySettings( | ||||
|     summary: Option[String], | ||||
|     enabled: Boolean, | ||||
|     channel: NotificationChannel, | ||||
|     query: ItemQuery, | ||||
|     query: Option[ItemQuery], | ||||
|     bookmark: Option[String], | ||||
|     schedule: CalEvent | ||||
| ) {} | ||||
|  | ||||
|   | ||||
| @@ -172,7 +172,8 @@ object RestServer { | ||||
|       "folder" -> FolderRoutes(restApp.backend, token), | ||||
|       "customfield" -> CustomFieldRoutes(restApp.backend, token), | ||||
|       "clientSettings" -> ClientSettingsRoutes(restApp.backend, token), | ||||
|       "notification" -> NotificationRoutes(cfg, restApp.backend, token) | ||||
|       "notification" -> NotificationRoutes(cfg, restApp.backend, token), | ||||
|       "querybookmark" -> BookmarkRoutes(restApp.backend, token) | ||||
|     ) | ||||
|  | ||||
|   def openRoutes[F[_]: Async]( | ||||
|   | ||||
| @@ -0,0 +1,66 @@ | ||||
| /* | ||||
|  * Copyright 2020 Eike K. & Contributors | ||||
|  * | ||||
|  * SPDX-License-Identifier: AGPL-3.0-or-later | ||||
|  */ | ||||
|  | ||||
| package docspell.restserver.routes | ||||
|  | ||||
| import cats.effect.Async | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.backend.BackendApp | ||||
| import docspell.backend.auth.AuthToken | ||||
| import docspell.backend.ops.OQueryBookmarks | ||||
| import docspell.common.Ident | ||||
| import docspell.restapi.model.BookmarkedQuery | ||||
| import docspell.restserver.conv.Conversions | ||||
|  | ||||
| import org.http4s.HttpRoutes | ||||
| import org.http4s.circe.CirceEntityDecoder._ | ||||
| import org.http4s.circe.CirceEntityEncoder._ | ||||
| import org.http4s.dsl.Http4sDsl | ||||
|  | ||||
| object BookmarkRoutes { | ||||
|  | ||||
|   def apply[F[_]: Async](backend: BackendApp[F], token: AuthToken): HttpRoutes[F] = { | ||||
|     val dsl = new Http4sDsl[F] {} | ||||
|     import dsl._ | ||||
|  | ||||
|     HttpRoutes.of { | ||||
|       case GET -> Root => | ||||
|         for { | ||||
|           all <- backend.bookmarks.getAll(token.account) | ||||
|           resp <- Ok(all.map(convert.toApi)) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ POST -> Root => | ||||
|         for { | ||||
|           data <- req.as[BookmarkedQuery] | ||||
|           res <- backend.bookmarks.create(token.account, convert.toModel(data)) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Bookmark created")) | ||||
|         } yield resp | ||||
|  | ||||
|       case req @ PUT -> Root => | ||||
|         for { | ||||
|           data <- req.as[BookmarkedQuery] | ||||
|           res <- backend.bookmarks.update(token.account, data.id, convert.toModel(data)) | ||||
|           resp <- Ok(Conversions.basicResult(res, "Bookmark updated")) | ||||
|         } yield resp | ||||
|  | ||||
|       case DELETE -> Root / Ident(id) => | ||||
|         for { | ||||
|           res <- backend.bookmarks.delete(token.account, id).attempt | ||||
|           resp <- Ok(Conversions.basicResult(res, "Bookmark deleted")) | ||||
|         } yield resp | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   object convert { | ||||
|     def toApi(b: OQueryBookmarks.Bookmark): BookmarkedQuery = | ||||
|       BookmarkedQuery(b.id, b.name, b.label, b.query, b.personal, b.created) | ||||
|  | ||||
|     def toModel(b: BookmarkedQuery): OQueryBookmarks.NewBookmark = | ||||
|       OQueryBookmarks.NewBookmark(b.name, b.label, b.query, b.personal) | ||||
|   } | ||||
| } | ||||
| @@ -8,6 +8,7 @@ package docspell.restserver.routes | ||||
|  | ||||
| import cats.effect._ | ||||
| import cats.implicits._ | ||||
| import cats.kernel.Semigroup | ||||
|  | ||||
| import docspell.backend.BackendApp | ||||
| import docspell.backend.auth.AuthToken | ||||
| @@ -27,25 +28,65 @@ object ClientSettingsRoutes { | ||||
|     import dsl._ | ||||
|  | ||||
|     HttpRoutes.of { | ||||
|       case req @ PUT -> Root / Ident(clientId) => | ||||
|         for { | ||||
|           data <- req.as[Json] | ||||
|           _ <- backend.clientSettings.save(clientId, user.account, data) | ||||
|           res <- Ok(BasicResult(true, "Settings stored")) | ||||
|         } yield res | ||||
|  | ||||
|       case GET -> Root / Ident(clientId) => | ||||
|         for { | ||||
|           data <- backend.clientSettings.load(clientId, user.account) | ||||
|           res <- data match { | ||||
|             case Some(d) => Ok(d.settingsData) | ||||
|           collData <- backend.clientSettings.loadCollective(clientId, user.account) | ||||
|           userData <- backend.clientSettings.loadUser(clientId, user.account) | ||||
|  | ||||
|           mergedData = collData.map(_.settingsData) |+| userData.map(_.settingsData) | ||||
|  | ||||
|           res <- mergedData match { | ||||
|             case Some(j) => Ok(j) | ||||
|             case None    => NotFound() | ||||
|           } | ||||
|         } yield res | ||||
|  | ||||
|       case DELETE -> Root / Ident(clientId) => | ||||
|       case req @ PUT -> Root / "user" / Ident(clientId) => | ||||
|         for { | ||||
|           flag <- backend.clientSettings.delete(clientId, user.account) | ||||
|           data <- req.as[Json] | ||||
|           _ <- backend.clientSettings.saveUser(clientId, user.account, data) | ||||
|           res <- Ok(BasicResult(true, "Settings stored")) | ||||
|         } yield res | ||||
|  | ||||
|       case GET -> Root / "user" / Ident(clientId) => | ||||
|         for { | ||||
|           data <- backend.clientSettings.loadUser(clientId, user.account) | ||||
|           res <- data match { | ||||
|             case Some(d) => Ok(d.settingsData) | ||||
|             case None    => Ok(Map.empty[String, String]) | ||||
|           } | ||||
|         } yield res | ||||
|  | ||||
|       case DELETE -> Root / "user" / Ident(clientId) => | ||||
|         for { | ||||
|           flag <- backend.clientSettings.deleteUser(clientId, user.account) | ||||
|           res <- Ok( | ||||
|             BasicResult( | ||||
|               flag, | ||||
|               if (flag) "Settings deleted" else "Deleting settings failed" | ||||
|             ) | ||||
|           ) | ||||
|         } yield res | ||||
|  | ||||
|       case req @ PUT -> Root / "collective" / Ident(clientId) => | ||||
|         for { | ||||
|           data <- req.as[Json] | ||||
|           _ <- backend.clientSettings.saveCollective(clientId, user.account, data) | ||||
|           res <- Ok(BasicResult(true, "Settings stored")) | ||||
|         } yield res | ||||
|  | ||||
|       case GET -> Root / "collective" / Ident(clientId) => | ||||
|         for { | ||||
|           data <- backend.clientSettings.loadCollective(clientId, user.account) | ||||
|           res <- data match { | ||||
|             case Some(d) => Ok(d.settingsData) | ||||
|             case None    => Ok(Map.empty[String, String]) | ||||
|           } | ||||
|         } yield res | ||||
|  | ||||
|       case DELETE -> Root / "collective" / Ident(clientId) => | ||||
|         for { | ||||
|           flag <- backend.clientSettings.deleteCollective(clientId, user.account) | ||||
|           res <- Ok( | ||||
|             BasicResult( | ||||
|               flag, | ||||
| @@ -55,4 +96,7 @@ object ClientSettingsRoutes { | ||||
|         } yield res | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   implicit def jsonSemigroup: Semigroup[Json] = | ||||
|     Semigroup.instance((a1, a2) => a1.deepMerge(a2)) | ||||
| } | ||||
|   | ||||
| @@ -117,11 +117,20 @@ object PeriodicQueryRoutes extends MailAddressCodec { | ||||
|     Sync[F] | ||||
|       .pure(for { | ||||
|         ch <- NotificationChannel.convert(settings.channel) | ||||
|         qstr <- ItemQueryParser | ||||
|           .asString(settings.query.expr) | ||||
|           .left | ||||
|           .map(err => new IllegalArgumentException(s"Query not renderable: $err")) | ||||
|       } yield (ch, ItemQueryString(qstr))) | ||||
|         qstr <- settings.query match { | ||||
|           case Some(q) => | ||||
|             ItemQueryParser | ||||
|               .asString(q.expr) | ||||
|               .left | ||||
|               .map(err => new IllegalArgumentException(s"Query not renderable: $err")) | ||||
|               .map(Option.apply) | ||||
|           case None => | ||||
|             Right(None) | ||||
|         } | ||||
|         _ <- | ||||
|           if (qstr.nonEmpty || settings.bookmark.nonEmpty) Right(()) | ||||
|           else Left(new IllegalArgumentException("No query or bookmark provided")) | ||||
|       } yield (ch, qstr.map(ItemQueryString.apply))) | ||||
|       .rethrow | ||||
|       .map { case (channel, qstr) => | ||||
|         UserTask( | ||||
| @@ -134,6 +143,7 @@ object PeriodicQueryRoutes extends MailAddressCodec { | ||||
|             user, | ||||
|             Right(channel), | ||||
|             qstr, | ||||
|             settings.bookmark, | ||||
|             Some(baseUrl / "app" / "item") | ||||
|           ) | ||||
|         ) | ||||
| @@ -155,7 +165,8 @@ object PeriodicQueryRoutes extends MailAddressCodec { | ||||
|       task.summary, | ||||
|       task.enabled, | ||||
|       ch, | ||||
|       ItemQueryParser.parseUnsafe(task.args.query.query), | ||||
|       task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe), | ||||
|       task.args.bookmark, | ||||
|       task.timer | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,12 @@ | ||||
| ALTER TABLE "client_settings" RENAME TO "client_settings_user"; | ||||
|  | ||||
| CREATE TABLE "client_settings_collective" ( | ||||
|   "id" varchar(254) not null primary key, | ||||
|   "client_id" varchar(254) not null, | ||||
|   "cid" varchar(254) not null, | ||||
|   "settings_data" text not null, | ||||
|   "created" timestamp not null, | ||||
|   "updated" timestamp not null, | ||||
|   foreign key ("cid") references "collective"("cid") on delete cascade, | ||||
|   unique ("client_id", "cid") | ||||
| ); | ||||
| @@ -0,0 +1,13 @@ | ||||
| CREATE TABLE "query_bookmark" ( | ||||
|   "id" varchar(254) not null primary key, | ||||
|   "name" varchar(254) not null, | ||||
|   "label" varchar(254), | ||||
|   "user_id" varchar(254), | ||||
|   "cid" varchar(254) not null, | ||||
|   "query" varchar(2000) not null, | ||||
|   "created" timestamp, | ||||
|   "__user_id" varchar(254) not null, | ||||
|   foreign key ("user_id") references "user_"("uid") on delete cascade, | ||||
|   foreign key ("cid") references "collective"("cid") on delete cascade, | ||||
|   unique("cid", "__user_id", "name") | ||||
| ) | ||||
| @@ -0,0 +1,12 @@ | ||||
| RENAME TABLE `client_settings` TO `client_settings_user`; | ||||
|  | ||||
| CREATE TABLE `client_settings_collective` ( | ||||
|   `id` varchar(254) not null primary key, | ||||
|   `client_id` varchar(254) not null, | ||||
|   `cid` varchar(254) not null, | ||||
|   `settings_data` text not null, | ||||
|   `created` timestamp not null, | ||||
|   `updated` timestamp not null, | ||||
|   foreign key (`cid`) references `collective`(`cid`) on delete cascade, | ||||
|   unique (`client_id`, `cid`) | ||||
| ); | ||||
| @@ -0,0 +1,13 @@ | ||||
| CREATE TABLE `query_bookmark` ( | ||||
|   `id` varchar(254) not null primary key, | ||||
|   `name` varchar(254) not null, | ||||
|   `label` varchar(254), | ||||
|   `user_id` varchar(254), | ||||
|   `cid` varchar(254) not null, | ||||
|   `query` varchar(2000) not null, | ||||
|   `created` timestamp, | ||||
|   `__user_id` varchar(254) not null, | ||||
|   foreign key (`user_id`) references `user_`(`uid`) on delete cascade, | ||||
|   foreign key (`cid`) references `collective`(`cid`) on delete cascade, | ||||
|   unique(`cid`, `__user_id`, `name`) | ||||
| ) | ||||
| @@ -0,0 +1,12 @@ | ||||
| ALTER TABLE "client_settings" RENAME TO "client_settings_user"; | ||||
|  | ||||
| CREATE TABLE "client_settings_collective" ( | ||||
|   "id" varchar(254) not null primary key, | ||||
|   "client_id" varchar(254) not null, | ||||
|   "cid" varchar(254) not null, | ||||
|   "settings_data" text not null, | ||||
|   "created" timestamp not null, | ||||
|   "updated" timestamp not null, | ||||
|   foreign key ("cid") references "collective"("cid") on delete cascade, | ||||
|   unique ("client_id", "cid") | ||||
| ); | ||||
| @@ -0,0 +1,13 @@ | ||||
| CREATE TABLE "query_bookmark" ( | ||||
|   "id" varchar(254) not null primary key, | ||||
|   "name" varchar(254) not null, | ||||
|   "label" varchar(254), | ||||
|   "user_id" varchar(254), | ||||
|   "cid" varchar(254) not null, | ||||
|   "query" varchar(2000) not null, | ||||
|   "created" timestamp, | ||||
|   "__user_id" varchar(254) not null, | ||||
|   foreign key ("user_id") references "user_"("uid") on delete cascade, | ||||
|   foreign key ("cid") references "collective"("cid") on delete cascade, | ||||
|   unique("cid", "__user_id", "name") | ||||
| ) | ||||
| @@ -0,0 +1,85 @@ | ||||
| /* | ||||
|  * Copyright 2020 Eike K. & Contributors | ||||
|  * | ||||
|  * SPDX-License-Identifier: AGPL-3.0-or-later | ||||
|  */ | ||||
|  | ||||
| package docspell.store.records | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.implicits._ | ||||
|  | ||||
| import docspell.common._ | ||||
| import docspell.store.qb.DSL._ | ||||
| import docspell.store.qb._ | ||||
|  | ||||
| import doobie._ | ||||
| import doobie.implicits._ | ||||
| import io.circe.Json | ||||
|  | ||||
| case class RClientSettingsCollective( | ||||
|     id: Ident, | ||||
|     clientId: Ident, | ||||
|     cid: Ident, | ||||
|     settingsData: Json, | ||||
|     updated: Timestamp, | ||||
|     created: Timestamp | ||||
| ) {} | ||||
|  | ||||
| object RClientSettingsCollective { | ||||
|  | ||||
|   final case class Table(alias: Option[String]) extends TableDef { | ||||
|     val tableName = "client_settings_collective" | ||||
|  | ||||
|     val id = Column[Ident]("id", this) | ||||
|     val clientId = Column[Ident]("client_id", this) | ||||
|     val cid = Column[Ident]("cid", this) | ||||
|     val settingsData = Column[Json]("settings_data", this) | ||||
|     val updated = Column[Timestamp]("updated", this) | ||||
|     val created = Column[Timestamp]("created", this) | ||||
|     val all = | ||||
|       NonEmptyList.of[Column[_]](id, clientId, cid, settingsData, updated, created) | ||||
|   } | ||||
|  | ||||
|   def as(alias: String): Table = Table(Some(alias)) | ||||
|   val T = Table(None) | ||||
|  | ||||
|   def insert(v: RClientSettingsCollective): ConnectionIO[Int] = { | ||||
|     val t = Table(None) | ||||
|     DML.insert( | ||||
|       t, | ||||
|       t.all, | ||||
|       fr"${v.id},${v.clientId},${v.cid},${v.settingsData},${v.updated},${v.created}" | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   def updateSettings( | ||||
|       clientId: Ident, | ||||
|       cid: Ident, | ||||
|       data: Json, | ||||
|       updateTs: Timestamp | ||||
|   ): ConnectionIO[Int] = | ||||
|     DML.update( | ||||
|       T, | ||||
|       T.clientId === clientId && T.cid === cid, | ||||
|       DML.set(T.settingsData.setTo(data), T.updated.setTo(updateTs)) | ||||
|     ) | ||||
|  | ||||
|   def upsert(clientId: Ident, cid: Ident, data: Json): ConnectionIO[Int] = | ||||
|     for { | ||||
|       id <- Ident.randomId[ConnectionIO] | ||||
|       now <- Timestamp.current[ConnectionIO] | ||||
|       nup <- updateSettings(clientId, cid, data, now) | ||||
|       nin <- | ||||
|         if (nup <= 0) insert(RClientSettingsCollective(id, clientId, cid, data, now, now)) | ||||
|         else 0.pure[ConnectionIO] | ||||
|     } yield nup + nin | ||||
|  | ||||
|   def delete(clientId: Ident, cid: Ident): ConnectionIO[Int] = | ||||
|     DML.delete(T, T.clientId === clientId && T.cid === cid) | ||||
|  | ||||
|   def find(clientId: Ident, cid: Ident): ConnectionIO[Option[RClientSettingsCollective]] = | ||||
|     run(select(T.all), from(T), T.clientId === clientId && T.cid === cid) | ||||
|       .query[RClientSettingsCollective] | ||||
|       .option | ||||
| } | ||||
| @@ -17,7 +17,7 @@ import doobie._ | ||||
| import doobie.implicits._ | ||||
| import io.circe.Json | ||||
| 
 | ||||
| case class RClientSettings( | ||||
| case class RClientSettingsUser( | ||||
|     id: Ident, | ||||
|     clientId: Ident, | ||||
|     userId: Ident, | ||||
| @@ -26,10 +26,10 @@ case class RClientSettings( | ||||
|     created: Timestamp | ||||
| ) {} | ||||
| 
 | ||||
| object RClientSettings { | ||||
| object RClientSettingsUser { | ||||
| 
 | ||||
|   final case class Table(alias: Option[String]) extends TableDef { | ||||
|     val tableName = "client_settings" | ||||
|     val tableName = "client_settings_user" | ||||
| 
 | ||||
|     val id = Column[Ident]("id", this) | ||||
|     val clientId = Column[Ident]("client_id", this) | ||||
| @@ -44,7 +44,7 @@ object RClientSettings { | ||||
|   def as(alias: String): Table = Table(Some(alias)) | ||||
|   val T = Table(None) | ||||
| 
 | ||||
|   def insert(v: RClientSettings): ConnectionIO[Int] = { | ||||
|   def insert(v: RClientSettingsUser): ConnectionIO[Int] = { | ||||
|     val t = Table(None) | ||||
|     DML.insert( | ||||
|       t, | ||||
| @@ -71,15 +71,15 @@ object RClientSettings { | ||||
|       now <- Timestamp.current[ConnectionIO] | ||||
|       nup <- updateSettings(clientId, userId, data, now) | ||||
|       nin <- | ||||
|         if (nup <= 0) insert(RClientSettings(id, clientId, userId, data, now, now)) | ||||
|         if (nup <= 0) insert(RClientSettingsUser(id, clientId, userId, data, now, now)) | ||||
|         else 0.pure[ConnectionIO] | ||||
|     } yield nup + nin | ||||
| 
 | ||||
|   def delete(clientId: Ident, userId: Ident): ConnectionIO[Int] = | ||||
|     DML.delete(T, T.clientId === clientId && T.userId === userId) | ||||
| 
 | ||||
|   def find(clientId: Ident, userId: Ident): ConnectionIO[Option[RClientSettings]] = | ||||
|   def find(clientId: Ident, userId: Ident): ConnectionIO[Option[RClientSettingsUser]] = | ||||
|     run(select(T.all), from(T), T.clientId === clientId && T.userId === userId) | ||||
|       .query[RClientSettings] | ||||
|       .query[RClientSettingsUser] | ||||
|       .option | ||||
| } | ||||
| @@ -0,0 +1,177 @@ | ||||
| /* | ||||
|  * Copyright 2020 Eike K. & Contributors | ||||
|  * | ||||
|  * SPDX-License-Identifier: AGPL-3.0-or-later | ||||
|  */ | ||||
|  | ||||
| package docspell.store.records | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.syntax.all._ | ||||
|  | ||||
| import docspell.common._ | ||||
| import docspell.query.ItemQuery | ||||
| import docspell.store.AddResult | ||||
| import docspell.store.qb.DSL._ | ||||
| import docspell.store.qb._ | ||||
|  | ||||
| import doobie._ | ||||
| import doobie.implicits._ | ||||
|  | ||||
| final case class RQueryBookmark( | ||||
|     id: Ident, | ||||
|     name: String, | ||||
|     label: Option[String], | ||||
|     userId: Option[Ident], | ||||
|     cid: Ident, | ||||
|     query: ItemQuery, | ||||
|     created: Timestamp | ||||
| ) { | ||||
|   def isPersonal: Boolean = | ||||
|     userId.isDefined | ||||
|  | ||||
|   def asGlobal: RQueryBookmark = | ||||
|     copy(userId = None) | ||||
|  | ||||
|   def asPersonal(userId: Ident): RQueryBookmark = | ||||
|     copy(userId = userId.some) | ||||
| } | ||||
|  | ||||
| object RQueryBookmark { | ||||
|   final case class Table(alias: Option[String]) extends TableDef { | ||||
|     val tableName = "query_bookmark"; | ||||
|  | ||||
|     val id = Column[Ident]("id", this) | ||||
|     val name = Column[String]("name", this) | ||||
|     val label = Column[String]("label", this) | ||||
|     val userId = Column[Ident]("user_id", this) | ||||
|     val cid = Column[Ident]("cid", this) | ||||
|     val query = Column[ItemQuery]("query", this) | ||||
|     val created = Column[Timestamp]("created", this) | ||||
|  | ||||
|     val internUserId = Column[String]("__user_id", this) | ||||
|  | ||||
|     val all: NonEmptyList[Column[_]] = | ||||
|       NonEmptyList.of(id, name, label, userId, cid, query, created) | ||||
|   } | ||||
|  | ||||
|   val T: Table = Table(None) | ||||
|   def as(alias: String): Table = Table(Some(alias)) | ||||
|  | ||||
|   def createNew( | ||||
|       account: AccountId, | ||||
|       name: String, | ||||
|       label: Option[String], | ||||
|       query: ItemQuery, | ||||
|       personal: Boolean | ||||
|   ): ConnectionIO[RQueryBookmark] = | ||||
|     for { | ||||
|       userId <- RUser.getIdByAccount(account) | ||||
|       curTime <- Timestamp.current[ConnectionIO] | ||||
|       id <- Ident.randomId[ConnectionIO] | ||||
|     } yield RQueryBookmark( | ||||
|       id, | ||||
|       name, | ||||
|       label, | ||||
|       if (personal) userId.some else None, | ||||
|       account.collective, | ||||
|       query, | ||||
|       curTime | ||||
|     ) | ||||
|  | ||||
|   def insert(r: RQueryBookmark): ConnectionIO[Int] = { | ||||
|     val userIdDummy = r.userId.getOrElse(Ident.unsafe("-")) | ||||
|     DML.insert( | ||||
|       T, | ||||
|       T.all.append(T.internUserId), | ||||
|       sql"${r.id},${r.name},${r.label},${r.userId},${r.cid},${r.query},${r.created},$userIdDummy" | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   def update(r: RQueryBookmark): ConnectionIO[Int] = | ||||
|     DML.update( | ||||
|       T, | ||||
|       T.id === r.id, | ||||
|       DML.set( | ||||
|         T.name.setTo(r.name), | ||||
|         T.label.setTo(r.label), | ||||
|         T.query.setTo(r.query) | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|   def deleteById(cid: Ident, id: Ident): ConnectionIO[Int] = | ||||
|     DML.delete(T, T.id === id && T.cid === cid) | ||||
|  | ||||
|   def nameExists(account: AccountId, name: String): ConnectionIO[Boolean] = { | ||||
|     val user = RUser.as("u") | ||||
|     val bm = RQueryBookmark.as("bm") | ||||
|  | ||||
|     val users = Select( | ||||
|       user.uid.s, | ||||
|       from(user), | ||||
|       user.cid === account.collective && user.login === account.user | ||||
|     ) | ||||
|     Select( | ||||
|       select(count(bm.id)), | ||||
|       from(bm), | ||||
|       bm.name === name && bm.cid === account.collective && (bm.userId.isNull || bm.userId | ||||
|         .in(users)) | ||||
|     ).build.query[Int].unique.map(_ > 0) | ||||
|   } | ||||
|  | ||||
|   // impl note: store.add doesn't work, because it checks for duplicate | ||||
|   // after trying to insert the check is necessary because a name | ||||
|   // should be unique across personal *and* collective bookmarks | ||||
|   def insertIfNotExists( | ||||
|       account: AccountId, | ||||
|       r: ConnectionIO[RQueryBookmark] | ||||
|   ): ConnectionIO[AddResult] = | ||||
|     for { | ||||
|       bm <- r | ||||
|       res <- | ||||
|         nameExists(account, bm.name).flatMap { | ||||
|           case true => | ||||
|             AddResult | ||||
|               .entityExists(s"A bookmark '${bm.name}' already exists.") | ||||
|               .pure[ConnectionIO] | ||||
|           case false => insert(bm).attempt.map(AddResult.fromUpdate) | ||||
|         } | ||||
|     } yield res | ||||
|  | ||||
|   def allForUser(account: AccountId): ConnectionIO[Vector[RQueryBookmark]] = { | ||||
|     val user = RUser.as("u") | ||||
|     val bm = RQueryBookmark.as("bm") | ||||
|  | ||||
|     val users = Select( | ||||
|       user.uid.s, | ||||
|       from(user), | ||||
|       user.cid === account.collective && user.login === account.user | ||||
|     ) | ||||
|     Select( | ||||
|       select(bm.all), | ||||
|       from(bm), | ||||
|       bm.cid === account.collective && (bm.userId.isNull || bm.userId.in(users)) | ||||
|     ).build.query[RQueryBookmark].to[Vector] | ||||
|   } | ||||
|  | ||||
|   def findByNameOrId( | ||||
|       account: AccountId, | ||||
|       nameOrId: String | ||||
|   ): ConnectionIO[Option[RQueryBookmark]] = { | ||||
|     val user = RUser.as("u") | ||||
|     val bm = RQueryBookmark.as("bm") | ||||
|  | ||||
|     val users = Select( | ||||
|       user.uid.s, | ||||
|       from(user), | ||||
|       user.cid === account.collective && user.login === account.user | ||||
|     ) | ||||
|     Select( | ||||
|       select(bm.all), | ||||
|       from(bm), | ||||
|       bm.cid === account.collective && | ||||
|         (bm.userId.isNull || bm.userId.in(users)) && | ||||
|         (bm.name === nameOrId || bm.id ==== nameOrId) | ||||
|     ).build.query[RQueryBookmark].option | ||||
|   } | ||||
| } | ||||
| @@ -138,6 +138,23 @@ object RShare { | ||||
|         .option | ||||
|     }) | ||||
|  | ||||
|   def findOneByCollective( | ||||
|       cid: Ident, | ||||
|       enabled: Option[Boolean], | ||||
|       nameOrId: String | ||||
|   ): ConnectionIO[Option[RShare]] = { | ||||
|     val s = RShare.as("s") | ||||
|     val u = RUser.as("u") | ||||
|  | ||||
|     Select( | ||||
|       select(s.all), | ||||
|       from(s).innerJoin(u, u.uid === s.userId), | ||||
|       u.cid === cid && | ||||
|         (s.name === nameOrId || s.id ==== nameOrId) &&? | ||||
|         enabled.map(e => s.enabled === e) | ||||
|     ).build.query[RShare].option | ||||
|   } | ||||
|  | ||||
|   def findAllByCollective( | ||||
|       cid: Ident, | ||||
|       ownerLogin: Option[Ident], | ||||
|   | ||||
| @@ -7,6 +7,8 @@ | ||||
| package docspell.store.records | ||||
|  | ||||
| import cats.data.NonEmptyList | ||||
| import cats.data.OptionT | ||||
| import cats.effect.Sync | ||||
|  | ||||
| import docspell.common._ | ||||
| import docspell.store.qb.DSL._ | ||||
| @@ -150,6 +152,13 @@ object RUser { | ||||
|       .query[Ident] | ||||
|       .option | ||||
|  | ||||
|   def getIdByAccount(account: AccountId): ConnectionIO[Ident] = | ||||
|     OptionT(findIdByAccount(account)).getOrElseF( | ||||
|       Sync[ConnectionIO].raiseError( | ||||
|         new Exception(s"No user found for: ${account.asString}") | ||||
|       ) | ||||
|     ) | ||||
|  | ||||
|   def updateLogin(accountId: AccountId): ConnectionIO[Int] = { | ||||
|     val t = Table(None) | ||||
|     def stmt(now: Timestamp) = | ||||
|   | ||||
| @@ -6,7 +6,8 @@ | ||||
|  | ||||
|  | ||||
| module Api exposing | ||||
|     ( addConcEquip | ||||
|     ( addBookmark | ||||
|     , addConcEquip | ||||
|     , addConcPerson | ||||
|     , addCorrOrg | ||||
|     , addCorrPerson | ||||
| @@ -15,6 +16,7 @@ module Api exposing | ||||
|     , addTag | ||||
|     , addTagsMultiple | ||||
|     , attachmentPreviewURL | ||||
|     , bookmarkNameExists | ||||
|     , cancelJob | ||||
|     , changeFolderName | ||||
|     , changePassword | ||||
| @@ -31,6 +33,7 @@ module Api exposing | ||||
|     , deleteAllItems | ||||
|     , deleteAttachment | ||||
|     , deleteAttachments | ||||
|     , deleteBookmark | ||||
|     , deleteCustomField | ||||
|     , deleteCustomValue | ||||
|     , deleteCustomValueMultiple | ||||
| @@ -52,6 +55,7 @@ module Api exposing | ||||
|     , disableOtp | ||||
|     , fileURL | ||||
|     , getAttachmentMeta | ||||
|     , getBookmarks | ||||
|     , getClientSettings | ||||
|     , getCollective | ||||
|     , getCollectiveSettings | ||||
| @@ -166,6 +170,7 @@ module Api exposing | ||||
|     , toggleTags | ||||
|     , twoFactor | ||||
|     , unconfirmMultiple | ||||
|     , updateBookmark | ||||
|     , updateHook | ||||
|     , updateNotifyDueItems | ||||
|     , updatePeriodicQuery | ||||
| @@ -182,6 +187,7 @@ module Api exposing | ||||
| import Api.Model.AttachmentMeta exposing (AttachmentMeta) | ||||
| import Api.Model.AuthResult exposing (AuthResult) | ||||
| import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Api.Model.CalEventCheck exposing (CalEventCheck) | ||||
| import Api.Model.CalEventCheckResult exposing (CalEventCheckResult) | ||||
| import Api.Model.Collective exposing (Collective) | ||||
| @@ -260,6 +266,7 @@ import Api.Model.User exposing (User) | ||||
| import Api.Model.UserList exposing (UserList) | ||||
| import Api.Model.UserPass exposing (UserPass) | ||||
| import Api.Model.VersionInfo exposing (VersionInfo) | ||||
| import Data.Bookmarks exposing (AllBookmarks, Bookmarks) | ||||
| import Data.ContactType exposing (ContactType) | ||||
| import Data.CustomFieldOrder exposing (CustomFieldOrder) | ||||
| import Data.EquipmentOrder exposing (EquipmentOrder) | ||||
| @@ -2261,7 +2268,7 @@ getClientSettings flags receive = | ||||
|                 Data.UiSettings.storedUiSettingsDecoder | ||||
|     in | ||||
|     Http2.authGet | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/clientSettings/webClient" | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/clientSettings/user/webClient" | ||||
|         , account = getAccount flags | ||||
|         , expect = Http.expectJson receive decoder | ||||
|         } | ||||
| @@ -2277,7 +2284,7 @@ saveClientSettings flags settings receive = | ||||
|             Data.UiSettings.storedUiSettingsEncode storedSettings | ||||
|     in | ||||
|     Http2.authPut | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/clientSettings/webClient" | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/clientSettings/user/webClient" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody encode | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
| @@ -2285,6 +2292,93 @@ saveClientSettings flags settings receive = | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Query Bookmarks | ||||
|  | ||||
|  | ||||
| bookmarkUri : Flags -> String | ||||
| bookmarkUri flags = | ||||
|     flags.config.baseUrl ++ "/api/v1/sec/querybookmark" | ||||
|  | ||||
|  | ||||
| getBookmarksTask : Flags -> Task.Task Http.Error Bookmarks | ||||
| getBookmarksTask flags = | ||||
|     Http2.authTask | ||||
|         { method = "GET" | ||||
|         , url = bookmarkUri flags | ||||
|         , account = getAccount flags | ||||
|         , body = Http.emptyBody | ||||
|         , resolver = Http2.jsonResolver Data.Bookmarks.bookmarksDecoder | ||||
|         , headers = [] | ||||
|         , timeout = Nothing | ||||
|         } | ||||
|  | ||||
|  | ||||
| getBookmarks : Flags -> (Result Http.Error AllBookmarks -> msg) -> Cmd msg | ||||
| getBookmarks flags receive = | ||||
|     let | ||||
|         bms = | ||||
|             getBookmarksTask flags | ||||
|  | ||||
|         shares = | ||||
|             getSharesTask flags "" False | ||||
|  | ||||
|         activeShare s = | ||||
|             s.enabled && s.name /= Nothing | ||||
|  | ||||
|         combine bm bs = | ||||
|             AllBookmarks (Data.Bookmarks.sort bm) (List.filter activeShare bs.items) | ||||
|     in | ||||
|     Task.map2 combine bms shares | ||||
|         |> Task.attempt receive | ||||
|  | ||||
|  | ||||
| addBookmark : Flags -> BookmarkedQuery -> (Result Http.Error BasicResult -> msg) -> Cmd msg | ||||
| addBookmark flags model receive = | ||||
|     Http2.authPost | ||||
|         { url = bookmarkUri flags | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.BookmarkedQuery.encode model) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| updateBookmark : Flags -> BookmarkedQuery -> (Result Http.Error BasicResult -> msg) -> Cmd msg | ||||
| updateBookmark flags model receive = | ||||
|     Http2.authPut | ||||
|         { url = bookmarkUri flags | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.BookmarkedQuery.encode model) | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| bookmarkNameExistsTask : Flags -> String -> Task.Task Http.Error Bool | ||||
| bookmarkNameExistsTask flags name = | ||||
|     let | ||||
|         load = | ||||
|             getBookmarksTask flags | ||||
|  | ||||
|         exists current = | ||||
|             Data.Bookmarks.exists name current | ||||
|     in | ||||
|     Task.map exists load | ||||
|  | ||||
|  | ||||
| bookmarkNameExists : Flags -> String -> (Result Http.Error Bool -> msg) -> Cmd msg | ||||
| bookmarkNameExists flags name receive = | ||||
|     bookmarkNameExistsTask flags name |> Task.attempt receive | ||||
|  | ||||
|  | ||||
| deleteBookmark : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg | ||||
| deleteBookmark flags id receive = | ||||
|     Http2.authDelete | ||||
|         { url = bookmarkUri flags ++ "/" ++ id | ||||
|         , account = getAccount flags | ||||
|         , expect = Http.expectJson receive Api.Model.BasicResult.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
|  | ||||
| --- OTP | ||||
|  | ||||
|  | ||||
| @@ -2331,10 +2425,12 @@ disableOtp flags otp receive = | ||||
| --- Share | ||||
|  | ||||
|  | ||||
| getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg | ||||
| getShares flags query owning receive = | ||||
|     Http2.authGet | ||||
|         { url = | ||||
| getSharesTask : Flags -> String -> Bool -> Task.Task Http.Error ShareList | ||||
| getSharesTask flags query owning = | ||||
|     Http2.authTask | ||||
|         { method = | ||||
|             "GET" | ||||
|         , url = | ||||
|             flags.config.baseUrl | ||||
|                 ++ "/api/v1/sec/share?q=" | ||||
|                 ++ Url.percentEncode query | ||||
| @@ -2345,10 +2441,18 @@ getShares flags query owning receive = | ||||
|                         "" | ||||
|                    ) | ||||
|         , account = getAccount flags | ||||
|         , expect = Http.expectJson receive Api.Model.ShareList.decoder | ||||
|         , body = Http.emptyBody | ||||
|         , resolver = Http2.jsonResolver Api.Model.ShareList.decoder | ||||
|         , headers = [] | ||||
|         , timeout = Nothing | ||||
|         } | ||||
|  | ||||
|  | ||||
| getShares : Flags -> String -> Bool -> (Result Http.Error ShareList -> msg) -> Cmd msg | ||||
| getShares flags query owning receive = | ||||
|     getSharesTask flags query owning |> Task.attempt receive | ||||
|  | ||||
|  | ||||
| getShare : Flags -> String -> (Result Http.Error ShareDetail -> msg) -> Cmd msg | ||||
| getShare flags id receive = | ||||
|     Http2.authGet | ||||
|   | ||||
| @@ -592,12 +592,12 @@ updateHome texts lmsg model = | ||||
| updateManageData : Page.ManageData.Data.Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| updateManageData lmsg model = | ||||
|     let | ||||
|         ( lm, lc ) = | ||||
|         ( lm, lc, ls ) = | ||||
|             Page.ManageData.Update.update model.flags lmsg model.manageDataModel | ||||
|     in | ||||
|     ( { model | manageDataModel = lm } | ||||
|     , Cmd.map ManageDataMsg lc | ||||
|     , Sub.none | ||||
|     , Sub.map ManageDataMsg ls | ||||
|     ) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										208
									
								
								modules/webapp/src/main/elm/Comp/BookmarkChooser.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,208 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkChooser exposing | ||||
|     ( Model | ||||
|     , Msg | ||||
|     , Selection | ||||
|     , emptySelection | ||||
|     , getQueries | ||||
|     , init | ||||
|     , isEmpty | ||||
|     , isEmptySelection | ||||
|     , update | ||||
|     , view | ||||
|     ) | ||||
|  | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Api.Model.ShareDetail exposing (ShareDetail) | ||||
| import Data.Bookmarks exposing (AllBookmarks) | ||||
| import Data.Icons as Icons | ||||
| import Html exposing (Html, a, div, i, label, span, text) | ||||
| import Html.Attributes exposing (class, classList, href) | ||||
| import Html.Events exposing (onClick) | ||||
| import Messages.Comp.BookmarkChooser exposing (Texts) | ||||
| import Set exposing (Set) | ||||
|  | ||||
|  | ||||
| type alias Model = | ||||
|     { all : AllBookmarks | ||||
|     } | ||||
|  | ||||
|  | ||||
| init : AllBookmarks -> Model | ||||
| init all = | ||||
|     { all = all | ||||
|     } | ||||
|  | ||||
|  | ||||
| isEmpty : Model -> Bool | ||||
| isEmpty model = | ||||
|     model.all == Data.Bookmarks.empty | ||||
|  | ||||
|  | ||||
| type alias Selection = | ||||
|     { bookmarks : Set String | ||||
|     , shares : Set String | ||||
|     } | ||||
|  | ||||
|  | ||||
| emptySelection : Selection | ||||
| emptySelection = | ||||
|     { bookmarks = Set.empty, shares = Set.empty } | ||||
|  | ||||
|  | ||||
| isEmptySelection : Selection -> Bool | ||||
| isEmptySelection sel = | ||||
|     sel == emptySelection | ||||
|  | ||||
|  | ||||
| type Kind | ||||
|     = Bookmark | ||||
|     | Share | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = Toggle Kind String | ||||
|  | ||||
|  | ||||
| getQueries : Model -> Selection -> List BookmarkedQuery | ||||
| getQueries model sel = | ||||
|     let | ||||
|         member set bm = | ||||
|             Set.member bm.id set | ||||
|  | ||||
|         filterBookmarks f bms = | ||||
|             List.filter f bms |> List.map identity | ||||
|     in | ||||
|     List.concat | ||||
|         [ filterBookmarks (member sel.bookmarks) model.all.bookmarks | ||||
|         , List.map shareToBookmark model.all.shares | ||||
|             |> List.filter (member sel.shares) | ||||
|         ] | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Update | ||||
|  | ||||
|  | ||||
| update : Msg -> Model -> Selection -> ( Model, Selection ) | ||||
| update msg model current = | ||||
|     let | ||||
|         toggle name set = | ||||
|             if Set.member name set then | ||||
|                 Set.remove name set | ||||
|  | ||||
|             else | ||||
|                 Set.insert name set | ||||
|     in | ||||
|     case msg of | ||||
|         Toggle kind id -> | ||||
|             case kind of | ||||
|                 Bookmark -> | ||||
|                     ( model, { current | bookmarks = toggle id current.bookmarks } ) | ||||
|  | ||||
|                 Share -> | ||||
|                     ( model, { current | shares = toggle id current.shares } ) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| view : Texts -> Model -> Selection -> Html Msg | ||||
| view texts model selection = | ||||
|     let | ||||
|         ( user, coll ) = | ||||
|             List.partition .personal model.all.bookmarks | ||||
|     in | ||||
|     div [ class "flex flex-col" ] | ||||
|         [ userBookmarks texts user selection | ||||
|         , collBookmarks texts coll selection | ||||
|         , shares texts model selection | ||||
|         ] | ||||
|  | ||||
|  | ||||
| titleDiv : String -> Html msg | ||||
| titleDiv label = | ||||
|     div [ class "text-sm opacity-75 py-0.5 italic" ] | ||||
|         [ text label | ||||
|  | ||||
|         --, text " ──" | ||||
|         ] | ||||
|  | ||||
|  | ||||
| userBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg | ||||
| userBookmarks texts model sel = | ||||
|     div | ||||
|         [ class "mb-2" | ||||
|         , classList [ ( "hidden", model == [] ) ] | ||||
|         ] | ||||
|         [ titleDiv texts.userLabel | ||||
|         , div [ class "flex flex-col space-y-2 md:space-y-1" ] | ||||
|             (List.map (mkItem "fa fa-bookmark" sel Bookmark) model) | ||||
|         ] | ||||
|  | ||||
|  | ||||
| collBookmarks : Texts -> List BookmarkedQuery -> Selection -> Html Msg | ||||
| collBookmarks texts model sel = | ||||
|     div | ||||
|         [ class "mb-2" | ||||
|         , classList [ ( "hidden", [] == model ) ] | ||||
|         ] | ||||
|         [ titleDiv texts.collectiveLabel | ||||
|         , div [ class "flex flex-col space-y-2 md:space-y-1" ] | ||||
|             (List.map (mkItem "fa fa-bookmark font-light" sel Bookmark) model) | ||||
|         ] | ||||
|  | ||||
|  | ||||
| shares : Texts -> Model -> Selection -> Html Msg | ||||
| shares texts model sel = | ||||
|     let | ||||
|         bms = | ||||
|             List.map shareToBookmark model.all.shares | ||||
|     in | ||||
|     div | ||||
|         [ class "" | ||||
|         , classList [ ( "hidden", List.isEmpty bms ) ] | ||||
|         ] | ||||
|         [ titleDiv texts.shareLabel | ||||
|         , div [ class "flex flex-col space-y-2 md:space-y-1" ] | ||||
|             (List.map (mkItem Icons.share sel Share) bms) | ||||
|         ] | ||||
|  | ||||
|  | ||||
| mkItem : String -> Selection -> Kind -> BookmarkedQuery -> Html Msg | ||||
| mkItem icon sel kind bm = | ||||
|     a | ||||
|         [ class "flex flex-row items-center rounded px-1 py-1 hover:bg-blue-100 dark:hover:bg-slate-600" | ||||
|         , href "#" | ||||
|         , onClick (Toggle kind bm.id) | ||||
|         ] | ||||
|         [ if isSelected sel kind bm.id then | ||||
|             i [ class "fa fa-check" ] [] | ||||
|  | ||||
|           else | ||||
|             i [ class icon ] [] | ||||
|         , span [ class "ml-2" ] [ text bm.name ] | ||||
|         ] | ||||
|  | ||||
|  | ||||
| isSelected : Selection -> Kind -> String -> Bool | ||||
| isSelected sel kind id = | ||||
|     Set.member id <| | ||||
|         case kind of | ||||
|             Bookmark -> | ||||
|                 sel.bookmarks | ||||
|  | ||||
|             Share -> | ||||
|                 sel.shares | ||||
|  | ||||
|  | ||||
| shareToBookmark : ShareDetail -> BookmarkedQuery | ||||
| shareToBookmark share = | ||||
|     BookmarkedQuery share.id (Maybe.withDefault "-" share.name) share.name share.query False 0 | ||||
							
								
								
									
										177
									
								
								modules/webapp/src/main/elm/Comp/BookmarkDropdown.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkDropdown exposing (Item(..), Model, Msg, getSelected, getSelectedId, init, initWith, update, view) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Api.Model.ShareDetail exposing (ShareDetail) | ||||
| import Comp.Dropdown exposing (Option) | ||||
| import Data.Bookmarks exposing (AllBookmarks) | ||||
| import Data.DropdownStyle | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (Html) | ||||
| import Http | ||||
| import Messages.Comp.BookmarkDropdown exposing (Texts) | ||||
| import Util.List | ||||
|  | ||||
|  | ||||
| type Model | ||||
|     = Model (Comp.Dropdown.Model Item) | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = DropdownMsg (Comp.Dropdown.Msg Item) | ||||
|     | GetBookmarksResp (Maybe String) (Result Http.Error AllBookmarks) | ||||
|  | ||||
|  | ||||
| initCmd : Flags -> Maybe String -> Cmd Msg | ||||
| initCmd flags selected = | ||||
|     Api.getBookmarks flags (GetBookmarksResp selected) | ||||
|  | ||||
|  | ||||
| type Item | ||||
|     = BM BookmarkedQuery | ||||
|     | Share ShareDetail | ||||
|  | ||||
|  | ||||
| toItems : AllBookmarks -> List Item | ||||
| toItems all = | ||||
|     List.map BM all.bookmarks | ||||
|         ++ List.map Share all.shares | ||||
|  | ||||
|  | ||||
| initWith : AllBookmarks -> Maybe String -> Model | ||||
| initWith bms selected = | ||||
|     let | ||||
|         items = | ||||
|             toItems bms | ||||
|  | ||||
|         findSel id = | ||||
|             Util.List.find | ||||
|                 (\b -> | ||||
|                     case b of | ||||
|                         BM m -> | ||||
|                             m.id == id | ||||
|  | ||||
|                         Share s -> | ||||
|                             s.id == id | ||||
|                 ) | ||||
|                 items | ||||
|     in | ||||
|     Model <| | ||||
|         Comp.Dropdown.makeSingleList | ||||
|             { options = items, selected = Maybe.andThen findSel selected } | ||||
|  | ||||
|  | ||||
| init : Flags -> Maybe String -> ( Model, Cmd Msg ) | ||||
| init flags selected = | ||||
|     ( Model Comp.Dropdown.makeSingle, initCmd flags selected ) | ||||
|  | ||||
|  | ||||
| getSelected : Model -> Maybe Item | ||||
| getSelected model = | ||||
|     case model of | ||||
|         Model dm -> | ||||
|             Comp.Dropdown.getSelected dm | ||||
|                 |> List.head | ||||
|  | ||||
|  | ||||
| getSelectedId : Model -> Maybe String | ||||
| getSelectedId model = | ||||
|     let | ||||
|         id item = | ||||
|             case item of | ||||
|                 BM b -> | ||||
|                     b.id | ||||
|  | ||||
|                 Share s -> | ||||
|                     s.id | ||||
|     in | ||||
|     getSelected model |> Maybe.map id | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Update | ||||
|  | ||||
|  | ||||
| update : Msg -> Model -> ( Model, Cmd Msg ) | ||||
| update msg model = | ||||
|     let | ||||
|         dmodel = | ||||
|             case model of | ||||
|                 Model a -> | ||||
|                     a | ||||
|     in | ||||
|     case msg of | ||||
|         GetBookmarksResp sel (Ok all) -> | ||||
|             ( initWith all sel, Cmd.none ) | ||||
|  | ||||
|         GetBookmarksResp _ (Err err) -> | ||||
|             ( model, Cmd.none ) | ||||
|  | ||||
|         DropdownMsg lm -> | ||||
|             let | ||||
|                 ( dm, dc ) = | ||||
|                     Comp.Dropdown.update lm dmodel | ||||
|             in | ||||
|             ( Model dm, Cmd.map DropdownMsg dc ) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| itemOption : Texts -> Item -> Option | ||||
| itemOption texts item = | ||||
|     case item of | ||||
|         BM b -> | ||||
|             { text = b.name | ||||
|             , additional = | ||||
|                 if b.personal then | ||||
|                     texts.personal | ||||
|  | ||||
|                 else | ||||
|                     texts.collective | ||||
|             } | ||||
|  | ||||
|         Share s -> | ||||
|             { text = Maybe.withDefault "-" s.name, additional = texts.share } | ||||
|  | ||||
|  | ||||
| itemColor : Item -> String | ||||
| itemColor item = | ||||
|     case item of | ||||
|         BM b -> | ||||
|             if b.personal then | ||||
|                 "text-cyan-600 dark:text-indigo-300" | ||||
|  | ||||
|             else | ||||
|                 "text-sky-600 dark:text-violet-300" | ||||
|  | ||||
|         Share _ -> | ||||
|             "text-blue-600 dark:text-purple-300" | ||||
|  | ||||
|  | ||||
| view : Texts -> UiSettings -> Model -> Html Msg | ||||
| view texts settings model = | ||||
|     let | ||||
|         viewSettings = | ||||
|             { makeOption = itemOption texts | ||||
|             , placeholder = texts.placeholder | ||||
|             , labelColor = \a -> \_ -> itemColor a | ||||
|             , style = Data.DropdownStyle.mainStyle | ||||
|             } | ||||
|  | ||||
|         dm = | ||||
|             case model of | ||||
|                 Model a -> | ||||
|                     a | ||||
|     in | ||||
|     Html.map DropdownMsg | ||||
|         (Comp.Dropdown.viewSingle2 viewSettings settings dm) | ||||
							
								
								
									
										391
									
								
								modules/webapp/src/main/elm/Comp/BookmarkManage.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,391 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkManage exposing (Model, Msg, init, loadBookmarks, update, view) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Comp.Basic as B | ||||
| import Comp.BookmarkQueryForm | ||||
| import Comp.BookmarkTable | ||||
| import Comp.ItemDetail.Model exposing (Msg(..)) | ||||
| import Comp.MenuBar as MB | ||||
| import Data.Bookmarks exposing (AllBookmarks) | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onClick) | ||||
| import Http | ||||
| import Messages.Comp.BookmarkManage exposing (Texts) | ||||
| import Page exposing (Page(..)) | ||||
| import Styles as S | ||||
|  | ||||
|  | ||||
| type FormError | ||||
|     = FormErrorNone | ||||
|     | FormErrorHttp Http.Error | ||||
|     | FormErrorInvalid | ||||
|     | FormErrorSubmit String | ||||
|  | ||||
|  | ||||
| type ViewMode | ||||
|     = Table | ||||
|     | Form | ||||
|  | ||||
|  | ||||
| type DeleteConfirm | ||||
|     = DeleteConfirmOff | ||||
|     | DeleteConfirmOn | ||||
|  | ||||
|  | ||||
| type alias Model = | ||||
|     { viewMode : ViewMode | ||||
|     , bookmarks : AllBookmarks | ||||
|     , formModel : Comp.BookmarkQueryForm.Model | ||||
|     , loading : Bool | ||||
|     , formError : FormError | ||||
|     , deleteConfirm : DeleteConfirm | ||||
|     } | ||||
|  | ||||
|  | ||||
| init : Flags -> ( Model, Cmd Msg ) | ||||
| init flags = | ||||
|     let | ||||
|         ( fm, fc ) = | ||||
|             Comp.BookmarkQueryForm.init | ||||
|     in | ||||
|     ( { viewMode = Table | ||||
|       , bookmarks = Data.Bookmarks.empty | ||||
|       , formModel = fm | ||||
|       , loading = False | ||||
|       , formError = FormErrorNone | ||||
|       , deleteConfirm = DeleteConfirmOff | ||||
|       } | ||||
|     , Cmd.batch | ||||
|         [ Cmd.map FormMsg fc | ||||
|         , Api.getBookmarks flags LoadBookmarksResp | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = LoadBookmarks | ||||
|     | TableMsg Comp.BookmarkTable.Msg | ||||
|     | FormMsg Comp.BookmarkQueryForm.Msg | ||||
|     | InitNewBookmark | ||||
|     | SetViewMode ViewMode | ||||
|     | Submit | ||||
|     | RequestDelete | ||||
|     | CancelDelete | ||||
|     | DeleteBookmarkNow String | ||||
|     | LoadBookmarksResp (Result Http.Error AllBookmarks) | ||||
|     | AddBookmarkResp (Result Http.Error BasicResult) | ||||
|     | UpdateBookmarkResp (Result Http.Error BasicResult) | ||||
|     | DeleteBookmarkResp (Result Http.Error BasicResult) | ||||
|  | ||||
|  | ||||
| loadBookmarks : Msg | ||||
| loadBookmarks = | ||||
|     LoadBookmarks | ||||
|  | ||||
|  | ||||
|  | ||||
| --- update | ||||
|  | ||||
|  | ||||
| update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| update flags msg model = | ||||
|     case msg of | ||||
|         InitNewBookmark -> | ||||
|             let | ||||
|                 ( bm, bc ) = | ||||
|                     Comp.BookmarkQueryForm.init | ||||
|  | ||||
|                 nm = | ||||
|                     { model | ||||
|                         | viewMode = Form | ||||
|                         , formError = FormErrorNone | ||||
|                         , formModel = bm | ||||
|                     } | ||||
|             in | ||||
|             ( nm, Cmd.map FormMsg bc, Sub.none ) | ||||
|  | ||||
|         SetViewMode vm -> | ||||
|             ( { model | viewMode = vm, formError = FormErrorNone } | ||||
|             , if vm == Table then | ||||
|                 Api.getBookmarks flags LoadBookmarksResp | ||||
|  | ||||
|               else | ||||
|                 Cmd.none | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         FormMsg lm -> | ||||
|             let | ||||
|                 ( fm, fc, fs ) = | ||||
|                     Comp.BookmarkQueryForm.update flags lm model.formModel | ||||
|             in | ||||
|             ( { model | formModel = fm, formError = FormErrorNone } | ||||
|             , Cmd.map FormMsg fc | ||||
|             , Sub.map FormMsg fs | ||||
|             ) | ||||
|  | ||||
|         TableMsg lm -> | ||||
|             let | ||||
|                 action = | ||||
|                     Comp.BookmarkTable.update lm | ||||
|             in | ||||
|             case action of | ||||
|                 Comp.BookmarkTable.Edit bookmark -> | ||||
|                     let | ||||
|                         ( bm, bc ) = | ||||
|                             Comp.BookmarkQueryForm.initWith bookmark | ||||
|                     in | ||||
|                     ( { model | ||||
|                         | viewMode = Form | ||||
|                         , formError = FormErrorNone | ||||
|                         , formModel = bm | ||||
|                       } | ||||
|                     , Cmd.map FormMsg bc | ||||
|                     , Sub.none | ||||
|                     ) | ||||
|  | ||||
|         RequestDelete -> | ||||
|             ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         CancelDelete -> | ||||
|             ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         DeleteBookmarkNow id -> | ||||
|             ( { model | deleteConfirm = DeleteConfirmOff, loading = True } | ||||
|             , Api.deleteBookmark flags id DeleteBookmarkResp | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         LoadBookmarks -> | ||||
|             ( { model | loading = True } | ||||
|             , Api.getBookmarks flags LoadBookmarksResp | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         LoadBookmarksResp (Ok list) -> | ||||
|             ( { model | loading = False, bookmarks = list, formError = FormErrorNone } | ||||
|             , Cmd.none | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         LoadBookmarksResp (Err err) -> | ||||
|             ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         Submit -> | ||||
|             case Comp.BookmarkQueryForm.get model.formModel of | ||||
|                 Just data -> | ||||
|                     if data.id /= "" then | ||||
|                         ( { model | loading = True }, Api.updateBookmark flags data AddBookmarkResp, Sub.none ) | ||||
|  | ||||
|                     else | ||||
|                         ( { model | loading = True }, Api.addBookmark flags data AddBookmarkResp, Sub.none ) | ||||
|  | ||||
|                 Nothing -> | ||||
|                     ( { model | formError = FormErrorInvalid }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         AddBookmarkResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 ( { model | loading = True, viewMode = Table }, Api.getBookmarks flags LoadBookmarksResp, Sub.none ) | ||||
|  | ||||
|             else | ||||
|                 ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         AddBookmarkResp (Err err) -> | ||||
|             ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         UpdateBookmarkResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 ( model, Api.getBookmarks flags LoadBookmarksResp, Sub.none ) | ||||
|  | ||||
|             else | ||||
|                 ( { model | loading = False, formError = FormErrorSubmit res.message }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         UpdateBookmarkResp (Err err) -> | ||||
|             ( { model | loading = False, formError = FormErrorHttp err }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         DeleteBookmarkResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 update flags (SetViewMode Table) { model | loading = False } | ||||
|  | ||||
|             else | ||||
|                 ( { model | formError = FormErrorSubmit res.message, loading = False }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         DeleteBookmarkResp (Err err) -> | ||||
|             ( { model | formError = FormErrorHttp err, loading = False }, Cmd.none, Sub.none ) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- view | ||||
|  | ||||
|  | ||||
| view : Texts -> UiSettings -> Flags -> Model -> Html Msg | ||||
| view texts settings flags model = | ||||
|     if model.viewMode == Table then | ||||
|         viewTable texts model | ||||
|  | ||||
|     else | ||||
|         viewForm texts settings flags model | ||||
|  | ||||
|  | ||||
| viewTable : Texts -> Model -> Html Msg | ||||
| viewTable texts model = | ||||
|     let | ||||
|         ( user, coll ) = | ||||
|             List.partition .personal model.bookmarks.bookmarks | ||||
|     in | ||||
|     div [ class "flex flex-col" ] | ||||
|         [ MB.view | ||||
|             { start = | ||||
|                 [] | ||||
|             , end = | ||||
|                 [ MB.PrimaryButton | ||||
|                     { tagger = InitNewBookmark | ||||
|                     , title = texts.createNewBookmark | ||||
|                     , icon = Just "fa fa-plus" | ||||
|                     , label = texts.newBookmark | ||||
|                     } | ||||
|                 ] | ||||
|             , rootClasses = "mb-4" | ||||
|             } | ||||
|         , div | ||||
|             [ class "flex flex-col" | ||||
|             , classList [ ( "hidden", user == [] ) ] | ||||
|             ] | ||||
|             [ h3 [ class S.header3 ] | ||||
|                 [ text texts.userBookmarks ] | ||||
|             , Html.map TableMsg | ||||
|                 (Comp.BookmarkTable.view texts.bookmarkTable user) | ||||
|             ] | ||||
|         , div | ||||
|             [ class "flex flex-col mt-3" | ||||
|             , classList [ ( "hidden", coll == [] ) ] | ||||
|             ] | ||||
|             [ h3 [ class S.header3 ] | ||||
|                 [ text texts.collectiveBookmarks ] | ||||
|             , Html.map TableMsg | ||||
|                 (Comp.BookmarkTable.view texts.bookmarkTable coll) | ||||
|             ] | ||||
|         , B.loadingDimmer | ||||
|             { label = "" | ||||
|             , active = model.loading | ||||
|             } | ||||
|         ] | ||||
|  | ||||
|  | ||||
| viewForm : Texts -> UiSettings -> Flags -> Model -> Html Msg | ||||
| viewForm texts _ _ model = | ||||
|     let | ||||
|         newBookmark = | ||||
|             model.formModel.bookmark.id == "" | ||||
|  | ||||
|         isValid = | ||||
|             Comp.BookmarkQueryForm.get model.formModel /= Nothing | ||||
|     in | ||||
|     div [] | ||||
|         [ Html.form [] | ||||
|             [ if newBookmark then | ||||
|                 h1 [ class S.header2 ] | ||||
|                     [ text texts.createNewBookmark | ||||
|                     ] | ||||
|  | ||||
|               else | ||||
|                 h1 [ class S.header2 ] | ||||
|                     [ text (Maybe.withDefault "" model.formModel.name) | ||||
|                     ] | ||||
|             , MB.view | ||||
|                 { start = | ||||
|                     [ MB.CustomElement <| | ||||
|                         B.primaryButton | ||||
|                             { handler = onClick Submit | ||||
|                             , title = texts.basics.submitThisForm | ||||
|                             , icon = "fa fa-save" | ||||
|                             , label = texts.basics.submit | ||||
|                             , disabled = not isValid | ||||
|                             , attrs = [ href "#" ] | ||||
|                             } | ||||
|                     , MB.SecondaryButton | ||||
|                         { tagger = SetViewMode Table | ||||
|                         , title = texts.basics.backToList | ||||
|                         , icon = Just "fa fa-arrow-left" | ||||
|                         , label = texts.basics.cancel | ||||
|                         } | ||||
|                     ] | ||||
|                 , end = | ||||
|                     if not newBookmark then | ||||
|                         [ MB.DeleteButton | ||||
|                             { tagger = RequestDelete | ||||
|                             , title = texts.deleteThisBookmark | ||||
|                             , icon = Just "fa fa-trash" | ||||
|                             , label = texts.basics.delete | ||||
|                             } | ||||
|                         ] | ||||
|  | ||||
|                     else | ||||
|                         [] | ||||
|                 , rootClasses = "mb-4" | ||||
|                 } | ||||
|             , div | ||||
|                 [ classList | ||||
|                     [ ( "hidden", model.formError == FormErrorNone ) | ||||
|                     ] | ||||
|                 , class "my-2" | ||||
|                 , class S.errorMessage | ||||
|                 ] | ||||
|                 [ case model.formError of | ||||
|                     FormErrorNone -> | ||||
|                         text "" | ||||
|  | ||||
|                     FormErrorHttp err -> | ||||
|                         text (texts.httpError err) | ||||
|  | ||||
|                     FormErrorInvalid -> | ||||
|                         text texts.correctFormErrors | ||||
|  | ||||
|                     FormErrorSubmit m -> | ||||
|                         text m | ||||
|                 ] | ||||
|             , div [] | ||||
|                 [ Html.map FormMsg (Comp.BookmarkQueryForm.view texts.bookmarkForm model.formModel) | ||||
|                 ] | ||||
|             , B.loadingDimmer | ||||
|                 { active = model.loading | ||||
|                 , label = texts.basics.loading | ||||
|                 } | ||||
|             , B.contentDimmer | ||||
|                 (model.deleteConfirm == DeleteConfirmOn) | ||||
|                 (div [ class "flex flex-col" ] | ||||
|                     [ div [ class "text-lg" ] | ||||
|                         [ i [ class "fa fa-info-circle mr-2" ] [] | ||||
|                         , text texts.reallyDeleteBookmark | ||||
|                         ] | ||||
|                     , div [ class "mt-4 flex flex-row items-center" ] | ||||
|                         [ B.deleteButton | ||||
|                             { label = texts.basics.yes | ||||
|                             , icon = "fa fa-check" | ||||
|                             , disabled = False | ||||
|                             , handler = onClick (DeleteBookmarkNow model.formModel.bookmark.id) | ||||
|                             , attrs = [ href "#" ] | ||||
|                             } | ||||
|                         , B.secondaryButton | ||||
|                             { label = texts.basics.no | ||||
|                             , icon = "fa fa-times" | ||||
|                             , disabled = False | ||||
|                             , handler = onClick CancelDelete | ||||
|                             , attrs = [ href "#", class "ml-2" ] | ||||
|                             } | ||||
|                         ] | ||||
|                     ] | ||||
|                 ) | ||||
|             ] | ||||
|         ] | ||||
							
								
								
									
										252
									
								
								modules/webapp/src/main/elm/Comp/BookmarkQueryForm.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,252 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkQueryForm exposing (Model, Msg, get, init, initQuery, initWith, update, view) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Comp.Basic as B | ||||
| import Comp.PowerSearchInput | ||||
| import Data.Flags exposing (Flags) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onCheck, onInput) | ||||
| import Http | ||||
| import Messages.Comp.BookmarkQueryForm exposing (Texts) | ||||
| import Styles as S | ||||
| import Throttle exposing (Throttle) | ||||
| import Time | ||||
| import Util.Maybe | ||||
|  | ||||
|  | ||||
| type alias Model = | ||||
|     { bookmark : BookmarkedQuery | ||||
|     , name : Maybe String | ||||
|     , nameExists : Bool | ||||
|     , queryModel : Comp.PowerSearchInput.Model | ||||
|     , isPersonal : Bool | ||||
|     , nameExistsThrottle : Throttle Msg | ||||
|     } | ||||
|  | ||||
|  | ||||
| initQuery : String -> ( Model, Cmd Msg ) | ||||
| initQuery q = | ||||
|     let | ||||
|         res = | ||||
|             Comp.PowerSearchInput.update | ||||
|                 (Comp.PowerSearchInput.setSearchString q) | ||||
|                 Comp.PowerSearchInput.init | ||||
|     in | ||||
|     ( { bookmark = Api.Model.BookmarkedQuery.empty | ||||
|       , name = Nothing | ||||
|       , nameExists = False | ||||
|       , queryModel = res.model | ||||
|       , isPersonal = True | ||||
|       , nameExistsThrottle = Throttle.create 1 | ||||
|       } | ||||
|     , Cmd.batch | ||||
|         [ Cmd.map QueryMsg res.cmd | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| init : ( Model, Cmd Msg ) | ||||
| init = | ||||
|     initQuery "" | ||||
|  | ||||
|  | ||||
| initWith : BookmarkedQuery -> ( Model, Cmd Msg ) | ||||
| initWith bm = | ||||
|     let | ||||
|         ( m, c ) = | ||||
|             initQuery bm.query | ||||
|     in | ||||
|     ( { m | ||||
|         | name = Just bm.name | ||||
|         , isPersonal = bm.personal | ||||
|         , bookmark = bm | ||||
|       } | ||||
|     , c | ||||
|     ) | ||||
|  | ||||
|  | ||||
| isValid : Model -> Bool | ||||
| isValid model = | ||||
|     List.all identity | ||||
|         [ Comp.PowerSearchInput.isValid model.queryModel | ||||
|         , model.name /= Nothing | ||||
|         , not model.nameExists | ||||
|         ] | ||||
|  | ||||
|  | ||||
| get : Model -> Maybe BookmarkedQuery | ||||
| get model = | ||||
|     let | ||||
|         qStr = | ||||
|             Maybe.withDefault "" model.queryModel.input | ||||
|  | ||||
|         bm = | ||||
|             model.bookmark | ||||
|     in | ||||
|     if isValid model then | ||||
|         Just | ||||
|             { bm | ||||
|                 | query = qStr | ||||
|                 , name = Maybe.withDefault "" model.name | ||||
|                 , personal = model.isPersonal | ||||
|             } | ||||
|  | ||||
|     else | ||||
|         Nothing | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = SetName String | ||||
|     | QueryMsg Comp.PowerSearchInput.Msg | ||||
|     | SetPersonal Bool | ||||
|     | NameExistsResp (Result Http.Error Bool) | ||||
|     | UpdateThrottle | ||||
|  | ||||
|  | ||||
| update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| update flags msg model = | ||||
|     let | ||||
|         nameCheck1 name = | ||||
|             Api.bookmarkNameExists flags name NameExistsResp | ||||
|  | ||||
|         throttleSub = | ||||
|             Throttle.ifNeeded | ||||
|                 (Time.every 150 (\_ -> UpdateThrottle)) | ||||
|                 model.nameExistsThrottle | ||||
|     in | ||||
|     case msg of | ||||
|         SetName n -> | ||||
|             let | ||||
|                 ( newThrottle, cmd ) = | ||||
|                     Throttle.try (nameCheck1 n) model.nameExistsThrottle | ||||
|             in | ||||
|             ( { model | name = Util.Maybe.fromString n, nameExistsThrottle = newThrottle } | ||||
|             , cmd | ||||
|             , throttleSub | ||||
|             ) | ||||
|  | ||||
|         SetPersonal flag -> | ||||
|             ( { model | isPersonal = flag }, Cmd.none, Sub.none ) | ||||
|  | ||||
|         QueryMsg lm -> | ||||
|             let | ||||
|                 res = | ||||
|                     Comp.PowerSearchInput.update lm model.queryModel | ||||
|             in | ||||
|             ( { model | queryModel = res.model } | ||||
|             , Cmd.map QueryMsg res.cmd | ||||
|             , Sub.map QueryMsg res.subs | ||||
|             ) | ||||
|  | ||||
|         NameExistsResp (Ok flag) -> | ||||
|             ( { model | nameExists = flag } | ||||
|             , Cmd.none | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         NameExistsResp (Err err) -> | ||||
|             ( model, Cmd.none, Sub.none ) | ||||
|  | ||||
|         UpdateThrottle -> | ||||
|             let | ||||
|                 ( newThrottle, cmd ) = | ||||
|                     Throttle.update model.nameExistsThrottle | ||||
|             in | ||||
|             ( { model | nameExistsThrottle = newThrottle } | ||||
|             , cmd | ||||
|             , throttleSub | ||||
|             ) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| view : Texts -> Model -> Html Msg | ||||
| view texts model = | ||||
|     let | ||||
|         queryInput = | ||||
|             div | ||||
|                 [ class "relative flex flex-grow flex-row" ] | ||||
|                 [ Html.map QueryMsg | ||||
|                     (Comp.PowerSearchInput.viewInput | ||||
|                         { placeholder = texts.queryLabel | ||||
|                         , extraAttrs = [] | ||||
|                         } | ||||
|                         model.queryModel | ||||
|                     ) | ||||
|                 , Html.map QueryMsg | ||||
|                     (Comp.PowerSearchInput.viewResult [] model.queryModel) | ||||
|                 ] | ||||
|     in | ||||
|     div | ||||
|         [ class "flex flex-col" ] | ||||
|         [ div [ class "mb-2" ] | ||||
|             [ label | ||||
|                 [ for "bookmark-name" | ||||
|                 , class S.inputLabel | ||||
|                 ] | ||||
|                 [ text texts.basics.name | ||||
|                 , B.inputRequired | ||||
|                 ] | ||||
|             , input | ||||
|                 [ type_ "text" | ||||
|                 , onInput SetName | ||||
|                 , placeholder texts.basics.name | ||||
|                 , value <| Maybe.withDefault "" model.name | ||||
|                 , id "bookmark-name" | ||||
|                 , class S.textInput | ||||
|                 ] | ||||
|                 [] | ||||
|             , span | ||||
|                 [ class S.warnMessagePlain | ||||
|                 , class "font-medium text-sm" | ||||
|                 , classList [ ( "invisible", not model.nameExists ) ] | ||||
|                 ] | ||||
|                 [ text texts.nameExistsWarning | ||||
|                 ] | ||||
|             ] | ||||
|         , div [ class "flex flex-col mb-4 " ] | ||||
|             [ label [ class "inline-flex items-center" ] | ||||
|                 [ input | ||||
|                     [ type_ "radio" | ||||
|                     , checked model.isPersonal | ||||
|                     , onCheck (\_ -> SetPersonal True) | ||||
|                     , class S.radioInput | ||||
|                     ] | ||||
|                     [] | ||||
|                 , span [ class "ml-2" ] [ text texts.userLocation ] | ||||
|                 , span [ class "ml-3 opacity-75 text-sm" ] [ text texts.userLocationText ] | ||||
|                 ] | ||||
|             , label [ class "inline-flex items-center" ] | ||||
|                 [ input | ||||
|                     [ type_ "radio" | ||||
|                     , checked (not model.isPersonal) | ||||
|                     , class S.radioInput | ||||
|                     , onCheck (\_ -> SetPersonal False) | ||||
|                     ] | ||||
|                     [] | ||||
|                 , span [ class "ml-2" ] [ text texts.collectiveLocation ] | ||||
|                 , span [ class "ml-3 opacity-75 text-sm" ] [ text texts.collectiveLocationText ] | ||||
|                 ] | ||||
|             ] | ||||
|         , div [ class "mb-4" ] | ||||
|             [ label | ||||
|                 [ for "sharequery" | ||||
|                 , class S.inputLabel | ||||
|                 ] | ||||
|                 [ text texts.queryLabel | ||||
|                 , B.inputRequired | ||||
|                 ] | ||||
|             , queryInput | ||||
|             ] | ||||
|         ] | ||||
							
								
								
									
										180
									
								
								modules/webapp/src/main/elm/Comp/BookmarkQueryManage.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,180 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkQueryManage exposing (..) | ||||
|  | ||||
| import Api | ||||
| import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Comp.Basic as B | ||||
| import Comp.BookmarkQueryForm | ||||
| import Data.Flags exposing (Flags) | ||||
| import Html exposing (Html, div, text) | ||||
| import Html.Attributes exposing (class, href) | ||||
| import Html.Events exposing (onClick) | ||||
| import Http | ||||
| import Messages.Comp.BookmarkQueryManage exposing (Texts) | ||||
| import Styles as S | ||||
|  | ||||
|  | ||||
| type alias Model = | ||||
|     { formModel : Comp.BookmarkQueryForm.Model | ||||
|     , loading : Bool | ||||
|     , formState : FormState | ||||
|     } | ||||
|  | ||||
|  | ||||
| type FormState | ||||
|     = FormStateNone | ||||
|     | FormStateError Http.Error | ||||
|     | FormStateSaveError String | ||||
|     | FormStateInvalid | ||||
|     | FormStateSaved | ||||
|  | ||||
|  | ||||
| init : String -> ( Model, Cmd Msg ) | ||||
| init query = | ||||
|     let | ||||
|         ( fm, fc ) = | ||||
|             Comp.BookmarkQueryForm.initQuery query | ||||
|     in | ||||
|     ( { formModel = fm | ||||
|       , loading = False | ||||
|       , formState = FormStateNone | ||||
|       } | ||||
|     , Cmd.map FormMsg fc | ||||
|     ) | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = Submit | ||||
|     | Cancel | ||||
|     | FormMsg Comp.BookmarkQueryForm.Msg | ||||
|     | SaveResp (Result Http.Error BasicResult) | ||||
|  | ||||
|  | ||||
|  | ||||
| --- Update | ||||
|  | ||||
|  | ||||
| type FormResult | ||||
|     = Submitted BookmarkedQuery | ||||
|     | Cancelled | ||||
|     | Done | ||||
|     | None | ||||
|  | ||||
|  | ||||
| type alias UpdateResult = | ||||
|     { model : Model | ||||
|     , cmd : Cmd Msg | ||||
|     , sub : Sub Msg | ||||
|     , outcome : FormResult | ||||
|     } | ||||
|  | ||||
|  | ||||
| update : Flags -> Msg -> Model -> UpdateResult | ||||
| update flags msg model = | ||||
|     let | ||||
|         empty = | ||||
|             { model = model | ||||
|             , cmd = Cmd.none | ||||
|             , sub = Sub.none | ||||
|             , outcome = None | ||||
|             } | ||||
|     in | ||||
|     case msg of | ||||
|         FormMsg lm -> | ||||
|             let | ||||
|                 ( fm, fc, fs ) = | ||||
|                     Comp.BookmarkQueryForm.update flags lm model.formModel | ||||
|             in | ||||
|             { model = { model | formModel = fm } | ||||
|             , cmd = Cmd.map FormMsg fc | ||||
|             , sub = Sub.map FormMsg fs | ||||
|             , outcome = None | ||||
|             } | ||||
|  | ||||
|         Submit -> | ||||
|             case Comp.BookmarkQueryForm.get model.formModel of | ||||
|                 Just data -> | ||||
|                     { empty | cmd = save flags data, outcome = Submitted data, model = { model | loading = True } } | ||||
|  | ||||
|                 Nothing -> | ||||
|                     { empty | model = { model | formState = FormStateInvalid } } | ||||
|  | ||||
|         Cancel -> | ||||
|             { model = model | ||||
|             , cmd = Cmd.none | ||||
|             , sub = Sub.none | ||||
|             , outcome = Cancelled | ||||
|             } | ||||
|  | ||||
|         SaveResp (Ok res) -> | ||||
|             if res.success then | ||||
|                 { empty | model = { model | loading = False, formState = FormStateSaved }, outcome = Done } | ||||
|  | ||||
|             else | ||||
|                 { empty | model = { model | loading = False, formState = FormStateSaveError res.message } } | ||||
|  | ||||
|         SaveResp (Err err) -> | ||||
|             { empty | model = { model | loading = False, formState = FormStateError err } } | ||||
|  | ||||
|  | ||||
| save : Flags -> BookmarkedQuery -> Cmd Msg | ||||
| save flags model = | ||||
|     Api.addBookmark flags model SaveResp | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| view : Texts -> Model -> Html Msg | ||||
| view texts model = | ||||
|     div [ class "relative" ] | ||||
|         [ B.loadingDimmer { label = "", active = model.loading } | ||||
|         , Html.map FormMsg (Comp.BookmarkQueryForm.view texts.form model.formModel) | ||||
|         , case model.formState of | ||||
|             FormStateNone -> | ||||
|                 div [ class "hidden" ] [] | ||||
|  | ||||
|             FormStateError err -> | ||||
|                 div [ class S.errorMessage ] | ||||
|                     [ text <| texts.httpError err | ||||
|                     ] | ||||
|  | ||||
|             FormStateInvalid -> | ||||
|                 div [ class S.errorMessage ] | ||||
|                     [ text texts.formInvalid | ||||
|                     ] | ||||
|  | ||||
|             FormStateSaveError m -> | ||||
|                 div [ class S.errorMessage ] | ||||
|                     [ text m | ||||
|                     ] | ||||
|  | ||||
|             FormStateSaved -> | ||||
|                 div [ class S.successMessage ] | ||||
|                     [ text texts.saved | ||||
|                     ] | ||||
|         , div [ class "flex flex-row space-x-2 py-2" ] | ||||
|             [ B.primaryButton | ||||
|                 { label = texts.basics.submit | ||||
|                 , icon = "fa fa-save" | ||||
|                 , disabled = False | ||||
|                 , handler = onClick Submit | ||||
|                 , attrs = [ href "#" ] | ||||
|                 } | ||||
|             , B.secondaryButton | ||||
|                 { label = texts.basics.cancel | ||||
|                 , icon = "fa fa-times" | ||||
|                 , disabled = False | ||||
|                 , handler = onClick Cancel | ||||
|                 , attrs = [ href "#" ] | ||||
|                 } | ||||
|             ] | ||||
|         ] | ||||
							
								
								
									
										67
									
								
								modules/webapp/src/main/elm/Comp/BookmarkTable.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,67 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Comp.BookmarkTable exposing | ||||
|     ( Msg(..) | ||||
|     , SelectAction(..) | ||||
|     , update | ||||
|     , view | ||||
|     ) | ||||
|  | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Comp.Basic as B | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Messages.Comp.BookmarkTable exposing (Texts) | ||||
| import Styles as S | ||||
|  | ||||
|  | ||||
| type Msg | ||||
|     = Select BookmarkedQuery | ||||
|  | ||||
|  | ||||
| type SelectAction | ||||
|     = Edit BookmarkedQuery | ||||
|  | ||||
|  | ||||
| update : Msg -> SelectAction | ||||
| update msg = | ||||
|     case msg of | ||||
|         Select share -> | ||||
|             Edit share | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View | ||||
|  | ||||
|  | ||||
| view : Texts -> List BookmarkedQuery -> Html Msg | ||||
| view texts bms = | ||||
|     table [ class S.tableMain ] | ||||
|         [ thead [] | ||||
|             [ tr [] | ||||
|                 [ th [ class "" ] [] | ||||
|                 , th [ class "text-left" ] | ||||
|                     [ text texts.basics.name | ||||
|                     ] | ||||
|                 ] | ||||
|             ] | ||||
|         , tbody [] | ||||
|             (List.map (renderBookmarkLine texts) bms) | ||||
|         ] | ||||
|  | ||||
|  | ||||
| renderBookmarkLine : Texts -> BookmarkedQuery -> Html Msg | ||||
| renderBookmarkLine texts bm = | ||||
|     tr | ||||
|         [ class S.tableRow | ||||
|         ] | ||||
|         [ B.editLinkTableCell texts.basics.edit (Select bm) | ||||
|         , td [ class "text-left py-4 md:py-2" ] | ||||
|             [ text bm.name | ||||
|             ] | ||||
|         ] | ||||
| @@ -41,6 +41,7 @@ menuItem : Texts -> Model msg -> ChannelType -> MB.DropdownMenu msg | ||||
| menuItem texts model ct = | ||||
|     { icon = Data.ChannelType.icon ct "w-6 h-6 text-center inline-block" | ||||
|     , label = texts ct | ||||
|     , disabled = False | ||||
|     , attrs = | ||||
|         [ href "" | ||||
|         , onClick (model.onItem ct) | ||||
|   | ||||
| @@ -834,7 +834,7 @@ viewIntern2 texts settings withButtons model = | ||||
|         ] | ||||
|     , case model.form of | ||||
|         TM tm -> | ||||
|             Html.map TagMsg (Comp.TagForm.view2 texts.tagForm tm) | ||||
|             Html.map TagMsg (Comp.TagForm.view2 texts.tagForm settings tm) | ||||
|  | ||||
|         PMR pm -> | ||||
|             Html.map PersonMsg (Comp.PersonForm.view2 texts.personForm True settings pm) | ||||
|   | ||||
| @@ -462,16 +462,17 @@ view2 cfg settings model = | ||||
|         viewMultiple2 cfg settings model | ||||
|  | ||||
|     else | ||||
|         viewSingle2 cfg model | ||||
|         viewSingle2 cfg settings model | ||||
|  | ||||
|  | ||||
| viewSingle2 : ViewSettings a -> Model a -> Html (Msg a) | ||||
| viewSingle2 cfg model = | ||||
| viewSingle2 : ViewSettings a -> UiSettings -> Model a -> Html (Msg a) | ||||
| viewSingle2 cfg settings model = | ||||
|     let | ||||
|         renderItem item = | ||||
|             a | ||||
|                 [ href "#" | ||||
|                 , class cfg.style.item | ||||
|                 , class (cfg.labelColor item.value settings) | ||||
|                 , classList | ||||
|                     [ ( cfg.style.itemActive, item.active ) | ||||
|                     , ( "font-semibold", item.selected ) | ||||
| @@ -480,7 +481,7 @@ viewSingle2 cfg model = | ||||
|                 , onKeyUp KeyPress | ||||
|                 ] | ||||
|                 [ text <| (.value >> cfg.makeOption >> .text) item | ||||
|                 , span [ class "text-gray-400 float-right" ] | ||||
|                 , span [ class "text-gray-400 opacity-75 float-right" ] | ||||
|                     [ text <| (.value >> cfg.makeOption >> .additional) item | ||||
|                     ] | ||||
|                 ] | ||||
|   | ||||
| @@ -228,6 +228,7 @@ attachHeader texts settings model _ attach = | ||||
|                     , items = | ||||
|                         [ { icon = i [ class "fa fa-download" ] [] | ||||
|                           , label = texts.downloadFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ download attachName | ||||
|                                 , href fileUrl | ||||
| @@ -235,6 +236,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class "fa fa-file" ] [] | ||||
|                           , label = texts.renameFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ href "#" | ||||
|                                 , onClick (EditAttachNameStart attach.id) | ||||
| @@ -242,6 +244,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class "fa fa-file-archive" ] [] | ||||
|                           , label = texts.downloadOriginalArchiveFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ href (fileUrl ++ "/archive") | ||||
|                                 , target "_new" | ||||
| @@ -250,6 +253,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class "fa fa-external-link-alt" ] [] | ||||
|                           , label = texts.originalFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ href (fileUrl ++ "/original") | ||||
|                                 , target "_new" | ||||
| @@ -263,6 +267,7 @@ attachHeader texts settings model _ attach = | ||||
|                                 else | ||||
|                                     i [ class "fa fa-toggle-off" ] [] | ||||
|                           , label = texts.viewExtractedData | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ onClick (AttachMetaClick attach.id) | ||||
|                                 , href "#" | ||||
| @@ -270,6 +275,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class "fa fa-redo-alt" ] [] | ||||
|                           , label = texts.reprocessFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ onClick (RequestReprocessFile attach.id) | ||||
|                                 , href "#" | ||||
| @@ -277,6 +283,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class Icons.showQr ] [] | ||||
|                           , label = texts.showQrCode | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ onClick (ToggleShowQrAttach attach.id) | ||||
|                                 , href "#" | ||||
| @@ -284,6 +291,7 @@ attachHeader texts settings model _ attach = | ||||
|                           } | ||||
|                         , { icon = i [ class "fa fa-trash" ] [] | ||||
|                           , label = texts.deleteThisFile | ||||
|                           , disabled = False | ||||
|                           , attrs = | ||||
|                                 [ onClick (RequestDeleteAttachment attach.id) | ||||
|                                 , href "#" | ||||
|   | ||||
| @@ -96,6 +96,7 @@ type alias DropdownData msg = | ||||
| type alias DropdownMenu msg = | ||||
|     { icon : Html msg | ||||
|     , label : String | ||||
|     , disabled : Bool | ||||
|     , attrs : List (Attribute msg) | ||||
|     } | ||||
|  | ||||
| @@ -171,10 +172,16 @@ makeDropdown model = | ||||
|         menuStyle = | ||||
|             "absolute right-0 bg-white dark:bg-slate-800 border dark:border-slate-700 z-50 dark:text-slate-300 shadow-lg transition duration-200 min-w-max  " | ||||
|  | ||||
|         itemStyle = | ||||
|             "transition-colors duration-200 items-center block px-4 py-2 text-normal hover:bg-gray-200 dark:hover:bg-slate-700 dark:hover:text-slate-50" | ||||
|         itemStyleBase = | ||||
|             "transition-colors duration-200 items-center block px-4 py-2 text-normal z-50 " | ||||
|  | ||||
|         menuItem m = | ||||
|         itemStyleHover = | ||||
|             " hover:bg-gray-200 dark:hover:bg-slate-700 dark:hover:text-slate-50" | ||||
|  | ||||
|         itemStyle = | ||||
|             itemStyleBase ++ itemStyleHover | ||||
|  | ||||
|         menuLink m = | ||||
|             a | ||||
|                 (class itemStyle :: m.attrs) | ||||
|                 [ m.icon | ||||
| @@ -185,6 +192,34 @@ makeDropdown model = | ||||
|                     [ text m.label | ||||
|                     ] | ||||
|                 ] | ||||
|  | ||||
|         disabledLink m = | ||||
|             a | ||||
|                 ([ href "#" | ||||
|                  , disabled True | ||||
|                  , class itemStyleBase | ||||
|                  , class "disabled" | ||||
|                  ] | ||||
|                     ++ m.attrs | ||||
|                 ) | ||||
|                 [ m.icon | ||||
|                 , span | ||||
|                     [ class "ml-2" | ||||
|                     , classList [ ( "hidden", m.label == "" ) ] | ||||
|                     ] | ||||
|                     [ text m.label | ||||
|                     ] | ||||
|                 ] | ||||
|  | ||||
|         menuItem m = | ||||
|             if m.label == "separator" then | ||||
|                 div [ class "py-1" ] [ hr [ class S.border ] [] ] | ||||
|  | ||||
|             else if m.disabled then | ||||
|                 disabledLink m | ||||
|  | ||||
|             else | ||||
|                 menuLink m | ||||
|     in | ||||
|     div [ class "relative" ] | ||||
|         [ a | ||||
|   | ||||
| @@ -17,6 +17,7 @@ module Comp.PeriodicQueryTaskForm exposing | ||||
|     ) | ||||
|  | ||||
| import Comp.Basic as B | ||||
| import Comp.BookmarkDropdown | ||||
| import Comp.CalEventInput | ||||
| import Comp.ChannelForm | ||||
| import Comp.MenuBar as MB | ||||
| @@ -44,6 +45,7 @@ type alias Model = | ||||
|     , scheduleModel : Comp.CalEventInput.Model | ||||
|     , queryModel : Comp.PowerSearchInput.Model | ||||
|     , channelModel : Comp.ChannelForm.Model | ||||
|     , bookmarkDropdown : Comp.BookmarkDropdown.Model | ||||
|     , formState : FormState | ||||
|     , loading : Int | ||||
|     } | ||||
| @@ -75,6 +77,7 @@ type Msg | ||||
|     | CalEventMsg Comp.CalEventInput.Msg | ||||
|     | QueryMsg Comp.PowerSearchInput.Msg | ||||
|     | ChannelMsg Comp.ChannelForm.Msg | ||||
|     | BookmarkMsg Comp.BookmarkDropdown.Msg | ||||
|     | StartOnce | ||||
|     | Cancel | ||||
|     | RequestDelete | ||||
| @@ -93,11 +96,14 @@ initWith flags s = | ||||
|  | ||||
|         res = | ||||
|             Comp.PowerSearchInput.update | ||||
|                 (Comp.PowerSearchInput.setSearchString s.query) | ||||
|                 (Comp.PowerSearchInput.setSearchString (Maybe.withDefault "" s.query)) | ||||
|                 Comp.PowerSearchInput.init | ||||
|  | ||||
|         ( cfm, cfc ) = | ||||
|             Comp.ChannelForm.initWith flags s.channel | ||||
|  | ||||
|         ( bm, bc ) = | ||||
|             Comp.BookmarkDropdown.init flags s.bookmark | ||||
|     in | ||||
|     ( { settings = s | ||||
|       , enabled = s.enabled | ||||
| @@ -105,6 +111,7 @@ initWith flags s = | ||||
|       , scheduleModel = sm | ||||
|       , queryModel = res.model | ||||
|       , channelModel = cfm | ||||
|       , bookmarkDropdown = bm | ||||
|       , formState = FormStateInitial | ||||
|       , loading = 0 | ||||
|       , summary = s.summary | ||||
| @@ -113,6 +120,7 @@ initWith flags s = | ||||
|         [ Cmd.map CalEventMsg sc | ||||
|         , Cmd.map QueryMsg res.cmd | ||||
|         , Cmd.map ChannelMsg cfc | ||||
|         , Cmd.map BookmarkMsg bc | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
| @@ -128,6 +136,9 @@ init flags ct = | ||||
|  | ||||
|         ( cfm, cfc ) = | ||||
|             Comp.ChannelForm.init flags ct | ||||
|  | ||||
|         ( bm, bc ) = | ||||
|             Comp.BookmarkDropdown.init flags Nothing | ||||
|     in | ||||
|     ( { settings = Data.PeriodicQuerySettings.empty ct | ||||
|       , enabled = False | ||||
| @@ -135,6 +146,7 @@ init flags ct = | ||||
|       , scheduleModel = sm | ||||
|       , queryModel = Comp.PowerSearchInput.init | ||||
|       , channelModel = cfm | ||||
|       , bookmarkDropdown = bm | ||||
|       , formState = FormStateInitial | ||||
|       , loading = 0 | ||||
|       , summary = Nothing | ||||
| @@ -142,6 +154,7 @@ init flags ct = | ||||
|     , Cmd.batch | ||||
|         [ Cmd.map CalEventMsg scmd | ||||
|         , Cmd.map ChannelMsg cfc | ||||
|         , Cmd.map BookmarkMsg bc | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
| @@ -172,27 +185,46 @@ makeSettings model = | ||||
|                 Nothing -> | ||||
|                     Err ValidateCalEventInvalid | ||||
|  | ||||
|         queryString = | ||||
|             Result.fromMaybe ValidateQueryStringRequired model.queryModel.input | ||||
|         query = | ||||
|             let | ||||
|                 qstr = | ||||
|                     model.queryModel.input | ||||
|  | ||||
|                 bm = | ||||
|                     Comp.BookmarkDropdown.getSelectedId model.bookmarkDropdown | ||||
|             in | ||||
|             case ( qstr, bm ) of | ||||
|                 ( Just _, Just _ ) -> | ||||
|                     Result.Ok ( qstr, bm ) | ||||
|  | ||||
|                 ( Just _, Nothing ) -> | ||||
|                     Result.Ok ( qstr, bm ) | ||||
|  | ||||
|                 ( Nothing, Just _ ) -> | ||||
|                     Result.Ok ( qstr, bm ) | ||||
|  | ||||
|                 ( Nothing, Nothing ) -> | ||||
|                     Result.Err ValidateQueryStringRequired | ||||
|  | ||||
|         channelM = | ||||
|             Result.fromMaybe | ||||
|                 ValidateChannelRequired | ||||
|                 (Comp.ChannelForm.getChannel model.channelModel) | ||||
|  | ||||
|         make timer channel query = | ||||
|         make timer channel q = | ||||
|             { prev | ||||
|                 | enabled = model.enabled | ||||
|                 , schedule = Data.CalEvent.makeEvent timer | ||||
|                 , summary = model.summary | ||||
|                 , channel = channel | ||||
|                 , query = query | ||||
|                 , query = Tuple.first q | ||||
|                 , bookmark = Tuple.second q | ||||
|             } | ||||
|     in | ||||
|     Result.map3 make | ||||
|         schedule_ | ||||
|         channelM | ||||
|         queryString | ||||
|         query | ||||
|  | ||||
|  | ||||
| withValidSettings : (PeriodicQuerySettings -> Action) -> Model -> UpdateResult | ||||
| @@ -257,6 +289,17 @@ update flags msg model = | ||||
|             , sub = Sub.none | ||||
|             } | ||||
|  | ||||
|         BookmarkMsg lm -> | ||||
|             let | ||||
|                 ( bm, bc ) = | ||||
|                     Comp.BookmarkDropdown.update lm model.bookmarkDropdown | ||||
|             in | ||||
|             { model = { model | bookmarkDropdown = bm } | ||||
|             , action = NoAction | ||||
|             , cmd = Cmd.map BookmarkMsg bc | ||||
|             , sub = Sub.none | ||||
|             } | ||||
|  | ||||
|         ToggleEnabled -> | ||||
|             { model = | ||||
|                 { model | ||||
| @@ -344,9 +387,14 @@ view texts extraClasses settings model = | ||||
|                     (Comp.PowerSearchInput.viewResult [] model.queryModel) | ||||
|                 ] | ||||
|  | ||||
|         formHeader txt = | ||||
|         formHeader txt req = | ||||
|             h2 [ class S.formHeader, class "mt-2" ] | ||||
|                 [ text txt | ||||
|                 , if req then | ||||
|                     B.inputRequired | ||||
|  | ||||
|                   else | ||||
|                     span [] [] | ||||
|                 ] | ||||
|     in | ||||
|     div | ||||
| @@ -438,23 +486,29 @@ view texts extraClasses settings model = | ||||
|                 ] | ||||
|             ] | ||||
|         , div [ class "mb-4" ] | ||||
|             [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) | ||||
|             [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) False | ||||
|             , Html.map ChannelMsg | ||||
|                 (Comp.ChannelForm.view texts.channelForm settings model.channelModel) | ||||
|             ] | ||||
|         , div [ class "mb-4" ] | ||||
|             [ formHeader texts.queryLabel | ||||
|             , label | ||||
|                 [ for "sharequery" | ||||
|                 , class S.inputLabel | ||||
|             [ formHeader texts.queryLabel True | ||||
|             , div [ class "mb-3" ] | ||||
|                 [ label [ class S.inputLabel ] | ||||
|                     [ text "Bookmark" ] | ||||
|                 , Html.map BookmarkMsg (Comp.BookmarkDropdown.view texts.bookmarkDropdown settings model.bookmarkDropdown) | ||||
|                 ] | ||||
|                 [ text texts.queryLabel | ||||
|                 , B.inputRequired | ||||
|             , div [ class "mb-3" ] | ||||
|                 [ label | ||||
|                     [ for "sharequery" | ||||
|                     , class S.inputLabel | ||||
|                     ] | ||||
|                     [ text texts.queryLabel | ||||
|                     ] | ||||
|                 , queryInput | ||||
|                 ] | ||||
|             , queryInput | ||||
|             ] | ||||
|         , div [ class "mb-4" ] | ||||
|             [ formHeader texts.schedule | ||||
|             [ formHeader texts.schedule False | ||||
|             , label [ class S.inputLabel ] | ||||
|                 [ text texts.schedule | ||||
|                 , B.inputRequired | ||||
|   | ||||
| @@ -16,6 +16,7 @@ module Comp.SearchMenu exposing | ||||
|     , isFulltextSearch | ||||
|     , isNamesSearch | ||||
|     , linkTargetMsg | ||||
|     , refreshBookmarks | ||||
|     , setFromStats | ||||
|     , textSearchString | ||||
|     , update | ||||
| @@ -33,6 +34,7 @@ import Api.Model.ItemQuery exposing (ItemQuery) | ||||
| import Api.Model.PersonList exposing (PersonList) | ||||
| import Api.Model.ReferenceList exposing (ReferenceList) | ||||
| import Api.Model.SearchStats exposing (SearchStats) | ||||
| import Comp.BookmarkChooser | ||||
| import Comp.CustomFieldMultiInput | ||||
| import Comp.DatePicker | ||||
| import Comp.Dropdown exposing (isDropdownChangeMsg) | ||||
| @@ -41,6 +43,7 @@ import Comp.LinkTarget exposing (LinkTarget) | ||||
| import Comp.MenuBar as MB | ||||
| import Comp.Tabs | ||||
| import Comp.TagSelect | ||||
| import Data.Bookmarks exposing (AllBookmarks) | ||||
| import Data.CustomFieldChange exposing (CustomFieldValueCollect) | ||||
| import Data.Direction exposing (Direction) | ||||
| import Data.DropdownStyle as DS | ||||
| @@ -96,6 +99,8 @@ type alias Model = | ||||
|     , customFieldModel : Comp.CustomFieldMultiInput.Model | ||||
|     , customValues : CustomFieldValueCollect | ||||
|     , sourceModel : Maybe String | ||||
|     , allBookmarks : Comp.BookmarkChooser.Model | ||||
|     , selectedBookmarks : Comp.BookmarkChooser.Selection | ||||
|     , openTabs : Set String | ||||
|     , searchMode : SearchMode | ||||
|     } | ||||
| @@ -141,6 +146,8 @@ init flags = | ||||
|     , customFieldModel = Comp.CustomFieldMultiInput.initWith [] | ||||
|     , customValues = Data.CustomFieldChange.emptyCollect | ||||
|     , sourceModel = Nothing | ||||
|     , allBookmarks = Comp.BookmarkChooser.init Data.Bookmarks.empty | ||||
|     , selectedBookmarks = Comp.BookmarkChooser.emptySelection | ||||
|     , openTabs = Set.fromList [ "Tags", "Inbox" ] | ||||
|     , searchMode = Data.SearchMode.Normal | ||||
|     } | ||||
| @@ -243,6 +250,10 @@ getItemQuery model = | ||||
|  | ||||
|         textSearch = | ||||
|             textSearchValue model.textSearchModel | ||||
|  | ||||
|         bookmarks = | ||||
|             List.map .query (Comp.BookmarkChooser.getQueries model.allBookmarks model.selectedBookmarks) | ||||
|                 |> List.map Q.Fragment | ||||
|     in | ||||
|     Q.and | ||||
|         [ when model.inboxCheckbox (Q.Inbox True) | ||||
| @@ -289,6 +300,7 @@ getItemQuery model = | ||||
|             |> Maybe.map Q.Dir | ||||
|         , textSearch.fullText | ||||
|             |> Maybe.map Q.Contents | ||||
|         , whenNotEmpty bookmarks Q.And | ||||
|         ] | ||||
|  | ||||
|  | ||||
| @@ -333,6 +345,7 @@ resetModel model = | ||||
|                 model.customFieldModel | ||||
|         , customValues = Data.CustomFieldChange.emptyCollect | ||||
|         , sourceModel = Nothing | ||||
|         , selectedBookmarks = Comp.BookmarkChooser.emptySelection | ||||
|         , searchMode = Data.SearchMode.Normal | ||||
|     } | ||||
|  | ||||
| @@ -380,6 +393,8 @@ type Msg | ||||
|     | GetAllTagsResp (Result Http.Error SearchStats) | ||||
|     | ToggleAkkordionTab String | ||||
|     | ToggleOpenAllAkkordionTabs | ||||
|     | AllBookmarksResp (Result Http.Error AllBookmarks) | ||||
|     | SelectBookmarkMsg Comp.BookmarkChooser.Msg | ||||
|  | ||||
|  | ||||
| setFromStats : SearchStats -> Msg | ||||
| @@ -426,6 +441,11 @@ type alias NextState = | ||||
|     } | ||||
|  | ||||
|  | ||||
| refreshBookmarks : Flags -> Cmd Msg | ||||
| refreshBookmarks flags = | ||||
|     Api.getBookmarks flags AllBookmarksResp | ||||
|  | ||||
|  | ||||
| update : Flags -> UiSettings -> Msg -> Model -> NextState | ||||
| update = | ||||
|     updateDrop DD.init | ||||
| @@ -488,6 +508,7 @@ updateDrop ddm flags settings msg model = | ||||
|                     , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp | ||||
|                     , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) | ||||
|                     , cdp | ||||
|                     , Api.getBookmarks flags AllBookmarksResp | ||||
|                     ] | ||||
|             , stateChange = False | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
| @@ -1040,6 +1061,31 @@ updateDrop ddm flags settings msg model = | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
|             } | ||||
|  | ||||
|         AllBookmarksResp (Ok bm) -> | ||||
|             { model = { model | allBookmarks = Comp.BookmarkChooser.init bm } | ||||
|             , cmd = Cmd.none | ||||
|             , stateChange = False | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
|             } | ||||
|  | ||||
|         AllBookmarksResp (Err err) -> | ||||
|             { model = model | ||||
|             , cmd = Cmd.none | ||||
|             , stateChange = False | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
|             } | ||||
|  | ||||
|         SelectBookmarkMsg lm -> | ||||
|             let | ||||
|                 ( next, sel ) = | ||||
|                     Comp.BookmarkChooser.update lm model.allBookmarks model.selectedBookmarks | ||||
|             in | ||||
|             { model = { model | allBookmarks = next, selectedBookmarks = sel } | ||||
|             , cmd = Cmd.none | ||||
|             , stateChange = sel /= model.selectedBookmarks | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
|             } | ||||
|  | ||||
|  | ||||
|  | ||||
| --- View2 | ||||
| @@ -1064,6 +1110,7 @@ viewDrop2 texts ddd flags cfg settings model = | ||||
|  | ||||
| type SearchTab | ||||
|     = TabInbox | ||||
|     | TabBookmarks | ||||
|     | TabTags | ||||
|     | TabTagCategories | ||||
|     | TabFolder | ||||
| @@ -1080,6 +1127,7 @@ type SearchTab | ||||
| allTabs : List SearchTab | ||||
| allTabs = | ||||
|     [ TabInbox | ||||
|     , TabBookmarks | ||||
|     , TabTags | ||||
|     , TabTagCategories | ||||
|     , TabFolder | ||||
| @@ -1100,6 +1148,9 @@ tabName tab = | ||||
|         TabInbox -> | ||||
|             "inbox" | ||||
|  | ||||
|         TabBookmarks -> | ||||
|             "bookmarks" | ||||
|  | ||||
|         TabTags -> | ||||
|             "tags" | ||||
|  | ||||
| @@ -1140,6 +1191,9 @@ findTab tab = | ||||
|         "inbox" -> | ||||
|             Just TabInbox | ||||
|  | ||||
|         "bookmarks" -> | ||||
|             Just TabBookmarks | ||||
|  | ||||
|         "tags" -> | ||||
|             Just TabTags | ||||
|  | ||||
| @@ -1215,6 +1269,16 @@ tabLook settings model tab = | ||||
|         TabInbox -> | ||||
|             activeWhen model.inboxCheckbox | ||||
|  | ||||
|         TabBookmarks -> | ||||
|             if Comp.BookmarkChooser.isEmpty model.allBookmarks then | ||||
|                 Comp.Tabs.Hidden | ||||
|  | ||||
|             else if not <| Comp.BookmarkChooser.isEmptySelection model.selectedBookmarks then | ||||
|                 Comp.Tabs.Active | ||||
|  | ||||
|             else | ||||
|                 Comp.Tabs.Normal | ||||
|  | ||||
|         TabTags -> | ||||
|             hiddenOr [ Data.Fields.Tag ] | ||||
|                 (activeWhenNotEmpty model.tagSelection.includeTags model.tagSelection.excludeTags) | ||||
| @@ -1329,52 +1393,15 @@ searchTabs texts ddd flags settings model = | ||||
|                     , label = texts.inbox | ||||
|                     , tagger = \_ -> ToggleInbox | ||||
|                     } | ||||
|             , div [ class "mt-2 hidden" ] | ||||
|                 [ label [ class S.inputLabel ] | ||||
|                     [ text | ||||
|                         (case model.textSearchModel of | ||||
|                             Fulltext _ -> | ||||
|                                 texts.fulltextSearch | ||||
|  | ||||
|                             Names _ -> | ||||
|                                 texts.searchInNames | ||||
|                         ) | ||||
|                     , a | ||||
|                         [ classList | ||||
|                             [ ( "hidden", not flags.config.fullTextSearchEnabled ) | ||||
|                             ] | ||||
|                         , class "float-right" | ||||
|                         , class S.link | ||||
|                         , href "#" | ||||
|                         , onClick SwapTextSearch | ||||
|                         , title texts.switchSearchModes | ||||
|                         ] | ||||
|                         [ i [ class "fa fa-exchange-alt" ] [] | ||||
|                         ] | ||||
|                     ] | ||||
|                 , input | ||||
|                     [ type_ "text" | ||||
|                     , onInput SetTextSearch | ||||
|                     , Util.Html.onKeyUpCode KeyUpMsg | ||||
|                     , textSearchString model.textSearchModel |> Maybe.withDefault "" |> value | ||||
|                     , case model.textSearchModel of | ||||
|                         Fulltext _ -> | ||||
|                             placeholder texts.contentSearch | ||||
|  | ||||
|                         Names _ -> | ||||
|                             placeholder texts.searchInNamesPlaceholder | ||||
|                     , class S.textInputSidebar | ||||
|                     ] | ||||
|                     [] | ||||
|                 , span [ class "opacity-50 text-sm" ] | ||||
|                     [ case model.textSearchModel of | ||||
|                         Fulltext _ -> | ||||
|                             text texts.fulltextSearchInfo | ||||
|  | ||||
|                         Names _ -> | ||||
|                             text texts.nameSearchInfo | ||||
|                     ] | ||||
|                 ] | ||||
|             ] | ||||
|       } | ||||
|     , { name = tabName TabBookmarks | ||||
|       , title = texts.bookmarks | ||||
|       , titleRight = [] | ||||
|       , info = Nothing | ||||
|       , body = | ||||
|             [ Html.map SelectBookmarkMsg | ||||
|                 (Comp.BookmarkChooser.view texts.bookmarkChooser model.allBookmarks model.selectedBookmarks) | ||||
|             ] | ||||
|       } | ||||
|     , { name = tabName TabTags | ||||
|   | ||||
| @@ -20,6 +20,7 @@ import Comp.Basic as B | ||||
| import Comp.Dropdown | ||||
| import Data.DropdownStyle as DS | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onInput) | ||||
| @@ -126,8 +127,8 @@ update _ msg model = | ||||
| --- View2 | ||||
|  | ||||
|  | ||||
| view2 : Texts -> Model -> Html Msg | ||||
| view2 texts model = | ||||
| view2 : Texts -> UiSettings -> Model -> Html Msg | ||||
| view2 texts settings model = | ||||
|     let | ||||
|         categoryCfg = | ||||
|             { makeOption = \s -> Comp.Dropdown.mkOption s | ||||
| @@ -170,6 +171,7 @@ view2 texts model = | ||||
|             , Html.map CatMsg | ||||
|                 (Comp.Dropdown.viewSingle2 | ||||
|                     categoryCfg | ||||
|                     settings | ||||
|                     model.catDropdown | ||||
|                 ) | ||||
|             ] | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import Comp.TagTable | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.TagOrder exposing (TagOrder) | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Html exposing (..) | ||||
| import Html.Attributes exposing (..) | ||||
| import Html.Events exposing (onSubmit) | ||||
| @@ -247,13 +248,13 @@ update flags msg model = | ||||
| --- View2 | ||||
|  | ||||
|  | ||||
| view2 : Texts -> Model -> Html Msg | ||||
| view2 texts model = | ||||
| view2 : Texts -> UiSettings -> Model -> Html Msg | ||||
| view2 texts settings model = | ||||
|     if model.viewMode == Table then | ||||
|         viewTable2 texts model | ||||
|  | ||||
|     else | ||||
|         viewForm2 texts model | ||||
|         viewForm2 texts settings model | ||||
|  | ||||
|  | ||||
| viewTable2 : Texts -> Model -> Html Msg | ||||
| @@ -290,8 +291,8 @@ viewTable2 texts model = | ||||
|         ] | ||||
|  | ||||
|  | ||||
| viewForm2 : Texts -> Model -> Html Msg | ||||
| viewForm2 texts model = | ||||
| viewForm2 : Texts -> UiSettings -> Model -> Html Msg | ||||
| viewForm2 texts settings model = | ||||
|     let | ||||
|         newTag = | ||||
|             model.tagFormModel.tag.id == "" | ||||
| @@ -373,7 +374,7 @@ viewForm2 texts model = | ||||
|                 FormErrorSubmit m -> | ||||
|                     text m | ||||
|             ] | ||||
|         , Html.map FormMsg (Comp.TagForm.view2 texts.tagForm model.tagFormModel) | ||||
|         , Html.map FormMsg (Comp.TagForm.view2 texts.tagForm settings model.tagFormModel) | ||||
|         , B.loadingDimmer | ||||
|             { active = model.loading | ||||
|             , label = texts.basics.loading | ||||
|   | ||||
							
								
								
									
										55
									
								
								modules/webapp/src/main/elm/Data/Bookmarks.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,55 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Data.Bookmarks exposing | ||||
|     ( AllBookmarks | ||||
|     , Bookmarks | ||||
|     , bookmarksDecoder | ||||
|     , empty | ||||
|     , exists | ||||
|     , sort | ||||
|     ) | ||||
|  | ||||
| import Api.Model.BookmarkedQuery exposing (BookmarkedQuery) | ||||
| import Api.Model.ShareDetail exposing (ShareDetail) | ||||
| import Json.Decode as D | ||||
|  | ||||
|  | ||||
| type alias AllBookmarks = | ||||
|     { bookmarks : List BookmarkedQuery | ||||
|     , shares : List ShareDetail | ||||
|     } | ||||
|  | ||||
|  | ||||
| empty : AllBookmarks | ||||
| empty = | ||||
|     AllBookmarks [] [] | ||||
|  | ||||
|  | ||||
| type alias Bookmarks = | ||||
|     List BookmarkedQuery | ||||
|  | ||||
|  | ||||
| {-| Checks wether a bookmark of this name already exists. | ||||
| -} | ||||
| exists : String -> Bookmarks -> Bool | ||||
| exists name bookmarks = | ||||
|     List.any (\b -> b.name == name) bookmarks | ||||
|  | ||||
|  | ||||
| sort : Bookmarks -> Bookmarks | ||||
| sort bms = | ||||
|     let | ||||
|         labelName b = | ||||
|             Maybe.withDefault b.name b.label | ||||
|     in | ||||
|     List.sortBy labelName bms | ||||
|  | ||||
|  | ||||
| bookmarksDecoder : D.Decoder Bookmarks | ||||
| bookmarksDecoder = | ||||
|     D.list Api.Model.BookmarkedQuery.decoder | ||||
| @@ -71,7 +71,7 @@ and list = | ||||
|             Nothing | ||||
|  | ||||
|         es -> | ||||
|             Just (And es) | ||||
|             Just (unwrap (And es)) | ||||
|  | ||||
|  | ||||
| request : SearchMode -> Maybe ItemQuery -> RQ.ItemQuery | ||||
| @@ -90,6 +90,32 @@ renderMaybe mq = | ||||
|         |> Maybe.withDefault "" | ||||
|  | ||||
|  | ||||
| unwrap : ItemQuery -> ItemQuery | ||||
| unwrap query = | ||||
|     case query of | ||||
|         And inner -> | ||||
|             case inner of | ||||
|                 first :: [] -> | ||||
|                     unwrap first | ||||
|  | ||||
|                 _ -> | ||||
|                     And (List.map unwrap inner) | ||||
|  | ||||
|         Or inner -> | ||||
|             case inner of | ||||
|                 first :: [] -> | ||||
|                     unwrap first | ||||
|  | ||||
|                 _ -> | ||||
|                     Or (List.map unwrap inner) | ||||
|  | ||||
|         Not (Not inner) -> | ||||
|             unwrap inner | ||||
|  | ||||
|         _ -> | ||||
|             query | ||||
|  | ||||
|  | ||||
| render : ItemQuery -> String | ||||
| render q = | ||||
|     let | ||||
| @@ -118,7 +144,7 @@ render q = | ||||
|             String.replace "\"" "\\\"" | ||||
|                 >> surround "\"" | ||||
|     in | ||||
|     case q of | ||||
|     case unwrap q of | ||||
|         And inner -> | ||||
|             List.map render inner | ||||
|                 |> String.join " " | ||||
|   | ||||
| @@ -18,7 +18,8 @@ type alias PeriodicQuerySettings = | ||||
|     , enabled : Bool | ||||
|     , summary : Maybe String | ||||
|     , channel : NotificationChannel | ||||
|     , query : String | ||||
|     , query : Maybe String | ||||
|     , bookmark : Maybe String | ||||
|     , schedule : String | ||||
|     } | ||||
|  | ||||
| @@ -29,19 +30,21 @@ empty ct = | ||||
|     , enabled = False | ||||
|     , summary = Nothing | ||||
|     , channel = Data.NotificationChannel.empty ct | ||||
|     , query = "" | ||||
|     , query = Nothing | ||||
|     , bookmark = Nothing | ||||
|     , schedule = "" | ||||
|     } | ||||
|  | ||||
|  | ||||
| decoder : D.Decoder PeriodicQuerySettings | ||||
| decoder = | ||||
|     D.map6 PeriodicQuerySettings | ||||
|     D.map7 PeriodicQuerySettings | ||||
|         (D.field "id" D.string) | ||||
|         (D.field "enabled" D.bool) | ||||
|         (D.field "summary" (D.maybe D.string)) | ||||
|         (D.maybe (D.field "summary" D.string)) | ||||
|         (D.field "channel" Data.NotificationChannel.decoder) | ||||
|         (D.field "query" D.string) | ||||
|         (D.maybe (D.field "query" D.string)) | ||||
|         (D.maybe (D.field "bookmark" D.string)) | ||||
|         (D.field "schedule" D.string) | ||||
|  | ||||
|  | ||||
| @@ -52,6 +55,7 @@ encode s = | ||||
|         , ( "enabled", E.bool s.enabled ) | ||||
|         , ( "summary", Maybe.map E.string s.summary |> Maybe.withDefault E.null ) | ||||
|         , ( "channel", Data.NotificationChannel.encode s.channel ) | ||||
|         , ( "query", E.string s.query ) | ||||
|         , ( "query", Maybe.map E.string s.query |> Maybe.withDefault E.null ) | ||||
|         , ( "bookmark", Maybe.map E.string s.bookmark |> Maybe.withDefault E.null ) | ||||
|         , ( "schedule", E.string s.schedule ) | ||||
|         ] | ||||
|   | ||||
| @@ -0,0 +1,40 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkChooser exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , userLabel : String | ||||
|     , collectiveLabel : String | ||||
|     , shareLabel : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , userLabel = "Personal" | ||||
|     , collectiveLabel = "Collective" | ||||
|     , shareLabel = "Shares" | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , userLabel = "Persönlich" | ||||
|     , collectiveLabel = "Kollektiv" | ||||
|     , shareLabel = "Freigaben" | ||||
|     } | ||||
| @@ -0,0 +1,43 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkDropdown exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , placeholder : String | ||||
|     , personal : String | ||||
|     , collective : String | ||||
|     , share : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , placeholder = "Bookmark…" | ||||
|     , personal = "Personal" | ||||
|     , collective = "Collective" | ||||
|     , share = "Share" | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , placeholder = "Bookmark…" | ||||
|     , personal = "Persönlich" | ||||
|     , collective = "Kollektiv" | ||||
|     , share = "Freigabe" | ||||
|     } | ||||
							
								
								
									
										65
									
								
								modules/webapp/src/main/elm/Messages/Comp/BookmarkManage.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,65 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkManage exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Http | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkQueryForm | ||||
| import Messages.Comp.BookmarkTable | ||||
| import Messages.Comp.HttpError | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , bookmarkTable : Messages.Comp.BookmarkTable.Texts | ||||
|     , bookmarkForm : Messages.Comp.BookmarkQueryForm.Texts | ||||
|     , httpError : Http.Error -> String | ||||
|     , newBookmark : String | ||||
|     , reallyDeleteBookmark : String | ||||
|     , createNewBookmark : String | ||||
|     , deleteThisBookmark : String | ||||
|     , correctFormErrors : String | ||||
|     , userBookmarks : String | ||||
|     , collectiveBookmarks : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , bookmarkTable = Messages.Comp.BookmarkTable.gb | ||||
|     , bookmarkForm = Messages.Comp.BookmarkQueryForm.gb | ||||
|     , httpError = Messages.Comp.HttpError.gb | ||||
|     , newBookmark = "New bookmark" | ||||
|     , reallyDeleteBookmark = "Really delete this bookmark?" | ||||
|     , createNewBookmark = "Create new bookmark" | ||||
|     , deleteThisBookmark = "Delete this bookmark" | ||||
|     , correctFormErrors = "Please correct the errors in the form." | ||||
|     , userBookmarks = "Personal bookmarks" | ||||
|     , collectiveBookmarks = "Collective bookmarks" | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , bookmarkTable = Messages.Comp.BookmarkTable.de | ||||
|     , bookmarkForm = Messages.Comp.BookmarkQueryForm.de | ||||
|     , httpError = Messages.Comp.HttpError.de | ||||
|     , newBookmark = "Neue Freigabe" | ||||
|     , reallyDeleteBookmark = "Diese Freigabe wirklich entfernen?" | ||||
|     , createNewBookmark = "Neue Freigabe erstellen" | ||||
|     , deleteThisBookmark = "Freigabe löschen" | ||||
|     , correctFormErrors = "Bitte korrigiere die Fehler im Formular." | ||||
|     , userBookmarks = "Persönliche Bookmarks" | ||||
|     , collectiveBookmarks = "Kollektivbookmarks" | ||||
|     } | ||||
| @@ -0,0 +1,49 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkQueryForm exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , queryLabel : String | ||||
|     , userLocation : String | ||||
|     , userLocationText : String | ||||
|     , collectiveLocation : String | ||||
|     , collectiveLocationText : String | ||||
|     , nameExistsWarning : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , queryLabel = "Query" | ||||
|     , userLocation = "User scope" | ||||
|     , userLocationText = "The bookmarked query is just for you" | ||||
|     , collectiveLocation = "Collective scope" | ||||
|     , collectiveLocationText = "The bookmarked query can be used and edited by all users" | ||||
|     , nameExistsWarning = "A bookmark with this name exists! Choose another name." | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , queryLabel = "Abfrage" | ||||
|     , userLocation = "Persönliches Bookmark" | ||||
|     , userLocationText = "Der Bookmark ist nur für dich" | ||||
|     , collectiveLocation = "Kollektiv-Bookmark" | ||||
|     , collectiveLocationText = "Der Bookmark kann von allen Benutzer verwendet werden" | ||||
|     , nameExistsWarning = "Der Bookmark existiert bereits! Verwende einen anderen Namen." | ||||
|     } | ||||
| @@ -0,0 +1,46 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkQueryManage exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Http | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkQueryForm | ||||
| import Messages.Comp.HttpError | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , form : Messages.Comp.BookmarkQueryForm.Texts | ||||
|     , httpError : Http.Error -> String | ||||
|     , formInvalid : String | ||||
|     , saved : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , form = Messages.Comp.BookmarkQueryForm.gb | ||||
|     , httpError = Messages.Comp.HttpError.gb | ||||
|     , formInvalid = "Please correct errors in the form" | ||||
|     , saved = "Bookmark saved" | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , form = Messages.Comp.BookmarkQueryForm.de | ||||
|     , httpError = Messages.Comp.HttpError.de | ||||
|     , formInvalid = "Bitte korrigiere das Formular" | ||||
|     , saved = "Bookmark gespeichert" | ||||
|     } | ||||
							
								
								
									
										34
									
								
								modules/webapp/src/main/elm/Messages/Comp/BookmarkTable.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | ||||
| {- | ||||
|    Copyright 2020 Eike K. & Contributors | ||||
|  | ||||
|    SPDX-License-Identifier: AGPL-3.0-or-later | ||||
| -} | ||||
|  | ||||
|  | ||||
| module Messages.Comp.BookmarkTable exposing | ||||
|     ( Texts | ||||
|     , de | ||||
|     , gb | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
|  | ||||
|  | ||||
| type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , user : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| gb : Texts | ||||
| gb = | ||||
|     { basics = Messages.Basics.gb | ||||
|     , user = "User" | ||||
|     } | ||||
|  | ||||
|  | ||||
| de : Texts | ||||
| de = | ||||
|     { basics = Messages.Basics.de | ||||
|     , user = "Benutzer" | ||||
|     } | ||||
| @@ -14,6 +14,7 @@ module Messages.Comp.PeriodicQueryTaskForm exposing | ||||
| import Data.ChannelType exposing (ChannelType) | ||||
| import Http | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkDropdown | ||||
| import Messages.Comp.CalEventInput | ||||
| import Messages.Comp.ChannelForm | ||||
| import Messages.Comp.HttpError | ||||
| @@ -24,6 +25,7 @@ type alias Texts = | ||||
|     { basics : Messages.Basics.Texts | ||||
|     , calEventInput : Messages.Comp.CalEventInput.Texts | ||||
|     , channelForm : Messages.Comp.ChannelForm.Texts | ||||
|     , bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts | ||||
|     , httpError : Http.Error -> String | ||||
|     , reallyDeleteTask : String | ||||
|     , startOnce : String | ||||
| @@ -49,6 +51,7 @@ gb = | ||||
|     , calEventInput = Messages.Comp.CalEventInput.gb | ||||
|     , channelForm = Messages.Comp.ChannelForm.gb | ||||
|     , httpError = Messages.Comp.HttpError.gb | ||||
|     , bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb | ||||
|     , reallyDeleteTask = "Really delete this notification task?" | ||||
|     , startOnce = "Start Once" | ||||
|     , startTaskNow = "Start this task now" | ||||
| @@ -66,7 +69,7 @@ gb = | ||||
|     , invalidCalEvent = "The calendar event is not valid." | ||||
|     , queryLabel = "Query" | ||||
|     , channelRequired = "A valid channel must be given." | ||||
|     , queryStringRequired = "A query string must be supplied" | ||||
|     , queryStringRequired = "A query string and/or bookmark must be supplied" | ||||
|     , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct | ||||
|     } | ||||
|  | ||||
| @@ -77,6 +80,7 @@ de = | ||||
|     , calEventInput = Messages.Comp.CalEventInput.de | ||||
|     , channelForm = Messages.Comp.ChannelForm.de | ||||
|     , httpError = Messages.Comp.HttpError.de | ||||
|     , bookmarkDropdown = Messages.Comp.BookmarkDropdown.de | ||||
|     , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?" | ||||
|     , startOnce = "Jetzt starten" | ||||
|     , startTaskNow = "Starte den Auftrag sofort" | ||||
| @@ -94,6 +98,6 @@ de = | ||||
|     , invalidCalEvent = "Das Kalenderereignis ist nicht gültig." | ||||
|     , queryLabel = "Abfrage" | ||||
|     , channelRequired = "Ein Versandkanal muss angegeben werden." | ||||
|     , queryStringRequired = "Eine Suchabfrage muss angegeben werden." | ||||
|     , queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark muss angegeben werden." | ||||
|     , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct | ||||
|     } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ module Messages.Comp.SearchMenu exposing | ||||
|  | ||||
| import Data.Direction exposing (Direction) | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkChooser | ||||
| import Messages.Comp.CustomFieldMultiInput | ||||
| import Messages.Comp.FolderSelect | ||||
| import Messages.Comp.TagSelect | ||||
| @@ -24,6 +25,7 @@ type alias Texts = | ||||
|     , customFieldMultiInput : Messages.Comp.CustomFieldMultiInput.Texts | ||||
|     , tagSelect : Messages.Comp.TagSelect.Texts | ||||
|     , folderSelect : Messages.Comp.FolderSelect.Texts | ||||
|     , bookmarkChooser : Messages.Comp.BookmarkChooser.Texts | ||||
|     , chooseDirection : String | ||||
|     , choosePerson : String | ||||
|     , chooseEquipment : String | ||||
| @@ -47,6 +49,7 @@ type alias Texts = | ||||
|     , searchInItemSource : String | ||||
|     , direction : Direction -> String | ||||
|     , trashcan : String | ||||
|     , bookmarks : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -56,6 +59,7 @@ gb = | ||||
|     , customFieldMultiInput = Messages.Comp.CustomFieldMultiInput.gb | ||||
|     , tagSelect = Messages.Comp.TagSelect.gb | ||||
|     , folderSelect = Messages.Comp.FolderSelect.gb | ||||
|     , bookmarkChooser = Messages.Comp.BookmarkChooser.gb | ||||
|     , chooseDirection = "Choose a direction…" | ||||
|     , choosePerson = "Choose a person" | ||||
|     , chooseEquipment = "Choose an equipment" | ||||
| @@ -79,6 +83,7 @@ gb = | ||||
|     , searchInItemSource = "Search in item source…" | ||||
|     , direction = Messages.Data.Direction.gb | ||||
|     , trashcan = "Trash" | ||||
|     , bookmarks = "Bookmarks" | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -88,6 +93,7 @@ de = | ||||
|     , customFieldMultiInput = Messages.Comp.CustomFieldMultiInput.de | ||||
|     , tagSelect = Messages.Comp.TagSelect.de | ||||
|     , folderSelect = Messages.Comp.FolderSelect.de | ||||
|     , bookmarkChooser = Messages.Comp.BookmarkChooser.de | ||||
|     , chooseDirection = "Wähle eine Richtung…" | ||||
|     , choosePerson = "Wähle eine Person…" | ||||
|     , chooseEquipment = "Wähle eine Ausstattung" | ||||
| @@ -111,4 +117,5 @@ de = | ||||
|     , searchInItemSource = "Suche in Dokumentquelle…" | ||||
|     , direction = Messages.Data.Direction.de | ||||
|     , trashcan = "Papierkorb" | ||||
|     , bookmarks = "Bookmarks" | ||||
|     } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ module Messages.Page.Home exposing | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkQueryManage | ||||
| import Messages.Comp.ItemCardList | ||||
| import Messages.Comp.ItemMerge | ||||
| import Messages.Comp.PublishItems | ||||
| @@ -26,6 +27,7 @@ type alias Texts = | ||||
|     , sideMenu : Messages.Page.HomeSideMenu.Texts | ||||
|     , itemMerge : Messages.Comp.ItemMerge.Texts | ||||
|     , publishItems : Messages.Comp.PublishItems.Texts | ||||
|     , bookmarkManage : Messages.Comp.BookmarkQueryManage.Texts | ||||
|     , contentSearch : String | ||||
|     , searchInNames : String | ||||
|     , selectModeTitle : String | ||||
| @@ -46,6 +48,7 @@ type alias Texts = | ||||
|     , mergeItemsTitle : Int -> String | ||||
|     , publishItemsTitle : Int -> String | ||||
|     , publishCurrentQueryTitle : String | ||||
|     , shareResults : String | ||||
|     , nothingSelectedToShare : String | ||||
|     , loadMore : String | ||||
|     , thatsAll : String | ||||
| @@ -53,6 +56,8 @@ type alias Texts = | ||||
|     , listView : String | ||||
|     , tileView : String | ||||
|     , expandCollapseRows : String | ||||
|     , bookmarkQuery : String | ||||
|     , nothingToBookmark : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -64,6 +69,7 @@ gb = | ||||
|     , sideMenu = Messages.Page.HomeSideMenu.gb | ||||
|     , itemMerge = Messages.Comp.ItemMerge.gb | ||||
|     , publishItems = Messages.Comp.PublishItems.gb | ||||
|     , bookmarkManage = Messages.Comp.BookmarkQueryManage.gb | ||||
|     , contentSearch = "Content search…" | ||||
|     , searchInNames = "Search in names…" | ||||
|     , selectModeTitle = "Select Mode" | ||||
| @@ -84,6 +90,7 @@ gb = | ||||
|     , mergeItemsTitle = \n -> "Merge " ++ String.fromInt n ++ " selected items" | ||||
|     , publishItemsTitle = \n -> "Publish " ++ String.fromInt n ++ " selected items" | ||||
|     , publishCurrentQueryTitle = "Publish current results" | ||||
|     , shareResults = "Share Results" | ||||
|     , nothingSelectedToShare = "Sharing everything doesn't work. You need to apply some criteria." | ||||
|     , loadMore = "Load more…" | ||||
|     , thatsAll = "That's all" | ||||
| @@ -91,6 +98,8 @@ gb = | ||||
|     , listView = "List view" | ||||
|     , tileView = "Tile view" | ||||
|     , expandCollapseRows = "Expand/Collapse all" | ||||
|     , bookmarkQuery = "Bookmark query" | ||||
|     , nothingToBookmark = "Nothing selected to bookmark" | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -102,6 +111,7 @@ de = | ||||
|     , sideMenu = Messages.Page.HomeSideMenu.de | ||||
|     , itemMerge = Messages.Comp.ItemMerge.de | ||||
|     , publishItems = Messages.Comp.PublishItems.de | ||||
|     , bookmarkManage = Messages.Comp.BookmarkQueryManage.de | ||||
|     , contentSearch = "Volltextsuche…" | ||||
|     , searchInNames = "Suche in Namen…" | ||||
|     , selectModeTitle = "Auswahlmodus" | ||||
| @@ -122,6 +132,7 @@ de = | ||||
|     , mergeItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente zusammenführen" | ||||
|     , publishItemsTitle = \n -> String.fromInt n ++ " gewählte Dokumente publizieren" | ||||
|     , publishCurrentQueryTitle = "Aktuelle Ansicht publizieren" | ||||
|     , shareResults = "Ergebnisse teilen" | ||||
|     , nothingSelectedToShare = "Alles kann nicht geteilt werden; es muss etwas gesucht werden." | ||||
|     , loadMore = "Mehr laden…" | ||||
|     , thatsAll = "Mehr gibt es nicht" | ||||
| @@ -129,4 +140,6 @@ de = | ||||
|     , listView = "Listenansicht" | ||||
|     , tileView = "Kachelansicht" | ||||
|     , expandCollapseRows = "Alle ein-/ausklappen" | ||||
|     , bookmarkQuery = "Abfrage merken" | ||||
|     , nothingToBookmark = "Keine Abfrage vorhanden" | ||||
|     } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ module Messages.Page.ManageData exposing | ||||
|     ) | ||||
|  | ||||
| import Messages.Basics | ||||
| import Messages.Comp.BookmarkManage | ||||
| import Messages.Comp.CustomFieldManage | ||||
| import Messages.Comp.EquipmentManage | ||||
| import Messages.Comp.FolderManage | ||||
| @@ -28,7 +29,9 @@ type alias Texts = | ||||
|     , personManage : Messages.Comp.PersonManage.Texts | ||||
|     , folderManage : Messages.Comp.FolderManage.Texts | ||||
|     , customFieldManage : Messages.Comp.CustomFieldManage.Texts | ||||
|     , bookmarkManage : Messages.Comp.BookmarkManage.Texts | ||||
|     , manageData : String | ||||
|     , bookmarks : String | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -41,7 +44,9 @@ gb = | ||||
|     , personManage = Messages.Comp.PersonManage.gb | ||||
|     , folderManage = Messages.Comp.FolderManage.gb | ||||
|     , customFieldManage = Messages.Comp.CustomFieldManage.gb | ||||
|     , bookmarkManage = Messages.Comp.BookmarkManage.gb | ||||
|     , manageData = "Manage Data" | ||||
|     , bookmarks = "Bookmarks" | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -54,5 +59,7 @@ de = | ||||
|     , personManage = Messages.Comp.PersonManage.de | ||||
|     , folderManage = Messages.Comp.FolderManage.de | ||||
|     , customFieldManage = Messages.Comp.CustomFieldManage.de | ||||
|     , bookmarkManage = Messages.Comp.BookmarkManage.de | ||||
|     , manageData = "Daten verwalten" | ||||
|     , bookmarks = "Bookmarks" | ||||
|     } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ module Page.Home.Data exposing | ||||
|     , SearchType(..) | ||||
|     , SelectActionMode(..) | ||||
|     , SelectViewModel | ||||
|     , TopWidgetModel(..) | ||||
|     , ViewMode(..) | ||||
|     , createQuery | ||||
|     , doSearchCmd | ||||
| @@ -30,6 +31,7 @@ import Api.Model.BasicResult exposing (BasicResult) | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Api.Model.SearchStats exposing (SearchStats) | ||||
| import Browser.Dom as Dom | ||||
| import Comp.BookmarkQueryManage | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemDetail.FormChange exposing (FormChange) | ||||
| import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) | ||||
| @@ -68,9 +70,15 @@ type alias Model = | ||||
|     , powerSearchInput : Comp.PowerSearchInput.Model | ||||
|     , viewMenuOpen : Bool | ||||
|     , itemRowsOpen : Set String | ||||
|     , topWidgetModel : TopWidgetModel | ||||
|     } | ||||
|  | ||||
|  | ||||
| type TopWidgetModel | ||||
|     = TopWidgetHidden | ||||
|     | BookmarkQuery Comp.BookmarkQueryManage.Model | ||||
|  | ||||
|  | ||||
| type ConfirmModalValue | ||||
|     = ConfirmReprocessItems | ||||
|     | ConfirmDelete | ||||
| @@ -137,6 +145,7 @@ init flags viewMode = | ||||
|     , powerSearchInput = Comp.PowerSearchInput.init | ||||
|     , viewMenuOpen = False | ||||
|     , itemRowsOpen = Set.empty | ||||
|     , topWidgetModel = TopWidgetHidden | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -238,6 +247,8 @@ type Msg | ||||
|     | ToggleShowGroups | ||||
|     | ToggleArrange ItemArrange | ||||
|     | ToggleExpandCollapseRows | ||||
|     | ToggleBookmarkCurrentQueryView | ||||
|     | BookmarkQueryMsg Comp.BookmarkQueryManage.Msg | ||||
|  | ||||
|  | ||||
| type SearchType | ||||
|   | ||||
| @@ -13,6 +13,7 @@ module Page.Home.Update exposing | ||||
| import Api | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Browser.Navigation as Nav | ||||
| import Comp.BookmarkQueryManage | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemDetail.FormChange exposing (FormChange(..)) | ||||
| import Comp.ItemDetail.MultiEditMenu exposing (SaveNameState(..)) | ||||
| @@ -872,7 +873,7 @@ update mId key flags texts settings msg model = | ||||
|                 cmd = | ||||
|                     Api.saveClientSettings flags newSettings (ClientSettingsSaveResp newSettings) | ||||
|             in | ||||
|             noSub ( model, cmd ) | ||||
|             noSub ( { model | viewMenuOpen = False }, cmd ) | ||||
|  | ||||
|         ClientSettingsSaveResp newSettings (Ok res) -> | ||||
|             if res.success then | ||||
| @@ -922,11 +923,69 @@ update mId key flags texts settings msg model = | ||||
|                         ( pm, pc ) = | ||||
|                             Comp.PublishItems.initQuery flags q | ||||
|                     in | ||||
|                     noSub ( { model | viewMode = PublishView pm }, Cmd.map PublishViewMsg pc ) | ||||
|                     noSub ( { model | viewMode = PublishView pm, viewMenuOpen = False }, Cmd.map PublishViewMsg pc ) | ||||
|  | ||||
|                 Nothing -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         ToggleBookmarkCurrentQueryView -> | ||||
|             case createQuery model of | ||||
|                 Just q -> | ||||
|                     case model.topWidgetModel of | ||||
|                         BookmarkQuery _ -> | ||||
|                             noSub ( { model | topWidgetModel = TopWidgetHidden, viewMenuOpen = False }, Cmd.none ) | ||||
|  | ||||
|                         TopWidgetHidden -> | ||||
|                             let | ||||
|                                 ( qm, qc ) = | ||||
|                                     Comp.BookmarkQueryManage.init (Q.render q) | ||||
|                             in | ||||
|                             noSub | ||||
|                                 ( { model | topWidgetModel = BookmarkQuery qm, viewMenuOpen = False } | ||||
|                                 , Cmd.map BookmarkQueryMsg qc | ||||
|                                 ) | ||||
|  | ||||
|                 Nothing -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         BookmarkQueryMsg lm -> | ||||
|             case model.topWidgetModel of | ||||
|                 BookmarkQuery bm -> | ||||
|                     let | ||||
|                         res = | ||||
|                             Comp.BookmarkQueryManage.update flags lm bm | ||||
|  | ||||
|                         nextModel = | ||||
|                             if | ||||
|                                 res.outcome | ||||
|                                     == Comp.BookmarkQueryManage.Cancelled | ||||
|                                     || res.outcome | ||||
|                                     == Comp.BookmarkQueryManage.Done | ||||
|                             then | ||||
|                                 TopWidgetHidden | ||||
|  | ||||
|                             else | ||||
|                                 BookmarkQuery res.model | ||||
|  | ||||
|                         refreshCmd = | ||||
|                             if res.outcome == Comp.BookmarkQueryManage.Done then | ||||
|                                 Cmd.map SearchMenuMsg (Comp.SearchMenu.refreshBookmarks flags) | ||||
|  | ||||
|                             else | ||||
|                                 Cmd.none | ||||
|                     in | ||||
|                     makeResult | ||||
|                         ( { model | topWidgetModel = nextModel } | ||||
|                         , Cmd.batch | ||||
|                             [ Cmd.map BookmarkQueryMsg res.cmd | ||||
|                             , refreshCmd | ||||
|                             ] | ||||
|                         , Sub.map BookmarkQueryMsg res.sub | ||||
|                         ) | ||||
|  | ||||
|                 TopWidgetHidden -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|  | ||||
|         PublishViewMsg lmsg -> | ||||
|             case model.viewMode of | ||||
|                 PublishView inPM -> | ||||
| @@ -942,7 +1001,10 @@ update mId key flags texts settings msg model = | ||||
|                                 ) | ||||
|  | ||||
|                         Comp.PublishItems.OutcomeDone -> | ||||
|                             noSub ( { model | viewMode = SearchView }, Cmd.none ) | ||||
|                             noSub | ||||
|                                 ( { model | viewMode = SearchView } | ||||
|                                 , Cmd.map SearchMenuMsg (Comp.SearchMenu.refreshBookmarks flags) | ||||
|                                 ) | ||||
|  | ||||
|                 _ -> | ||||
|                     noSub ( model, Cmd.none ) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ module Page.Home.View2 exposing (viewContent, viewSidebar) | ||||
|  | ||||
| import Api | ||||
| import Comp.Basic as B | ||||
| import Comp.BookmarkQueryManage | ||||
| import Comp.ConfirmModal | ||||
| import Comp.ItemCardList | ||||
| import Comp.ItemMerge | ||||
| @@ -103,7 +104,21 @@ mainView texts flags settings model = | ||||
|             body | ||||
|  | ||||
|         Nothing -> | ||||
|             itemCardList texts flags settings model | ||||
|             bookmarkQueryWidget texts settings flags model | ||||
|                 ++ itemCardList texts flags settings model | ||||
|  | ||||
|  | ||||
| bookmarkQueryWidget : Texts -> UiSettings -> Flags -> Model -> List (Html Msg) | ||||
| bookmarkQueryWidget texts settings flags model = | ||||
|     case model.topWidgetModel of | ||||
|         BookmarkQuery m -> | ||||
|             [ div [ class "px-2 mb-4 border-l border-r border-b dark:border-slate-600" ] | ||||
|                 [ Html.map BookmarkQueryMsg (Comp.BookmarkQueryManage.view texts.bookmarkManage m) | ||||
|                 ] | ||||
|             ] | ||||
|  | ||||
|         TopWidgetHidden -> | ||||
|             [] | ||||
|  | ||||
|  | ||||
| itemPublishView : Texts -> UiSettings -> Flags -> SelectViewModel -> List (Html Msg) | ||||
| @@ -254,29 +269,26 @@ defaultMenuBar texts flags settings model = | ||||
|  | ||||
|         isListView = | ||||
|             settings.itemSearchArrange == Data.ItemArrange.List | ||||
|  | ||||
|         menuSep = | ||||
|             { icon = i [] [] | ||||
|             , label = "separator" | ||||
|             , disabled = False | ||||
|             , attrs = | ||||
|                 [] | ||||
|             } | ||||
|     in | ||||
|     MB.view | ||||
|         { end = | ||||
|         { start = | ||||
|             [ MB.CustomElement <| | ||||
|                 B.secondaryBasicButton | ||||
|                     { label = "" | ||||
|                     , icon = Icons.share | ||||
|                     , disabled = createQuery model == Nothing | ||||
|                     , handler = onClick TogglePublishCurrentQueryView | ||||
|                     , attrs = | ||||
|                         [ title <| | ||||
|                             if createQuery model == Nothing then | ||||
|                                 texts.nothingSelectedToShare | ||||
|                 if settings.powerSearchEnabled then | ||||
|                     powerSearchBar | ||||
|  | ||||
|                             else | ||||
|                                 texts.publishCurrentQueryTitle | ||||
|                         , classList | ||||
|                             [ ( btnStyle, True ) | ||||
|                             ] | ||||
|                         , href "#" | ||||
|                         ] | ||||
|                     } | ||||
|             , MB.CustomElement <| | ||||
|                 else | ||||
|                     simpleSearchBar | ||||
|             ] | ||||
|         , end = | ||||
|             [ MB.CustomElement <| | ||||
|                 B.secondaryBasicButton | ||||
|                     { label = "" | ||||
|                     , icon = | ||||
| @@ -300,7 +312,7 @@ defaultMenuBar texts flags settings model = | ||||
|                     ] | ||||
|                 } | ||||
|             , MB.Dropdown | ||||
|                 { linkIcon = "fa fa-grip-vertical" | ||||
|                 { linkIcon = "fa fa-bars" | ||||
|                 , label = "" | ||||
|                 , linkClass = | ||||
|                     [ ( S.secondaryBasicButton, True ) | ||||
| @@ -314,6 +326,7 @@ defaultMenuBar texts flags settings model = | ||||
|  | ||||
|                             else | ||||
|                                 i [ class "fa fa-square font-thin" ] [] | ||||
|                       , disabled = False | ||||
|                       , label = texts.showItemGroups | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
| @@ -326,6 +339,7 @@ defaultMenuBar texts flags settings model = | ||||
|  | ||||
|                             else | ||||
|                                 i [ class "fa fa-list" ] [] | ||||
|                       , disabled = False | ||||
|                       , label = texts.listView | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
| @@ -338,6 +352,7 @@ defaultMenuBar texts flags settings model = | ||||
|  | ||||
|                             else | ||||
|                                 i [ class "fa fa-th-large" ] [] | ||||
|                       , disabled = False | ||||
|                       , label = texts.tileView | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
| @@ -346,36 +361,67 @@ defaultMenuBar texts flags settings model = | ||||
|                       } | ||||
|                     , { icon = i [ class "fa fa-compress" ] [] | ||||
|                       , label = texts.expandCollapseRows | ||||
|                       , disabled = False | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
|                             , classList [ ( "hidden", not isListView ) ] | ||||
|                             , onClick ToggleExpandCollapseRows | ||||
|                             ] | ||||
|                       } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|         , start = | ||||
|             [ MB.CustomElement <| | ||||
|                 if settings.powerSearchEnabled then | ||||
|                     powerSearchBar | ||||
|                     , menuSep | ||||
|                     , { label = texts.shareResults | ||||
|                       , icon = Icons.shareIcon "" | ||||
|                       , disabled = createQuery model == Nothing | ||||
|                       , attrs = | ||||
|                             [ title <| | ||||
|                                 if createQuery model == Nothing then | ||||
|                                     texts.nothingSelectedToShare | ||||
|  | ||||
|                 else | ||||
|                     simpleSearchBar | ||||
|             , MB.CustomButton | ||||
|                 { tagger = TogglePreviewFullWidth | ||||
|                 , label = "" | ||||
|                 , icon = Just "fa fa-expand" | ||||
|                 , title = | ||||
|                     if settings.cardPreviewFullWidth then | ||||
|                         texts.fullHeightPreviewTitle | ||||
|                                 else | ||||
|                                     texts.publishCurrentQueryTitle | ||||
|                             , href "#" | ||||
|                             , if createQuery model == Nothing then | ||||
|                                 class "" | ||||
|  | ||||
|                     else | ||||
|                         texts.fullWidthPreviewTitle | ||||
|                 , inputClass = | ||||
|                     [ ( btnStyle, True ) | ||||
|                     , ( "hidden sm:inline-block", False ) | ||||
|                     , ( "bg-gray-200 dark:bg-slate-600", settings.cardPreviewFullWidth ) | ||||
|                               else | ||||
|                                 onClick TogglePublishCurrentQueryView | ||||
|                             ] | ||||
|                       } | ||||
|                     , { label = texts.bookmarkQuery | ||||
|                       , icon = i [ class "fa fa-bookmark" ] [] | ||||
|                       , disabled = createQuery model == Nothing | ||||
|                       , attrs = | ||||
|                             [ title <| | ||||
|                                 if createQuery model == Nothing then | ||||
|                                     texts.nothingToBookmark | ||||
|  | ||||
|                                 else | ||||
|                                     texts.bookmarkQuery | ||||
|                             , href "#" | ||||
|                             , if createQuery model == Nothing then | ||||
|                                 class "" | ||||
|  | ||||
|                               else | ||||
|                                 onClick ToggleBookmarkCurrentQueryView | ||||
|                             ] | ||||
|                       } | ||||
|                     , { label = | ||||
|                             if settings.cardPreviewFullWidth then | ||||
|                                 texts.fullHeightPreviewTitle | ||||
|  | ||||
|                             else | ||||
|                                 texts.fullWidthPreviewTitle | ||||
|                       , icon = i [ class "fa fa-expand" ] [] | ||||
|                       , disabled = False | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
|                             , onClick TogglePreviewFullWidth | ||||
|                             , classList | ||||
|                                 [ ( "hidden sm:inline-block", False ) | ||||
|                                 , ( "bg-gray-200 dark:bg-slate-600", settings.cardPreviewFullWidth ) | ||||
|                                 ] | ||||
|                             ] | ||||
|                       } | ||||
|                     ] | ||||
|                 } | ||||
|             ] | ||||
|   | ||||
| @@ -12,6 +12,7 @@ module Page.ManageData.Data exposing | ||||
|     , init | ||||
|     ) | ||||
|  | ||||
| import Comp.BookmarkManage | ||||
| import Comp.CustomFieldManage | ||||
| import Comp.EquipmentManage | ||||
| import Comp.FolderManage | ||||
| @@ -29,6 +30,7 @@ type alias Model = | ||||
|     , personManageModel : Comp.PersonManage.Model | ||||
|     , folderManageModel : Comp.FolderManage.Model | ||||
|     , fieldManageModel : Comp.CustomFieldManage.Model | ||||
|     , bookmarkModel : Comp.BookmarkManage.Model | ||||
|     } | ||||
|  | ||||
|  | ||||
| @@ -37,6 +39,9 @@ init flags = | ||||
|     let | ||||
|         ( m2, c2 ) = | ||||
|             Comp.TagManage.update flags Comp.TagManage.LoadTags Comp.TagManage.emptyModel | ||||
|  | ||||
|         ( bm, bc ) = | ||||
|             Comp.BookmarkManage.init flags | ||||
|     in | ||||
|     ( { currentTab = Just TagTab | ||||
|       , tagManageModel = m2 | ||||
| @@ -45,8 +50,12 @@ init flags = | ||||
|       , personManageModel = Comp.PersonManage.emptyModel | ||||
|       , folderManageModel = Comp.FolderManage.empty | ||||
|       , fieldManageModel = Comp.CustomFieldManage.empty | ||||
|       , bookmarkModel = bm | ||||
|       } | ||||
|     , Cmd.map TagManageMsg c2 | ||||
|     , Cmd.batch | ||||
|         [ Cmd.map TagManageMsg c2 | ||||
|         , Cmd.map BookmarkMsg bc | ||||
|         ] | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @@ -57,6 +66,7 @@ type Tab | ||||
|     | PersonTab | ||||
|     | FolderTab | ||||
|     | CustomFieldTab | ||||
|     | BookmarkTab | ||||
|  | ||||
|  | ||||
| type Msg | ||||
| @@ -67,3 +77,4 @@ type Msg | ||||
|     | PersonManageMsg Comp.PersonManage.Msg | ||||
|     | FolderMsg Comp.FolderManage.Msg | ||||
|     | CustomFieldMsg Comp.CustomFieldManage.Msg | ||||
|     | BookmarkMsg Comp.BookmarkManage.Msg | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|  | ||||
| module Page.ManageData.Update exposing (update) | ||||
|  | ||||
| import Comp.BookmarkManage | ||||
| import Comp.CustomFieldManage | ||||
| import Comp.EquipmentManage | ||||
| import Comp.FolderManage | ||||
| @@ -17,7 +18,7 @@ import Data.Flags exposing (Flags) | ||||
| import Page.ManageData.Data exposing (..) | ||||
|  | ||||
|  | ||||
| update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) | ||||
| update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Sub Msg ) | ||||
| update flags msg model = | ||||
|     case msg of | ||||
|         SetTab t -> | ||||
| @@ -43,42 +44,49 @@ update flags msg model = | ||||
|                         ( sm, sc ) = | ||||
|                             Comp.FolderManage.init flags | ||||
|                     in | ||||
|                     ( { m | folderManageModel = sm }, Cmd.map FolderMsg sc ) | ||||
|                     ( { m | folderManageModel = sm }, Cmd.map FolderMsg sc, Sub.none ) | ||||
|  | ||||
|                 CustomFieldTab -> | ||||
|                     let | ||||
|                         ( cm, cc ) = | ||||
|                             Comp.CustomFieldManage.init flags | ||||
|                     in | ||||
|                     ( { m | fieldManageModel = cm }, Cmd.map CustomFieldMsg cc ) | ||||
|                     ( { m | fieldManageModel = cm }, Cmd.map CustomFieldMsg cc, Sub.none ) | ||||
|  | ||||
|                 BookmarkTab -> | ||||
|                     let | ||||
|                         ( bm, bc ) = | ||||
|                             Comp.BookmarkManage.init flags | ||||
|                     in | ||||
|                     ( { m | bookmarkModel = bm }, Cmd.map BookmarkMsg bc, Sub.none ) | ||||
|  | ||||
|         TagManageMsg m -> | ||||
|             let | ||||
|                 ( m2, c2 ) = | ||||
|                     Comp.TagManage.update flags m model.tagManageModel | ||||
|             in | ||||
|             ( { model | tagManageModel = m2 }, Cmd.map TagManageMsg c2 ) | ||||
|             ( { model | tagManageModel = m2 }, Cmd.map TagManageMsg c2, Sub.none ) | ||||
|  | ||||
|         EquipManageMsg m -> | ||||
|             let | ||||
|                 ( m2, c2 ) = | ||||
|                     Comp.EquipmentManage.update flags m model.equipManageModel | ||||
|             in | ||||
|             ( { model | equipManageModel = m2 }, Cmd.map EquipManageMsg c2 ) | ||||
|             ( { model | equipManageModel = m2 }, Cmd.map EquipManageMsg c2, Sub.none ) | ||||
|  | ||||
|         OrgManageMsg m -> | ||||
|             let | ||||
|                 ( m2, c2 ) = | ||||
|                     Comp.OrgManage.update flags m model.orgManageModel | ||||
|             in | ||||
|             ( { model | orgManageModel = m2 }, Cmd.map OrgManageMsg c2 ) | ||||
|             ( { model | orgManageModel = m2 }, Cmd.map OrgManageMsg c2, Sub.none ) | ||||
|  | ||||
|         PersonManageMsg m -> | ||||
|             let | ||||
|                 ( m2, c2 ) = | ||||
|                     Comp.PersonManage.update flags m model.personManageModel | ||||
|             in | ||||
|             ( { model | personManageModel = m2 }, Cmd.map PersonManageMsg c2 ) | ||||
|             ( { model | personManageModel = m2 }, Cmd.map PersonManageMsg c2, Sub.none ) | ||||
|  | ||||
|         FolderMsg lm -> | ||||
|             let | ||||
| @@ -87,6 +95,7 @@ update flags msg model = | ||||
|             in | ||||
|             ( { model | folderManageModel = m2 } | ||||
|             , Cmd.map FolderMsg c2 | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         CustomFieldMsg lm -> | ||||
| @@ -96,4 +105,15 @@ update flags msg model = | ||||
|             in | ||||
|             ( { model | fieldManageModel = m2 } | ||||
|             , Cmd.map CustomFieldMsg c2 | ||||
|             , Sub.none | ||||
|             ) | ||||
|  | ||||
|         BookmarkMsg lm -> | ||||
|             let | ||||
|                 ( m2, c2, s2 ) = | ||||
|                     Comp.BookmarkManage.update flags lm model.bookmarkModel | ||||
|             in | ||||
|             ( { model | bookmarkModel = m2 } | ||||
|             , Cmd.map BookmarkMsg c2 | ||||
|             , Sub.map BookmarkMsg s2 | ||||
|             ) | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|  | ||||
| module Page.ManageData.View2 exposing (viewContent, viewSidebar) | ||||
|  | ||||
| import Comp.BookmarkManage | ||||
| import Comp.CustomFieldManage | ||||
| import Comp.EquipmentManage | ||||
| import Comp.FolderManage | ||||
| @@ -121,6 +122,18 @@ viewSidebar texts visible _ settings model = | ||||
|                     [ text texts.basics.customFields | ||||
|                     ] | ||||
|                 ] | ||||
|             , a | ||||
|                 [ href "#" | ||||
|                 , onClick (SetTab BookmarkTab) | ||||
|                 , menuEntryActive model BookmarkTab | ||||
|                 , class S.sidebarLink | ||||
|                 ] | ||||
|                 [ i [ class "fa fa-bookmark" ] [] | ||||
|                 , span | ||||
|                     [ class "ml-3" ] | ||||
|                     [ text texts.bookmarks | ||||
|                     ] | ||||
|                 ] | ||||
|             ] | ||||
|         ] | ||||
|  | ||||
| @@ -133,7 +146,7 @@ viewContent texts flags settings model = | ||||
|         ] | ||||
|         (case model.currentTab of | ||||
|             Just TagTab -> | ||||
|                 viewTags texts model | ||||
|                 viewTags texts settings model | ||||
|  | ||||
|             Just EquipTab -> | ||||
|                 viewEquip texts model | ||||
| @@ -150,6 +163,9 @@ viewContent texts flags settings model = | ||||
|             Just CustomFieldTab -> | ||||
|                 viewCustomFields texts flags settings model | ||||
|  | ||||
|             Just BookmarkTab -> | ||||
|                 viewBookmarks texts flags settings model | ||||
|  | ||||
|             Nothing -> | ||||
|                 [] | ||||
|         ) | ||||
| @@ -164,8 +180,8 @@ menuEntryActive model tab = | ||||
|         class "" | ||||
|  | ||||
|  | ||||
| viewTags : Texts -> Model -> List (Html Msg) | ||||
| viewTags texts model = | ||||
| viewTags : Texts -> UiSettings -> Model -> List (Html Msg) | ||||
| viewTags texts settings model = | ||||
|     [ h2 | ||||
|         [ class S.header1 | ||||
|         , class "inline-flex items-center" | ||||
| @@ -178,6 +194,7 @@ viewTags texts model = | ||||
|     , Html.map TagManageMsg | ||||
|         (Comp.TagManage.view2 | ||||
|             texts.tagManage | ||||
|             settings | ||||
|             model.tagManageModel | ||||
|         ) | ||||
|     ] | ||||
| @@ -274,3 +291,18 @@ viewCustomFields texts flags _ model = | ||||
|             model.fieldManageModel | ||||
|         ) | ||||
|     ] | ||||
|  | ||||
|  | ||||
| viewBookmarks : Texts -> Flags -> UiSettings -> Model -> List (Html Msg) | ||||
| viewBookmarks texts flags settings model = | ||||
|     [ h2 | ||||
|         [ class S.header1 | ||||
|         , class "inline-flex items-center" | ||||
|         ] | ||||
|         [ i [ class "fa fa-bookmark" ] [] | ||||
|         , div [ class "ml-2" ] | ||||
|             [ text texts.bookmarks | ||||
|             ] | ||||
|         ] | ||||
|     , Html.map BookmarkMsg (Comp.BookmarkManage.view texts.bookmarkManage settings flags model.bookmarkModel) | ||||
|     ] | ||||
|   | ||||
| @@ -125,6 +125,7 @@ view texts flags model = | ||||
|                             else | ||||
|                                 i [ class "fa fa-square font-thin" ] [] | ||||
|                       , label = texts.showItemGroups | ||||
|                       , disabled = False | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
|                             , onClick ToggleShowGroups | ||||
| @@ -132,6 +133,7 @@ view texts flags model = | ||||
|                       } | ||||
|                     , { icon = i [ class "fa fa-list" ] [] | ||||
|                       , label = texts.listView | ||||
|                       , disabled = False | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
|                             , onClick (ToggleArrange Data.ItemArrange.List) | ||||
| @@ -139,6 +141,7 @@ view texts flags model = | ||||
|                       } | ||||
|                     , { icon = i [ class "fa fa-th-large" ] [] | ||||
|                       , label = texts.tileView | ||||
|                       , disabled = False | ||||
|                       , attrs = | ||||
|                             [ href "#" | ||||
|                             , onClick (ToggleArrange Data.ItemArrange.Cards) | ||||
|   | ||||
| @@ -55,6 +55,11 @@ errorText = | ||||
|     " text-red-600 dark:text-orange-800 " | ||||
|  | ||||
|  | ||||
| warnMessagePlain : String | ||||
| warnMessagePlain = | ||||
|     "  text-yellow-800 dark:text-amber-200 " | ||||
|  | ||||
|  | ||||
| warnMessage : String | ||||
| warnMessage = | ||||
|     warnMessageColors ++ " border dark:bg-opacity-25 px-2 py-2 rounded " | ||||
| @@ -62,12 +67,17 @@ warnMessage = | ||||
|  | ||||
| warnMessageColors : String | ||||
| warnMessageColors = | ||||
|     " border-yellow-800 bg-yellow-50 text-yellow-800 dark:border-amber-200 dark:bg-amber-800 dark:text-amber-200 " | ||||
|     warnMessagePlain ++ " border-yellow-800 bg-yellow-50 dark:border-amber-200 dark:bg-amber-800 " | ||||
|  | ||||
|  | ||||
| infoMessagePlain : String | ||||
| infoMessagePlain = | ||||
|     " text-blue-800 dark:text-sky-200 " | ||||
|  | ||||
|  | ||||
| infoMessageBase : String | ||||
| infoMessageBase = | ||||
|     " border border-blue-800 bg-blue-100 text-blue-800 dark:border-sky-200 dark:bg-sky-800 dark:text-sky-200 dark:bg-opacity-25 " | ||||
|     infoMessagePlain ++ " border border-blue-800 bg-blue-100 dark:border-sky-200 dark:bg-sky-800 dark:bg-opacity-25 " | ||||
|  | ||||
|  | ||||
| infoMessage : String | ||||
|   | ||||
| @@ -19,6 +19,7 @@ description = "A list of features and limitations." | ||||
|   into searchable PDF/A pdfs. | ||||
| - A powerful [query language](@/docs/query/_index.md) to find | ||||
|   documents | ||||
| - use [bookmarks](@/docs/webapp/bookmarks.md) to save more complex queries | ||||
| - Non-destructive: all your uploaded files are never modified and can | ||||
|   always be downloaded untouched | ||||
| - Organize files using tags, folders, [Custom | ||||
|   | ||||
| @@ -140,8 +140,8 @@ list. When using `&`, results are only concatenated if all lists are | ||||
| not empty; otherwise the result is the empty list. | ||||
|  | ||||
| This example filters all `count` equal to `6` and all `name` equal to | ||||
| `max`. Since there are now `count`s with value `6`, the final result | ||||
| is empty. | ||||
| `max`. Since there are no `count`s with value `6`, the final result is | ||||
| empty. | ||||
|  | ||||
| ``` | ||||
| query: [count=6 & name=max] | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/bookmarks-01.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/bookmarks-02.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 63 KiB | 
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/bookmarks-03.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 25 KiB | 
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/bookmarks-04.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 54 KiB | 
							
								
								
									
										54
									
								
								website/site/content/docs/webapp/bookmarks.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| +++ | ||||
| title = "Bookmarks" | ||||
| weight = 35 | ||||
| [extra] | ||||
| mktoc = true | ||||
| +++ | ||||
|  | ||||
| Bookmarks allow you to save queries under a name and refer to it from the search menu. | ||||
|  | ||||
| ## Creating bookmarks | ||||
|  | ||||
| Bookmarks can be created from the search view. Apply some criteria to | ||||
| select items and then click on the top left menu. | ||||
|  | ||||
| {{ figure(file="bookmarks-02.png") }} | ||||
|  | ||||
| This opens a small form right below the search bar where you can | ||||
| adjust the query and enter the name. You can also decide whether this | ||||
| bookmark is for all users or just for you. | ||||
|  | ||||
| {{ figure(file="bookmarks-03.png") }} | ||||
|  | ||||
| The other way is to go to *Manage Data* where you can edit and delete | ||||
| existing bookmarks and also create new ones. | ||||
|  | ||||
|  | ||||
| ## Using bookmarks | ||||
|  | ||||
| Once you save a bookmark, the search menu will display a new section | ||||
| that shows you all your bookmarks as well as your shares. Clicking one | ||||
| "enables" it, meaning the query is used in conjunction with other | ||||
| criteria. | ||||
|  | ||||
| <div class="columns is-centered"> | ||||
|   {{ imgnormal(file="bookmarks-01.png", width="") }} | ||||
| </div> | ||||
|  | ||||
| An active bookmark has a check icon next to its name. | ||||
|  | ||||
| Note about shares: Shares are also displayed, since they are also | ||||
| bookmarks. However, the list is restricted to only show shares that | ||||
| have a name and are enabled. Since this is an internal view (only for | ||||
| registered users), expired shares are shown as well. | ||||
|  | ||||
| ## Managing bookmarks | ||||
|  | ||||
| The *Manage Data* page has a section for bookmarks. There you can | ||||
| delete and edit bookmarks. | ||||
|  | ||||
| {{ figure(file="bookmarks-04.png") }} | ||||
|  | ||||
| The personal bookmarks are only visible to you. The collective | ||||
| bookmarks are visible to every user in the collective, which also | ||||
| means that every user can edit and delete them. | ||||
| @@ -1,6 +1,6 @@ | ||||
| +++ | ||||
| title = "Customize Item Card" | ||||
| weight = 32 | ||||
| weight = 39 | ||||
| [extra] | ||||
| mktoc = true | ||||
| +++ | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/notification-06.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 159 KiB | 
							
								
								
									
										
											BIN
										
									
								
								website/site/content/docs/webapp/notification-07.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 133 KiB | 
| @@ -146,6 +146,7 @@ items tagged *Todo* etc. | ||||
|  | ||||
| ## Due Items Task | ||||
|  | ||||
| {{ figure(file="notification-06.png") }} | ||||
|  | ||||
| The settings allow to customize the query for searching items. You can | ||||
| choose to only include items that have one or more tags (these are | ||||
| @@ -175,8 +176,11 @@ selected. In other words, only items with an overdue time of *at most* | ||||
| ## Generic Query Task | ||||
|  | ||||
| This is the generic version of the *Due Items Task*. Instead of | ||||
| selecting teh items via form elements, you can define a custom | ||||
| [query](@/docs/query/_index.md). | ||||
| selecting the items via form elements, you can define a custom | ||||
| [query](@/docs/query/_index.md) and optionally in combination with a | ||||
| [bookmark](@/docs/webapp/bookmarks.md). | ||||
|  | ||||
| {{ figure(file="notification-07.png") }} | ||||
|  | ||||
| ## Schedule | ||||
|  | ||||
|   | ||||
| Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 165 KiB | 
| Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 116 KiB | 
| Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 130 KiB | 
| @@ -87,7 +87,8 @@ settings](@/docs/webapp/emailsettings.md#smtp-settings) defined). | ||||
|  | ||||
| When at the search page, add some criteria until you have the results | ||||
| you want to publish. In the screenshot below all items with tag | ||||
| `Manual` are selected. Then click the *Share Button*. | ||||
| `Manual` are selected. Then click the *Share Results* item in the | ||||
| menu. | ||||
|  | ||||
| {{ figure(file="share-01.png") }} | ||||
|  | ||||
| @@ -119,8 +120,9 @@ provided from your address book. If you type in an arbitrary address | ||||
| current address. You can hit *Backspace* two times to remove the last | ||||
| e-mail address. | ||||
|  | ||||
| The new share can now be found in *Collective Profile -> Shares*. | ||||
| Clicking *Done* brings you back to the search results. | ||||
| The new share can now be found in *Collective Profile -> Shares* and | ||||
| is also added to the *Bookmarks* section in the search menu. Clicking | ||||
| *Done* brings you back to the search results. | ||||
|  | ||||
| ## Creating from selecting items | ||||
|  | ||||
|   | ||||