mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-02-22 14:03:26 +00:00
Merge pull request #1006 from eikek/feature/347-delete-items
Feature/347 delete items
This commit is contained in:
commit
fe7d64d989
@ -238,6 +238,10 @@ val openapiScalaSettings = Seq(
|
||||
field.copy(typeDef =
|
||||
TypeDef("EquipmentUse", Imports("docspell.common.EquipmentUse"))
|
||||
)
|
||||
case "searchmode" =>
|
||||
field =>
|
||||
field
|
||||
.copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode")))
|
||||
}))
|
||||
)
|
||||
|
||||
|
@ -18,8 +18,7 @@ import docspell.store.UpdateResult
|
||||
import docspell.store.queries.QCollective
|
||||
import docspell.store.queue.JobQueue
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.UserTask
|
||||
import docspell.store.usertask.UserTaskStore
|
||||
import docspell.store.usertask.{UserTask, UserTaskScope, UserTaskStore}
|
||||
import docspell.store.{AddResult, Store}
|
||||
|
||||
import com.github.eikek.calev._
|
||||
@ -62,6 +61,8 @@ trait OCollective[F[_]] {
|
||||
|
||||
def startLearnClassifier(collective: Ident): F[Unit]
|
||||
|
||||
def startEmptyTrash(collective: Ident): F[Unit]
|
||||
|
||||
/** Submits a task that (re)generates the preview images for all
|
||||
* attachments of the given collective.
|
||||
*/
|
||||
@ -147,9 +148,14 @@ object OCollective {
|
||||
.transact(RCollective.updateSettings(collective, sett))
|
||||
.attempt
|
||||
.map(AddResult.fromUpdate)
|
||||
.flatMap(res => updateLearnClassifierTask(collective, sett) *> res.pure[F])
|
||||
.flatMap(res =>
|
||||
updateLearnClassifierTask(collective, sett) *> updateEmptyTrashTask(
|
||||
collective,
|
||||
sett
|
||||
) *> res.pure[F]
|
||||
)
|
||||
|
||||
def updateLearnClassifierTask(coll: Ident, sett: Settings) =
|
||||
private def updateLearnClassifierTask(coll: Ident, sett: Settings): F[Unit] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
on = sett.classifier.map(_.enabled).getOrElse(false)
|
||||
@ -162,7 +168,23 @@ object OCollective {
|
||||
None,
|
||||
LearnClassifierArgs(coll)
|
||||
)
|
||||
_ <- uts.updateOneTask(AccountId(coll, LearnClassifierArgs.taskName), ut)
|
||||
_ <- uts.updateOneTask(UserTaskScope(coll), ut)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
private def updateEmptyTrashTask(coll: Ident, sett: Settings): F[Unit] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
timer = sett.emptyTrash.getOrElse(CalEvent.unsafe(""))
|
||||
ut = UserTask(
|
||||
id,
|
||||
EmptyTrashArgs.taskName,
|
||||
true,
|
||||
timer,
|
||||
None,
|
||||
EmptyTrashArgs(coll)
|
||||
)
|
||||
_ <- uts.updateOneTask(UserTaskScope(coll), ut)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
@ -176,7 +198,23 @@ object OCollective {
|
||||
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
||||
None,
|
||||
LearnClassifierArgs(collective)
|
||||
).encode.toPeriodicTask(AccountId(collective, LearnClassifierArgs.taskName))
|
||||
).encode.toPeriodicTask(UserTaskScope(collective))
|
||||
job <- ut.toJob
|
||||
_ <- queue.insert(job)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def startEmptyTrash(collective: Ident): F[Unit] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
ut <- UserTask(
|
||||
id,
|
||||
EmptyTrashArgs.taskName,
|
||||
true,
|
||||
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
|
||||
None,
|
||||
EmptyTrashArgs(collective)
|
||||
).encode.toPeriodicTask(UserTaskScope(collective))
|
||||
job <- ut.toJob
|
||||
_ <- queue.insert(job)
|
||||
_ <- joex.notifyAllNodes
|
||||
|
@ -124,6 +124,8 @@ trait OItem[F[_]] {
|
||||
collective: Ident
|
||||
): F[AddResult]
|
||||
|
||||
def restore(items: NonEmptyList[Ident], collective: Ident): F[UpdateResult]
|
||||
|
||||
def setItemDate(
|
||||
item: NonEmptyList[Ident],
|
||||
date: Option[Timestamp],
|
||||
@ -144,6 +146,8 @@ trait OItem[F[_]] {
|
||||
|
||||
def deleteAttachment(id: Ident, collective: Ident): F[Int]
|
||||
|
||||
def setDeletedState(items: NonEmptyList[Ident], collective: Ident): F[Int]
|
||||
|
||||
def deleteAttachmentMultiple(
|
||||
attachments: NonEmptyList[Ident],
|
||||
collective: Ident
|
||||
@ -580,6 +584,17 @@ object OItem {
|
||||
.attempt
|
||||
.map(AddResult.fromUpdate)
|
||||
|
||||
def restore(
|
||||
items: NonEmptyList[Ident],
|
||||
collective: Ident
|
||||
): F[UpdateResult] =
|
||||
UpdateResult.fromUpdate(
|
||||
store
|
||||
.transact(
|
||||
RItem.restoreStateForCollective(items, ItemState.Created, collective)
|
||||
)
|
||||
)
|
||||
|
||||
def setItemDate(
|
||||
items: NonEmptyList[Ident],
|
||||
date: Option[Timestamp],
|
||||
@ -612,6 +627,9 @@ object OItem {
|
||||
n = results.sum
|
||||
} yield n
|
||||
|
||||
def setDeletedState(items: NonEmptyList[Ident], collective: Ident): F[Int] =
|
||||
store.transact(RItem.setState(items, collective, ItemState.Deleted))
|
||||
|
||||
def getProposals(item: Ident, collective: Ident): F[MetaProposalList] =
|
||||
store.transact(QAttachment.getMetaProposals(item, collective))
|
||||
|
||||
|
@ -23,6 +23,8 @@ import doobie.implicits._
|
||||
trait OItemSearch[F[_]] {
|
||||
def findItem(id: Ident, collective: Ident): F[Option[ItemData]]
|
||||
|
||||
def findDeleted(collective: Ident, limit: Int): F[Vector[RItem]]
|
||||
|
||||
def findItems(maxNoteLen: Int)(q: Query, batch: Batch): F[Vector[ListItem]]
|
||||
|
||||
/** Same as `findItems` but does more queries per item to find all tags. */
|
||||
@ -145,6 +147,13 @@ object OItemSearch {
|
||||
.toVector
|
||||
}
|
||||
|
||||
def findDeleted(collective: Ident, limit: Int): F[Vector[RItem]] =
|
||||
store
|
||||
.transact(RItem.findDeleted(collective, limit))
|
||||
.take(limit.toLong)
|
||||
.compile
|
||||
.toVector
|
||||
|
||||
def findItemsWithTags(
|
||||
maxNoteLen: Int
|
||||
)(q: Query, batch: Batch): F[Vector[ListItemWithTags]] =
|
||||
|
@ -19,7 +19,10 @@ import docspell.store.queries.SearchSummary
|
||||
|
||||
import org.log4s.getLogger
|
||||
|
||||
/** A "porcelain" api on top of OFulltext and OItemSearch. */
|
||||
/** A "porcelain" api on top of OFulltext and OItemSearch. This takes
|
||||
* care of restricting the items to a subset, e.g. only items that
|
||||
* have a "valid" state.
|
||||
*/
|
||||
trait OSimpleSearch[F[_]] {
|
||||
|
||||
/** Search for items using the given query and optional fulltext
|
||||
@ -36,7 +39,7 @@ trait OSimpleSearch[F[_]] {
|
||||
* and not the results.
|
||||
*/
|
||||
def searchSummary(
|
||||
useFTS: Boolean
|
||||
settings: StatsSettings
|
||||
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary]
|
||||
|
||||
/** Calls `search` by parsing the given query string into a query that
|
||||
@ -53,12 +56,12 @@ trait OSimpleSearch[F[_]] {
|
||||
* results.
|
||||
*/
|
||||
final def searchSummaryByString(
|
||||
useFTS: Boolean
|
||||
settings: StatsSettings
|
||||
)(fix: Query.Fix, q: ItemQueryString)(implicit
|
||||
F: Applicative[F]
|
||||
): F[StringSearchResult[SearchSummary]] =
|
||||
OSimpleSearch.applySearch[F, SearchSummary](fix, q)((iq, fts) =>
|
||||
searchSummary(useFTS)(iq, fts)
|
||||
searchSummary(settings)(iq, fts)
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,7 +86,12 @@ object OSimpleSearch {
|
||||
batch: Batch,
|
||||
useFTS: Boolean,
|
||||
resolveDetails: Boolean,
|
||||
maxNoteLen: Int
|
||||
maxNoteLen: Int,
|
||||
searchMode: SearchMode
|
||||
)
|
||||
final case class StatsSettings(
|
||||
useFTS: Boolean,
|
||||
searchMode: SearchMode
|
||||
)
|
||||
|
||||
sealed trait Items {
|
||||
@ -214,7 +222,11 @@ object OSimpleSearch {
|
||||
// 1. fulltext only if fulltextQuery.isDefined && q.isEmpty && useFTS
|
||||
// 2. sql+fulltext if fulltextQuery.isDefined && q.nonEmpty && useFTS
|
||||
// 3. sql-only else (if fulltextQuery.isEmpty || !useFTS)
|
||||
val validItemQuery = q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
|
||||
val validItemQuery =
|
||||
settings.searchMode match {
|
||||
case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
|
||||
case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
|
||||
}
|
||||
fulltextQuery match {
|
||||
case Some(ftq) if settings.useFTS =>
|
||||
if (q.isEmpty) {
|
||||
@ -267,18 +279,24 @@ object OSimpleSearch {
|
||||
}
|
||||
|
||||
def searchSummary(
|
||||
useFTS: Boolean
|
||||
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary] =
|
||||
settings: StatsSettings
|
||||
)(q: Query, fulltextQuery: Option[String]): F[SearchSummary] = {
|
||||
val validItemQuery =
|
||||
settings.searchMode match {
|
||||
case SearchMode.Trashed => q.withFix(_.andQuery(ItemQuery.Expr.Trashed))
|
||||
case SearchMode.Normal => q.withFix(_.andQuery(ItemQuery.Expr.ValidItemStates))
|
||||
}
|
||||
fulltextQuery match {
|
||||
case Some(ftq) if useFTS =>
|
||||
case Some(ftq) if settings.useFTS =>
|
||||
if (q.isEmpty)
|
||||
fts.findIndexOnlySummary(q.fix.account, OFulltext.FtsInput(ftq))
|
||||
else
|
||||
fts
|
||||
.findItemsSummary(q, OFulltext.FtsInput(ftq))
|
||||
.findItemsSummary(validItemQuery, OFulltext.FtsInput(ftq))
|
||||
|
||||
case _ =>
|
||||
is.findItemsSummary(q)
|
||||
is.findItemsSummary(validItemQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,47 +21,47 @@ trait OUserTask[F[_]] {
|
||||
|
||||
/** Return the settings for all scan-mailbox tasks of the current user.
|
||||
*/
|
||||
def getScanMailbox(account: AccountId): Stream[F, UserTask[ScanMailboxArgs]]
|
||||
def getScanMailbox(scope: UserTaskScope): Stream[F, UserTask[ScanMailboxArgs]]
|
||||
|
||||
/** Find a scan-mailbox task by the given id. */
|
||||
def findScanMailbox(
|
||||
id: Ident,
|
||||
account: AccountId
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[ScanMailboxArgs]]
|
||||
|
||||
/** Updates the scan-mailbox tasks and notifies the joex nodes.
|
||||
*/
|
||||
def submitScanMailbox(
|
||||
account: AccountId,
|
||||
scope: UserTaskScope,
|
||||
task: UserTask[ScanMailboxArgs]
|
||||
): F[Unit]
|
||||
|
||||
/** Return the settings for all the notify-due-items task of the
|
||||
* current user.
|
||||
*/
|
||||
def getNotifyDueItems(account: AccountId): Stream[F, UserTask[NotifyDueItemsArgs]]
|
||||
def getNotifyDueItems(scope: UserTaskScope): Stream[F, UserTask[NotifyDueItemsArgs]]
|
||||
|
||||
/** Find a notify-due-items task by the given id. */
|
||||
def findNotifyDueItems(
|
||||
id: Ident,
|
||||
account: AccountId
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[NotifyDueItemsArgs]]
|
||||
|
||||
/** Updates the notify-due-items tasks and notifies the joex nodes.
|
||||
*/
|
||||
def submitNotifyDueItems(
|
||||
account: AccountId,
|
||||
scope: UserTaskScope,
|
||||
task: UserTask[NotifyDueItemsArgs]
|
||||
): F[Unit]
|
||||
|
||||
/** Removes a user task with the given id. */
|
||||
def deleteTask(account: AccountId, id: Ident): F[Unit]
|
||||
def deleteTask(scope: UserTaskScope, id: Ident): F[Unit]
|
||||
|
||||
/** Discards the schedule and immediately submits the task to the job
|
||||
* executor's queue. It will not update the corresponding periodic
|
||||
* task.
|
||||
*/
|
||||
def executeNow[A](account: AccountId, task: UserTask[A])(implicit
|
||||
def executeNow[A](scope: UserTaskScope, task: UserTask[A])(implicit
|
||||
E: Encoder[A]
|
||||
): F[Unit]
|
||||
}
|
||||
@ -75,57 +75,59 @@ object OUserTask {
|
||||
): Resource[F, OUserTask[F]] =
|
||||
Resource.pure[F, OUserTask[F]](new OUserTask[F] {
|
||||
|
||||
def executeNow[A](account: AccountId, task: UserTask[A])(implicit
|
||||
def executeNow[A](scope: UserTaskScope, task: UserTask[A])(implicit
|
||||
E: Encoder[A]
|
||||
): F[Unit] =
|
||||
for {
|
||||
ptask <- task.encode.toPeriodicTask(account)
|
||||
ptask <- task.encode.toPeriodicTask(scope)
|
||||
job <- ptask.toJob
|
||||
_ <- queue.insert(job)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def getScanMailbox(account: AccountId): Stream[F, UserTask[ScanMailboxArgs]] =
|
||||
def getScanMailbox(scope: UserTaskScope): Stream[F, UserTask[ScanMailboxArgs]] =
|
||||
store
|
||||
.getByName[ScanMailboxArgs](account, ScanMailboxArgs.taskName)
|
||||
.getByName[ScanMailboxArgs](scope, ScanMailboxArgs.taskName)
|
||||
|
||||
def findScanMailbox(
|
||||
id: Ident,
|
||||
account: AccountId
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[ScanMailboxArgs]] =
|
||||
OptionT(getScanMailbox(account).find(_.id == id).compile.last)
|
||||
OptionT(getScanMailbox(scope).find(_.id == id).compile.last)
|
||||
|
||||
def deleteTask(account: AccountId, id: Ident): F[Unit] =
|
||||
def deleteTask(scope: UserTaskScope, id: Ident): F[Unit] =
|
||||
(for {
|
||||
_ <- store.getByIdRaw(account, id)
|
||||
_ <- OptionT.liftF(store.deleteTask(account, id))
|
||||
_ <- store.getByIdRaw(scope, id)
|
||||
_ <- OptionT.liftF(store.deleteTask(scope, id))
|
||||
} yield ()).getOrElse(())
|
||||
|
||||
def submitScanMailbox(
|
||||
account: AccountId,
|
||||
scope: UserTaskScope,
|
||||
task: UserTask[ScanMailboxArgs]
|
||||
): F[Unit] =
|
||||
for {
|
||||
_ <- store.updateTask[ScanMailboxArgs](account, task)
|
||||
_ <- store.updateTask[ScanMailboxArgs](scope, task)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
|
||||
def getNotifyDueItems(account: AccountId): Stream[F, UserTask[NotifyDueItemsArgs]] =
|
||||
def getNotifyDueItems(
|
||||
scope: UserTaskScope
|
||||
): Stream[F, UserTask[NotifyDueItemsArgs]] =
|
||||
store
|
||||
.getByName[NotifyDueItemsArgs](account, NotifyDueItemsArgs.taskName)
|
||||
.getByName[NotifyDueItemsArgs](scope, NotifyDueItemsArgs.taskName)
|
||||
|
||||
def findNotifyDueItems(
|
||||
id: Ident,
|
||||
account: AccountId
|
||||
scope: UserTaskScope
|
||||
): OptionT[F, UserTask[NotifyDueItemsArgs]] =
|
||||
OptionT(getNotifyDueItems(account).find(_.id == id).compile.last)
|
||||
OptionT(getNotifyDueItems(scope).find(_.id == id).compile.last)
|
||||
|
||||
def submitNotifyDueItems(
|
||||
account: AccountId,
|
||||
scope: UserTaskScope,
|
||||
task: UserTask[NotifyDueItemsArgs]
|
||||
): F[Unit] =
|
||||
for {
|
||||
_ <- store.updateTask[NotifyDueItemsArgs](account, task)
|
||||
_ <- store.updateTask[NotifyDueItemsArgs](scope, task)
|
||||
_ <- joex.notifyAllNodes
|
||||
} yield ()
|
||||
})
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.common
|
||||
|
||||
import docspell.common.syntax.all._
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import io.circe._
|
||||
import io.circe.generic.semiauto._
|
||||
|
||||
/** Arguments to the empty-trash task.
|
||||
*
|
||||
* This task is run periodically to really delete all soft-deleted
|
||||
* items. These are items with state `ItemState.Deleted`.
|
||||
*/
|
||||
case class EmptyTrashArgs(
|
||||
collective: Ident
|
||||
) {
|
||||
|
||||
def makeSubject: String =
|
||||
"Empty trash"
|
||||
|
||||
}
|
||||
|
||||
object EmptyTrashArgs {
|
||||
|
||||
val taskName = Ident.unsafe("empty-trash")
|
||||
|
||||
val defaultSchedule = CalEvent.unsafe("*-*-1/7 03:00:00")
|
||||
|
||||
implicit val jsonEncoder: Encoder[EmptyTrashArgs] =
|
||||
deriveEncoder[EmptyTrashArgs]
|
||||
implicit val jsonDecoder: Decoder[EmptyTrashArgs] =
|
||||
deriveDecoder[EmptyTrashArgs]
|
||||
|
||||
def parse(str: String): Either[Throwable, EmptyTrashArgs] =
|
||||
str.parseJsonAs[EmptyTrashArgs]
|
||||
|
||||
}
|
@ -28,11 +28,13 @@ object ItemState {
|
||||
case object Processing extends ItemState
|
||||
case object Created extends ItemState
|
||||
case object Confirmed extends ItemState
|
||||
case object Deleted extends ItemState
|
||||
|
||||
def premature: ItemState = Premature
|
||||
def processing: ItemState = Processing
|
||||
def created: ItemState = Created
|
||||
def confirmed: ItemState = Confirmed
|
||||
def deleted: ItemState = Deleted
|
||||
|
||||
def fromString(str: String): Either[String, ItemState] =
|
||||
str.toLowerCase match {
|
||||
@ -40,6 +42,7 @@ object ItemState {
|
||||
case "processing" => Right(Processing)
|
||||
case "created" => Right(Created)
|
||||
case "confirmed" => Right(Confirmed)
|
||||
case "deleted" => Right(Deleted)
|
||||
case _ => Left(s"Invalid item state: $str")
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.common
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
|
||||
import io.circe.Decoder
|
||||
import io.circe.Encoder
|
||||
|
||||
sealed trait SearchMode { self: Product =>
|
||||
|
||||
final def name: String =
|
||||
productPrefix.toLowerCase
|
||||
|
||||
}
|
||||
|
||||
object SearchMode {
|
||||
|
||||
final case object Normal extends SearchMode
|
||||
final case object Trashed extends SearchMode
|
||||
|
||||
def fromString(str: String): Either[String, SearchMode] =
|
||||
str.toLowerCase match {
|
||||
case "normal" => Right(Normal)
|
||||
case "trashed" => Right(Trashed)
|
||||
case _ => Left(s"Invalid search mode: $str")
|
||||
}
|
||||
|
||||
val all: NonEmptyList[SearchMode] =
|
||||
NonEmptyList.of(Normal, Trashed)
|
||||
|
||||
def unsafe(str: String): SearchMode =
|
||||
fromString(str).fold(sys.error, identity)
|
||||
|
||||
implicit val jsonDecoder: Decoder[SearchMode] =
|
||||
Decoder.decodeString.emap(fromString)
|
||||
implicit val jsonEncoder: Encoder[SearchMode] =
|
||||
Encoder.encodeString.contramap(_.name)
|
||||
}
|
@ -18,6 +18,7 @@ import docspell.common._
|
||||
import docspell.ftsclient.FtsClient
|
||||
import docspell.ftssolr.SolrFtsClient
|
||||
import docspell.joex.analysis.RegexNerFile
|
||||
import docspell.joex.emptytrash._
|
||||
import docspell.joex.fts.{MigrationTask, ReIndexTask}
|
||||
import docspell.joex.hk._
|
||||
import docspell.joex.learn.LearnClassifierTask
|
||||
@ -33,7 +34,7 @@ import docspell.joex.scheduler._
|
||||
import docspell.joexapi.client.JoexClient
|
||||
import docspell.store.Store
|
||||
import docspell.store.queue._
|
||||
import docspell.store.records.RJobLog
|
||||
import docspell.store.records.{REmptyTrashSetting, RJobLog}
|
||||
|
||||
import emil.javamail._
|
||||
import org.http4s.blaze.client.BlazeClientBuilder
|
||||
@ -76,11 +77,23 @@ final class JoexAppImpl[F[_]: Async](
|
||||
HouseKeepingTask
|
||||
.periodicTask[F](cfg.houseKeeping.schedule)
|
||||
.flatMap(pstore.insert) *>
|
||||
scheduleEmptyTrashTasks *>
|
||||
MigrationTask.job.flatMap(queue.insertIfNew) *>
|
||||
AllPreviewsTask
|
||||
.job(MakePreviewArgs.StoreMode.WhenMissing, None)
|
||||
.flatMap(queue.insertIfNew) *>
|
||||
AllPageCountTask.job.flatMap(queue.insertIfNew)
|
||||
|
||||
private def scheduleEmptyTrashTasks: F[Unit] =
|
||||
store
|
||||
.transact(
|
||||
REmptyTrashSetting.findForAllCollectives(EmptyTrashArgs.defaultSchedule, 50)
|
||||
)
|
||||
.evalMap(es => EmptyTrashTask.periodicTask(es.cid, es.schedule))
|
||||
.evalMap(pstore.insert)
|
||||
.compile
|
||||
.drain
|
||||
|
||||
}
|
||||
|
||||
object JoexAppImpl {
|
||||
@ -94,16 +107,17 @@ object JoexAppImpl {
|
||||
for {
|
||||
httpClient <- BlazeClientBuilder[F](clientEC).resource
|
||||
client = JoexClient(httpClient)
|
||||
store <- Store.create(cfg.jdbc, connectEC)
|
||||
queue <- JobQueue(store)
|
||||
pstore <- PeriodicTaskStore.create(store)
|
||||
nodeOps <- ONode(store)
|
||||
joex <- OJoex(client, store)
|
||||
upload <- OUpload(store, queue, cfg.files, joex)
|
||||
fts <- createFtsClient(cfg)(httpClient)
|
||||
itemOps <- OItem(store, fts, queue, joex)
|
||||
analyser <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig)
|
||||
regexNer <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, store)
|
||||
store <- Store.create(cfg.jdbc, connectEC)
|
||||
queue <- JobQueue(store)
|
||||
pstore <- PeriodicTaskStore.create(store)
|
||||
nodeOps <- ONode(store)
|
||||
joex <- OJoex(client, store)
|
||||
upload <- OUpload(store, queue, cfg.files, joex)
|
||||
fts <- createFtsClient(cfg)(httpClient)
|
||||
itemOps <- OItem(store, fts, queue, joex)
|
||||
itemSearchOps <- OItemSearch(store)
|
||||
analyser <- TextAnalyser.create[F](cfg.textAnalysis.textAnalysisConfig)
|
||||
regexNer <- RegexNerFile(cfg.textAnalysis.regexNerFileConfig, store)
|
||||
javaEmil =
|
||||
JavaMailEmil(Settings.defaultSettings.copy(debug = cfg.mailDebug))
|
||||
sch <- SchedulerBuilder(cfg.scheduler, store)
|
||||
@ -206,6 +220,13 @@ object JoexAppImpl {
|
||||
AllPageCountTask.onCancel[F]
|
||||
)
|
||||
)
|
||||
.withTask(
|
||||
JobTask.json(
|
||||
EmptyTrashArgs.taskName,
|
||||
EmptyTrashTask[F](itemOps, itemSearchOps),
|
||||
EmptyTrashTask.onCancel[F]
|
||||
)
|
||||
)
|
||||
.resource
|
||||
psch <- PeriodicScheduler.create(
|
||||
cfg.periodicScheduler,
|
||||
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.joex.emptytrash
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.ops.{OItem, OItemSearch}
|
||||
import docspell.common._
|
||||
import docspell.joex.scheduler._
|
||||
import docspell.store.records.{RItem, RPeriodicTask}
|
||||
import docspell.store.usertask.{UserTask, UserTaskScope}
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
|
||||
object EmptyTrashTask {
|
||||
type Args = EmptyTrashArgs
|
||||
|
||||
def onCancel[F[_]]: Task[F, Args, Unit] =
|
||||
Task.log(_.warn("Cancelling empty-trash task"))
|
||||
|
||||
private val pageSize = 20
|
||||
|
||||
def periodicTask[F[_]: Sync](collective: Ident, ce: CalEvent): F[RPeriodicTask] =
|
||||
Ident
|
||||
.randomId[F]
|
||||
.flatMap(id =>
|
||||
UserTask(
|
||||
id,
|
||||
EmptyTrashArgs.taskName,
|
||||
true,
|
||||
ce,
|
||||
None,
|
||||
EmptyTrashArgs(collective)
|
||||
).encode.toPeriodicTask(UserTaskScope(collective))
|
||||
)
|
||||
|
||||
def apply[F[_]: Async](
|
||||
itemOps: OItem[F],
|
||||
itemSearchOps: OItemSearch[F]
|
||||
): Task[F, Args, Unit] =
|
||||
Task { ctx =>
|
||||
val collId = ctx.args.collective
|
||||
for {
|
||||
_ <- ctx.logger.info(s"Starting removing all soft-deleted items")
|
||||
nDeleted <- deleteAll(collId, itemOps, itemSearchOps, ctx)
|
||||
_ <- ctx.logger.info(s"Finished deleting ${nDeleted} items")
|
||||
} yield ()
|
||||
}
|
||||
|
||||
private def deleteAll[F[_]: Async](
|
||||
collective: Ident,
|
||||
itemOps: OItem[F],
|
||||
itemSearchOps: OItemSearch[F],
|
||||
ctx: Context[F, _]
|
||||
): F[Int] =
|
||||
Stream
|
||||
.eval(itemSearchOps.findDeleted(collective, pageSize))
|
||||
.evalMap(deleteChunk(collective, itemOps, ctx))
|
||||
.repeat
|
||||
.takeWhile(_ > 0)
|
||||
.compile
|
||||
.foldMonoid
|
||||
|
||||
private def deleteChunk[F[_]: Async](
|
||||
collective: Ident,
|
||||
itemOps: OItem[F],
|
||||
ctx: Context[F, _]
|
||||
)(chunk: Vector[RItem]): F[Int] =
|
||||
if (chunk.isEmpty) {
|
||||
0.pure[F]
|
||||
} else {
|
||||
ctx.logger.info(s"Deleting next ${chunk.size} items …") *>
|
||||
chunk.traverse(i =>
|
||||
ctx.logger.debug(s"Delete item ${i.id.id} / ${i.name} now") *>
|
||||
itemOps.deleteItem(i.id, collective)
|
||||
) *> chunk.size.pure[F]
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import docspell.common._
|
||||
import docspell.joex.Config
|
||||
import docspell.joex.scheduler.Task
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.UserTaskScope
|
||||
|
||||
import com.github.eikek.calev._
|
||||
|
||||
@ -36,11 +37,10 @@ object HouseKeepingTask {
|
||||
RPeriodicTask
|
||||
.createJson(
|
||||
true,
|
||||
UserTaskScope(DocspellSystem.taskGroup),
|
||||
taskName,
|
||||
DocspellSystem.taskGroup,
|
||||
(),
|
||||
"Docspell house-keeping",
|
||||
DocspellSystem.taskGroup,
|
||||
Priority.Low,
|
||||
ce,
|
||||
None
|
||||
|
@ -125,7 +125,8 @@ object ItemQuery {
|
||||
final case class ChecksumMatch(checksum: String) extends Expr
|
||||
final case class AttachId(id: String) extends Expr
|
||||
|
||||
case object ValidItemStates extends Expr
|
||||
final case object ValidItemStates extends Expr
|
||||
final case object Trashed extends Expr
|
||||
|
||||
// things that can be expressed with terms above
|
||||
sealed trait MacroExpr extends Expr {
|
||||
|
@ -75,9 +75,10 @@ object ExprUtil {
|
||||
expr
|
||||
case AttachId(_) =>
|
||||
expr
|
||||
|
||||
case ValidItemStates =>
|
||||
expr
|
||||
case Trashed =>
|
||||
expr
|
||||
}
|
||||
|
||||
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =
|
||||
|
@ -1136,6 +1136,27 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/collective/emptytrash/startonce:
|
||||
post:
|
||||
operationId: "sec-collective-emptytrash-start-now"
|
||||
tags: [ Collective ]
|
||||
summary: Starts the empty trash task
|
||||
description: |
|
||||
Submits a task to remove all items from the database that have
|
||||
been "soft-deleted". This task is also run periodically and
|
||||
can be triggered here to be immediatly submitted.
|
||||
|
||||
The request is empty, settings are used from the collective.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/user:
|
||||
get:
|
||||
operationId: "sec-user-get-all"
|
||||
@ -1478,6 +1499,7 @@ paths:
|
||||
- $ref: "#/components/parameters/limit"
|
||||
- $ref: "#/components/parameters/offset"
|
||||
- $ref: "#/components/parameters/withDetails"
|
||||
- $ref: "#/components/parameters/searchMode"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
@ -1576,6 +1598,7 @@ paths:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/q"
|
||||
- $ref: "#/components/parameters/searchMode"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
@ -1607,7 +1630,9 @@ paths:
|
||||
tags: [ Item ]
|
||||
summary: Delete an item.
|
||||
description: |
|
||||
Delete an item and all its data permanently.
|
||||
Delete an item and all its data. This is a "soft delete", the
|
||||
item is still in the database and can be undeleted. A periodic
|
||||
job will eventually remove this item from the database.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
@ -1619,6 +1644,26 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
/sec/item/{id}/restore:
|
||||
post:
|
||||
operationId: "sec-item-restore-by-id"
|
||||
tags: [ Item ]
|
||||
summary: Restore a deleted item.
|
||||
description: |
|
||||
A deleted item can be restored as long it is still in the
|
||||
database. This action sets the item state to `created`.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/id"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/item/{id}/tags:
|
||||
put:
|
||||
operationId: "sec-item-get-tags"
|
||||
@ -2305,6 +2350,29 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/items/restoreAll:
|
||||
post:
|
||||
operationId: "sec-items-restore-all"
|
||||
tags:
|
||||
- Item (Multi Edit)
|
||||
summary: Restore multiple items.
|
||||
description: |
|
||||
Given a list of item ids, restores all of them.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdList"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/items/tags:
|
||||
post:
|
||||
operationId: "sec-items-add-all-tags"
|
||||
@ -4112,6 +4180,16 @@ components:
|
||||
withDetails:
|
||||
type: boolean
|
||||
default: false
|
||||
searchMode:
|
||||
type: string
|
||||
format: searchmode
|
||||
enum:
|
||||
- normal
|
||||
- trashed
|
||||
default: normal
|
||||
description: |
|
||||
Specify whether the search query should apply to
|
||||
soft-deleted items or not.
|
||||
query:
|
||||
type: string
|
||||
description: |
|
||||
@ -4569,6 +4647,7 @@ components:
|
||||
required:
|
||||
- incomingCount
|
||||
- outgoingCount
|
||||
- deletedCount
|
||||
- itemSize
|
||||
- tagCloud
|
||||
properties:
|
||||
@ -4578,6 +4657,9 @@ components:
|
||||
outgoingCount:
|
||||
type: integer
|
||||
format: int32
|
||||
deletedCount:
|
||||
type: integer
|
||||
format: int32
|
||||
itemSize:
|
||||
type: integer
|
||||
format: int64
|
||||
@ -5185,6 +5267,7 @@ components:
|
||||
- language
|
||||
- integrationEnabled
|
||||
- classifier
|
||||
- emptyTrashSchedule
|
||||
properties:
|
||||
language:
|
||||
type: string
|
||||
@ -5194,6 +5277,9 @@ components:
|
||||
description: |
|
||||
Whether the collective has the integration endpoint
|
||||
enabled.
|
||||
emptyTrashSchedule:
|
||||
type: string
|
||||
format: calevent
|
||||
classifier:
|
||||
$ref: "#/components/schemas/ClassifierSetting"
|
||||
|
||||
@ -5834,6 +5920,13 @@ components:
|
||||
description: Whether to return details to each item.
|
||||
schema:
|
||||
type: boolean
|
||||
searchMode:
|
||||
name: searchMode
|
||||
in: query
|
||||
description: Whether to search in soft-deleted items only.
|
||||
schema:
|
||||
type: string
|
||||
format: searchmode
|
||||
name:
|
||||
name: name
|
||||
in: path
|
||||
|
@ -63,6 +63,7 @@ trait Conversions {
|
||||
ItemInsights(
|
||||
d.incoming,
|
||||
d.outgoing,
|
||||
d.deleted,
|
||||
d.bytes,
|
||||
mkTagCloud(d.tags)
|
||||
)
|
||||
|
@ -7,6 +7,7 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import docspell.common.ContactKind
|
||||
import docspell.common.SearchMode
|
||||
|
||||
import org.http4s.ParseFailure
|
||||
import org.http4s.QueryParamDecoder
|
||||
@ -23,6 +24,11 @@ object QueryParam {
|
||||
implicit val queryStringDecoder: QueryParamDecoder[QueryString] =
|
||||
QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase))
|
||||
|
||||
implicit val searchModeDecoder: QueryParamDecoder[SearchMode] =
|
||||
QueryParamDecoder[String].emap(str =>
|
||||
SearchMode.fromString(str).left.map(s => ParseFailure(str, s))
|
||||
)
|
||||
|
||||
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||
|
||||
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
||||
@ -35,6 +41,7 @@ object QueryParam {
|
||||
object Limit extends OptionalQueryParamDecoderMatcher[Int]("limit")
|
||||
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
|
||||
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
|
||||
object SearchKind extends OptionalQueryParamDecoderMatcher[SearchMode]("searchMode")
|
||||
|
||||
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OCollective
|
||||
import docspell.common.ListType
|
||||
import docspell.common.{EmptyTrashArgs, ListType}
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s._
|
||||
@ -55,7 +55,8 @@ object CollectiveRoutes {
|
||||
settings.classifier.categoryList,
|
||||
settings.classifier.listType
|
||||
)
|
||||
)
|
||||
),
|
||||
Some(settings.emptyTrashSchedule)
|
||||
)
|
||||
res <-
|
||||
backend.collective
|
||||
@ -70,6 +71,7 @@ object CollectiveRoutes {
|
||||
CollectiveSettings(
|
||||
c.language,
|
||||
c.integrationEnabled,
|
||||
c.emptyTrash.getOrElse(EmptyTrashArgs.defaultSchedule),
|
||||
ClassifierSetting(
|
||||
c.classifier.map(_.itemCount).getOrElse(0),
|
||||
c.classifier
|
||||
@ -101,6 +103,12 @@ object CollectiveRoutes {
|
||||
resp <- Ok(BasicResult(true, "Task submitted"))
|
||||
} yield resp
|
||||
|
||||
case POST -> Root / "emptytrash" / "startonce" =>
|
||||
for {
|
||||
_ <- backend.collective.startEmptyTrash(user.account.collective)
|
||||
resp <- Ok(BasicResult(true, "Task submitted"))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
for {
|
||||
collDb <- backend.collective.find(user.account.collective)
|
||||
|
@ -179,7 +179,7 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
for {
|
||||
json <- req.as[IdList]
|
||||
items <- readIds[F](json.ids)
|
||||
n <- backend.item.deleteItemMultiple(items, user.account.collective)
|
||||
n <- backend.item.setDeletedState(items, user.account.collective)
|
||||
res = BasicResult(
|
||||
n > 0,
|
||||
if (n > 0) "Item(s) deleted" else "Item deletion failed."
|
||||
@ -187,6 +187,14 @@ object ItemMultiRoutes extends MultiIdSupport {
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "restoreAll" =>
|
||||
for {
|
||||
json <- req.as[IdList]
|
||||
items <- readIds[F](json.ids)
|
||||
res <- backend.item.restore(items, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item(s) deleted"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / "customfield" =>
|
||||
for {
|
||||
json <- req.as[ItemsAndFieldValue]
|
||||
|
@ -49,7 +49,7 @@ object ItemRoutes {
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / "search" :? QP.Query(q) :? QP.Limit(limit) :? QP.Offset(
|
||||
offset
|
||||
) :? QP.WithDetails(detailFlag) =>
|
||||
) :? QP.WithDetails(detailFlag) :? QP.SearchKind(searchMode) =>
|
||||
val batch = Batch(offset.getOrElse(0), limit.getOrElse(cfg.maxItemPageSize))
|
||||
.restrictLimitTo(cfg.maxItemPageSize)
|
||||
val itemQuery = ItemQueryString(q)
|
||||
@ -57,15 +57,20 @@ object ItemRoutes {
|
||||
batch,
|
||||
cfg.fullTextSearch.enabled,
|
||||
detailFlag.getOrElse(false),
|
||||
cfg.maxNoteLength
|
||||
cfg.maxNoteLength,
|
||||
searchMode.getOrElse(SearchMode.Normal)
|
||||
)
|
||||
val fixQuery = Query.Fix(user.account, None, None)
|
||||
searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
||||
|
||||
case GET -> Root / "searchStats" :? QP.Query(q) =>
|
||||
case GET -> Root / "searchStats" :? QP.Query(q) :? QP.SearchKind(searchMode) =>
|
||||
val itemQuery = ItemQueryString(q)
|
||||
val fixQuery = Query.Fix(user.account, None, None)
|
||||
searchItemStats(backend, dsl)(cfg.fullTextSearch.enabled, fixQuery, itemQuery)
|
||||
val settings = OSimpleSearch.StatsSettings(
|
||||
useFTS = cfg.fullTextSearch.enabled,
|
||||
searchMode = searchMode.getOrElse(SearchMode.Normal)
|
||||
)
|
||||
searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
|
||||
|
||||
case req @ POST -> Root / "search" =>
|
||||
for {
|
||||
@ -81,7 +86,8 @@ object ItemRoutes {
|
||||
batch,
|
||||
cfg.fullTextSearch.enabled,
|
||||
userQuery.withDetails.getOrElse(false),
|
||||
cfg.maxNoteLength
|
||||
cfg.maxNoteLength,
|
||||
searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal)
|
||||
)
|
||||
fixQuery = Query.Fix(user.account, None, None)
|
||||
resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
|
||||
@ -92,11 +98,11 @@ object ItemRoutes {
|
||||
userQuery <- req.as[ItemQuery]
|
||||
itemQuery = ItemQueryString(userQuery.query)
|
||||
fixQuery = Query.Fix(user.account, None, None)
|
||||
resp <- searchItemStats(backend, dsl)(
|
||||
cfg.fullTextSearch.enabled,
|
||||
fixQuery,
|
||||
itemQuery
|
||||
settings = OSimpleSearch.StatsSettings(
|
||||
useFTS = cfg.fullTextSearch.enabled,
|
||||
searchMode = userQuery.searchMode.getOrElse(SearchMode.Normal)
|
||||
)
|
||||
resp <- searchItemStats(backend, dsl)(settings, fixQuery, itemQuery)
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root / "searchIndex" =>
|
||||
@ -144,6 +150,12 @@ object ItemRoutes {
|
||||
resp <- Ok(Conversions.basicResult(res, "Item back to created."))
|
||||
} yield resp
|
||||
|
||||
case POST -> Root / Ident(id) / "restore" =>
|
||||
for {
|
||||
res <- backend.item.restore(NonEmptyList.of(id), user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item restored."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root / Ident(id) / "tags" =>
|
||||
for {
|
||||
tags <- req.as[StringList].map(_.items)
|
||||
@ -393,7 +405,7 @@ object ItemRoutes {
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
n <- backend.item.deleteItem(id, user.account.collective)
|
||||
n <- backend.item.setDeletedState(NonEmptyList.of(id), user.account.collective)
|
||||
res = BasicResult(n > 0, if (n > 0) "Item deleted" else "Item deletion failed.")
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
@ -440,13 +452,18 @@ object ItemRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
def searchItemStats[F[_]: Sync](
|
||||
private def searchItemStats[F[_]: Sync](
|
||||
backend: BackendApp[F],
|
||||
dsl: Http4sDsl[F]
|
||||
)(ftsEnabled: Boolean, fixQuery: Query.Fix, itemQuery: ItemQueryString) = {
|
||||
)(
|
||||
settings: OSimpleSearch.StatsSettings,
|
||||
fixQuery: Query.Fix,
|
||||
itemQuery: ItemQueryString
|
||||
) = {
|
||||
import dsl._
|
||||
|
||||
backend.simpleSearch
|
||||
.searchSummaryByString(ftsEnabled)(fixQuery, itemQuery)
|
||||
.searchSummaryByString(settings)(fixQuery, itemQuery)
|
||||
.flatMap {
|
||||
case StringSearchResult.Success(summary) =>
|
||||
Ok(Conversions.mkSearchStats(summary))
|
||||
|
@ -38,7 +38,7 @@ object NotifyDueItemsRoutes {
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
task <- ut.findNotifyDueItems(id, user.account)
|
||||
task <- ut.findNotifyDueItems(id, UserTaskScope(user.account))
|
||||
res <- OptionT.liftF(taskToSettings(user.account, backend, task))
|
||||
resp <- OptionT.liftF(Ok(res))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
@ -49,7 +49,7 @@ object NotifyDueItemsRoutes {
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.executeNow(user.account, task)
|
||||
ut.executeNow(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Submitted successfully."))
|
||||
resp <- Ok(res)
|
||||
@ -58,7 +58,7 @@ object NotifyDueItemsRoutes {
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <-
|
||||
ut.deleteTask(user.account, id)
|
||||
ut.deleteTask(UserTaskScope(user.account), id)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Deleted successfully"))
|
||||
resp <- Ok(res)
|
||||
@ -69,7 +69,7 @@ object NotifyDueItemsRoutes {
|
||||
for {
|
||||
task <- makeTask(data.id, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.submitNotifyDueItems(user.account, task)
|
||||
ut.submitNotifyDueItems(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully"))
|
||||
resp <- Ok(res)
|
||||
@ -87,14 +87,14 @@ object NotifyDueItemsRoutes {
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
|
||||
res <-
|
||||
ut.submitNotifyDueItems(user.account, task)
|
||||
ut.submitNotifyDueItems(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully."))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
ut.getNotifyDueItems(user.account)
|
||||
ut.getNotifyDueItems(UserTaskScope(user.account))
|
||||
.evalMap(task => taskToSettings(user.account, backend, task))
|
||||
.compile
|
||||
.toVector
|
||||
|
@ -35,7 +35,7 @@ object ScanMailboxRoutes {
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / Ident(id) =>
|
||||
(for {
|
||||
task <- ut.findScanMailbox(id, user.account)
|
||||
task <- ut.findScanMailbox(id, UserTaskScope(user.account))
|
||||
res <- OptionT.liftF(taskToSettings(user.account, backend, task))
|
||||
resp <- OptionT.liftF(Ok(res))
|
||||
} yield resp).getOrElseF(NotFound())
|
||||
@ -46,7 +46,7 @@ object ScanMailboxRoutes {
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, user.account, data)
|
||||
res <-
|
||||
ut.executeNow(user.account, task)
|
||||
ut.executeNow(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Submitted successfully."))
|
||||
resp <- Ok(res)
|
||||
@ -55,7 +55,7 @@ object ScanMailboxRoutes {
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
res <-
|
||||
ut.deleteTask(user.account, id)
|
||||
ut.deleteTask(UserTaskScope(user.account), id)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Deleted successfully."))
|
||||
resp <- Ok(res)
|
||||
@ -66,7 +66,7 @@ object ScanMailboxRoutes {
|
||||
for {
|
||||
task <- makeTask(data.id, user.account, data)
|
||||
res <-
|
||||
ut.submitScanMailbox(user.account, task)
|
||||
ut.submitScanMailbox(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully."))
|
||||
resp <- Ok(res)
|
||||
@ -84,14 +84,14 @@ object ScanMailboxRoutes {
|
||||
newId <- Ident.randomId[F]
|
||||
task <- makeTask(newId, user.account, data)
|
||||
res <-
|
||||
ut.submitScanMailbox(user.account, task)
|
||||
ut.submitScanMailbox(UserTaskScope(user.account), task)
|
||||
.attempt
|
||||
.map(Conversions.basicResult(_, "Saved successfully."))
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
ut.getScanMailbox(user.account)
|
||||
ut.getScanMailbox(UserTaskScope(user.account))
|
||||
.evalMap(task => taskToSettings(user.account, backend, task))
|
||||
.compile
|
||||
.toVector
|
||||
|
@ -0,0 +1,6 @@
|
||||
CREATE TABLE "empty_trash_setting" (
|
||||
"cid" varchar(254) not null primary key,
|
||||
"schedule" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
foreign key ("cid") references "collective"("cid")
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
UPDATE "periodic_task"
|
||||
SET submitter = group_
|
||||
WHERE submitter = 'learn-classifier';
|
@ -0,0 +1,6 @@
|
||||
CREATE TABLE `empty_trash_setting` (
|
||||
`cid` varchar(254) not null primary key,
|
||||
`schedule` varchar(254) not null,
|
||||
`created` timestamp not null,
|
||||
foreign key (`cid`) references `collective`(`cid`)
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
UPDATE `periodic_task`
|
||||
SET submitter = group_
|
||||
WHERE submitter = 'learn-classifier';
|
@ -0,0 +1,6 @@
|
||||
CREATE TABLE "empty_trash_setting" (
|
||||
"cid" varchar(254) not null primary key,
|
||||
"schedule" varchar(254) not null,
|
||||
"created" timestamp not null,
|
||||
foreign key ("cid") references "collective"("cid")
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
UPDATE "periodic_task"
|
||||
SET submitter = group_
|
||||
WHERE submitter = 'learn-classifier';
|
@ -126,6 +126,9 @@ object ItemQueryGenerator {
|
||||
case Expr.ValidItemStates =>
|
||||
tables.item.state.in(ItemState.validStates)
|
||||
|
||||
case Expr.Trashed =>
|
||||
tables.item.state === ItemState.Deleted
|
||||
|
||||
case Expr.TagIdsMatch(op, tags) =>
|
||||
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
|
||||
Nel
|
||||
|
@ -65,6 +65,7 @@ object QCollective {
|
||||
case class InsightData(
|
||||
incoming: Int,
|
||||
outgoing: Int,
|
||||
deleted: Int,
|
||||
bytes: Long,
|
||||
tags: List[TagCount]
|
||||
)
|
||||
@ -73,12 +74,21 @@ object QCollective {
|
||||
val q0 = Select(
|
||||
count(i.id).s,
|
||||
from(i),
|
||||
i.cid === coll && i.incoming === Direction.incoming
|
||||
i.cid === coll && i.incoming === Direction.incoming && i.state.in(
|
||||
ItemState.validStates
|
||||
)
|
||||
).build.query[Int].unique
|
||||
val q1 = Select(
|
||||
count(i.id).s,
|
||||
from(i),
|
||||
i.cid === coll && i.incoming === Direction.outgoing
|
||||
i.cid === coll && i.incoming === Direction.outgoing && i.state.in(
|
||||
ItemState.validStates
|
||||
)
|
||||
).build.query[Int].unique
|
||||
val q2 = Select(
|
||||
count(i.id).s,
|
||||
from(i),
|
||||
i.cid === coll && i.state === ItemState.Deleted
|
||||
).build.query[Int].unique
|
||||
|
||||
val fileSize = sql"""
|
||||
@ -102,19 +112,20 @@ object QCollective {
|
||||
) as t""".query[Option[Long]].unique
|
||||
|
||||
for {
|
||||
n0 <- q0
|
||||
n1 <- q1
|
||||
n2 <- fileSize
|
||||
n3 <- tagCloud(coll)
|
||||
} yield InsightData(n0, n1, n2.getOrElse(0L), n3)
|
||||
incoming <- q0
|
||||
outgoing <- q1
|
||||
size <- fileSize
|
||||
tags <- tagCloud(coll)
|
||||
deleted <- q2
|
||||
} yield InsightData(incoming, outgoing, deleted, size.getOrElse(0L), tags)
|
||||
}
|
||||
|
||||
def tagCloud(coll: Ident): ConnectionIO[List[TagCount]] = {
|
||||
val sql =
|
||||
Select(
|
||||
select(t.all).append(count(ti.itemId).s),
|
||||
from(ti).innerJoin(t, ti.tagId === t.tid),
|
||||
t.cid === coll
|
||||
from(ti).innerJoin(t, ti.tagId === t.tid).innerJoin(i, i.id === ti.itemId),
|
||||
t.cid === coll && i.state.in(ItemState.validStates)
|
||||
).groupBy(t.name, t.tid, t.category)
|
||||
|
||||
sql.build.query[TagCount].to[List]
|
||||
|
@ -12,7 +12,7 @@ import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.records._
|
||||
import docspell.store.usertask.UserTask
|
||||
import docspell.store.usertask.{UserTask, UserTaskScope}
|
||||
|
||||
import doobie._
|
||||
|
||||
@ -54,15 +54,15 @@ object QUserTask {
|
||||
)
|
||||
).query[RPeriodicTask].option.map(_.map(makeUserTask))
|
||||
|
||||
def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
|
||||
def insert(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
|
||||
for {
|
||||
r <- task.toPeriodicTask[ConnectionIO](account)
|
||||
r <- task.toPeriodicTask[ConnectionIO](scope)
|
||||
n <- RPeriodicTask.insert(r)
|
||||
} yield n
|
||||
|
||||
def update(account: AccountId, task: UserTask[String]): ConnectionIO[Int] =
|
||||
def update(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
|
||||
for {
|
||||
r <- task.toPeriodicTask[ConnectionIO](account)
|
||||
r <- task.toPeriodicTask[ConnectionIO](scope)
|
||||
n <- RPeriodicTask.update(r)
|
||||
} yield n
|
||||
|
||||
|
@ -13,6 +13,7 @@ import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import com.github.eikek.calev._
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
@ -73,17 +74,21 @@ object RCollective {
|
||||
T.integration.setTo(settings.integrationEnabled)
|
||||
)
|
||||
)
|
||||
cls <-
|
||||
Timestamp
|
||||
.current[ConnectionIO]
|
||||
.map(now => settings.classifier.map(_.toRecord(cid, now)))
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
cls = settings.classifier.map(_.toRecord(cid, now))
|
||||
n2 <- cls match {
|
||||
case Some(cr) =>
|
||||
RClassifierSetting.update(cr)
|
||||
case None =>
|
||||
RClassifierSetting.delete(cid)
|
||||
}
|
||||
} yield n1 + n2
|
||||
n3 <- settings.emptyTrash match {
|
||||
case Some(trashSchedule) =>
|
||||
REmptyTrashSetting.update(REmptyTrashSetting(cid, trashSchedule, now))
|
||||
case None =>
|
||||
REmptyTrashSetting.delete(cid)
|
||||
}
|
||||
} yield n1 + n2 + n3
|
||||
|
||||
// this hides categories that have been deleted in the meantime
|
||||
// they are finally removed from the json array once the learn classifier task is run
|
||||
@ -99,6 +104,7 @@ object RCollective {
|
||||
import RClassifierSetting.stringListMeta
|
||||
val c = RCollective.as("c")
|
||||
val cs = RClassifierSetting.as("cs")
|
||||
val es = REmptyTrashSetting.as("es")
|
||||
|
||||
Select(
|
||||
select(
|
||||
@ -107,9 +113,10 @@ object RCollective {
|
||||
cs.schedule.s,
|
||||
cs.itemCount.s,
|
||||
cs.categories.s,
|
||||
cs.listType.s
|
||||
cs.listType.s,
|
||||
es.schedule.s
|
||||
),
|
||||
from(c).leftJoin(cs, cs.cid === c.id),
|
||||
from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id),
|
||||
c.id === coll
|
||||
).build.query[Settings].option
|
||||
}
|
||||
@ -160,7 +167,8 @@ object RCollective {
|
||||
case class Settings(
|
||||
language: Language,
|
||||
integrationEnabled: Boolean,
|
||||
classifier: Option[RClassifierSetting.Classifier]
|
||||
classifier: Option[RClassifierSetting.Classifier],
|
||||
emptyTrash: Option[CalEvent]
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import com.github.eikek.calev._
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
final case class REmptyTrashSetting(
|
||||
cid: Ident,
|
||||
schedule: CalEvent,
|
||||
created: Timestamp
|
||||
)
|
||||
|
||||
object REmptyTrashSetting {
|
||||
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "empty_trash_setting"
|
||||
|
||||
val cid = Column[Ident]("cid", this)
|
||||
val schedule = Column[CalEvent]("schedule", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all = NonEmptyList.of[Column[_]](cid, schedule, created)
|
||||
}
|
||||
|
||||
val T = Table(None)
|
||||
def as(alias: String): Table =
|
||||
Table(Some(alias))
|
||||
|
||||
def insert(v: REmptyTrashSetting): ConnectionIO[Int] =
|
||||
DML.insert(
|
||||
T,
|
||||
T.all,
|
||||
fr"${v.cid},${v.schedule},${v.created}"
|
||||
)
|
||||
|
||||
def update(v: REmptyTrashSetting): ConnectionIO[Int] =
|
||||
for {
|
||||
n1 <- DML.update(
|
||||
T,
|
||||
T.cid === v.cid,
|
||||
DML.set(
|
||||
T.schedule.setTo(v.schedule)
|
||||
)
|
||||
)
|
||||
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
|
||||
} yield n1 + n2
|
||||
|
||||
def findById(id: Ident): ConnectionIO[Option[REmptyTrashSetting]] = {
|
||||
val sql = run(select(T.all), from(T), T.cid === id)
|
||||
sql.query[REmptyTrashSetting].option
|
||||
}
|
||||
|
||||
def findForAllCollectives(
|
||||
default: CalEvent,
|
||||
chunkSize: Int
|
||||
): Stream[ConnectionIO, REmptyTrashSetting] = {
|
||||
val c = RCollective.as("c")
|
||||
val e = REmptyTrashSetting.as("e")
|
||||
val sql = run(
|
||||
select(
|
||||
c.id.s,
|
||||
coalesce(e.schedule.s, const(default)).s,
|
||||
coalesce(e.created.s, c.created.s).s
|
||||
),
|
||||
from(c).leftJoin(e, e.cid === c.id)
|
||||
)
|
||||
sql.query[REmptyTrashSetting].streamWithChunkSize(chunkSize)
|
||||
}
|
||||
|
||||
def delete(coll: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.cid === coll)
|
||||
|
||||
}
|
@ -9,6 +9,7 @@ package docspell.store.records
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
@ -152,7 +153,21 @@ object RItem {
|
||||
t <- currentTime
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
T.id.in(itemIds) && T.cid === coll && T.state.in(ItemState.validStates),
|
||||
DML.set(T.state.setTo(itemState), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def restoreStateForCollective(
|
||||
itemIds: NonEmptyList[Ident],
|
||||
itemState: ItemState,
|
||||
coll: Ident
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll && T.state === ItemState.deleted,
|
||||
DML.set(T.state.setTo(itemState), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
@ -336,6 +351,20 @@ object RItem {
|
||||
def deleteByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.id === itemId && T.cid === coll)
|
||||
|
||||
def setState(
|
||||
itemIds: NonEmptyList[Ident],
|
||||
coll: Ident,
|
||||
state: ItemState
|
||||
): ConnectionIO[Int] =
|
||||
for {
|
||||
t <- currentTime
|
||||
n <- DML.update(
|
||||
T,
|
||||
T.id.in(itemIds) && T.cid === coll,
|
||||
DML.set(T.state.setTo(state), T.updated.setTo(t))
|
||||
)
|
||||
} yield n
|
||||
|
||||
def existsById(itemId: Ident): ConnectionIO[Boolean] =
|
||||
Select(count(T.id).s, from(T), T.id === itemId).build.query[Int].unique.map(_ > 0)
|
||||
|
||||
@ -360,6 +389,11 @@ object RItem {
|
||||
def findById(itemId: Ident): ConnectionIO[Option[RItem]] =
|
||||
run(select(T.all), from(T), T.id === itemId).query[RItem].option
|
||||
|
||||
def findDeleted(collective: Ident, chunkSize: Int): Stream[ConnectionIO, RItem] =
|
||||
run(select(T.all), from(T), T.cid === collective && T.state === ItemState.deleted)
|
||||
.query[RItem]
|
||||
.streamWithChunkSize(chunkSize)
|
||||
|
||||
def checkByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[Ident]] =
|
||||
Select(T.id.s, from(T), T.id === itemId && T.cid === coll).build.query[Ident].option
|
||||
|
||||
|
@ -13,6 +13,7 @@ import cats.implicits._
|
||||
import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
import docspell.store.usertask.UserTaskScope
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import doobie._
|
||||
@ -67,11 +68,10 @@ object RPeriodicTask {
|
||||
|
||||
def create[F[_]: Sync](
|
||||
enabled: Boolean,
|
||||
scope: UserTaskScope,
|
||||
task: Ident,
|
||||
group: Ident,
|
||||
args: String,
|
||||
subject: String,
|
||||
submitter: Ident,
|
||||
priority: Priority,
|
||||
timer: CalEvent,
|
||||
summary: Option[String]
|
||||
@ -86,10 +86,10 @@ object RPeriodicTask {
|
||||
id,
|
||||
enabled,
|
||||
task,
|
||||
group,
|
||||
scope.collective,
|
||||
args,
|
||||
subject,
|
||||
submitter,
|
||||
scope.fold(_.user, identity),
|
||||
priority,
|
||||
None,
|
||||
None,
|
||||
@ -107,22 +107,20 @@ object RPeriodicTask {
|
||||
|
||||
def createJson[F[_]: Sync, A](
|
||||
enabled: Boolean,
|
||||
scope: UserTaskScope,
|
||||
task: Ident,
|
||||
group: Ident,
|
||||
args: A,
|
||||
subject: String,
|
||||
submitter: Ident,
|
||||
priority: Priority,
|
||||
timer: CalEvent,
|
||||
summary: Option[String]
|
||||
)(implicit E: Encoder[A]): F[RPeriodicTask] =
|
||||
create[F](
|
||||
enabled,
|
||||
scope,
|
||||
task,
|
||||
group,
|
||||
E(args).noSpaces,
|
||||
subject,
|
||||
submitter,
|
||||
priority,
|
||||
timer,
|
||||
summary
|
||||
|
@ -43,16 +43,15 @@ object UserTask {
|
||||
.map(a => ut.copy(args = a))
|
||||
|
||||
def toPeriodicTask[F[_]: Sync](
|
||||
account: AccountId
|
||||
scope: UserTaskScope
|
||||
): F[RPeriodicTask] =
|
||||
RPeriodicTask
|
||||
.create[F](
|
||||
ut.enabled,
|
||||
scope,
|
||||
ut.name,
|
||||
account.collective,
|
||||
ut.args,
|
||||
s"${account.user.id}: ${ut.name.id}",
|
||||
account.user,
|
||||
s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}",
|
||||
Priority.Low,
|
||||
ut.timer,
|
||||
ut.summary
|
||||
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Docspell Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.store.usertask
|
||||
|
||||
import docspell.common._
|
||||
|
||||
sealed trait UserTaskScope { self: Product =>
|
||||
|
||||
def name: String =
|
||||
productPrefix.toLowerCase
|
||||
|
||||
def collective: Ident
|
||||
|
||||
def fold[A](fa: AccountId => A, fb: Ident => A): A
|
||||
|
||||
/** Maps to the account or uses the collective for both parts if the
|
||||
* scope is collective wide.
|
||||
*/
|
||||
private[usertask] def toAccountId: AccountId =
|
||||
AccountId(collective, fold(_.user, identity))
|
||||
}
|
||||
|
||||
object UserTaskScope {
|
||||
|
||||
final case class Account(account: AccountId) extends UserTaskScope {
|
||||
val collective = account.collective
|
||||
|
||||
def fold[A](fa: AccountId => A, fb: Ident => A): A =
|
||||
fa(account)
|
||||
}
|
||||
|
||||
final case class Collective(collective: Ident) extends UserTaskScope {
|
||||
def fold[A](fa: AccountId => A, fb: Ident => A): A =
|
||||
fb(collective)
|
||||
}
|
||||
|
||||
def collective(id: Ident): UserTaskScope =
|
||||
Collective(id)
|
||||
|
||||
def account(accountId: AccountId): UserTaskScope =
|
||||
Account(accountId)
|
||||
|
||||
def apply(accountId: AccountId): UserTaskScope =
|
||||
UserTaskScope.account(accountId)
|
||||
|
||||
def apply(collective: Ident): UserTaskScope =
|
||||
UserTaskScope.collective(collective)
|
||||
}
|
@ -22,13 +22,15 @@ import io.circe._
|
||||
* once.
|
||||
*
|
||||
* This class defines methods at a higher level, dealing with
|
||||
* `UserTask` and `AccountId` instead of directly using
|
||||
* `UserTask` and `UserTaskScope` instead of directly using
|
||||
* `RPeriodicTask`. A user task is associated to a specific user (not
|
||||
* just the collective).
|
||||
* just the collective). But it can be associated to the whole
|
||||
* collective by using the collective as submitter, too. This is
|
||||
* abstracted in `UserTaskScope`.
|
||||
*
|
||||
* implNote: The mapping is as follows: The collective is the task
|
||||
* group. The submitter property contains the username. Once a task
|
||||
* is saved to the database, it can only be refernced uniquely by its
|
||||
* is saved to the database, it can only be referenced uniquely by its
|
||||
* id. A user may submit multiple same tasks (with different
|
||||
* properties).
|
||||
*/
|
||||
@ -36,22 +38,22 @@ trait UserTaskStore[F[_]] {
|
||||
|
||||
/** Return all tasks of the given user.
|
||||
*/
|
||||
def getAll(account: AccountId): Stream[F, UserTask[String]]
|
||||
def getAll(scope: UserTaskScope): Stream[F, UserTask[String]]
|
||||
|
||||
/** Return all tasks of the given name and user. The task's arguments
|
||||
* are returned as stored in the database.
|
||||
*/
|
||||
def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]]
|
||||
def getByNameRaw(scope: UserTaskScope, name: Ident): Stream[F, UserTask[String]]
|
||||
|
||||
/** Return all tasks of the given name and user. The task's arguments
|
||||
* are decoded using the given json decoder.
|
||||
*/
|
||||
def getByName[A](account: AccountId, name: Ident)(implicit
|
||||
def getByName[A](scope: UserTaskScope, name: Ident)(implicit
|
||||
D: Decoder[A]
|
||||
): Stream[F, UserTask[A]]
|
||||
|
||||
/** Return a user-task with the given id. */
|
||||
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]]
|
||||
def getByIdRaw(scope: UserTaskScope, id: Ident): OptionT[F, UserTask[String]]
|
||||
|
||||
/** Updates or inserts the given task.
|
||||
*
|
||||
@ -59,23 +61,23 @@ trait UserTaskStore[F[_]] {
|
||||
* exists, a new one is created. Otherwise the existing task is
|
||||
* updated.
|
||||
*/
|
||||
def updateTask[A](account: AccountId, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
|
||||
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
|
||||
|
||||
/** Delete the task with the given id of the given user.
|
||||
*/
|
||||
def deleteTask(account: AccountId, id: Ident): F[Int]
|
||||
def deleteTask(scope: UserTaskScope, id: Ident): F[Int]
|
||||
|
||||
/** Return the task of the given user and name. If multiple exists, an
|
||||
* error is returned. The task's arguments are returned as stored
|
||||
* in the database.
|
||||
*/
|
||||
def getOneByNameRaw(account: AccountId, name: Ident): OptionT[F, UserTask[String]]
|
||||
def getOneByNameRaw(scope: UserTaskScope, name: Ident): OptionT[F, UserTask[String]]
|
||||
|
||||
/** Return the task of the given user and name. If multiple exists, an
|
||||
* error is returned. The task's arguments are decoded using the
|
||||
* given json decoder.
|
||||
*/
|
||||
def getOneByName[A](account: AccountId, name: Ident)(implicit
|
||||
def getOneByName[A](scope: UserTaskScope, name: Ident)(implicit
|
||||
D: Decoder[A]
|
||||
): OptionT[F, UserTask[A]]
|
||||
|
||||
@ -83,20 +85,20 @@ trait UserTaskStore[F[_]] {
|
||||
*
|
||||
* Unlike `updateTask`, this ensures that there is at most one task
|
||||
* of some name in the db. Multiple same tasks (task with same
|
||||
* name) may not be allowed to run, dependening on what they do.
|
||||
* name) may not be allowed to run, depending on what they do.
|
||||
* This is not ensured by the database, though.
|
||||
*
|
||||
* If there are currently mutliple tasks with same name as `ut` for
|
||||
* If there are currently multiple tasks with same name as `ut` for
|
||||
* the user `account`, they will all be removed and the given task
|
||||
* inserted!
|
||||
*/
|
||||
def updateOneTask[A](account: AccountId, ut: UserTask[A])(implicit
|
||||
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
|
||||
E: Encoder[A]
|
||||
): F[UserTask[String]]
|
||||
|
||||
/** Delete all tasks of the given user that have name `name'.
|
||||
*/
|
||||
def deleteAll(account: AccountId, name: Ident): F[Int]
|
||||
def deleteAll(scope: UserTaskScope, name: Ident): F[Int]
|
||||
}
|
||||
|
||||
object UserTaskStore {
|
||||
@ -104,47 +106,47 @@ object UserTaskStore {
|
||||
def apply[F[_]: Async](store: Store[F]): Resource[F, UserTaskStore[F]] =
|
||||
Resource.pure[F, UserTaskStore[F]](new UserTaskStore[F] {
|
||||
|
||||
def getAll(account: AccountId): Stream[F, UserTask[String]] =
|
||||
store.transact(QUserTask.findAll(account))
|
||||
def getAll(scope: UserTaskScope): Stream[F, UserTask[String]] =
|
||||
store.transact(QUserTask.findAll(scope.toAccountId))
|
||||
|
||||
def getByNameRaw(account: AccountId, name: Ident): Stream[F, UserTask[String]] =
|
||||
store.transact(QUserTask.findByName(account, name))
|
||||
def getByNameRaw(scope: UserTaskScope, name: Ident): Stream[F, UserTask[String]] =
|
||||
store.transact(QUserTask.findByName(scope.toAccountId, name))
|
||||
|
||||
def getByIdRaw(account: AccountId, id: Ident): OptionT[F, UserTask[String]] =
|
||||
OptionT(store.transact(QUserTask.findById(account, id)))
|
||||
def getByIdRaw(scope: UserTaskScope, id: Ident): OptionT[F, UserTask[String]] =
|
||||
OptionT(store.transact(QUserTask.findById(scope.toAccountId, id)))
|
||||
|
||||
def getByName[A](account: AccountId, name: Ident)(implicit
|
||||
def getByName[A](scope: UserTaskScope, name: Ident)(implicit
|
||||
D: Decoder[A]
|
||||
): Stream[F, UserTask[A]] =
|
||||
getByNameRaw(account, name).flatMap(_.decode match {
|
||||
getByNameRaw(scope, name).flatMap(_.decode match {
|
||||
case Right(ua) => Stream.emit(ua)
|
||||
case Left(err) => Stream.raiseError[F](new Exception(err))
|
||||
})
|
||||
|
||||
def updateTask[A](account: AccountId, ut: UserTask[A])(implicit
|
||||
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
|
||||
E: Encoder[A]
|
||||
): F[Int] = {
|
||||
val exists = QUserTask.exists(ut.id)
|
||||
val insert = QUserTask.insert(account, ut.encode)
|
||||
val insert = QUserTask.insert(scope, ut.encode)
|
||||
store.add(insert, exists).flatMap {
|
||||
case AddResult.Success =>
|
||||
1.pure[F]
|
||||
case AddResult.EntityExists(_) =>
|
||||
store.transact(QUserTask.update(account, ut.encode))
|
||||
store.transact(QUserTask.update(scope, ut.encode))
|
||||
case AddResult.Failure(ex) =>
|
||||
Async[F].raiseError(ex)
|
||||
}
|
||||
}
|
||||
|
||||
def deleteTask(account: AccountId, id: Ident): F[Int] =
|
||||
store.transact(QUserTask.delete(account, id))
|
||||
def deleteTask(scope: UserTaskScope, id: Ident): F[Int] =
|
||||
store.transact(QUserTask.delete(scope.toAccountId, id))
|
||||
|
||||
def getOneByNameRaw(
|
||||
account: AccountId,
|
||||
scope: UserTaskScope,
|
||||
name: Ident
|
||||
): OptionT[F, UserTask[String]] =
|
||||
OptionT(
|
||||
getByNameRaw(account, name)
|
||||
getByNameRaw(scope, name)
|
||||
.take(2)
|
||||
.compile
|
||||
.toList
|
||||
@ -155,32 +157,34 @@ object UserTaskStore {
|
||||
}
|
||||
)
|
||||
|
||||
def getOneByName[A](account: AccountId, name: Ident)(implicit
|
||||
def getOneByName[A](scope: UserTaskScope, name: Ident)(implicit
|
||||
D: Decoder[A]
|
||||
): OptionT[F, UserTask[A]] =
|
||||
getOneByNameRaw(account, name)
|
||||
getOneByNameRaw(scope, name)
|
||||
.semiflatMap(_.decode match {
|
||||
case Right(ua) => ua.pure[F]
|
||||
case Left(err) => Async[F].raiseError(new Exception(err))
|
||||
})
|
||||
|
||||
def updateOneTask[A](account: AccountId, ut: UserTask[A])(implicit
|
||||
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
|
||||
E: Encoder[A]
|
||||
): F[UserTask[String]] =
|
||||
getByNameRaw(account, ut.name).compile.toList.flatMap {
|
||||
getByNameRaw(scope, ut.name).compile.toList.flatMap {
|
||||
case a :: rest =>
|
||||
val task = ut.copy(id = a.id).encode
|
||||
for {
|
||||
_ <- store.transact(QUserTask.update(account, task))
|
||||
_ <- store.transact(rest.traverse(t => QUserTask.delete(account, t.id)))
|
||||
_ <- store.transact(QUserTask.update(scope, task))
|
||||
_ <- store.transact(
|
||||
rest.traverse(t => QUserTask.delete(scope.toAccountId, t.id))
|
||||
)
|
||||
} yield task
|
||||
case Nil =>
|
||||
val task = ut.encode
|
||||
store.transact(QUserTask.insert(account, task)).map(_ => task)
|
||||
store.transact(QUserTask.insert(scope, task)).map(_ => task)
|
||||
}
|
||||
|
||||
def deleteAll(account: AccountId, name: Ident): F[Int] =
|
||||
store.transact(QUserTask.deleteAll(account, name))
|
||||
def deleteAll(scope: UserTaskScope, name: Ident): F[Int] =
|
||||
store.transact(QUserTask.deleteAll(scope.toAccountId, name))
|
||||
})
|
||||
|
||||
}
|
||||
|
@ -99,6 +99,8 @@ module Api exposing
|
||||
, removeTagsMultiple
|
||||
, reprocessItem
|
||||
, reprocessMultiple
|
||||
, restoreAllItems
|
||||
, restoreItem
|
||||
, saveClientSettings
|
||||
, sendMail
|
||||
, setAttachmentName
|
||||
@ -128,6 +130,7 @@ module Api exposing
|
||||
, setTagsMultiple
|
||||
, setUnconfirmed
|
||||
, startClassifier
|
||||
, startEmptyTrash
|
||||
, startOnceNotifyDueItems
|
||||
, startOnceScanMailbox
|
||||
, startReIndex
|
||||
@ -994,6 +997,19 @@ startClassifier flags receive =
|
||||
}
|
||||
|
||||
|
||||
startEmptyTrash :
|
||||
Flags
|
||||
-> (Result Http.Error BasicResult -> msg)
|
||||
-> Cmd msg
|
||||
startEmptyTrash flags receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/collective/emptytrash/startonce"
|
||||
, account = getAccount flags
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
getTagCloud : Flags -> (Result Http.Error TagCloud -> msg) -> Cmd msg
|
||||
getTagCloud flags receive =
|
||||
Http2.authGet
|
||||
@ -1676,6 +1692,20 @@ deleteAllItems flags ids receive =
|
||||
}
|
||||
|
||||
|
||||
restoreAllItems :
|
||||
Flags
|
||||
-> Set String
|
||||
-> (Result Http.Error BasicResult -> msg)
|
||||
-> Cmd msg
|
||||
restoreAllItems flags ids receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/items/restoreAll"
|
||||
, account = getAccount flags
|
||||
, body = Http.jsonBody (Api.Model.IdList.encode (IdList (Set.toList ids)))
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
|
||||
--- Item
|
||||
|
||||
@ -1973,6 +2003,16 @@ setUnconfirmed flags item receive =
|
||||
}
|
||||
|
||||
|
||||
restoreItem : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
restoreItem flags item receive =
|
||||
Http2.authPost
|
||||
{ url = flags.config.baseUrl ++ "/api/v1/sec/item/" ++ item ++ "/restore"
|
||||
, account = getAccount flags
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
|
||||
}
|
||||
|
||||
|
||||
deleteItem : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
|
||||
deleteItem flags item receive =
|
||||
Http2.authDelete
|
||||
|
@ -20,7 +20,9 @@ import Api.Model.CollectiveSettings exposing (CollectiveSettings)
|
||||
import Comp.Basic as B
|
||||
import Comp.ClassifierSettingsForm
|
||||
import Comp.Dropdown
|
||||
import Comp.EmptyTrashForm
|
||||
import Comp.MenuBar as MB
|
||||
import Data.CalEvent
|
||||
import Data.DropdownStyle as DS
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.Language exposing (Language)
|
||||
@ -41,6 +43,8 @@ type alias Model =
|
||||
, fullTextReIndexResult : FulltextReindexResult
|
||||
, classifierModel : Comp.ClassifierSettingsForm.Model
|
||||
, startClassifierResult : ClassifierResult
|
||||
, emptyTrashModel : Comp.EmptyTrashForm.Model
|
||||
, startEmptyTrashResult : EmptyTrashResult
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +54,11 @@ type ClassifierResult
|
||||
| ClassifierResultSubmitError String
|
||||
| ClassifierResultOk
|
||||
|
||||
type EmptyTrashResult
|
||||
= EmptyTrashResultInitial
|
||||
| EmptyTrashResultHttpError Http.Error
|
||||
| EmptyTrashResultSubmitError String
|
||||
| EmptyTrashResultOk
|
||||
|
||||
type FulltextReindexResult
|
||||
= FulltextReindexInitial
|
||||
@ -68,6 +77,9 @@ init flags settings =
|
||||
|
||||
( cm, cc ) =
|
||||
Comp.ClassifierSettingsForm.init flags settings.classifier
|
||||
|
||||
( em, ec ) =
|
||||
Comp.EmptyTrashForm.init flags settings.emptyTrashSchedule
|
||||
in
|
||||
( { langModel =
|
||||
Comp.Dropdown.makeSingleList
|
||||
@ -80,8 +92,10 @@ init flags settings =
|
||||
, fullTextReIndexResult = FulltextReindexInitial
|
||||
, classifierModel = cm
|
||||
, startClassifierResult = ClassifierResultInitial
|
||||
, emptyTrashModel = em
|
||||
, startEmptyTrashResult = EmptyTrashResultInitial
|
||||
}
|
||||
, Cmd.map ClassifierSettingMsg cc
|
||||
, Cmd.batch [ Cmd.map ClassifierSettingMsg cc, Cmd.map EmptyTrashMsg ec ]
|
||||
)
|
||||
|
||||
|
||||
@ -96,6 +110,10 @@ getSettings model =
|
||||
|> Maybe.withDefault model.initSettings.language
|
||||
, integrationEnabled = model.intEnabled
|
||||
, classifier = cls
|
||||
, emptyTrashSchedule =
|
||||
Comp.EmptyTrashForm.getSettings model.emptyTrashModel
|
||||
|> Maybe.withDefault Data.CalEvent.everyMonth
|
||||
|> Data.CalEvent.makeEvent
|
||||
}
|
||||
)
|
||||
(Comp.ClassifierSettingsForm.getSettings
|
||||
@ -110,9 +128,12 @@ type Msg
|
||||
| TriggerReIndex
|
||||
| TriggerReIndexResult (Result Http.Error BasicResult)
|
||||
| ClassifierSettingMsg Comp.ClassifierSettingsForm.Msg
|
||||
| EmptyTrashMsg Comp.EmptyTrashForm.Msg
|
||||
| SaveSettings
|
||||
| StartClassifierTask
|
||||
| StartEmptyTrashTask
|
||||
| StartClassifierResp (Result Http.Error BasicResult)
|
||||
| StartEmptyTrashResp (Result Http.Error BasicResult)
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe CollectiveSettings )
|
||||
@ -188,6 +209,18 @@ update flags msg model =
|
||||
, Nothing
|
||||
)
|
||||
|
||||
EmptyTrashMsg lmsg ->
|
||||
let
|
||||
( cm, cc ) =
|
||||
Comp.EmptyTrashForm.update flags lmsg model.emptyTrashModel
|
||||
in
|
||||
( { model
|
||||
| emptyTrashModel = cm
|
||||
}
|
||||
, Cmd.map EmptyTrashMsg cc
|
||||
, Nothing
|
||||
)
|
||||
|
||||
SaveSettings ->
|
||||
case getSettings model of
|
||||
Just s ->
|
||||
@ -199,6 +232,10 @@ update flags msg model =
|
||||
StartClassifierTask ->
|
||||
( model, Api.startClassifier flags StartClassifierResp, Nothing )
|
||||
|
||||
StartEmptyTrashTask ->
|
||||
( model, Api.startEmptyTrash flags StartEmptyTrashResp, Nothing )
|
||||
|
||||
|
||||
StartClassifierResp (Ok br) ->
|
||||
( { model
|
||||
| startClassifierResult =
|
||||
@ -218,6 +255,24 @@ update flags msg model =
|
||||
, Nothing
|
||||
)
|
||||
|
||||
StartEmptyTrashResp (Ok br) ->
|
||||
( { model
|
||||
| startEmptyTrashResult =
|
||||
if br.success then
|
||||
EmptyTrashResultOk
|
||||
|
||||
else
|
||||
EmptyTrashResultSubmitError br.message
|
||||
}
|
||||
, Cmd.none
|
||||
, Nothing
|
||||
)
|
||||
|
||||
StartEmptyTrashResp (Err err) ->
|
||||
( { model | startEmptyTrashResult = EmptyTrashResultHttpError err }
|
||||
, Cmd.none
|
||||
, Nothing
|
||||
)
|
||||
|
||||
|
||||
--- View2
|
||||
@ -257,7 +312,7 @@ view2 flags texts settings model =
|
||||
, end = []
|
||||
, rootClasses = "mb-4"
|
||||
}
|
||||
, h3 [ class S.header3 ]
|
||||
, h2 [ class S.header2 ]
|
||||
[ text texts.documentLanguage
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
@ -279,8 +334,8 @@ view2 flags texts settings model =
|
||||
[ ( "hidden", not flags.config.integrationEnabled )
|
||||
]
|
||||
]
|
||||
[ h3
|
||||
[ class S.header3
|
||||
[ h2
|
||||
[ class S.header2
|
||||
]
|
||||
[ text texts.integrationEndpoint
|
||||
]
|
||||
@ -311,8 +366,8 @@ view2 flags texts settings model =
|
||||
[ ( "hidden", not flags.config.fullTextSearchEnabled )
|
||||
]
|
||||
]
|
||||
[ h3
|
||||
[ class S.header3 ]
|
||||
[ h2
|
||||
[ class S.header2 ]
|
||||
[ text texts.fulltextSearch ]
|
||||
, div
|
||||
[ class "mb-4" ]
|
||||
@ -348,8 +403,8 @@ view2 flags texts settings model =
|
||||
[ ( " hidden", not flags.config.showClassificationSettings )
|
||||
]
|
||||
]
|
||||
[ h3
|
||||
[ class S.header3 ]
|
||||
[ h2
|
||||
[ class S.header2 ]
|
||||
[ text texts.autoTagging
|
||||
]
|
||||
, div
|
||||
@ -371,6 +426,28 @@ view2 flags texts settings model =
|
||||
]
|
||||
]
|
||||
]
|
||||
, div []
|
||||
[ h2 [ class S.header2 ]
|
||||
[ text texts.emptyTrash
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ Html.map EmptyTrashMsg
|
||||
(Comp.EmptyTrashForm.view texts.emptyTrashForm
|
||||
settings
|
||||
model.emptyTrashModel
|
||||
)
|
||||
, div [ class "flex flex-row justify-end" ]
|
||||
[ B.secondaryBasicButton
|
||||
{ handler = onClick StartEmptyTrashTask
|
||||
, icon = "fa fa-play"
|
||||
, label = texts.startNow
|
||||
, disabled = model.emptyTrashModel.schedule == Nothing
|
||||
, attrs = [ href "#" ]
|
||||
}
|
||||
, renderEmptyTrashResultMessage texts model.startEmptyTrashResult
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@ -427,3 +504,38 @@ renderFulltextReindexResultMessage texts result =
|
||||
|
||||
FulltextReindexSubmitError m ->
|
||||
text m
|
||||
|
||||
renderEmptyTrashResultMessage : Texts -> EmptyTrashResult -> Html msg
|
||||
renderEmptyTrashResultMessage texts result =
|
||||
let
|
||||
isSuccess =
|
||||
case result of
|
||||
EmptyTrashResultOk ->
|
||||
True
|
||||
|
||||
_ ->
|
||||
False
|
||||
|
||||
isError =
|
||||
not isSuccess
|
||||
in
|
||||
div
|
||||
[ classList
|
||||
[ ( S.errorMessage, isError )
|
||||
, ( S.successMessage, isSuccess )
|
||||
, ( "hidden", result == EmptyTrashResultInitial )
|
||||
]
|
||||
]
|
||||
[ case result of
|
||||
EmptyTrashResultInitial ->
|
||||
text ""
|
||||
|
||||
EmptyTrashResultOk ->
|
||||
text texts.emptyTrashTaskStarted
|
||||
|
||||
EmptyTrashResultHttpError err ->
|
||||
text (texts.httpError err)
|
||||
|
||||
EmptyTrashResultSubmitError m ->
|
||||
text m
|
||||
]
|
||||
|
106
modules/webapp/src/main/elm/Comp/EmptyTrashForm.elm
Normal file
106
modules/webapp/src/main/elm/Comp/EmptyTrashForm.elm
Normal file
@ -0,0 +1,106 @@
|
||||
{-
|
||||
Copyright 2020 Docspell Contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Comp.EmptyTrashForm exposing
|
||||
( Model
|
||||
, Msg
|
||||
, getSettings
|
||||
, init
|
||||
, update
|
||||
, view
|
||||
)
|
||||
|
||||
import Api
|
||||
import Comp.CalEventInput
|
||||
import Comp.Dropdown
|
||||
import Comp.FixedDropdown
|
||||
import Comp.IntField
|
||||
import Data.CalEvent exposing (CalEvent)
|
||||
import Data.DropdownStyle as DS
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.ListType exposing (ListType)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (..)
|
||||
import Http
|
||||
import Markdown
|
||||
import Messages.Comp.EmptyTrashForm exposing (Texts)
|
||||
import Styles as S
|
||||
import Util.Tag
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ scheduleModel : Comp.CalEventInput.Model
|
||||
, schedule : Maybe CalEvent
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= ScheduleMsg Comp.CalEventInput.Msg
|
||||
|
||||
|
||||
init : Flags -> String -> ( Model, Cmd Msg )
|
||||
init flags schedule =
|
||||
let
|
||||
newSchedule =
|
||||
Data.CalEvent.fromEvent schedule
|
||||
|> Maybe.withDefault Data.CalEvent.everyMonth
|
||||
|
||||
( cem, cec ) =
|
||||
Comp.CalEventInput.init flags newSchedule
|
||||
in
|
||||
( { scheduleModel = cem
|
||||
, schedule = Just newSchedule
|
||||
}
|
||||
, Cmd.map ScheduleMsg cec
|
||||
)
|
||||
|
||||
|
||||
getSettings : Model -> Maybe CalEvent
|
||||
getSettings model =
|
||||
model.schedule
|
||||
|
||||
|
||||
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update flags msg model =
|
||||
case msg of
|
||||
ScheduleMsg lmsg ->
|
||||
let
|
||||
( cm, cc, ce ) =
|
||||
Comp.CalEventInput.update
|
||||
flags
|
||||
model.schedule
|
||||
lmsg
|
||||
model.scheduleModel
|
||||
in
|
||||
( { model
|
||||
| scheduleModel = cm
|
||||
, schedule = ce
|
||||
}
|
||||
, Cmd.map ScheduleMsg cc
|
||||
)
|
||||
|
||||
|
||||
|
||||
--- View2
|
||||
|
||||
|
||||
view : Texts -> UiSettings -> Model -> Html Msg
|
||||
view texts _ model =
|
||||
div []
|
||||
[ div [ class "mb-4" ]
|
||||
[ label [ class S.inputLabel ]
|
||||
[ text texts.schedule ]
|
||||
, Html.map ScheduleMsg
|
||||
(Comp.CalEventInput.view2
|
||||
texts.calEventInput
|
||||
""
|
||||
model.schedule
|
||||
model.scheduleModel
|
||||
)
|
||||
]
|
||||
]
|
@ -149,13 +149,19 @@ update ddm msg model =
|
||||
view2 : Texts -> ViewConfig -> UiSettings -> Model -> ItemLight -> Html Msg
|
||||
view2 texts cfg settings model item =
|
||||
let
|
||||
isConfirmed =
|
||||
item.state /= "created"
|
||||
isCreated =
|
||||
item.state == "created"
|
||||
|
||||
isDeleted =
|
||||
item.state == "deleted"
|
||||
|
||||
cardColor =
|
||||
if not isConfirmed then
|
||||
if isCreated then
|
||||
"text-blue-500 dark:text-lightblue-500"
|
||||
|
||||
else if isDeleted then
|
||||
"text-red-600 dark:text-orange-600"
|
||||
|
||||
else
|
||||
""
|
||||
|
||||
@ -207,7 +213,7 @@ view2 texts cfg settings model item =
|
||||
[ previewImage2 settings cardAction model item
|
||||
]
|
||||
)
|
||||
++ [ mainContent2 texts cardAction cardColor isConfirmed settings cfg item
|
||||
++ [ mainContent2 texts cardAction cardColor isCreated isDeleted settings cfg item
|
||||
, metaDataContent2 texts settings item
|
||||
, notesContent2 settings item
|
||||
, fulltextResultsContent2 item
|
||||
@ -293,11 +299,12 @@ mainContent2 :
|
||||
-> List (Attribute Msg)
|
||||
-> String
|
||||
-> Bool
|
||||
-> Bool
|
||||
-> UiSettings
|
||||
-> ViewConfig
|
||||
-> ItemLight
|
||||
-> Html Msg
|
||||
mainContent2 texts cardAction cardColor isConfirmed settings _ item =
|
||||
mainContent2 texts _ cardColor isCreated isDeleted settings _ item =
|
||||
let
|
||||
dirIcon =
|
||||
i
|
||||
@ -353,12 +360,22 @@ mainContent2 texts cardAction cardColor isConfirmed settings _ item =
|
||||
[ classList
|
||||
[ ( "absolute right-1 top-1 text-4xl", True )
|
||||
, ( cardColor, True )
|
||||
, ( "hidden", isConfirmed )
|
||||
, ( "hidden", not isCreated )
|
||||
]
|
||||
, title texts.new
|
||||
]
|
||||
[ i [ class "ml-2 fa fa-exclamation-circle" ] []
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "absolute right-1 top-1 text-4xl", True )
|
||||
, ( cardColor, True )
|
||||
, ( "hidden", not isDeleted )
|
||||
]
|
||||
, title texts.basics.deleted
|
||||
]
|
||||
[ i [ class "ml-2 fa fa-trash-alt" ] []
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "opacity-75", True )
|
||||
|
@ -118,30 +118,57 @@ view texts settings model =
|
||||
]
|
||||
, True
|
||||
)
|
||||
|
||||
isDeleted =
|
||||
model.item.state == "deleted"
|
||||
|
||||
isCreated =
|
||||
model.item.state == "created"
|
||||
in
|
||||
div [ class "flex flex-col pb-2" ]
|
||||
[ div [ class "flex flex-row items-center text-2xl" ]
|
||||
[ i
|
||||
[ classList
|
||||
[ ( "hidden", Data.UiSettings.fieldHidden settings Data.Fields.Direction )
|
||||
[ if isDeleted then
|
||||
div
|
||||
[ classList
|
||||
[ ( "text-red-500 dark:text-orange-600 text-4xl", True )
|
||||
, ( "hidden", not isDeleted )
|
||||
]
|
||||
, title texts.basics.deleted
|
||||
]
|
||||
, class (Data.Direction.iconFromString2 model.item.direction)
|
||||
, class "mr-2"
|
||||
, title model.item.direction
|
||||
]
|
||||
[]
|
||||
[ i [ class "mr-2 fa fa-trash-alt" ] []
|
||||
]
|
||||
|
||||
else
|
||||
i
|
||||
[ classList
|
||||
[ ( "hidden", Data.UiSettings.fieldHidden settings Data.Fields.Direction )
|
||||
]
|
||||
, class (Data.Direction.iconFromString2 model.item.direction)
|
||||
, class "mr-2"
|
||||
, title model.item.direction
|
||||
]
|
||||
[]
|
||||
, div [ class "flex-grow ml-1 flex flex-col" ]
|
||||
[ div [ class "flex flex-row items-center font-semibold" ]
|
||||
[ text model.item.name
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", model.item.state /= "created" )
|
||||
[ ( "hidden", not isCreated )
|
||||
]
|
||||
, class "ml-3 text-base label bg-blue-500 dark:bg-lightblue-500 text-white rounded-lg"
|
||||
]
|
||||
[ text texts.new
|
||||
, i [ class "fa fa-exclamation ml-2" ] []
|
||||
]
|
||||
, div
|
||||
[ classList
|
||||
[ ( "hidden", not isDeleted )
|
||||
]
|
||||
, class "ml-3 text-base label bg-red-500 dark:bg-orange-500 text-white rounded-lg"
|
||||
]
|
||||
[ text texts.basics.deleted
|
||||
, i [ class "fa fa-exclamation ml-2" ] []
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
@ -339,6 +339,7 @@ type Msg
|
||||
| RequestReprocessItem
|
||||
| ReprocessItemConfirmed
|
||||
| ToggleSelectView
|
||||
| RestoreItem
|
||||
|
||||
|
||||
type SaveNameState
|
||||
|
@ -1604,6 +1604,9 @@ update key flags inav settings msg model =
|
||||
, cmd
|
||||
)
|
||||
|
||||
RestoreItem ->
|
||||
resultModelCmd ( model, Api.restoreItem flags model.item.id SaveResp )
|
||||
|
||||
|
||||
|
||||
--- Helper
|
||||
|
@ -188,15 +188,27 @@ menuBar texts inav settings model =
|
||||
]
|
||||
[ i [ class "fa fa-redo" ] []
|
||||
]
|
||||
, MB.CustomElement <|
|
||||
a
|
||||
[ class S.deleteButton
|
||||
, href "#"
|
||||
, onClick RequestDelete
|
||||
, title texts.deleteThisItem
|
||||
]
|
||||
[ i [ class "fa fa-trash" ] []
|
||||
]
|
||||
, if model.item.state == "deleted" then
|
||||
MB.CustomElement <|
|
||||
a
|
||||
[ class S.undeleteButton
|
||||
, href "#"
|
||||
, onClick RestoreItem
|
||||
, title texts.undeleteThisItem
|
||||
]
|
||||
[ i [ class "fa fa-trash-restore" ] []
|
||||
]
|
||||
|
||||
else
|
||||
MB.CustomElement <|
|
||||
a
|
||||
[ class S.deleteButton
|
||||
, href "#"
|
||||
, onClick RequestDelete
|
||||
, title texts.deleteThisItem
|
||||
]
|
||||
[ i [ class "fa fa-trash" ] []
|
||||
]
|
||||
]
|
||||
, rootClasses = "mb-2"
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ import Data.Fields
|
||||
import Data.Flags exposing (Flags)
|
||||
import Data.ItemQuery as Q exposing (ItemQuery)
|
||||
import Data.PersonUse
|
||||
import Data.SearchMode exposing (SearchMode)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import DatePicker exposing (DatePicker)
|
||||
import Html exposing (..)
|
||||
@ -89,6 +90,7 @@ type alias Model =
|
||||
, customValues : CustomFieldValueCollect
|
||||
, sourceModel : Maybe String
|
||||
, openTabs : Set String
|
||||
, searchMode : SearchMode
|
||||
}
|
||||
|
||||
|
||||
@ -133,6 +135,7 @@ init flags =
|
||||
, customValues = Data.CustomFieldChange.emptyCollect
|
||||
, sourceModel = Nothing
|
||||
, openTabs = Set.fromList [ "Tags", "Inbox" ]
|
||||
, searchMode = Data.SearchMode.Normal
|
||||
}
|
||||
|
||||
|
||||
@ -323,6 +326,7 @@ resetModel model =
|
||||
model.customFieldModel
|
||||
, customValues = Data.CustomFieldChange.emptyCollect
|
||||
, sourceModel = Nothing
|
||||
, searchMode = Data.SearchMode.Normal
|
||||
}
|
||||
|
||||
|
||||
@ -343,6 +347,7 @@ type Msg
|
||||
| FromDueDateMsg Comp.DatePicker.Msg
|
||||
| UntilDueDateMsg Comp.DatePicker.Msg
|
||||
| ToggleInbox
|
||||
| ToggleSearchMode
|
||||
| GetOrgResp (Result Http.Error ReferenceList)
|
||||
| GetEquipResp (Result Http.Error EquipmentList)
|
||||
| GetPersonResp (Result Http.Error PersonList)
|
||||
@ -683,6 +688,24 @@ updateDrop ddm flags settings msg model =
|
||||
, dragDrop = DD.DragDropData ddm Nothing
|
||||
}
|
||||
|
||||
ToggleSearchMode ->
|
||||
let
|
||||
current =
|
||||
model.searchMode
|
||||
|
||||
next =
|
||||
if current == Data.SearchMode.Normal then
|
||||
Data.SearchMode.Trashed
|
||||
|
||||
else
|
||||
Data.SearchMode.Normal
|
||||
in
|
||||
{ model = { model | searchMode = next }
|
||||
, cmd = Cmd.none
|
||||
, stateChange = True
|
||||
, dragDrop = DD.DragDropData ddm Nothing
|
||||
}
|
||||
|
||||
FromDateMsg m ->
|
||||
let
|
||||
( dp, event ) =
|
||||
@ -962,6 +985,7 @@ type SearchTab
|
||||
| TabDueDate
|
||||
| TabSource
|
||||
| TabDirection
|
||||
| TabTrashed
|
||||
|
||||
|
||||
allTabs : List SearchTab
|
||||
@ -977,6 +1001,7 @@ allTabs =
|
||||
, TabDueDate
|
||||
, TabSource
|
||||
, TabDirection
|
||||
, TabTrashed
|
||||
]
|
||||
|
||||
|
||||
@ -1016,6 +1041,9 @@ tabName tab =
|
||||
TabDirection ->
|
||||
"direction"
|
||||
|
||||
TabTrashed ->
|
||||
"trashed"
|
||||
|
||||
|
||||
findTab : Comp.Tabs.Tab msg -> Maybe SearchTab
|
||||
findTab tab =
|
||||
@ -1053,6 +1081,9 @@ findTab tab =
|
||||
"direction" ->
|
||||
Just TabDirection
|
||||
|
||||
"trashed" ->
|
||||
Just TabTrashed
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
@ -1099,6 +1130,9 @@ searchTabState settings model tab =
|
||||
Just TabInbox ->
|
||||
False
|
||||
|
||||
Just TabTrashed ->
|
||||
False
|
||||
|
||||
Nothing ->
|
||||
False
|
||||
|
||||
@ -1447,4 +1481,18 @@ searchTabs texts ddd flags settings model =
|
||||
)
|
||||
]
|
||||
}
|
||||
, { name = tabName TabTrashed
|
||||
, title = texts.trashcan
|
||||
, titleRight = []
|
||||
, info = Nothing
|
||||
, body =
|
||||
[ MB.viewItem <|
|
||||
MB.Checkbox
|
||||
{ id = "trashed"
|
||||
, value = model.searchMode == Data.SearchMode.Trashed
|
||||
, label = texts.trashcan
|
||||
, tagger = \_ -> ToggleSearchMode
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -21,6 +21,7 @@ module Data.ItemQuery exposing
|
||||
import Api.Model.CustomFieldValue exposing (CustomFieldValue)
|
||||
import Api.Model.ItemQuery as RQ
|
||||
import Data.Direction exposing (Direction)
|
||||
import Data.SearchMode exposing (SearchMode)
|
||||
|
||||
|
||||
type TagMatch
|
||||
@ -73,12 +74,13 @@ and list =
|
||||
Just (And es)
|
||||
|
||||
|
||||
request : Maybe ItemQuery -> RQ.ItemQuery
|
||||
request mq =
|
||||
request : SearchMode -> Maybe ItemQuery -> RQ.ItemQuery
|
||||
request smode mq =
|
||||
{ offset = Nothing
|
||||
, limit = Nothing
|
||||
, withDetails = Just True
|
||||
, query = renderMaybe mq
|
||||
, searchMode = Data.SearchMode.asString smode |> Just
|
||||
}
|
||||
|
||||
|
||||
|
40
modules/webapp/src/main/elm/Data/SearchMode.elm
Normal file
40
modules/webapp/src/main/elm/Data/SearchMode.elm
Normal file
@ -0,0 +1,40 @@
|
||||
{-
|
||||
Copyright 2020 Docspell Contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Data.SearchMode exposing
|
||||
( SearchMode(..)
|
||||
, asString
|
||||
, fromString
|
||||
)
|
||||
|
||||
|
||||
type SearchMode
|
||||
= Normal
|
||||
| Trashed
|
||||
|
||||
|
||||
fromString : String -> Maybe SearchMode
|
||||
fromString str =
|
||||
case String.toLower str of
|
||||
"normal" ->
|
||||
Just Normal
|
||||
|
||||
"trashed" ->
|
||||
Just Trashed
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
|
||||
|
||||
asString : SearchMode -> String
|
||||
asString smode =
|
||||
case smode of
|
||||
Normal ->
|
||||
"normal"
|
||||
|
||||
Trashed ->
|
||||
"trashed"
|
@ -15,6 +15,7 @@ module Messages.Basics exposing
|
||||
type alias Texts =
|
||||
{ incoming : String
|
||||
, outgoing : String
|
||||
, deleted : String
|
||||
, tags : String
|
||||
, items : String
|
||||
, submit : String
|
||||
@ -51,6 +52,7 @@ gb : Texts
|
||||
gb =
|
||||
{ incoming = "Incoming"
|
||||
, outgoing = "Outgoing"
|
||||
, deleted = "Deleted"
|
||||
, tags = "Tags"
|
||||
, items = "Items"
|
||||
, submit = "Submit"
|
||||
@ -92,6 +94,7 @@ de : Texts
|
||||
de =
|
||||
{ incoming = "Eingehend"
|
||||
, outgoing = "Ausgehend"
|
||||
, deleted = "Gelöscht"
|
||||
, tags = "Tags"
|
||||
, items = "Dokumente"
|
||||
, submit = "Speichern"
|
||||
|
@ -15,6 +15,7 @@ import Data.Language exposing (Language)
|
||||
import Http
|
||||
import Messages.Basics
|
||||
import Messages.Comp.ClassifierSettingsForm
|
||||
import Messages.Comp.EmptyTrashForm
|
||||
import Messages.Comp.HttpError
|
||||
import Messages.Data.Language
|
||||
|
||||
@ -22,6 +23,7 @@ import Messages.Data.Language
|
||||
type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, classifierSettingsForm : Messages.Comp.ClassifierSettingsForm.Texts
|
||||
, emptyTrashForm : Messages.Comp.EmptyTrashForm.Texts
|
||||
, httpError : Http.Error -> String
|
||||
, save : String
|
||||
, saveSettings : String
|
||||
@ -37,8 +39,10 @@ type alias Texts =
|
||||
, startNow : String
|
||||
, languageLabel : Language -> String
|
||||
, classifierTaskStarted : String
|
||||
, emptyTrashTaskStarted : String
|
||||
, fulltextReindexSubmitted : String
|
||||
, fulltextReindexOkMissing : String
|
||||
, emptyTrash : String
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +50,7 @@ gb : Texts
|
||||
gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, classifierSettingsForm = Messages.Comp.ClassifierSettingsForm.gb
|
||||
, emptyTrashForm = Messages.Comp.EmptyTrashForm.gb
|
||||
, httpError = Messages.Comp.HttpError.gb
|
||||
, save = "Save"
|
||||
, saveSettings = "Save Settings"
|
||||
@ -65,9 +70,11 @@ gb =
|
||||
, startNow = "Start now"
|
||||
, languageLabel = Messages.Data.Language.gb
|
||||
, classifierTaskStarted = "Classifier task started."
|
||||
, emptyTrashTaskStarted = "Empty trash task started."
|
||||
, fulltextReindexSubmitted = "Fulltext Re-Index started."
|
||||
, fulltextReindexOkMissing =
|
||||
"Please type OK in the field if you really want to start re-indexing your data."
|
||||
, emptyTrash = "Empty Trash"
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +82,7 @@ de : Texts
|
||||
de =
|
||||
{ basics = Messages.Basics.de
|
||||
, classifierSettingsForm = Messages.Comp.ClassifierSettingsForm.de
|
||||
, emptyTrashForm = Messages.Comp.EmptyTrashForm.de
|
||||
, httpError = Messages.Comp.HttpError.de
|
||||
, save = "Speichern"
|
||||
, saveSettings = "Einstellungen speichern"
|
||||
@ -94,7 +102,9 @@ de =
|
||||
, startNow = "Jetzt starten"
|
||||
, languageLabel = Messages.Data.Language.de
|
||||
, classifierTaskStarted = "Kategorisierung gestartet."
|
||||
, emptyTrashTaskStarted = "Papierkorb löschen gestartet."
|
||||
, fulltextReindexSubmitted = "Volltext Neu-Indexierung gestartet."
|
||||
, fulltextReindexOkMissing =
|
||||
"Bitte tippe OK in das Feld ein, wenn Du wirklich den Index neu erzeugen möchtest."
|
||||
, emptyTrash = "Papierkorb löschen"
|
||||
}
|
||||
|
38
modules/webapp/src/main/elm/Messages/Comp/EmptyTrashForm.elm
Normal file
38
modules/webapp/src/main/elm/Messages/Comp/EmptyTrashForm.elm
Normal file
@ -0,0 +1,38 @@
|
||||
{-
|
||||
Copyright 2020 Docspell Contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
-}
|
||||
|
||||
|
||||
module Messages.Comp.EmptyTrashForm exposing
|
||||
( Texts
|
||||
, de
|
||||
, gb
|
||||
)
|
||||
|
||||
import Messages.Basics
|
||||
import Messages.Comp.CalEventInput
|
||||
|
||||
|
||||
type alias Texts =
|
||||
{ basics : Messages.Basics.Texts
|
||||
, calEventInput : Messages.Comp.CalEventInput.Texts
|
||||
, schedule : String
|
||||
}
|
||||
|
||||
|
||||
gb : Texts
|
||||
gb =
|
||||
{ basics = Messages.Basics.gb
|
||||
, calEventInput = Messages.Comp.CalEventInput.gb
|
||||
, schedule = "Schedule"
|
||||
}
|
||||
|
||||
|
||||
de : Texts
|
||||
de =
|
||||
{ basics = Messages.Basics.de
|
||||
, calEventInput = Messages.Comp.CalEventInput.de
|
||||
, schedule = "Zeitplan"
|
||||
}
|
@ -46,6 +46,7 @@ type alias Texts =
|
||||
, unconfirmItemMetadata : String
|
||||
, reprocessItem : String
|
||||
, deleteThisItem : String
|
||||
, undeleteThisItem : String
|
||||
, sentEmails : String
|
||||
, sendThisItemViaEmail : String
|
||||
, itemId : String
|
||||
@ -79,6 +80,7 @@ gb =
|
||||
, unconfirmItemMetadata = "Un-confirm item metadata"
|
||||
, reprocessItem = "Reprocess this item"
|
||||
, deleteThisItem = "Delete this item"
|
||||
, undeleteThisItem = "Restore this item"
|
||||
, sentEmails = "Sent E-Mails"
|
||||
, sendThisItemViaEmail = "Send this item via E-Mail"
|
||||
, itemId = "Item ID"
|
||||
@ -112,6 +114,7 @@ de =
|
||||
, unconfirmItemMetadata = "Widerrufe Bestätigung"
|
||||
, reprocessItem = "Das Dokument erneut verarbeiten"
|
||||
, deleteThisItem = "Das Dokument löschen"
|
||||
, undeleteThisItem = "Das Dokument wiederherstellen"
|
||||
, sentEmails = "Versendete E-Mails"
|
||||
, sendThisItemViaEmail = "Sende dieses Dokument via E-Mail"
|
||||
, itemId = "Dokument-ID"
|
||||
|
@ -46,6 +46,7 @@ type alias Texts =
|
||||
, sourceTab : String
|
||||
, searchInItemSource : String
|
||||
, direction : Direction -> String
|
||||
, trashcan : String
|
||||
}
|
||||
|
||||
|
||||
@ -77,6 +78,7 @@ gb =
|
||||
, sourceTab = "Source"
|
||||
, searchInItemSource = "Search in item source…"
|
||||
, direction = Messages.Data.Direction.gb
|
||||
, trashcan = "Trash"
|
||||
}
|
||||
|
||||
|
||||
@ -108,4 +110,5 @@ de =
|
||||
, sourceTab = "Quelle"
|
||||
, searchInItemSource = "Suche in Dokumentquelle…"
|
||||
, direction = Messages.Data.Direction.de
|
||||
, trashcan = "Papierkorb"
|
||||
}
|
||||
|
@ -30,9 +30,11 @@ type alias Texts =
|
||||
, powerSearchPlaceholder : String
|
||||
, reallyReprocessQuestion : String
|
||||
, reallyDeleteQuestion : String
|
||||
, reallyRestoreQuestion : String
|
||||
, editSelectedItems : Int -> String
|
||||
, reprocessSelectedItems : Int -> String
|
||||
, deleteSelectedItems : Int -> String
|
||||
, undeleteSelectedItems : Int -> String
|
||||
, selectAllVisible : String
|
||||
, selectNone : String
|
||||
, resetSearchForm : String
|
||||
@ -54,9 +56,11 @@ gb =
|
||||
, powerSearchPlaceholder = "Search query …"
|
||||
, reallyReprocessQuestion = "Really reprocess all selected items? Metadata of unconfirmed items may change."
|
||||
, reallyDeleteQuestion = "Really delete all selected items?"
|
||||
, reallyRestoreQuestion = "Really restore all selected items?"
|
||||
, editSelectedItems = \n -> "Edit " ++ String.fromInt n ++ " selected items"
|
||||
, reprocessSelectedItems = \n -> "Reprocess " ++ String.fromInt n ++ " selected items"
|
||||
, deleteSelectedItems = \n -> "Delete " ++ String.fromInt n ++ " selected items"
|
||||
, undeleteSelectedItems = \n -> "Restore " ++ String.fromInt n ++ " selected items"
|
||||
, selectAllVisible = "Select all visible"
|
||||
, selectNone = "Select none"
|
||||
, resetSearchForm = "Reset search form"
|
||||
@ -78,9 +82,11 @@ de =
|
||||
, powerSearchPlaceholder = "Suchanfrage…"
|
||||
, reallyReprocessQuestion = "Wirklich die gewählten Dokumente neu verarbeiten? Die Metadaten von nicht bestätigten Dokumenten können sich dabei ändern."
|
||||
, reallyDeleteQuestion = "Wirklich alle gewählten Dokumente löschen?"
|
||||
, reallyRestoreQuestion = "Wirklich alle gewählten Dokumente wiederherstellen?"
|
||||
, editSelectedItems = \n -> "Ändere " ++ String.fromInt n ++ " gewählte Dokumente"
|
||||
, reprocessSelectedItems = \n -> "Erneute Verarbeitung von " ++ String.fromInt n ++ " gewählten Dokumenten"
|
||||
, deleteSelectedItems = \n -> "Lösche " ++ String.fromInt n ++ " gewählte Dokumente"
|
||||
, undeleteSelectedItems = \n -> "Stelle " ++ String.fromInt n ++ " gewählte Dokumente wieder her"
|
||||
, selectAllVisible = "Wähle alle Dokumente in der Liste"
|
||||
, selectNone = "Wähle alle Dokumente ab"
|
||||
, resetSearchForm = "Suchformular zurücksetzen"
|
||||
|
@ -171,6 +171,7 @@ viewInsights texts flags model =
|
||||
[ stats (String.fromInt (model.insights.incomingCount + model.insights.outgoingCount)) texts.basics.items
|
||||
, stats (String.fromInt model.insights.incomingCount) texts.basics.incoming
|
||||
, stats (String.fromInt model.insights.outgoingCount) texts.basics.outgoing
|
||||
, stats (String.fromInt model.insights.deletedCount) texts.basics.deleted
|
||||
]
|
||||
]
|
||||
, div
|
||||
|
@ -68,6 +68,7 @@ type alias Model =
|
||||
type ConfirmModalValue
|
||||
= ConfirmReprocessItems
|
||||
| ConfirmDelete
|
||||
| ConfirmRestore
|
||||
|
||||
|
||||
type alias SelectViewModel =
|
||||
@ -185,7 +186,9 @@ type Msg
|
||||
| SelectAllItems
|
||||
| SelectNoItems
|
||||
| RequestDeleteSelected
|
||||
| RequestRestoreSelected
|
||||
| DeleteSelectedConfirmed
|
||||
| RestoreSelectedConfirmed
|
||||
| CloseConfirmModal
|
||||
| EditSelectedItems
|
||||
| EditMenuMsg Comp.ItemDetail.MultiEditMenu.Msg
|
||||
@ -214,6 +217,7 @@ type SelectActionMode
|
||||
| DeleteSelected
|
||||
| EditSelected
|
||||
| ReprocessSelected
|
||||
| RestoreSelected
|
||||
|
||||
|
||||
type alias SearchParam =
|
||||
@ -239,7 +243,7 @@ doSearchDefaultCmd : SearchParam -> Model -> Cmd Msg
|
||||
doSearchDefaultCmd param model =
|
||||
let
|
||||
smask =
|
||||
Q.request <|
|
||||
Q.request model.searchMenuModel.searchMode <|
|
||||
Q.and
|
||||
[ Comp.SearchMenu.getItemQuery model.searchMenuModel
|
||||
, Maybe.map Q.Fragment model.powerSearchInput.input
|
||||
|
@ -23,6 +23,7 @@ import Data.Flags exposing (Flags)
|
||||
import Data.ItemQuery as Q
|
||||
import Data.ItemSelection
|
||||
import Data.Items
|
||||
import Data.SearchMode exposing (SearchMode)
|
||||
import Data.UiSettings exposing (UiSettings)
|
||||
import Page exposing (Page(..))
|
||||
import Page.Home.Data exposing (..)
|
||||
@ -360,6 +361,28 @@ update mId key flags settings msg model =
|
||||
|
||||
_ ->
|
||||
noSub ( model, Cmd.none )
|
||||
RestoreSelectedConfirmed ->
|
||||
case model.viewMode of
|
||||
SelectView svm ->
|
||||
let
|
||||
cmd =
|
||||
Api.restoreAllItems flags svm.ids DeleteAllResp
|
||||
in
|
||||
noSub
|
||||
( { model
|
||||
| viewMode =
|
||||
SelectView
|
||||
{ svm
|
||||
| confirmModal = Nothing
|
||||
, action = RestoreSelected
|
||||
}
|
||||
}
|
||||
, cmd
|
||||
)
|
||||
|
||||
_ ->
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
|
||||
DeleteAllResp (Ok res) ->
|
||||
if res.success then
|
||||
@ -468,6 +491,29 @@ update mId key flags settings msg model =
|
||||
_ ->
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
RequestRestoreSelected ->
|
||||
case model.viewMode of
|
||||
SelectView svm ->
|
||||
if svm.ids == Set.empty then
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
else
|
||||
let
|
||||
model_ =
|
||||
{ model
|
||||
| viewMode =
|
||||
SelectView
|
||||
{ svm
|
||||
| action = RestoreSelected
|
||||
, confirmModal = Just ConfirmRestore
|
||||
}
|
||||
}
|
||||
in
|
||||
noSub ( model_, Cmd.none )
|
||||
|
||||
_ ->
|
||||
noSub ( model, Cmd.none )
|
||||
|
||||
EditSelectedItems ->
|
||||
case model.viewMode of
|
||||
SelectView svm ->
|
||||
@ -548,7 +594,7 @@ update mId key flags settings msg model =
|
||||
case model.viewMode of
|
||||
SelectView svm ->
|
||||
-- replace changed items in the view
|
||||
noSub ( nm, loadChangedItems flags svm.ids )
|
||||
noSub ( nm, loadChangedItems flags model.searchMenuModel.searchMode svm.ids )
|
||||
|
||||
_ ->
|
||||
noSub ( nm, Cmd.none )
|
||||
@ -717,8 +763,8 @@ replaceItems model newItems =
|
||||
{ model | itemListModel = newList }
|
||||
|
||||
|
||||
loadChangedItems : Flags -> Set String -> Cmd Msg
|
||||
loadChangedItems flags ids =
|
||||
loadChangedItems : Flags -> SearchMode -> Set String -> Cmd Msg
|
||||
loadChangedItems flags smode ids =
|
||||
if Set.isEmpty ids then
|
||||
Cmd.none
|
||||
|
||||
@ -728,7 +774,7 @@ loadChangedItems flags ids =
|
||||
Set.toList ids
|
||||
|
||||
searchInit =
|
||||
Q.request (Just <| Q.ItemIdIn idList)
|
||||
Q.request smode (Just <| Q.ItemIdIn idList)
|
||||
|
||||
search =
|
||||
{ searchInit
|
||||
|
@ -78,6 +78,14 @@ confirmModal texts model =
|
||||
texts.basics.yes
|
||||
texts.basics.no
|
||||
texts.reallyDeleteQuestion
|
||||
ConfirmRestore ->
|
||||
Comp.ConfirmModal.defaultSettings
|
||||
RestoreSelectedConfirmed
|
||||
CloseConfirmModal
|
||||
texts.basics.yes
|
||||
texts.basics.no
|
||||
texts.reallyRestoreQuestion
|
||||
|
||||
in
|
||||
case model.viewMode of
|
||||
SelectView svm ->
|
||||
@ -264,6 +272,16 @@ editMenuBar texts model svm =
|
||||
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == DeleteSelected )
|
||||
]
|
||||
}
|
||||
, MB.CustomButton
|
||||
{ tagger = RequestRestoreSelected
|
||||
, label = ""
|
||||
, icon = Just "fa fa-trash-restore"
|
||||
, title = texts.undeleteSelectedItems selectCount
|
||||
, inputClass =
|
||||
[ ( btnStyle, True )
|
||||
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == RestoreSelected )
|
||||
]
|
||||
}
|
||||
]
|
||||
, end =
|
||||
[ MB.CustomButton
|
||||
|
@ -200,6 +200,11 @@ deleteButton =
|
||||
" rounded my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 hover:bg-red-600 hover:text-white dark:hover:text-white dark:hover:bg-orange-500 dark:hover:text-bluegray-900 "
|
||||
|
||||
|
||||
undeleteButton : String
|
||||
undeleteButton =
|
||||
" rounded my-auto whitespace-nowrap border border-green-500 dark:border-lightgreen-500 text-green-500 dark:text-lightgreen-500 text-center px-4 py-2 shadow-none focus:outline-none focus:ring focus:ring-opacity-75 hover:bg-green-600 hover:text-white dark:hover:text-white dark:hover:bg-lightgreen-500 dark:hover:text-bluegray-900 "
|
||||
|
||||
|
||||
deleteLabel : String
|
||||
deleteLabel =
|
||||
"label my-auto whitespace-nowrap border border-red-500 dark:border-lightred-500 text-red-500 dark:text-orange-500 text-center focus:outline-none focus:ring focus:ring-opacity-75 hover:bg-red-600 hover:text-white dark:hover:text-white dark:hover:bg-orange-500 dark:hover:text-bluegray-900"
|
||||
|
Loading…
Reference in New Issue
Block a user