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