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