Merge pull request #1009 from eikek/fixup/347-delete-duration

Use a minimum age of items to remove
This commit is contained in:
mergify[bot] 2021-08-15 10:50:30 +00:00 committed by GitHub
commit 50387cd378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 307 additions and 125 deletions

View File

@ -242,6 +242,10 @@ val openapiScalaSettings = Seq(
field => field =>
field field
.copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode"))) .copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode")))
case "duration" =>
field =>
field
.copy(typeDef = TypeDef("Duration", Imports("docspell.common.Duration")))
})) }))
) )

View File

@ -61,7 +61,7 @@ trait OCollective[F[_]] {
def startLearnClassifier(collective: Ident): F[Unit] def startLearnClassifier(collective: Ident): F[Unit]
def startEmptyTrash(collective: Ident): F[Unit] def startEmptyTrash(args: EmptyTrashArgs): 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.
@ -88,6 +88,8 @@ object OCollective {
val Settings = RCollective.Settings val Settings = RCollective.Settings
type Classifier = RClassifierSetting.Classifier type Classifier = RClassifierSetting.Classifier
val Classifier = RClassifierSetting.Classifier val Classifier = RClassifierSetting.Classifier
type EmptyTrash = REmptyTrashSetting.EmptyTrash
val EmptyTrash = REmptyTrashSetting.EmptyTrash
sealed trait PassResetResult sealed trait PassResetResult
object PassResetResult { object PassResetResult {
@ -160,51 +162,47 @@ object OCollective {
id <- Ident.randomId[F] id <- Ident.randomId[F]
on = sett.classifier.map(_.enabled).getOrElse(false) on = sett.classifier.map(_.enabled).getOrElse(false)
timer = sett.classifier.map(_.schedule).getOrElse(CalEvent.unsafe("")) timer = sett.classifier.map(_.schedule).getOrElse(CalEvent.unsafe(""))
args = LearnClassifierArgs(coll)
ut = UserTask( ut = UserTask(
id, id,
LearnClassifierArgs.taskName, LearnClassifierArgs.taskName,
on, on,
timer, timer,
None, None,
LearnClassifierArgs(coll) args
) )
_ <- uts.updateOneTask(UserTaskScope(coll), ut) _ <- uts.updateOneTask(UserTaskScope(coll), args.makeSubject.some, ut)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
private def updateEmptyTrashTask(coll: Ident, sett: Settings): F[Unit] = private def updateEmptyTrashTask(coll: Ident, sett: Settings): F[Unit] =
for { for {
id <- Ident.randomId[F] id <- Ident.randomId[F]
timer = sett.emptyTrash.getOrElse(CalEvent.unsafe("")) settings = sett.emptyTrash.getOrElse(EmptyTrash.default)
ut = UserTask( args = EmptyTrashArgs(coll, settings.minAge)
id, ut = UserTask(id, EmptyTrashArgs.taskName, true, settings.schedule, None, args)
EmptyTrashArgs.taskName, _ <- uts.updateOneTask(UserTaskScope(coll), args.makeSubject.some, ut)
true,
timer,
None,
EmptyTrashArgs(coll)
)
_ <- uts.updateOneTask(UserTaskScope(coll), ut)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
def startLearnClassifier(collective: Ident): F[Unit] = def startLearnClassifier(collective: Ident): F[Unit] =
for { for {
id <- Ident.randomId[F] id <- Ident.randomId[F]
args = LearnClassifierArgs(collective)
ut <- UserTask( ut <- UserTask(
id, id,
LearnClassifierArgs.taskName, LearnClassifierArgs.taskName,
true, true,
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All), CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
None, None,
LearnClassifierArgs(collective) args
).encode.toPeriodicTask(UserTaskScope(collective)) ).encode.toPeriodicTask(UserTaskScope(collective), args.makeSubject.some)
job <- ut.toJob job <- ut.toJob
_ <- queue.insert(job) _ <- queue.insert(job)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
def startEmptyTrash(collective: Ident): F[Unit] = def startEmptyTrash(args: EmptyTrashArgs): F[Unit] =
for { for {
id <- Ident.randomId[F] id <- Ident.randomId[F]
ut <- UserTask( ut <- UserTask(
@ -213,8 +211,8 @@ object OCollective {
true, true,
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All), CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
None, None,
EmptyTrashArgs(collective) args
).encode.toPeriodicTask(UserTaskScope(collective)) ).encode.toPeriodicTask(UserTaskScope(args.collective), args.makeSubject.some)
job <- ut.toJob job <- ut.toJob
_ <- queue.insert(job) _ <- queue.insert(job)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes

View File

@ -23,7 +23,7 @@ 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 findDeleted(collective: Ident, maxUpdate: Timestamp, 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]]
@ -147,9 +147,13 @@ object OItemSearch {
.toVector .toVector
} }
def findDeleted(collective: Ident, limit: Int): F[Vector[RItem]] = def findDeleted(
collective: Ident,
maxUpdate: Timestamp,
limit: Int
): F[Vector[RItem]] =
store store
.transact(RItem.findDeleted(collective, limit)) .transact(RItem.findDeleted(collective, maxUpdate, limit))
.take(limit.toLong) .take(limit.toLong)
.compile .compile
.toVector .toVector

View File

@ -33,6 +33,7 @@ trait OUserTask[F[_]] {
*/ */
def submitScanMailbox( def submitScanMailbox(
scope: UserTaskScope, scope: UserTaskScope,
subject: Option[String],
task: UserTask[ScanMailboxArgs] task: UserTask[ScanMailboxArgs]
): F[Unit] ): F[Unit]
@ -51,6 +52,7 @@ trait OUserTask[F[_]] {
*/ */
def submitNotifyDueItems( def submitNotifyDueItems(
scope: UserTaskScope, scope: UserTaskScope,
subject: Option[String],
task: UserTask[NotifyDueItemsArgs] task: UserTask[NotifyDueItemsArgs]
): F[Unit] ): F[Unit]
@ -61,8 +63,8 @@ trait OUserTask[F[_]] {
* 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](scope: UserTaskScope, task: UserTask[A])(implicit def executeNow[A](scope: UserTaskScope, subject: Option[String], task: UserTask[A])(
E: Encoder[A] implicit E: Encoder[A]
): F[Unit] ): F[Unit]
} }
@ -75,11 +77,11 @@ 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](scope: UserTaskScope, task: UserTask[A])(implicit def executeNow[A](scope: UserTaskScope, subject: Option[String], task: UserTask[A])(
E: Encoder[A] implicit E: Encoder[A]
): F[Unit] = ): F[Unit] =
for { for {
ptask <- task.encode.toPeriodicTask(scope) ptask <- task.encode.toPeriodicTask(scope, subject)
job <- ptask.toJob job <- ptask.toJob
_ <- queue.insert(job) _ <- queue.insert(job)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
@ -103,10 +105,11 @@ object OUserTask {
def submitScanMailbox( def submitScanMailbox(
scope: UserTaskScope, scope: UserTaskScope,
subject: Option[String],
task: UserTask[ScanMailboxArgs] task: UserTask[ScanMailboxArgs]
): F[Unit] = ): F[Unit] =
for { for {
_ <- store.updateTask[ScanMailboxArgs](scope, task) _ <- store.updateTask[ScanMailboxArgs](scope, subject, task)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
@ -124,10 +127,11 @@ object OUserTask {
def submitNotifyDueItems( def submitNotifyDueItems(
scope: UserTaskScope, scope: UserTaskScope,
subject: Option[String],
task: UserTask[NotifyDueItemsArgs] task: UserTask[NotifyDueItemsArgs]
): F[Unit] = ): F[Unit] =
for { for {
_ <- store.updateTask[NotifyDueItemsArgs](scope, task) _ <- store.updateTask[NotifyDueItemsArgs](scope, subject, task)
_ <- joex.notifyAllNodes _ <- joex.notifyAllNodes
} yield () } yield ()
}) })

View File

@ -18,11 +18,12 @@ import io.circe.generic.semiauto._
* items. These are items with state `ItemState.Deleted`. * items. These are items with state `ItemState.Deleted`.
*/ */
case class EmptyTrashArgs( case class EmptyTrashArgs(
collective: Ident collective: Ident,
minAge: Duration
) { ) {
def makeSubject: String = def makeSubject: String =
"Empty trash" s"Empty Trash: Remove older than ${minAge.toJava}"
} }

View File

@ -87,9 +87,11 @@ final class JoexAppImpl[F[_]: Async](
private def scheduleEmptyTrashTasks: F[Unit] = private def scheduleEmptyTrashTasks: F[Unit] =
store store
.transact( .transact(
REmptyTrashSetting.findForAllCollectives(EmptyTrashArgs.defaultSchedule, 50) REmptyTrashSetting.findForAllCollectives(OCollective.EmptyTrash.default, 50)
)
.evalMap(es =>
EmptyTrashTask.periodicTask(EmptyTrashArgs(es.cid, es.minAge), es.schedule)
) )
.evalMap(es => EmptyTrashTask.periodicTask(es.cid, es.schedule))
.evalMap(pstore.insert) .evalMap(pstore.insert)
.compile .compile
.drain .drain

View File

@ -26,7 +26,7 @@ object EmptyTrashTask {
private val pageSize = 20 private val pageSize = 20
def periodicTask[F[_]: Sync](collective: Ident, ce: CalEvent): F[RPeriodicTask] = def periodicTask[F[_]: Sync](args: EmptyTrashArgs, ce: CalEvent): F[RPeriodicTask] =
Ident Ident
.randomId[F] .randomId[F]
.flatMap(id => .flatMap(id =>
@ -36,8 +36,8 @@ object EmptyTrashTask {
true, true,
ce, ce,
None, None,
EmptyTrashArgs(collective) args
).encode.toPeriodicTask(UserTaskScope(collective)) ).encode.toPeriodicTask(UserTaskScope(args.collective), args.makeSubject.some)
) )
def apply[F[_]: Async]( def apply[F[_]: Async](
@ -45,23 +45,27 @@ object EmptyTrashTask {
itemSearchOps: OItemSearch[F] itemSearchOps: OItemSearch[F]
): Task[F, Args, Unit] = ): Task[F, Args, Unit] =
Task { ctx => Task { ctx =>
val collId = ctx.args.collective
for { for {
_ <- ctx.logger.info(s"Starting removing all soft-deleted items") now <- Timestamp.current[F]
nDeleted <- deleteAll(collId, itemOps, itemSearchOps, ctx) maxDate = now.minus(ctx.args.minAge)
_ <- ctx.logger.info(
s"Starting removing all soft-deleted items older than ${maxDate.asString}"
)
nDeleted <- deleteAll(ctx.args, maxDate, itemOps, itemSearchOps, ctx)
_ <- ctx.logger.info(s"Finished deleting ${nDeleted} items") _ <- ctx.logger.info(s"Finished deleting ${nDeleted} items")
} yield () } yield ()
} }
private def deleteAll[F[_]: Async]( private def deleteAll[F[_]: Async](
collective: Ident, args: Args,
maxUpdate: Timestamp,
itemOps: OItem[F], itemOps: OItem[F],
itemSearchOps: OItemSearch[F], itemSearchOps: OItemSearch[F],
ctx: Context[F, _] ctx: Context[F, _]
): F[Int] = ): F[Int] =
Stream Stream
.eval(itemSearchOps.findDeleted(collective, pageSize)) .eval(itemSearchOps.findDeleted(args.collective, maxUpdate, pageSize))
.evalMap(deleteChunk(collective, itemOps, ctx)) .evalMap(deleteChunk(args.collective, itemOps, ctx))
.repeat .repeat
.takeWhile(_ > 0) .takeWhile(_ > 0)
.compile .compile

View File

@ -1149,6 +1149,11 @@ paths:
The request is empty, settings are used from the collective. The request is empty, settings are used from the collective.
security: security:
- authTokenHeader: [] - authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/EmptyTrashSetting"
responses: responses:
200: 200:
description: Ok description: Ok
@ -5267,7 +5272,7 @@ components:
- language - language
- integrationEnabled - integrationEnabled
- classifier - classifier
- emptyTrashSchedule - emptyTrash
properties: properties:
language: language:
type: string type: string
@ -5277,11 +5282,24 @@ 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"
emptyTrash:
$ref: "#/components/schemas/EmptyTrashSetting"
EmptyTrashSetting:
description: |
Settings for clearing the trash of items.
required:
- schedule
- minAge
properties:
schedule:
type: string
format: calevent
minAge:
type: integer
format: duration
ClassifierSetting: ClassifierSetting:
description: | description: |

View File

@ -12,7 +12,8 @@ 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.{EmptyTrashArgs, ListType} import docspell.common.EmptyTrashArgs
import docspell.common.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._
@ -56,7 +57,12 @@ object CollectiveRoutes {
settings.classifier.listType settings.classifier.listType
) )
), ),
Some(settings.emptyTrashSchedule) Some(
OCollective.EmptyTrash(
settings.emptyTrash.schedule,
settings.emptyTrash.minAge
)
)
) )
res <- res <-
backend.collective backend.collective
@ -67,11 +73,11 @@ object CollectiveRoutes {
case GET -> Root / "settings" => case GET -> Root / "settings" =>
for { for {
settDb <- backend.collective.findSettings(user.account.collective) settDb <- backend.collective.findSettings(user.account.collective)
trash = settDb.flatMap(_.emptyTrash).getOrElse(OCollective.EmptyTrash.default)
sett = settDb.map(c => sett = settDb.map(c =>
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
@ -79,6 +85,10 @@ object CollectiveRoutes {
.getOrElse(CalEvent.unsafe("*-1/3-01 01:00:00")), .getOrElse(CalEvent.unsafe("*-1/3-01 01:00:00")),
c.classifier.map(_.categories).getOrElse(Nil), c.classifier.map(_.categories).getOrElse(Nil),
c.classifier.map(_.listType).getOrElse(ListType.whitelist) c.classifier.map(_.listType).getOrElse(ListType.whitelist)
),
EmptyTrashSetting(
trash.schedule,
trash.minAge
) )
) )
) )
@ -103,9 +113,12 @@ object CollectiveRoutes {
resp <- Ok(BasicResult(true, "Task submitted")) resp <- Ok(BasicResult(true, "Task submitted"))
} yield resp } yield resp
case POST -> Root / "emptytrash" / "startonce" => case req @ POST -> Root / "emptytrash" / "startonce" =>
for { for {
_ <- backend.collective.startEmptyTrash(user.account.collective) data <- req.as[EmptyTrashSetting]
_ <- backend.collective.startEmptyTrash(
EmptyTrashArgs(user.account.collective, data.minAge)
)
resp <- Ok(BasicResult(true, "Task submitted")) resp <- Ok(BasicResult(true, "Task submitted"))
} yield resp } yield resp

View File

@ -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(UserTaskScope(user.account), task) ut.executeNow(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Submitted successfully.")) .map(Conversions.basicResult(_, "Submitted 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(UserTaskScope(user.account), task) ut.submitNotifyDueItems(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Saved successfully")) .map(Conversions.basicResult(_, "Saved successfully"))
resp <- Ok(res) resp <- Ok(res)
@ -87,7 +87,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.submitNotifyDueItems(UserTaskScope(user.account), task) ut.submitNotifyDueItems(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Saved successfully.")) .map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res) resp <- Ok(res)

View File

@ -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(UserTaskScope(user.account), task) ut.executeNow(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Submitted successfully.")) .map(Conversions.basicResult(_, "Submitted 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(UserTaskScope(user.account), task) ut.submitScanMailbox(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Saved successfully.")) .map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res) resp <- Ok(res)
@ -84,7 +84,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.submitScanMailbox(UserTaskScope(user.account), task) ut.submitScanMailbox(UserTaskScope(user.account), None, task)
.attempt .attempt
.map(Conversions.basicResult(_, "Saved successfully.")) .map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res) resp <- Ok(res)

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE `empty_trash_setting`
ADD COLUMN (`min_age` bigint);
UPDATE `empty_trash_setting`
SET `min_age` = 604800000;
ALTER TABLE `empty_trash_setting`
MODIFY `min_age` bigint NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -34,6 +34,9 @@ trait DoobieMeta extends EmilDoobieMeta {
e.apply(a).noSpaces e.apply(a).noSpaces
) )
implicit val metaDuration: Meta[Duration] =
Meta[Long].imap(Duration.millis)(_.millis)
implicit val metaCollectiveState: Meta[CollectiveState] = implicit val metaCollectiveState: Meta[CollectiveState] =
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString) Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)

View File

@ -54,15 +54,23 @@ object QUserTask {
) )
).query[RPeriodicTask].option.map(_.map(makeUserTask)) ).query[RPeriodicTask].option.map(_.map(makeUserTask))
def insert(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] = def insert(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for { for {
r <- task.toPeriodicTask[ConnectionIO](scope) r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.insert(r) n <- RPeriodicTask.insert(r)
} yield n } yield n
def update(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] = def update(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for { for {
r <- task.toPeriodicTask[ConnectionIO](scope) r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.update(r) n <- RPeriodicTask.update(r)
} yield n } yield n

View File

@ -13,7 +13,6 @@ 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._
@ -75,16 +74,15 @@ object RCollective {
) )
) )
now <- Timestamp.current[ConnectionIO] now <- Timestamp.current[ConnectionIO]
cls = settings.classifier.map(_.toRecord(cid, now)) n2 <- settings.classifier match {
n2 <- cls match { case Some(cls) =>
case Some(cr) => RClassifierSetting.update(cls.toRecord(cid, now))
RClassifierSetting.update(cr)
case None => case None =>
RClassifierSetting.delete(cid) RClassifierSetting.delete(cid)
} }
n3 <- settings.emptyTrash match { n3 <- settings.emptyTrash match {
case Some(trashSchedule) => case Some(trash) =>
REmptyTrashSetting.update(REmptyTrashSetting(cid, trashSchedule, now)) REmptyTrashSetting.update(trash.toRecord(cid, now))
case None => case None =>
REmptyTrashSetting.delete(cid) REmptyTrashSetting.delete(cid)
} }
@ -114,7 +112,8 @@ object RCollective {
cs.itemCount.s, cs.itemCount.s,
cs.categories.s, cs.categories.s,
cs.listType.s, cs.listType.s,
es.schedule.s es.schedule.s,
es.minAge.s
), ),
from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id), from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id),
c.id === coll c.id === coll
@ -168,7 +167,7 @@ object RCollective {
language: Language, language: Language,
integrationEnabled: Boolean, integrationEnabled: Boolean,
classifier: Option[RClassifierSetting.Classifier], classifier: Option[RClassifierSetting.Classifier],
emptyTrash: Option[CalEvent] emptyTrash: Option[REmptyTrashSetting.EmptyTrash]
) )
} }

View File

@ -21,6 +21,7 @@ import doobie.implicits._
final case class REmptyTrashSetting( final case class REmptyTrashSetting(
cid: Ident, cid: Ident,
schedule: CalEvent, schedule: CalEvent,
minAge: Duration,
created: Timestamp created: Timestamp
) )
@ -31,8 +32,9 @@ object REmptyTrashSetting {
val cid = Column[Ident]("cid", this) val cid = Column[Ident]("cid", this)
val schedule = Column[CalEvent]("schedule", this) val schedule = Column[CalEvent]("schedule", this)
val minAge = Column[Duration]("min_age", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val all = NonEmptyList.of[Column[_]](cid, schedule, created) val all = NonEmptyList.of[Column[_]](cid, schedule, minAge, created)
} }
val T = Table(None) val T = Table(None)
@ -43,7 +45,7 @@ object REmptyTrashSetting {
DML.insert( DML.insert(
T, T,
T.all, T.all,
fr"${v.cid},${v.schedule},${v.created}" fr"${v.cid},${v.schedule},${v.minAge},${v.created}"
) )
def update(v: REmptyTrashSetting): ConnectionIO[Int] = def update(v: REmptyTrashSetting): ConnectionIO[Int] =
@ -52,7 +54,8 @@ object REmptyTrashSetting {
T, T,
T.cid === v.cid, T.cid === v.cid,
DML.set( DML.set(
T.schedule.setTo(v.schedule) T.schedule.setTo(v.schedule),
T.minAge.setTo(v.minAge)
) )
) )
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO] n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
@ -64,7 +67,7 @@ object REmptyTrashSetting {
} }
def findForAllCollectives( def findForAllCollectives(
default: CalEvent, default: EmptyTrash,
chunkSize: Int chunkSize: Int
): Stream[ConnectionIO, REmptyTrashSetting] = { ): Stream[ConnectionIO, REmptyTrashSetting] = {
val c = RCollective.as("c") val c = RCollective.as("c")
@ -72,7 +75,8 @@ object REmptyTrashSetting {
val sql = run( val sql = run(
select( select(
c.id.s, c.id.s,
coalesce(e.schedule.s, const(default)).s, coalesce(e.schedule.s, const(default.schedule)).s,
coalesce(e.minAge.s, const(default.minAge)).s,
coalesce(e.created.s, c.created.s).s coalesce(e.created.s, c.created.s).s
), ),
from(c).leftJoin(e, e.cid === c.id) from(c).leftJoin(e, e.cid === c.id)
@ -83,4 +87,13 @@ object REmptyTrashSetting {
def delete(coll: Ident): ConnectionIO[Int] = def delete(coll: Ident): ConnectionIO[Int] =
DML.delete(T, T.cid === coll) DML.delete(T, T.cid === coll)
final case class EmptyTrash(schedule: CalEvent, minAge: Duration) {
def toRecord(coll: Ident, created: Timestamp): REmptyTrashSetting =
REmptyTrashSetting(coll, schedule, minAge, created)
}
object EmptyTrash {
val default = EmptyTrash(EmptyTrashArgs.defaultSchedule, Duration.days(7))
def fromRecord(r: REmptyTrashSetting): EmptyTrash =
EmptyTrash(r.schedule, r.minAge)
}
} }

View File

@ -389,8 +389,16 @@ 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] = def findDeleted(
run(select(T.all), from(T), T.cid === collective && T.state === ItemState.deleted) collective: Ident,
maxUpdated: Timestamp,
chunkSize: Int
): Stream[ConnectionIO, RItem] =
run(
select(T.all),
from(T),
T.cid === collective && T.state === ItemState.deleted && T.updated < maxUpdated
)
.query[RItem] .query[RItem]
.streamWithChunkSize(chunkSize) .streamWithChunkSize(chunkSize)

View File

@ -43,7 +43,8 @@ object UserTask {
.map(a => ut.copy(args = a)) .map(a => ut.copy(args = a))
def toPeriodicTask[F[_]: Sync]( def toPeriodicTask[F[_]: Sync](
scope: UserTaskScope scope: UserTaskScope,
subject: Option[String]
): F[RPeriodicTask] = ): F[RPeriodicTask] =
RPeriodicTask RPeriodicTask
.create[F]( .create[F](
@ -51,7 +52,7 @@ object UserTask {
scope, scope,
ut.name, ut.name,
ut.args, ut.args,
s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}", subject.getOrElse(s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}"),
Priority.Low, Priority.Low,
ut.timer, ut.timer,
ut.summary ut.summary

View File

@ -61,7 +61,9 @@ 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](scope: UserTaskScope, ut: UserTask[A])(implicit E: Encoder[A]): F[Int] def updateTask[A](scope: UserTaskScope, subject: Option[String], 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.
*/ */
@ -92,8 +94,8 @@ trait UserTaskStore[F[_]] {
* 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](scope: UserTaskScope, ut: UserTask[A])(implicit def updateOneTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
E: Encoder[A] implicit 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'.
@ -123,16 +125,16 @@ object UserTaskStore {
case Left(err) => Stream.raiseError[F](new Exception(err)) case Left(err) => Stream.raiseError[F](new Exception(err))
}) })
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit def updateTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
E: Encoder[A] implicit E: Encoder[A]
): F[Int] = { ): F[Int] = {
val exists = QUserTask.exists(ut.id) val exists = QUserTask.exists(ut.id)
val insert = QUserTask.insert(scope, ut.encode) val insert = QUserTask.insert(scope, subject, 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(scope, ut.encode)) store.transact(QUserTask.update(scope, subject, ut.encode))
case AddResult.Failure(ex) => case AddResult.Failure(ex) =>
Async[F].raiseError(ex) Async[F].raiseError(ex)
} }
@ -166,21 +168,25 @@ object UserTaskStore {
case Left(err) => Async[F].raiseError(new Exception(err)) case Left(err) => Async[F].raiseError(new Exception(err))
}) })
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit def updateOneTask[A](
scope: UserTaskScope,
subject: Option[String],
ut: UserTask[A]
)(implicit
E: Encoder[A] E: Encoder[A]
): F[UserTask[String]] = ): F[UserTask[String]] =
getByNameRaw(scope, 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(scope, task)) _ <- store.transact(QUserTask.update(scope, subject, task))
_ <- store.transact( _ <- store.transact(
rest.traverse(t => QUserTask.delete(scope.toAccountId, t.id)) 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(scope, task)).map(_ => task) store.transact(QUserTask.insert(scope, subject, task)).map(_ => task)
} }
def deleteAll(scope: UserTaskScope, name: Ident): F[Int] = def deleteAll(scope: UserTaskScope, name: Ident): F[Int] =

View File

@ -158,6 +158,7 @@ import Api.Model.CustomFieldValue exposing (CustomFieldValue)
import Api.Model.DirectionValue exposing (DirectionValue) import Api.Model.DirectionValue exposing (DirectionValue)
import Api.Model.EmailSettings exposing (EmailSettings) import Api.Model.EmailSettings exposing (EmailSettings)
import Api.Model.EmailSettingsList exposing (EmailSettingsList) import Api.Model.EmailSettingsList exposing (EmailSettingsList)
import Api.Model.EmptyTrashSetting exposing (EmptyTrashSetting)
import Api.Model.Equipment exposing (Equipment) import Api.Model.Equipment exposing (Equipment)
import Api.Model.EquipmentList exposing (EquipmentList) import Api.Model.EquipmentList exposing (EquipmentList)
import Api.Model.FolderDetail exposing (FolderDetail) import Api.Model.FolderDetail exposing (FolderDetail)
@ -999,13 +1000,14 @@ startClassifier flags receive =
startEmptyTrash : startEmptyTrash :
Flags Flags
-> EmptyTrashSetting
-> (Result Http.Error BasicResult -> msg) -> (Result Http.Error BasicResult -> msg)
-> Cmd msg -> Cmd msg
startEmptyTrash flags receive = startEmptyTrash flags setting receive =
Http2.authPost Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/collective/emptytrash/startonce" { url = flags.config.baseUrl ++ "/api/v1/sec/collective/emptytrash/startonce"
, account = getAccount flags , account = getAccount flags
, body = Http.emptyBody , body = Http.jsonBody (Api.Model.EmptyTrashSetting.encode setting)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder , expect = Http.expectJson receive Api.Model.BasicResult.decoder
} }

View File

@ -22,7 +22,6 @@ import Comp.ClassifierSettingsForm
import Comp.Dropdown import Comp.Dropdown
import Comp.EmptyTrashForm 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)
@ -54,11 +53,14 @@ type ClassifierResult
| ClassifierResultSubmitError String | ClassifierResultSubmitError String
| ClassifierResultOk | ClassifierResultOk
type EmptyTrashResult type EmptyTrashResult
= EmptyTrashResultInitial = EmptyTrashResultInitial
| EmptyTrashResultHttpError Http.Error | EmptyTrashResultHttpError Http.Error
| EmptyTrashResultSubmitError String | EmptyTrashResultSubmitError String
| EmptyTrashResultOk | EmptyTrashResultOk
| EmptyTrashResultInvalidForm
type FulltextReindexResult type FulltextReindexResult
= FulltextReindexInitial = FulltextReindexInitial
@ -79,7 +81,7 @@ init flags settings =
Comp.ClassifierSettingsForm.init flags settings.classifier Comp.ClassifierSettingsForm.init flags settings.classifier
( em, ec ) = ( em, ec ) =
Comp.EmptyTrashForm.init flags settings.emptyTrashSchedule Comp.EmptyTrashForm.init flags settings.emptyTrash
in in
( { langModel = ( { langModel =
Comp.Dropdown.makeSingleList Comp.Dropdown.makeSingleList
@ -101,24 +103,21 @@ init flags settings =
getSettings : Model -> Maybe CollectiveSettings getSettings : Model -> Maybe CollectiveSettings
getSettings model = getSettings model =
Maybe.map Maybe.map2
(\cls -> (\cls ->
{ language = \trash ->
Comp.Dropdown.getSelected model.langModel { language =
|> List.head Comp.Dropdown.getSelected model.langModel
|> Maybe.map Data.Language.toIso3 |> List.head
|> Maybe.withDefault model.initSettings.language |> Maybe.map Data.Language.toIso3
, integrationEnabled = model.intEnabled |> Maybe.withDefault model.initSettings.language
, classifier = cls , integrationEnabled = model.intEnabled
, emptyTrashSchedule = , classifier = cls
Comp.EmptyTrashForm.getSettings model.emptyTrashModel , emptyTrash = trash
|> Maybe.withDefault Data.CalEvent.everyMonth }
|> Data.CalEvent.makeEvent
}
)
(Comp.ClassifierSettingsForm.getSettings
model.classifierModel
) )
(Comp.ClassifierSettingsForm.getSettings model.classifierModel)
(Comp.EmptyTrashForm.getSettings model.emptyTrashModel)
type Msg type Msg
@ -233,8 +232,20 @@ update flags msg model =
( model, Api.startClassifier flags StartClassifierResp, Nothing ) ( model, Api.startClassifier flags StartClassifierResp, Nothing )
StartEmptyTrashTask -> StartEmptyTrashTask ->
( model, Api.startEmptyTrash flags StartEmptyTrashResp, Nothing ) case getSettings model of
Just settings ->
( model
, Api.startEmptyTrash flags
settings.emptyTrash
StartEmptyTrashResp
, Nothing
)
Nothing ->
( { model | startEmptyTrashResult = EmptyTrashResultInvalidForm }
, Cmd.none
, Nothing
)
StartClassifierResp (Ok br) -> StartClassifierResp (Ok br) ->
( { model ( { model
@ -275,6 +286,7 @@ update flags msg model =
) )
--- View2 --- View2
@ -471,6 +483,7 @@ renderClassifierResultMessage texts result =
, ( S.successMessage, isSuccess ) , ( S.successMessage, isSuccess )
, ( "hidden", result == ClassifierResultInitial ) , ( "hidden", result == ClassifierResultInitial )
] ]
, class "ml-2"
] ]
[ case result of [ case result of
ClassifierResultInitial -> ClassifierResultInitial ->
@ -505,6 +518,7 @@ renderFulltextReindexResultMessage texts result =
FulltextReindexSubmitError m -> FulltextReindexSubmitError m ->
text m text m
renderEmptyTrashResultMessage : Texts -> EmptyTrashResult -> Html msg renderEmptyTrashResultMessage : Texts -> EmptyTrashResult -> Html msg
renderEmptyTrashResultMessage texts result = renderEmptyTrashResultMessage texts result =
let let
@ -525,6 +539,7 @@ renderEmptyTrashResultMessage texts result =
, ( S.successMessage, isSuccess ) , ( S.successMessage, isSuccess )
, ( "hidden", result == EmptyTrashResultInitial ) , ( "hidden", result == EmptyTrashResultInitial )
] ]
, class "ml-2"
] ]
[ case result of [ case result of
EmptyTrashResultInitial -> EmptyTrashResultInitial ->
@ -538,4 +553,7 @@ renderEmptyTrashResultMessage texts result =
EmptyTrashResultSubmitError m -> EmptyTrashResultSubmitError m ->
text m text m
EmptyTrashResultInvalidForm ->
text texts.emptyTrashStartInvalidForm
] ]

View File

@ -14,40 +14,36 @@ module Comp.EmptyTrashForm exposing
, view , view
) )
import Api import Api.Model.EmptyTrashSetting exposing (EmptyTrashSetting)
import Comp.CalEventInput import Comp.CalEventInput
import Comp.Dropdown
import Comp.FixedDropdown
import Comp.IntField import Comp.IntField
import Data.CalEvent exposing (CalEvent) import Data.CalEvent exposing (CalEvent)
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ListType exposing (ListType)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Http
import Markdown
import Messages.Comp.EmptyTrashForm exposing (Texts) import Messages.Comp.EmptyTrashForm exposing (Texts)
import Styles as S import Styles as S
import Util.Tag
type alias Model = type alias Model =
{ scheduleModel : Comp.CalEventInput.Model { scheduleModel : Comp.CalEventInput.Model
, schedule : Maybe CalEvent , schedule : Maybe CalEvent
, minAgeModel : Comp.IntField.Model
, minAgeDays : Maybe Int
} }
type Msg type Msg
= ScheduleMsg Comp.CalEventInput.Msg = ScheduleMsg Comp.CalEventInput.Msg
| MinAgeMsg Comp.IntField.Msg
init : Flags -> String -> ( Model, Cmd Msg ) init : Flags -> EmptyTrashSetting -> ( Model, Cmd Msg )
init flags schedule = init flags settings =
let let
newSchedule = newSchedule =
Data.CalEvent.fromEvent schedule Data.CalEvent.fromEvent settings.schedule
|> Maybe.withDefault Data.CalEvent.everyMonth |> Maybe.withDefault Data.CalEvent.everyMonth
( cem, cec ) = ( cem, cec ) =
@ -55,14 +51,34 @@ init flags schedule =
in in
( { scheduleModel = cem ( { scheduleModel = cem
, schedule = Just newSchedule , schedule = Just newSchedule
, minAgeModel = Comp.IntField.init (Just 0) Nothing False
, minAgeDays = Just <| millisToDays settings.minAge
} }
, Cmd.map ScheduleMsg cec , Cmd.map ScheduleMsg cec
) )
getSettings : Model -> Maybe CalEvent millisToDays : Int -> Int
millisToDays millis =
round <| toFloat millis / 1000 / 60 / 60 / 24
daysToMillis : Int -> Int
daysToMillis days =
days * 24 * 60 * 60 * 1000
getSettings : Model -> Maybe EmptyTrashSetting
getSettings model = getSettings model =
model.schedule Maybe.map2
(\sch ->
\age ->
{ schedule = Data.CalEvent.makeEvent sch
, minAge = daysToMillis age
}
)
model.schedule
model.minAgeDays
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
@ -84,6 +100,18 @@ update flags msg model =
, Cmd.map ScheduleMsg cc , Cmd.map ScheduleMsg cc
) )
MinAgeMsg lmsg ->
let
( mm, newAge ) =
Comp.IntField.update lmsg model.minAgeModel
in
( { model
| minAgeModel = mm
, minAgeDays = newAge
}
, Cmd.none
)
--- View2 --- View2
@ -103,4 +131,16 @@ view texts _ model =
model.scheduleModel model.scheduleModel
) )
] ]
, div [ class "mb-4" ]
[ let
settings : Comp.IntField.ViewSettings
settings =
{ number = model.minAgeDays
, label = texts.minAge
, classes = ""
, info = texts.minAgeInfo
}
in
Html.map MinAgeMsg (Comp.IntField.view settings model.minAgeModel)
]
] ]

View File

@ -47,13 +47,13 @@ init min max opt =
tooLow : Model -> Int -> Bool tooLow : Model -> Int -> Bool
tooLow model n = tooLow model n =
Maybe.map ((<) n) model.min Maybe.map ((<) n) model.min
|> Maybe.withDefault (not model.optional) |> Maybe.withDefault False
tooHigh : Model -> Int -> Bool tooHigh : Model -> Int -> Bool
tooHigh model n = tooHigh model n =
Maybe.map ((>) n) model.max Maybe.map ((>) n) model.max
|> Maybe.withDefault (not model.optional) |> Maybe.withDefault False
update : Msg -> Model -> ( Model, Maybe Int ) update : Msg -> Model -> ( Model, Maybe Int )

View File

@ -40,6 +40,7 @@ type alias Texts =
, languageLabel : Language -> String , languageLabel : Language -> String
, classifierTaskStarted : String , classifierTaskStarted : String
, emptyTrashTaskStarted : String , emptyTrashTaskStarted : String
, emptyTrashStartInvalidForm : String
, fulltextReindexSubmitted : String , fulltextReindexSubmitted : String
, fulltextReindexOkMissing : String , fulltextReindexOkMissing : String
, emptyTrash : String , emptyTrash : String
@ -71,6 +72,7 @@ gb =
, languageLabel = Messages.Data.Language.gb , languageLabel = Messages.Data.Language.gb
, classifierTaskStarted = "Classifier task started." , classifierTaskStarted = "Classifier task started."
, emptyTrashTaskStarted = "Empty trash task started." , emptyTrashTaskStarted = "Empty trash task started."
, emptyTrashStartInvalidForm = "The empty-trash form contains errors."
, 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."
@ -103,6 +105,7 @@ de =
, languageLabel = Messages.Data.Language.de , languageLabel = Messages.Data.Language.de
, classifierTaskStarted = "Kategorisierung gestartet." , classifierTaskStarted = "Kategorisierung gestartet."
, emptyTrashTaskStarted = "Papierkorb löschen gestartet." , emptyTrashTaskStarted = "Papierkorb löschen gestartet."
, emptyTrashStartInvalidForm = "Das Papierkorb-Löschen Formular ist fehlerhaft!"
, 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."

View File

@ -19,6 +19,8 @@ type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, calEventInput : Messages.Comp.CalEventInput.Texts , calEventInput : Messages.Comp.CalEventInput.Texts
, schedule : String , schedule : String
, minAge : String
, minAgeInfo : String
} }
@ -27,6 +29,8 @@ gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, calEventInput = Messages.Comp.CalEventInput.gb , calEventInput = Messages.Comp.CalEventInput.gb
, schedule = "Schedule" , schedule = "Schedule"
, minAge = "Minimum Age (Days)"
, minAgeInfo = "The minimum age in days of an items to be removed. The last-update time is used."
} }
@ -35,4 +39,6 @@ de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, calEventInput = Messages.Comp.CalEventInput.de , calEventInput = Messages.Comp.CalEventInput.de
, schedule = "Zeitplan" , schedule = "Zeitplan"
, minAge = "Mindestalter (Tage)"
, minAgeInfo = "Das Mindestalter (in Tagen) der Dokumente, die gelöscht werden. Es wird das Datum der letzten Veränderung verwendet."
} }

View File

@ -16,6 +16,7 @@ import Comp.SearchMenu
import Comp.SearchStatsView import Comp.SearchStatsView
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemSelection import Data.ItemSelection
import Data.SearchMode
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -78,6 +79,7 @@ confirmModal texts model =
texts.basics.yes texts.basics.yes
texts.basics.no texts.basics.no
texts.reallyDeleteQuestion texts.reallyDeleteQuestion
ConfirmRestore -> ConfirmRestore ->
Comp.ConfirmModal.defaultSettings Comp.ConfirmModal.defaultSettings
RestoreSelectedConfirmed RestoreSelectedConfirmed
@ -85,7 +87,6 @@ confirmModal texts model =
texts.basics.yes texts.basics.yes
texts.basics.no texts.basics.no
texts.reallyRestoreQuestion texts.reallyRestoreQuestion
in in
case model.viewMode of case model.viewMode of
SelectView svm -> SelectView svm ->
@ -270,6 +271,7 @@ editMenuBar texts model svm =
, inputClass = , inputClass =
[ ( btnStyle, True ) [ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == DeleteSelected ) , ( "bg-gray-200 dark:bg-bluegray-600", svm.action == DeleteSelected )
, ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Trashed )
] ]
} }
, MB.CustomButton , MB.CustomButton
@ -280,6 +282,7 @@ editMenuBar texts model svm =
, inputClass = , inputClass =
[ ( btnStyle, True ) [ ( btnStyle, True )
, ( "bg-gray-200 dark:bg-bluegray-600", svm.action == RestoreSelected ) , ( "bg-gray-200 dark:bg-bluegray-600", svm.action == RestoreSelected )
, ( "hidden", model.searchMenuModel.searchMode == Data.SearchMode.Normal )
] ]
} }
] ]