diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala index 1bee773b..d7417fd9 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OCollective.scala @@ -150,6 +150,7 @@ object OCollective { LearnClassifierArgs.taskName, on, timer, + None, LearnClassifierArgs(coll) ) _ <- uts.updateOneTask(AccountId(coll, LearnClassifierArgs.taskName), ut) @@ -164,6 +165,7 @@ object OCollective { LearnClassifierArgs.taskName, true, CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All), + None, LearnClassifierArgs(collective) ).encode.toPeriodicTask(AccountId(collective, LearnClassifierArgs.taskName)) job <- ut.toJob diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala index 0dd2348c..73b9a015 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OFulltext.scala @@ -1,5 +1,6 @@ package docspell.backend.ops +import cats.data.NonEmptyList import cats.effect._ import cats.implicits._ import fs2.Stream @@ -9,6 +10,8 @@ import docspell.backend.ops.OItemSearch._ import docspell.common._ import docspell.common.syntax.all._ import docspell.ftsclient._ +import docspell.query.ItemQuery._ +import docspell.query.ItemQueryDsl._ import docspell.store.queries.{QFolder, QItem, SelectedItem} import docspell.store.queue.JobQueue import docspell.store.records.RJob @@ -125,12 +128,18 @@ object OFulltext { .map(_.minBy(-_.score)) .map(r => SelectedItem(r.itemId, r.score)) .toSet + now <- Timestamp.current[F] itemsWithTags <- store .transact( QItem.findItemsWithTags( account.collective, - QItem.findSelectedItems(Query.empty(account), maxNoteLen, select) + QItem.findSelectedItems( + Query.all(account), + now.toUtcDate, + maxNoteLen, + select + ) ) ) .take(batch.limit.toLong) @@ -165,7 +174,13 @@ object OFulltext { .flatMap(r => Stream.emits(r.results.map(_.itemId))) .compile .to(Set) - q = Query.empty(account).withFix(_.copy(itemIds = itemIds.some)) + itemIdsQuery = NonEmptyList + .fromList(itemIds.toList) + .map(ids => Attr.ItemId.in(ids.map(_.id))) + .getOrElse(Attr.ItemId.notExists) + q = Query + .all(account) + .withFix(_.copy(query = itemIdsQuery.some)) res <- store.transact(QItem.searchStats(now.toUtcDate)(q)) } yield res } @@ -221,7 +236,11 @@ object OFulltext { .flatMap(r => Stream.emits(r.results.map(_.itemId))) .compile .to(Set) - qnext = q.withFix(_.copy(itemIds = items.some)) + itemIdsQuery = NonEmptyList + .fromList(items.toList) + .map(ids => Attr.ItemId.in(ids.map(_.id))) + .getOrElse(Attr.ItemId.notExists) + qnext = q.withFix(_.copy(query = itemIdsQuery.some)) now <- Timestamp.current[F] res <- store.transact(QItem.searchStats(now.toUtcDate)(qnext)) } yield res diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala index 1c5e54df..7ca0337e 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OSimpleSearch.scala @@ -129,7 +129,7 @@ object OSimpleSearch { def makeQuery(iq: ItemQuery): F[StringSearchResult[Items]] = iq.findFulltext match { case FulltextExtract.Result.Success(expr, ftq) => - search(settings)(Query(fix, Query.QueryExpr(iq.copy(expr = expr))), ftq) + search(settings)(Query(fix, Query.QueryExpr(expr.some)), ftq) .map(StringSearchResult.Success.apply) case other: FulltextExtract.FailureResult => StringSearchResult.fulltextMismatch[Items](other).pure[F] @@ -152,7 +152,7 @@ object OSimpleSearch { def makeQuery(iq: ItemQuery): F[StringSearchResult[SearchSummary]] = iq.findFulltext match { case FulltextExtract.Result.Success(expr, ftq) => - searchSummary(useFTS)(Query(fix, Query.QueryExpr(iq.copy(expr = expr))), ftq) + searchSummary(useFTS)(Query(fix, Query.QueryExpr(expr.some)), ftq) .map(StringSearchResult.Success.apply) case other: FulltextExtract.FailureResult => StringSearchResult.fulltextMismatch[SearchSummary](other).pure[F] diff --git a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala index 1728045d..1670ea1b 100644 --- a/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/hk/HouseKeepingTask.scala @@ -36,7 +36,8 @@ object HouseKeepingTask { "Docspell house-keeping", DocspellSystem.taskGroup, Priority.Low, - ce + ce, + None ) .map(_.copy(id = periodicId)) } diff --git a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala index 4ce26507..391fadd5 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala @@ -1,6 +1,6 @@ package docspell.joex.notify -import cats.data.OptionT +import cats.data.{NonEmptyList => Nel, OptionT} import cats.effect._ import cats.implicits._ @@ -8,6 +8,9 @@ import docspell.backend.ops.OItemSearch.{Batch, ListItem, Query} import docspell.common._ import docspell.joex.mail.EmilHeader import docspell.joex.scheduler.{Context, Task} +import docspell.query.Date +import docspell.query.ItemQuery._ +import docspell.query.ItemQueryDsl._ import docspell.store.queries.QItem import docspell.store.records._ @@ -69,18 +72,24 @@ object NotifyDueItemsTask { def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[ListItem]] = for { now <- Timestamp.current[F] + rightDate = Date((now + Duration.days(ctx.args.remindDays.toLong)).toMillis) q = Query - .empty(ctx.args.account) + .all(ctx.args.account) .withOrder(orderAsc = _.dueDate) + .withFix(_.copy(query = Expr.ValidItemStates.some)) .withCond(_ => - Query.QueryForm.empty.copy( - states = ItemState.validStates.toList, - tagsInclude = ctx.args.tagsInclude, - tagsExclude = ctx.args.tagsExclude, - dueDateFrom = - ctx.args.daysBack.map(back => now - Duration.days(back.toLong)), - dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong)) + Query.QueryExpr( + Attr.DueDate <= rightDate &&? + ctx.args.daysBack.map(back => + Attr.DueDate >= Date((now - Duration.days(back.toLong)).toMillis) + ) &&? + Nel + .fromList(ctx.args.tagsInclude) + .map(ids => Q.tagIdsEq(ids.map(_.id))) &&? + Nel + .fromList(ctx.args.tagsExclude) + .map(ids => Q.tagIdsIn(ids.map(_.id)).negate) ) ) res <- diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index e2b7ef06..2bd1f410 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -119,6 +119,8 @@ object ItemQuery { final case class ChecksumMatch(checksum: String) extends Expr final case class AttachId(id: String) extends Expr + case object ValidItemStates extends Expr + // things that can be expressed with terms above sealed trait MacroExpr extends Expr { def body: Expr diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQueryDsl.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQueryDsl.scala new file mode 100644 index 00000000..a016de90 --- /dev/null +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQueryDsl.scala @@ -0,0 +1,76 @@ +package docspell.query + +import cats.data.NonEmptyList + +import docspell.query.ItemQuery._ +import docspell.query.internal.ExprUtil + +object ItemQueryDsl { + + implicit final class StringAttrDsl(val attr: Attr.StringAttr) extends AnyVal { + def ===(value: String): Expr = + Expr.SimpleExpr(Operator.Eq, Property(attr, value)) + + def <=(value: String): Expr = + Expr.SimpleExpr(Operator.Lte, Property(attr, value)) + def >=(value: String): Expr = + Expr.SimpleExpr(Operator.Gte, Property(attr, value)) + + def <(value: String): Expr = + Expr.SimpleExpr(Operator.Lt, Property(attr, value)) + def >(value: String): Expr = + Expr.SimpleExpr(Operator.Gt, Property(attr, value)) + + def in(values: NonEmptyList[String]): Expr = + Expr.InExpr(attr, values) + + def exists: Expr = + Expr.Exists(attr) + + def notExists: Expr = + Expr.NotExpr(exists) + } + + implicit final class DateAttrDsl(val attr: Attr.DateAttr) extends AnyVal { + def <=(value: Date): Expr = + Expr.SimpleExpr(Operator.Lte, Property(attr, value)) + + def >=(value: Date): Expr = + Expr.SimpleExpr(Operator.Gte, Property(attr, value)) + } + + implicit final class ExprDsl(val expr: Expr) extends AnyVal { + def &&(other: Expr): Expr = + ExprUtil.reduce(Expr.and(expr, other)) + + def ||(other: Expr): Expr = + ExprUtil.reduce(Expr.or(expr, other)) + + def &&?(other: Option[Expr]): Expr = + other.map(e => &&(e)).getOrElse(expr) + + def ||?(other: Option[Expr]): Expr = + other.map(e => ||(e)).getOrElse(expr) + + def negate: Expr = + ExprUtil.reduce(Expr.NotExpr(expr)) + + def unary_! : Expr = + negate + } + + object Q { + def tagIdsIn(values: NonEmptyList[String]): Expr = + Expr.TagIdsMatch(TagOperator.AnyMatch, values) + + def tagIdsEq(values: NonEmptyList[String]): Expr = + Expr.TagIdsMatch(TagOperator.AllMatch, values) + + def tagsIn(values: NonEmptyList[String]): Expr = + Expr.TagsMatch(TagOperator.AnyMatch, values) + + def tagsEq(values: NonEmptyList[String]): Expr = + Expr.TagsMatch(TagOperator.AllMatch, values) + + } +} diff --git a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala index df81983f..8f7b6c2c 100644 --- a/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala +++ b/modules/query/shared/src/main/scala/docspell/query/internal/ExprUtil.scala @@ -69,6 +69,9 @@ object ExprUtil { expr case AttachId(_) => expr + + case ValidItemStates => + expr } private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] = diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 3b18422c..d579b278 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1309,73 +1309,6 @@ paths: schema: $ref: "#/components/schemas/BasicResult" - - /sec/item/searchForm: - post: - tags: [ Item Search ] - summary: Search for items. - deprecated: true - description: | - Search for items given a search form. The results are grouped - by month and are sorted by item date (newest first). Tags and - attachments are *not* resolved. The results will always - contain an empty list for item tags and attachments. Use - `/searchFormWithTags` to also retrieve all tags and a list of - attachments of an item. - - The `fulltext` field can be used to restrict the results by - using full-text search in the documents contents. - - The customfields used in the search query are allowed to be - specified by either field id or field name. The values may - contain the wildcard `*` at beginning or end. - - **NOTE** This is deprecated in favor for using a search query. - security: - - authTokenHeader: [] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ItemSearch" - responses: - 200: - description: Ok - content: - application/json: - schema: - $ref: "#/components/schemas/ItemLightList" - /sec/item/searchFormWithTags: - post: - tags: [ Item Search ] - summary: Search for items. - deprecated: true - description: | - Search for items given a search form. The results are grouped - by month by default. For each item, its tags and attachments - are also returned. This uses more queries and is therefore - slower, but returns all tags to an item as well as their - attachments with some minor details. - - The `fulltext` field can be used to restrict the results by - using full-text search in the documents contents. - - **NOTE** This is deprecated in favor for using search query. - security: - - authTokenHeader: [] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ItemSearch" - responses: - 200: - description: Ok - content: - application/json: - schema: - $ref: "#/components/schemas/ItemLightList" - /sec/item/search: get: tags: [ Item Search ] @@ -1457,29 +1390,6 @@ paths: schema: $ref: "#/components/schemas/ItemLightList" - /sec/item/searchFormStats: - post: - tags: [ Item Search ] - summary: Get basic statistics about the data of a search. - deprecated: true - description: | - Takes a search query and returns a summary about the results. - - **NOTE** This is deprecated in favor of using a search query. - security: - - authTokenHeader: [] - requestBody: - content: - application/json: - schema: - $ref: "#/components/schemas/ItemSearch" - responses: - 200: - description: Ok - content: - application/json: - schema: - $ref: "#/components/schemas/SearchStats" /sec/item/searchStats: post: tags: [ Item Search ] @@ -3943,6 +3853,8 @@ components: format: ident enabled: type: boolean + summary: + type: string imapConnection: type: string format: ident @@ -4102,6 +4014,8 @@ components: format: ident enabled: type: boolean + summary: + type: string smtpConnection: type: string format: ident @@ -5268,104 +5182,6 @@ components: type: array items: $ref: "#/components/schemas/ItemLight" - ItemSearch: - description: | - A structure for a search form. - required: - - tagsInclude - - tagsExclude - - tagCategoriesInclude - - tagCategoriesExclude - - inbox - - offset - - limit - - customValues - properties: - tagsInclude: - type: array - items: - type: string - format: ident - tagsExclude: - type: array - items: - type: string - format: ident - tagCategoriesInclude: - type: array - items: - type: string - tagCategoriesExclude: - type: array - items: - type: string - inbox: - type: boolean - offset: - type: integer - format: int32 - limit: - type: integer - format: int32 - description: | - The maximum number of results to return. Note that this - limit is a soft limit, there is some hard limit on the - server, too. - direction: - type: string - format: direction - enum: - - incoming - - outgoing - name: - type: string - description: | - Search in item names. - allNames: - type: string - description: | - Search in item names, correspondents, concerned entities - and notes. - fullText: - type: string - description: | - A query searching the contents of documents. If only this - field is set, then a fulltext-only search is done. - corrOrg: - type: string - format: ident - corrPerson: - type: string - format: ident - concPerson: - type: string - format: ident - concEquip: - type: string - format: ident - folder: - type: string - format: ident - dateFrom: - type: integer - format: date-time - dateUntil: - type: integer - format: date-time - dueDateFrom: - type: integer - format: date-time - dueDateUntil: - type: integer - format: date-time - itemSubset: - $ref: "#/components/schemas/IdList" - customValues: - type: array - items: - $ref: "#/components/schemas/CustomFieldValue" - source: - type: string ItemLight: description: | An item with only a few important properties. diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index b826488c..ed4d5f0e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -143,36 +143,6 @@ trait Conversions { // item list - def mkQuery(m: ItemSearch, account: AccountId): OItemSearch.Query = - OItemSearch.Query( - OItemSearch.Query.Fix(account, None, None), - OItemSearch.Query.QueryForm( - m.name, - if (m.inbox) Seq(ItemState.Created) - else ItemState.validStates.toList, - m.direction, - m.corrPerson, - m.corrOrg, - m.concPerson, - m.concEquip, - m.folder, - m.tagsInclude.map(Ident.unsafe), - m.tagsExclude.map(Ident.unsafe), - m.tagCategoriesInclude, - m.tagCategoriesExclude, - m.dateFrom, - m.dateUntil, - m.dueDateFrom, - m.dueDateUntil, - m.allNames, - m.itemSubset - .map(_.ids.flatMap(i => Ident.fromString(i).toOption).toSet) - .filter(_.nonEmpty), - m.customValues.map(mkCustomValue), - m.source - ) - ) - def mkCustomValue(v: CustomFieldValue): OItemSearch.CustomValue = OItemSearch.CustomValue(v.field, v.value) diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 2e7fbbb5..ea38bf70 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -1,6 +1,5 @@ package docspell.restserver.routes -import cats.Monoid import cats.data.NonEmptyList import cats.effect._ import cats.implicits._ @@ -16,6 +15,7 @@ import docspell.common._ import docspell.common.syntax.all._ import docspell.query.FulltextExtract.Result.TooMany import docspell.query.FulltextExtract.Result.UnsupportedPosition +import docspell.query.ItemQuery.Expr import docspell.restapi.model._ import docspell.restserver.Config import docspell.restserver.conv.Conversions @@ -62,12 +62,12 @@ object ItemRoutes { detailFlag.getOrElse(false), cfg.maxNoteLength ) - val fixQuery = Query.Fix(user.account, None, None) + val fixQuery = Query.Fix(user.account, Some(Expr.ValidItemStates), None) searchItems(backend, dsl)(settings, fixQuery, itemQuery) case GET -> Root / "searchStats" :? QP.Query(q) => val itemQuery = ItemQueryString(q) - val fixQuery = Query.Fix(user.account, None, None) + val fixQuery = Query.Fix(user.account, Some(Expr.ValidItemStates), None) searchItemStats(backend, dsl)(cfg.fullTextSearch.enabled, fixQuery, itemQuery) case req @ POST -> Root / "search" => @@ -86,7 +86,7 @@ object ItemRoutes { userQuery.withDetails.getOrElse(false), cfg.maxNoteLength ) - fixQuery = Query.Fix(user.account, None, None) + fixQuery = Query.Fix(user.account, Some(Expr.ValidItemStates), None) resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery) } yield resp @@ -94,7 +94,7 @@ object ItemRoutes { for { userQuery <- req.as[ItemQuery] itemQuery = ItemQueryString(userQuery.query) - fixQuery = Query.Fix(user.account, None, None) + fixQuery = Query.Fix(user.account, Some(Expr.ValidItemStates), None) resp <- searchItemStats(backend, dsl)( cfg.fullTextSearch.enabled, fixQuery, @@ -102,85 +102,6 @@ object ItemRoutes { ) } yield resp - //DEPRECATED - case req @ POST -> Root / "searchForm" => - for { - mask <- req.as[ItemSearch] - _ <- logger.ftrace(s"Got search mask: $mask") - query = Conversions.mkQuery(mask, user.account) - _ <- logger.ftrace(s"Running query: $query") - resp <- mask match { - case SearchFulltextOnly(ftq) if cfg.fullTextSearch.enabled => - val ftsIn = OFulltext.FtsInput(ftq.query) - for { - items <- backend.fulltext.findIndexOnly(cfg.maxNoteLength)( - ftsIn, - user.account, - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemListWithTagsFtsPlain(items)) - } yield ok - - case SearchWithFulltext(fq) if cfg.fullTextSearch.enabled => - for { - items <- backend.fulltext.findItems(cfg.maxNoteLength)( - query, - OFulltext.FtsInput(fq), - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemListFts(items)) - } yield ok - - case _ => - for { - items <- backend.itemSearch.findItems(cfg.maxNoteLength)( - query, - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemList(items)) - } yield ok - } - } yield resp - - //DEPRECATED - case req @ POST -> Root / "searchFormWithTags" => - for { - mask <- req.as[ItemSearch] - _ <- logger.ftrace(s"Got search mask: $mask") - query = Conversions.mkQuery(mask, user.account) - _ <- logger.ftrace(s"Running query: $query") - resp <- mask match { - case SearchFulltextOnly(ftq) if cfg.fullTextSearch.enabled => - val ftsIn = OFulltext.FtsInput(ftq.query) - for { - items <- backend.fulltext.findIndexOnly(cfg.maxNoteLength)( - ftsIn, - user.account, - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemListWithTagsFtsPlain(items)) - } yield ok - - case SearchWithFulltext(fq) if cfg.fullTextSearch.enabled => - for { - items <- backend.fulltext.findItemsWithTags(cfg.maxNoteLength)( - query, - OFulltext.FtsInput(fq), - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemListWithTagsFts(items)) - } yield ok - case _ => - for { - items <- backend.itemSearch.findItemsWithTags(cfg.maxNoteLength)( - query, - Batch(mask.offset, mask.limit).restrictLimitTo(cfg.maxItemPageSize) - ) - ok <- Ok(Conversions.mkItemListWithTags(items)) - } yield ok - } - } yield resp - case req @ POST -> Root / "searchIndex" => for { mask <- req.as[ItemQuery] @@ -204,26 +125,6 @@ object ItemRoutes { } } yield resp - //DEPRECATED - case req @ POST -> Root / "searchFormStats" => - for { - mask <- req.as[ItemSearch] - query = Conversions.mkQuery(mask, user.account) - stats <- mask match { - case SearchFulltextOnly(ftq) if cfg.fullTextSearch.enabled => - logger.finfo(s"Make index only summary: $ftq") *> - backend.fulltext.findIndexOnlySummary( - user.account, - OFulltext.FtsInput(ftq.query) - ) - case SearchWithFulltext(fq) if cfg.fullTextSearch.enabled => - backend.fulltext.findItemsSummary(query, OFulltext.FtsInput(fq)) - case _ => - backend.itemSearch.findItemsSummary(query) - } - resp <- Ok(Conversions.mkSearchStats(stats)) - } yield resp - case GET -> Root / Ident(id) => for { item <- backend.itemSearch.findItem(id, user.account.collective) @@ -560,40 +461,4 @@ object ItemRoutes { def notEmpty: Option[String] = opt.map(_.trim).filter(_.nonEmpty) } - - object SearchFulltextOnly { - implicit private val identMonoid: Monoid[Ident] = - Monoid.instance(Ident.unsafe(""), _ / _) - - implicit private val timestampMonoid: Monoid[Timestamp] = - Monoid.instance(Timestamp.Epoch, (a, _) => a) - - implicit private val directionMonoid: Monoid[Direction] = - Monoid.instance(Direction.Incoming, (a, _) => a) - - implicit private val idListMonoid: Monoid[IdList] = - Monoid.instance(IdList(Nil), (a, b) => IdList(a.ids ++ b.ids)) - - implicit private val boolMonoid: Monoid[Boolean] = - Monoid.instance(false, _ || _) - - private val itemSearchMonoid: Monoid[ItemSearch] = - cats.derived.semiauto.monoid - - def unapply(m: ItemSearch): Option[ItemQuery] = - m.fullText match { - case Some(fq) => - val me = m.copy(fullText = None, offset = 0, limit = 0) - if (itemSearchMonoid.empty == me) - Some(ItemQuery(m.offset.some, m.limit.some, Some(false), fq)) - else None - case _ => - None - } - } - - object SearchWithFulltext { - def unapply(m: ItemSearch): Option[String] = - m.fullText - } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala index 240dcc43..720634f1 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala @@ -112,6 +112,7 @@ object NotifyDueItemsRoutes { NotifyDueItemsArgs.taskName, settings.enabled, settings.schedule, + settings.summary, NotifyDueItemsArgs( user, settings.smtpConnection, @@ -144,6 +145,7 @@ object NotifyDueItemsRoutes { } yield NotificationSettings( task.id, task.enabled, + task.summary, conn.getOrElse(Ident.unsafe("")), task.args.recipients, task.timer, diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala index 1eec5f2a..3ac87316 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ScanMailboxRoutes.scala @@ -105,6 +105,7 @@ object ScanMailboxRoutes { ScanMailboxArgs.taskName, settings.enabled, settings.schedule, + settings.summary, ScanMailboxArgs( user, settings.imapConnection, @@ -139,6 +140,7 @@ object ScanMailboxRoutes { } yield ScanMailboxSettings( task.id, task.enabled, + task.summary, conn.getOrElse(Ident.unsafe("")), task.args.folders, task.timer, diff --git a/modules/store/src/main/resources/db/migration/h2/V1.22.0__add_task_name.sql b/modules/store/src/main/resources/db/migration/h2/V1.22.0__add_task_name.sql new file mode 100644 index 00000000..7dfdc2f4 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.22.0__add_task_name.sql @@ -0,0 +1,2 @@ +ALTER TABLE "periodic_task" +ADD COLUMN "summary" varchar(254); diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.22.0__add_task_name.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.22.0__add_task_name.sql new file mode 100644 index 00000000..0fcd7f09 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.22.0__add_task_name.sql @@ -0,0 +1,2 @@ +ALTER TABLE `periodic_task` +ADD COLUMN `summary` varchar(254); diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.22.0__add_task_name.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.22.0__add_task_name.sql new file mode 100644 index 00000000..7dfdc2f4 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.22.0__add_task_name.sql @@ -0,0 +1,2 @@ +ALTER TABLE "periodic_task" +ADD COLUMN "summary" varchar(254); diff --git a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala index 6a721270..203c4a0d 100644 --- a/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala +++ b/modules/store/src/main/scala/docspell/store/qb/generator/ItemQueryGenerator.scala @@ -117,6 +117,9 @@ object ItemQueryGenerator { if (flag) tables.item.state === ItemState.created else tables.item.state === ItemState.confirmed + case Expr.ValidItemStates => + tables.item.state.in(ItemState.validStates) + case Expr.TagIdsMatch(op, tags) => val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption) Nel diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index c1ee5f2c..637a6890 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -98,30 +98,7 @@ object QItem { cv.itemId === itemId ).build.query[ItemFieldValue].to[Vector] - private def findCustomFieldValuesForColl( - coll: Ident, - values: Seq[CustomValue] - ): Option[Select] = { - val cf = RCustomField.as("cf") - val cv = RCustomFieldValue.as("cv") - - def singleSelect(v: CustomValue) = - Select( - cv.itemId.s, - from(cv).innerJoin(cf, cv.field === cf.id), - where( - cf.cid === coll && - (cf.name === v.field || cf.id === v.field) && - cv.value.like(QueryWildcard(v.value.toLowerCase)) - ) - ) - - Nel - .fromList(values.toList) - .map(nel => intersect(nel.map(singleSelect))) - } - - private def findItemsBase(q: Query.Fix, noteMaxLen: Int): Select = { + private def findItemsBase(q: Query.Fix, today: LocalDate, noteMaxLen: Int): Select = { val attachs = AttachCountTable("cta") val coll = q.account.collective @@ -169,9 +146,7 @@ object QItem { .leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll) .leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll), where( - i.cid === coll &&? q.itemIds.map(s => - Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull) - ) + i.cid === coll &&? q.query.map(qs => queryCondFromExpr(today, coll, qs)) && or( i.folder.isNull, i.folder.in(QFolder.findMemberFolderIds(q.account)) @@ -184,54 +159,17 @@ object QItem { ) } - def queryCondFromForm(coll: Ident, q: Query.QueryForm): Condition = - Condition.unit &&? - q.direction.map(d => i.incoming === d) &&? - q.name.map(n => i.name.like(QueryWildcard.lower(n))) &&? - Nel.fromList(q.states.toList).map(nel => i.state.in(nel)) &&? - q.allNames - .map(QueryWildcard.lower) - .map(n => - org.name.like(n) || - pers0.name.like(n) || - pers1.name.like(n) || - equip.name.like(n) || - i.name.like(n) || - i.notes.like(n) - ) &&? - q.corrPerson.map(p => pers0.pid === p) &&? - q.corrOrg.map(o => org.oid === o) &&? - q.concPerson.map(p => pers1.pid === p) &&? - q.concEquip.map(e => equip.eid === e) &&? - q.folder.map(fid => f.id === fid) &&? - q.dateFrom.map(d => coalesce(i.itemDate.s, i.created.s) >= d) &&? - q.dateTo.map(d => coalesce(i.itemDate.s, i.created.s) <= d) &&? - q.dueDateFrom.map(d => i.dueDate > d) &&? - q.dueDateTo.map(d => i.dueDate < d) &&? - q.source.map(n => i.source.like(QueryWildcard.lower(n))) &&? - q.itemIds.map(s => - Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull) - ) &&? - TagItemName - .itemsWithAllTagAndCategory(q.tagsInclude, q.tagCategoryIncl) - .map(subsel => i.id.in(subsel)) &&? - TagItemName - .itemsWithEitherTagOrCategory(q.tagsExclude, q.tagCategoryExcl) - .map(subsel => i.id.notIn(subsel)) &&? - findCustomFieldValuesForColl(coll, q.customValues) - .map(itemIds => i.id.in(itemIds)) - - def queryCondFromExpr(today: LocalDate, coll: Ident, q: ItemQuery): Condition = { + def queryCondFromExpr(today: LocalDate, coll: Ident, q: ItemQuery.Expr): Condition = { val tables = Tables(i, org, pers0, pers1, equip, f, a, m, AttachCountTable("cta")) - ItemQueryGenerator.fromExpr(today, tables, coll)(q.expr) + ItemQueryGenerator.fromExpr(today, tables, coll)(q) } def queryCondition(today: LocalDate, coll: Ident, cond: Query.QueryCond): Condition = cond match { - case fm: Query.QueryForm => - queryCondFromForm(coll, fm) - case expr: Query.QueryExpr => - queryCondFromExpr(today, coll, expr.q) + case Query.QueryExpr(Some(expr)) => + queryCondFromExpr(today, coll, expr) + case Query.QueryExpr(None) => + Condition.unit } def findItems( @@ -240,7 +178,7 @@ object QItem { maxNoteLen: Int, batch: Batch ): Stream[ConnectionIO, ListItem] = { - val sql = findItemsBase(q.fix, maxNoteLen) + val sql = findItemsBase(q.fix, today, maxNoteLen) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .limit(batch) .build @@ -263,7 +201,7 @@ object QItem { .innerJoin(i, i.id === ti.itemId) val tagCloud = - findItemsBase(q.fix, 0).unwrap + findItemsBase(q.fix, today, 0).unwrap .withSelect(select(tag.all).append(count(i.id).as("num"))) .changeFrom(_.prepend(tagFrom)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) @@ -281,7 +219,7 @@ object QItem { } def searchCountSummary(today: LocalDate)(q: Query): ConnectionIO[Int] = - findItemsBase(q.fix, 0).unwrap + findItemsBase(q.fix, today, 0).unwrap .withSelect(Nel.of(count(i.id).as("num"))) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .build @@ -290,7 +228,7 @@ object QItem { def searchFolderSummary(today: LocalDate)(q: Query): ConnectionIO[List[FolderCount]] = { val fu = RUser.as("fu") - findItemsBase(q.fix, 0).unwrap + findItemsBase(q.fix, today, 0).unwrap .withSelect(select(f.id, f.name, f.owner, fu.login).append(count(i.id).as("num"))) .changeFrom(_.innerJoin(fu, fu.uid === f.owner)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) @@ -307,7 +245,7 @@ object QItem { .innerJoin(i, i.id === cv.itemId) val base = - findItemsBase(q.fix, 0).unwrap + findItemsBase(q.fix, today, 0).unwrap .changeFrom(_.prepend(fieldJoin)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .groupBy(GroupBy(cf.all)) @@ -359,6 +297,7 @@ object QItem { def findSelectedItems( q: Query, + today: LocalDate, maxNoteLen: Int, items: Set[SelectedItem] ): Stream[ConnectionIO, ListItem] = @@ -386,7 +325,7 @@ object QItem { ) ) - val from = findItemsBase(q.fix, maxNoteLen) + val from = findItemsBase(q.fix, today, maxNoteLen) .appendCte(cte) .appendSelect(Tids.weight.s) .changeFrom(_.innerJoin(Tids, Tids.itemId === i.id)) diff --git a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala index 13fbbc89..8ed3a3d0 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala @@ -85,6 +85,6 @@ object QUserTask { ) def makeUserTask(r: RPeriodicTask): UserTask[String] = - UserTask(r.id, r.task, r.enabled, r.timer, r.args) + UserTask(r.id, r.task, r.enabled, r.timer, r.summary, r.args) } diff --git a/modules/store/src/main/scala/docspell/store/queries/Query.scala b/modules/store/src/main/scala/docspell/store/queries/Query.scala index 6be04a1e..5ebf6a21 100644 --- a/modules/store/src/main/scala/docspell/store/queries/Query.scala +++ b/modules/store/src/main/scala/docspell/store/queries/Query.scala @@ -26,12 +26,12 @@ object Query { case class Fix( account: AccountId, - itemIds: Option[Set[Ident]], + query: Option[ItemQuery.Expr], orderAsc: Option[RItem.Table => Column[_]] ) { def isEmpty: Boolean = - itemIds.isEmpty + query.isEmpty } sealed trait QueryCond { @@ -41,64 +41,17 @@ object Query { !isEmpty } - case class QueryForm( - name: Option[String], - states: Seq[ItemState], - direction: Option[Direction], - corrPerson: Option[Ident], - corrOrg: Option[Ident], - concPerson: Option[Ident], - concEquip: Option[Ident], - folder: Option[Ident], - tagsInclude: List[Ident], - tagsExclude: List[Ident], - tagCategoryIncl: List[String], - tagCategoryExcl: List[String], - dateFrom: Option[Timestamp], - dateTo: Option[Timestamp], - dueDateFrom: Option[Timestamp], - dueDateTo: Option[Timestamp], - allNames: Option[String], - itemIds: Option[Set[Ident]], - customValues: Seq[CustomValue], - source: Option[String] - ) extends QueryCond { - + case class QueryExpr(q: Option[ItemQuery.Expr]) extends QueryCond { def isEmpty: Boolean = - this == QueryForm.empty - } - object QueryForm { - val empty = - QueryForm( - None, - Seq.empty, - None, - None, - None, - None, - None, - None, - Nil, - Nil, - Nil, - Nil, - None, - None, - None, - None, - None, - None, - Seq.empty, - None - ) + q.isEmpty } - case class QueryExpr(q: ItemQuery) extends QueryCond { - def isEmpty: Boolean = - q.expr == ItemQuery.all.expr + object QueryExpr { + def apply(q: ItemQuery.Expr): QueryExpr = + QueryExpr(Some(q)) } - def empty(account: AccountId): Query = - Query(Fix(account, None, None), QueryForm.empty) + def all(account: AccountId): Query = + Query(Fix(account, None, None), QueryExpr(None)) } diff --git a/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala b/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala index 8e0383cf..74c9982c 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPeriodicTask.scala @@ -30,7 +30,8 @@ case class RPeriodicTask( marked: Option[Timestamp], timer: CalEvent, nextrun: Timestamp, - created: Timestamp + created: Timestamp, + summary: Option[String] ) { def toJob[F[_]: Sync]: F[RJob] = @@ -66,7 +67,8 @@ object RPeriodicTask { subject: String, submitter: Ident, priority: Priority, - timer: CalEvent + timer: CalEvent, + summary: Option[String] ): F[RPeriodicTask] = Ident .randomId[F] @@ -91,7 +93,8 @@ object RPeriodicTask { .map(_.toInstant) .map(Timestamp.apply) .getOrElse(Timestamp.Epoch), - now + now, + summary ) } ) @@ -104,9 +107,20 @@ object RPeriodicTask { subject: String, submitter: Ident, priority: Priority, - timer: CalEvent + timer: CalEvent, + summary: Option[String] )(implicit E: Encoder[A]): F[RPeriodicTask] = - create[F](enabled, task, group, E(args).noSpaces, subject, submitter, priority, timer) + create[F]( + enabled, + task, + group, + E(args).noSpaces, + subject, + submitter, + priority, + timer, + summary + ) final case class Table(alias: Option[String]) extends TableDef { val tableName = "periodic_task" @@ -124,6 +138,7 @@ object RPeriodicTask { val timer = Column[CalEvent]("timer", this) val nextrun = Column[Timestamp]("nextrun", this) val created = Column[Timestamp]("created", this) + val summary = Column[String]("summary", this) val all = NonEmptyList.of[Column[_]]( id, enabled, @@ -137,7 +152,8 @@ object RPeriodicTask { marked, timer, nextrun, - created + created, + summary ) } @@ -151,7 +167,7 @@ object RPeriodicTask { T.all, fr"${v.id},${v.enabled},${v.task},${v.group},${v.args}," ++ fr"${v.subject},${v.submitter},${v.priority},${v.worker}," ++ - fr"${v.marked},${v.timer},${v.nextrun},${v.created}" + fr"${v.marked},${v.timer},${v.nextrun},${v.created},${v.summary}" ) def update(v: RPeriodicTask): ConnectionIO[Int] = @@ -168,7 +184,8 @@ object RPeriodicTask { T.worker.setTo(v.worker), T.marked.setTo(v.marked), T.timer.setTo(v.timer), - T.nextrun.setTo(v.nextrun) + T.nextrun.setTo(v.nextrun), + T.summary.setTo(v.summary) ) ) diff --git a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala index 9d038083..81d5330f 100644 --- a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala +++ b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala @@ -16,6 +16,7 @@ case class UserTask[A]( name: Ident, enabled: Boolean, timer: CalEvent, + summary: Option[String], args: A ) { @@ -47,7 +48,8 @@ object UserTask { s"${account.user.id}: ${ut.name.id}", account.user, Priority.Low, - ut.timer + ut.timer, + ut.summary ) .map(r => r.copy(id = ut.id)) } diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index b7869df7..968f8041 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -162,7 +162,6 @@ import Api.Model.ItemInsights exposing (ItemInsights) import Api.Model.ItemLightList exposing (ItemLightList) import Api.Model.ItemProposals exposing (ItemProposals) import Api.Model.ItemQuery exposing (ItemQuery) -import Api.Model.ItemSearch exposing (ItemSearch) import Api.Model.ItemUploadMeta exposing (ItemUploadMeta) import Api.Model.ItemsAndDate exposing (ItemsAndDate) import Api.Model.ItemsAndDirection exposing (ItemsAndDirection) diff --git a/modules/webapp/src/main/elm/Comp/NotificationForm.elm b/modules/webapp/src/main/elm/Comp/NotificationForm.elm index 81ebb11c..c5c5932d 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationForm.elm @@ -28,6 +28,7 @@ import Data.UiSettings exposing (UiSettings) import Data.Validated exposing (Validated(..)) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onInput) import Http import Styles as S import Util.Http @@ -52,6 +53,7 @@ type alias Model = , formMsg : Maybe BasicResult , loading : Int , yesNoDelete : Comp.YesNoDimmer.Model + , summary : Maybe String } @@ -79,6 +81,7 @@ type Msg | Cancel | RequestDelete | YesNoDeleteMsg Comp.YesNoDimmer.Msg + | SetSummary String initWith : Flags -> NotificationSettings -> ( Model, Cmd Msg ) @@ -121,6 +124,7 @@ initWith flags s = , formMsg = Nothing , loading = im.loading , yesNoDelete = Comp.YesNoDimmer.emptyModel + , summary = s.summary } , Cmd.batch [ nc @@ -158,6 +162,7 @@ init flags = , formMsg = Nothing , loading = 2 , yesNoDelete = Comp.YesNoDimmer.emptyModel + , summary = Nothing } , Cmd.batch [ Api.getMailSettings flags "" ConnResp @@ -203,6 +208,7 @@ makeSettings model = , capOverdue = model.capOverdue , enabled = model.enabled , schedule = Data.CalEvent.makeEvent timer + , summary = model.summary } in Data.Validated.map4 make @@ -450,6 +456,12 @@ update flags msg model = , Cmd.none ) + SetSummary str -> + ( { model | summary = Util.Maybe.fromString str } + , NoAction + , Cmd.none + ) + --- View2 @@ -473,6 +485,14 @@ view2 extraClasses settings model = let dimmerSettings = Comp.YesNoDimmer.defaultSettings2 "Really delete this notification task?" + + startOnceBtn = + MB.SecondaryButton + { tagger = StartOnce + , label = "Start Once" + , title = "Start this task now" + , icon = Just "fa fa-play" + } in div [ class "flex flex-col md:relative" @@ -501,7 +521,8 @@ view2 extraClasses settings model = ] , end = if model.settings.id /= "" then - [ MB.DeleteButton + [ startOnceBtn + , MB.DeleteButton { tagger = RequestDelete , label = "Delete" , title = "Delete this task" @@ -510,7 +531,8 @@ view2 extraClasses settings model = ] else - [] + [ startOnceBtn + ] , rootClasses = "mb-4" } , div @@ -534,6 +556,22 @@ view2 extraClasses settings model = , id = "notify-enabled" } ] + , div [ class "mb-4" ] + [ label [ class S.inputLabel ] + [ text "Summary" + ] + , input + [ type_ "text" + , onInput SetSummary + , class S.textInput + , Maybe.withDefault "" model.summary + |> value + ] + [] + , span [ class "opacity-50 text-sm" ] + [ text "Some human readable name, only for displaying" + ] + ] , div [ class "mb-4" ] [ label [ class S.inputLabel ] [ text "Send via" diff --git a/modules/webapp/src/main/elm/Comp/NotificationList.elm b/modules/webapp/src/main/elm/Comp/NotificationList.elm index 486afe36..c87bf16b 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationList.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationList.elm @@ -54,13 +54,13 @@ view2 _ items = , th [ class "text-center mr-2" ] [ i [ class "fa fa-check" ] [] ] + , th [ class "text-left " ] [ text "Summary" ] , th [ class "text-left hidden sm:table-cell mr-2" ] [ text "Schedule" ] , th [ class "text-left mr-2" ] [ text "Connection" ] , th [ class "text-left hidden sm:table-cell mr-2" ] [ text "Recipients" ] - , th [ class "text-center " ] [ text "Remind Days" ] ] ] , tbody [] @@ -76,6 +76,10 @@ viewItem2 item = , td [ class "w-px whitespace-nowrap px-2 text-center" ] [ Util.Html.checkbox2 item.enabled ] + , td [ class "text-left" ] + [ Maybe.withDefault "" item.summary + |> text + ] , td [ class "text-left hidden sm:table-cell mr-2" ] [ code [ class "font-mono text-sm" ] [ text item.schedule @@ -87,8 +91,4 @@ viewItem2 item = , td [ class "text-left hidden sm:table-cell mr-2" ] [ String.join ", " item.recipients |> text ] - , td [ class "text-center" ] - [ String.fromInt item.remindDays - |> text - ] ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationManage.elm b/modules/webapp/src/main/elm/Comp/NotificationManage.elm index eb88db3f..e1b2467a 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationManage.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationManage.elm @@ -221,6 +221,7 @@ view2 settings model = , ( S.successMessage, Maybe.map .success model.result == Just True ) , ( "hidden", model.result == Nothing ) ] + , class "mb-2" ] [ Maybe.map .message model.result |> Maybe.withDefault "" diff --git a/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm b/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm index 9949719b..3e23a981 100644 --- a/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm +++ b/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm @@ -75,6 +75,7 @@ type alias Model = , languageModel : Comp.FixedDropdown.Model Language , language : Maybe Language , postHandleAll : Bool + , summary : Maybe String , openTabs : Set String } @@ -121,6 +122,7 @@ type Msg | RemoveLanguage | TogglePostHandleAll | ToggleAkkordionTab String + | SetSummary String initWith : Flags -> ScanMailboxSettings -> ( Model, Cmd Msg ) @@ -167,6 +169,7 @@ initWith flags s = Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all) , language = Maybe.andThen Data.Language.fromString s.language , postHandleAll = Maybe.withDefault False s.postHandleAll + , summary = s.summary } , Cmd.batch [ Api.getImapSettings flags "" ConnResp @@ -221,6 +224,7 @@ init flags = Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all) , language = Nothing , postHandleAll = False + , summary = Nothing , openTabs = Set.insert (tabTitle TabGeneral) Set.empty } , Cmd.batch @@ -283,6 +287,7 @@ makeSettings model = |> Just , language = Maybe.map Data.Language.toIso3 model.language , postHandleAll = Just model.postHandleAll + , summary = model.summary } in Data.Validated.map3 make @@ -689,6 +694,12 @@ update flags msg model = , Cmd.none ) + SetSummary str -> + ( { model | summary = Util.Maybe.fromString str } + , NoAction + , Cmd.none + ) + --- View2 @@ -870,6 +881,22 @@ viewGeneral2 settings model = [ text "Mailbox" , B.inputRequired ] + , div [ class "mb-4" ] + [ label [ class S.inputLabel ] + [ text "Summary" + ] + , input + [ type_ "text" + , onInput SetSummary + , class S.textInput + , Maybe.withDefault "" model.summary + |> value + ] + [] + , span [ class "opacity-50 text-sm" ] + [ text "Some human readable name, only for displaying" + ] + ] , Html.map ConnMsg (Comp.Dropdown.view2 DS.mainStyle diff --git a/modules/webapp/src/main/elm/Comp/ScanMailboxList.elm b/modules/webapp/src/main/elm/Comp/ScanMailboxList.elm index 40a4f820..e58b33d5 100644 --- a/modules/webapp/src/main/elm/Comp/ScanMailboxList.elm +++ b/modules/webapp/src/main/elm/Comp/ScanMailboxList.elm @@ -54,12 +54,11 @@ view2 _ items = , th [ class "" ] [ i [ class "fa fa-check" ] [] ] - , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Schedule" ] - , th [ class "text-left mr-2" ] [ text "Connection" ] - , th [ class "text-left mr-2" ] [ text "Folders" ] - , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Received Since" ] - , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Target" ] - , th [ class "hidden md:table-cell" ] [ text "Delete" ] + , th [ class "text-left" ] [ text "Summary" ] + , th [ class "text-left mr-2" ] [ text "Schedule" ] + , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Connection" ] + , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Folders" ] + , th [ class "text-left mr-2 hidden lg:table-cell" ] [ text "Received Since" ] ] ] , tbody [] @@ -75,28 +74,24 @@ viewItem2 item = , td [ class "w-px px-2" ] [ Util.Html.checkbox2 item.enabled ] - , td [ class "mr-2 hidden md:table-cell" ] + , td [ class "text-left" ] + [ Maybe.withDefault "–" item.summary |> text + ] + , td [ class "mr-2" ] [ code [ class "font-mono text-sm" ] [ text item.schedule ] ] - , td [ class "text-left mr-2" ] + , td [ class "text-left mr-2 hidden md:table-cell" ] [ text item.imapConnection ] - , td [ class "text-left mr-2" ] + , td [ class "text-left mr-2 hidden md:table-cell" ] [ String.join ", " item.folders |> text ] - , td [ class "text-left mr-2 hidden md:table-cell" ] + , td [ class "text-left mr-2 hidden lg:table-cell" ] [ Maybe.map String.fromInt item.receivedSinceHours |> Maybe.withDefault "-" |> text , text " h" ] - , td [ class "text-left mr-2 hidden md:table-cell" ] - [ Maybe.withDefault "-" item.targetFolder - |> text - ] - , td [ class "w-px px-2 hidden md:table-cell" ] - [ Util.Html.checkbox2 item.deleteMail - ] ] diff --git a/modules/webapp/src/main/elm/Page/Home/View2.elm b/modules/webapp/src/main/elm/Page/Home/View2.elm index 1453057d..15602b8a 100644 --- a/modules/webapp/src/main/elm/Page/Home/View2.elm +++ b/modules/webapp/src/main/elm/Page/Home/View2.elm @@ -7,7 +7,6 @@ import Comp.MenuBar as MB import Comp.PowerSearchInput import Comp.SearchMenu import Comp.SearchStatsView -import Comp.YesNoDimmer import Data.Flags exposing (Flags) import Data.ItemSelection import Data.UiSettings exposing (UiSettings) @@ -53,19 +52,6 @@ viewContent flags settings model = deleteSelectedDimmer : Model -> List (Html Msg) deleteSelectedDimmer model = - let - selectAction = - case model.viewMode of - SelectView svm -> - svm.action - - _ -> - NoneAction - - deleteAllDimmer : Comp.YesNoDimmer.Settings - deleteAllDimmer = - Comp.YesNoDimmer.defaultSettings2 "Really delete all selected items?" - in case model.viewMode of SelectView svm -> case svm.confirmModal of diff --git a/modules/webapp/src/main/elm/Page/Upload/View2.elm b/modules/webapp/src/main/elm/Page/Upload/View2.elm index 3d510083..a685c50b 100644 --- a/modules/webapp/src/main/elm/Page/Upload/View2.elm +++ b/modules/webapp/src/main/elm/Page/Upload/View2.elm @@ -34,35 +34,37 @@ viewContent mid _ _ model = [ id "content" , class S.content ] - [ div [ class "px-0 flex flex-col" ] - [ div [ class "py-4" ] - [ renderForm model - ] - , div [ class "py-0" ] - [ Html.map DropzoneMsg - (Comp.Dropzone.view2 model.dropzone) - ] - , div [ class "py-4" ] - [ a - [ class S.primaryButton - , href "#" - , onClick SubmitUpload + [ div [ class "container mx-auto" ] + [ div [ class "px-0 flex flex-col" ] + [ div [ class "py-4" ] + [ renderForm model ] - [ text "Submit" + , div [ class "py-0" ] + [ Html.map DropzoneMsg + (Comp.Dropzone.view2 model.dropzone) ] - , a - [ class S.secondaryButton - , class "ml-2" - , href "#" - , onClick Clear - ] - [ text "Reset" + , div [ class "py-4" ] + [ a + [ class S.primaryButton + , href "#" + , onClick SubmitUpload + ] + [ text "Submit" + ] + , a + [ class S.secondaryButton + , class "ml-2" + , href "#" + , onClick Clear + ] + [ text "Reset" + ] ] ] + , renderErrorMsg model + , renderSuccessMsg (Util.Maybe.nonEmpty mid) model + , renderUploads model ] - , renderErrorMsg model - , renderSuccessMsg (Util.Maybe.nonEmpty mid) model - , renderUploads model ] diff --git a/modules/webapp/src/main/elm/Styles.elm b/modules/webapp/src/main/elm/Styles.elm index 0be1a211..93cb4dd0 100644 --- a/modules/webapp/src/main/elm/Styles.elm +++ b/modules/webapp/src/main/elm/Styles.elm @@ -18,7 +18,7 @@ sidebarMenuItemActive = content : String content = - "container mx-auto px-2 h-screen-12 overflow-y-auto scrollbar-main scrollbar-thin" + "w-full mx-auto px-2 h-screen-12 overflow-y-auto scrollbar-main scrollbar-thin" sidebarLink : String diff --git a/tools/export-files/export-files.sh b/tools/export-files/export-files.sh index 374faf9c..b11817eb 100755 --- a/tools/export-files/export-files.sh +++ b/tools/export-files/export-files.sh @@ -70,7 +70,7 @@ fi set -o errexit -o pipefail -o noclobber -o nounset LOGIN_URL="$BASE_URL/api/v1/open/auth/login" -SEARCH_URL="$BASE_URL/api/v1/sec/item/searchWithTags" +SEARCH_URL="$BASE_URL/api/v1/sec/item/search" INSIGHT_URL="$BASE_URL/api/v1/sec/collective/insights" DETAIL_URL="$BASE_URL/api/v1/sec/item" ATTACH_URL="$BASE_URL/api/v1/sec/attachment" @@ -108,11 +108,11 @@ mcurl() { errout "Login to Docspell." errout "Using url: $BASE_URL" -if [ -z "$DS_USER" ]; then +if [ -z "${DS_USER:-}" ]; then errout -n "Account: " read DS_USER fi -if [ -z "$DS_PASS" ]; then +if [ -z "${DS_PASS:-}" ]; then errout -n "Password: " read -s DS_PASS fi @@ -152,7 +152,7 @@ listItems() { OFFSET="${1:-0}" LIMIT="${2:-50}" errout "Get next items with offset=$OFFSET, limit=$LIMIT" - REQ="{\"offset\":$OFFSET, \"limit\":$LIMIT, \"tagsInclude\":[],\"tagsExclude\":[],\"tagCategoriesInclude\":[], \"tagCategoriesExclude\":[],\"customValues\":[],\"inbox\":false}" + REQ="{\"offset\":$OFFSET, \"limit\":$LIMIT, \"withDetails\":true, \"query\":\"\"}" mcurl -XPOST -H 'ContentType: application/json' -d "$REQ" "$SEARCH_URL" | "$JQ_CMD" -r '.groups[].items[]|.id' }