Merge pull request #727 from eikek/next-fixes

Next fixes
This commit is contained in:
mergify[bot] 2021-03-27 21:30:30 +00:00 committed by GitHub
commit 121051f234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 318 additions and 583 deletions

View File

@ -150,6 +150,7 @@ object OCollective {
LearnClassifierArgs.taskName, LearnClassifierArgs.taskName,
on, on,
timer, timer,
None,
LearnClassifierArgs(coll) LearnClassifierArgs(coll)
) )
_ <- uts.updateOneTask(AccountId(coll, LearnClassifierArgs.taskName), ut) _ <- uts.updateOneTask(AccountId(coll, LearnClassifierArgs.taskName), ut)
@ -164,6 +165,7 @@ object OCollective {
LearnClassifierArgs.taskName, LearnClassifierArgs.taskName,
true, true,
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All), CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
None,
LearnClassifierArgs(collective) LearnClassifierArgs(collective)
).encode.toPeriodicTask(AccountId(collective, LearnClassifierArgs.taskName)) ).encode.toPeriodicTask(AccountId(collective, LearnClassifierArgs.taskName))
job <- ut.toJob job <- ut.toJob

View File

@ -1,5 +1,6 @@
package docspell.backend.ops package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import fs2.Stream import fs2.Stream
@ -9,6 +10,8 @@ import docspell.backend.ops.OItemSearch._
import docspell.common._ import docspell.common._
import docspell.common.syntax.all._ import docspell.common.syntax.all._
import docspell.ftsclient._ import docspell.ftsclient._
import docspell.query.ItemQuery._
import docspell.query.ItemQueryDsl._
import docspell.store.queries.{QFolder, QItem, SelectedItem} import docspell.store.queries.{QFolder, QItem, SelectedItem}
import docspell.store.queue.JobQueue import docspell.store.queue.JobQueue
import docspell.store.records.RJob import docspell.store.records.RJob
@ -125,12 +128,18 @@ object OFulltext {
.map(_.minBy(-_.score)) .map(_.minBy(-_.score))
.map(r => SelectedItem(r.itemId, r.score)) .map(r => SelectedItem(r.itemId, r.score))
.toSet .toSet
now <- Timestamp.current[F]
itemsWithTags <- itemsWithTags <-
store store
.transact( .transact(
QItem.findItemsWithTags( QItem.findItemsWithTags(
account.collective, account.collective,
QItem.findSelectedItems(Query.empty(account), maxNoteLen, select) QItem.findSelectedItems(
Query.all(account),
now.toUtcDate,
maxNoteLen,
select
)
) )
) )
.take(batch.limit.toLong) .take(batch.limit.toLong)
@ -165,7 +174,13 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId))) .flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile .compile
.to(Set) .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)) res <- store.transact(QItem.searchStats(now.toUtcDate)(q))
} yield res } yield res
} }
@ -221,7 +236,11 @@ object OFulltext {
.flatMap(r => Stream.emits(r.results.map(_.itemId))) .flatMap(r => Stream.emits(r.results.map(_.itemId)))
.compile .compile
.to(Set) .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] now <- Timestamp.current[F]
res <- store.transact(QItem.searchStats(now.toUtcDate)(qnext)) res <- store.transact(QItem.searchStats(now.toUtcDate)(qnext))
} yield res } yield res

View File

@ -129,7 +129,7 @@ object OSimpleSearch {
def makeQuery(iq: ItemQuery): F[StringSearchResult[Items]] = def makeQuery(iq: ItemQuery): F[StringSearchResult[Items]] =
iq.findFulltext match { iq.findFulltext match {
case FulltextExtract.Result.Success(expr, ftq) => 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) .map(StringSearchResult.Success.apply)
case other: FulltextExtract.FailureResult => case other: FulltextExtract.FailureResult =>
StringSearchResult.fulltextMismatch[Items](other).pure[F] StringSearchResult.fulltextMismatch[Items](other).pure[F]
@ -152,7 +152,7 @@ object OSimpleSearch {
def makeQuery(iq: ItemQuery): F[StringSearchResult[SearchSummary]] = def makeQuery(iq: ItemQuery): F[StringSearchResult[SearchSummary]] =
iq.findFulltext match { iq.findFulltext match {
case FulltextExtract.Result.Success(expr, ftq) => 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) .map(StringSearchResult.Success.apply)
case other: FulltextExtract.FailureResult => case other: FulltextExtract.FailureResult =>
StringSearchResult.fulltextMismatch[SearchSummary](other).pure[F] StringSearchResult.fulltextMismatch[SearchSummary](other).pure[F]

View File

@ -36,7 +36,8 @@ object HouseKeepingTask {
"Docspell house-keeping", "Docspell house-keeping",
DocspellSystem.taskGroup, DocspellSystem.taskGroup,
Priority.Low, Priority.Low,
ce ce,
None
) )
.map(_.copy(id = periodicId)) .map(_.copy(id = periodicId))
} }

View File

@ -1,6 +1,6 @@
package docspell.joex.notify package docspell.joex.notify
import cats.data.OptionT import cats.data.{NonEmptyList => Nel, OptionT}
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
@ -8,6 +8,9 @@ import docspell.backend.ops.OItemSearch.{Batch, ListItem, Query}
import docspell.common._ import docspell.common._
import docspell.joex.mail.EmilHeader import docspell.joex.mail.EmilHeader
import docspell.joex.scheduler.{Context, Task} 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.queries.QItem
import docspell.store.records._ import docspell.store.records._
@ -69,18 +72,24 @@ object NotifyDueItemsTask {
def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[ListItem]] = def findItems[F[_]: Sync](ctx: Context[F, Args]): F[Vector[ListItem]] =
for { for {
now <- Timestamp.current[F] now <- Timestamp.current[F]
rightDate = Date((now + Duration.days(ctx.args.remindDays.toLong)).toMillis)
q = q =
Query Query
.empty(ctx.args.account) .all(ctx.args.account)
.withOrder(orderAsc = _.dueDate) .withOrder(orderAsc = _.dueDate)
.withFix(_.copy(query = Expr.ValidItemStates.some))
.withCond(_ => .withCond(_ =>
Query.QueryForm.empty.copy( Query.QueryExpr(
states = ItemState.validStates.toList, Attr.DueDate <= rightDate &&?
tagsInclude = ctx.args.tagsInclude, ctx.args.daysBack.map(back =>
tagsExclude = ctx.args.tagsExclude, Attr.DueDate >= Date((now - Duration.days(back.toLong)).toMillis)
dueDateFrom = ) &&?
ctx.args.daysBack.map(back => now - Duration.days(back.toLong)), Nel
dueDateTo = Some(now + Duration.days(ctx.args.remindDays.toLong)) .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 <- res <-

View File

@ -119,6 +119,8 @@ object ItemQuery {
final case class ChecksumMatch(checksum: String) extends Expr final case class ChecksumMatch(checksum: String) extends Expr
final case class AttachId(id: String) extends Expr final case class AttachId(id: String) extends Expr
case object ValidItemStates extends Expr
// things that can be expressed with terms above // things that can be expressed with terms above
sealed trait MacroExpr extends Expr { sealed trait MacroExpr extends Expr {
def body: Expr def body: Expr

View File

@ -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)
}
}

View File

@ -69,6 +69,9 @@ object ExprUtil {
expr expr
case AttachId(_) => case AttachId(_) =>
expr expr
case ValidItemStates =>
expr
} }
private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] = private def spliceAnd(nodes: Nel[Expr]): Nel[Expr] =

View File

@ -1309,73 +1309,6 @@ paths:
schema: schema:
$ref: "#/components/schemas/BasicResult" $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: /sec/item/search:
get: get:
tags: [ Item Search ] tags: [ Item Search ]
@ -1457,29 +1390,6 @@ paths:
schema: schema:
$ref: "#/components/schemas/ItemLightList" $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: /sec/item/searchStats:
post: post:
tags: [ Item Search ] tags: [ Item Search ]
@ -3943,6 +3853,8 @@ components:
format: ident format: ident
enabled: enabled:
type: boolean type: boolean
summary:
type: string
imapConnection: imapConnection:
type: string type: string
format: ident format: ident
@ -4102,6 +4014,8 @@ components:
format: ident format: ident
enabled: enabled:
type: boolean type: boolean
summary:
type: string
smtpConnection: smtpConnection:
type: string type: string
format: ident format: ident
@ -5268,104 +5182,6 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ItemLight" $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: ItemLight:
description: | description: |
An item with only a few important properties. An item with only a few important properties.

View File

@ -143,36 +143,6 @@ trait Conversions {
// item list // 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 = def mkCustomValue(v: CustomFieldValue): OItemSearch.CustomValue =
OItemSearch.CustomValue(v.field, v.value) OItemSearch.CustomValue(v.field, v.value)

View File

@ -1,6 +1,5 @@
package docspell.restserver.routes package docspell.restserver.routes
import cats.Monoid
import cats.data.NonEmptyList import cats.data.NonEmptyList
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
@ -16,6 +15,7 @@ import docspell.common._
import docspell.common.syntax.all._ import docspell.common.syntax.all._
import docspell.query.FulltextExtract.Result.TooMany import docspell.query.FulltextExtract.Result.TooMany
import docspell.query.FulltextExtract.Result.UnsupportedPosition import docspell.query.FulltextExtract.Result.UnsupportedPosition
import docspell.query.ItemQuery.Expr
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.Config import docspell.restserver.Config
import docspell.restserver.conv.Conversions import docspell.restserver.conv.Conversions
@ -62,12 +62,12 @@ object ItemRoutes {
detailFlag.getOrElse(false), detailFlag.getOrElse(false),
cfg.maxNoteLength 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) searchItems(backend, dsl)(settings, fixQuery, itemQuery)
case GET -> Root / "searchStats" :? QP.Query(q) => case GET -> Root / "searchStats" :? QP.Query(q) =>
val itemQuery = ItemQueryString(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) searchItemStats(backend, dsl)(cfg.fullTextSearch.enabled, fixQuery, itemQuery)
case req @ POST -> Root / "search" => case req @ POST -> Root / "search" =>
@ -86,7 +86,7 @@ object ItemRoutes {
userQuery.withDetails.getOrElse(false), userQuery.withDetails.getOrElse(false),
cfg.maxNoteLength 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) resp <- searchItems(backend, dsl)(settings, fixQuery, itemQuery)
} yield resp } yield resp
@ -94,7 +94,7 @@ object ItemRoutes {
for { for {
userQuery <- req.as[ItemQuery] userQuery <- req.as[ItemQuery]
itemQuery = ItemQueryString(userQuery.query) itemQuery = ItemQueryString(userQuery.query)
fixQuery = Query.Fix(user.account, None, None) fixQuery = Query.Fix(user.account, Some(Expr.ValidItemStates), None)
resp <- searchItemStats(backend, dsl)( resp <- searchItemStats(backend, dsl)(
cfg.fullTextSearch.enabled, cfg.fullTextSearch.enabled,
fixQuery, fixQuery,
@ -102,85 +102,6 @@ object ItemRoutes {
) )
} yield resp } 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" => case req @ POST -> Root / "searchIndex" =>
for { for {
mask <- req.as[ItemQuery] mask <- req.as[ItemQuery]
@ -204,26 +125,6 @@ object ItemRoutes {
} }
} yield resp } 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) => case GET -> Root / Ident(id) =>
for { for {
item <- backend.itemSearch.findItem(id, user.account.collective) item <- backend.itemSearch.findItem(id, user.account.collective)
@ -560,40 +461,4 @@ object ItemRoutes {
def notEmpty: Option[String] = def notEmpty: Option[String] =
opt.map(_.trim).filter(_.nonEmpty) 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
}
} }

View File

@ -112,6 +112,7 @@ object NotifyDueItemsRoutes {
NotifyDueItemsArgs.taskName, NotifyDueItemsArgs.taskName,
settings.enabled, settings.enabled,
settings.schedule, settings.schedule,
settings.summary,
NotifyDueItemsArgs( NotifyDueItemsArgs(
user, user,
settings.smtpConnection, settings.smtpConnection,
@ -144,6 +145,7 @@ object NotifyDueItemsRoutes {
} yield NotificationSettings( } yield NotificationSettings(
task.id, task.id,
task.enabled, task.enabled,
task.summary,
conn.getOrElse(Ident.unsafe("")), conn.getOrElse(Ident.unsafe("")),
task.args.recipients, task.args.recipients,
task.timer, task.timer,

View File

@ -105,6 +105,7 @@ object ScanMailboxRoutes {
ScanMailboxArgs.taskName, ScanMailboxArgs.taskName,
settings.enabled, settings.enabled,
settings.schedule, settings.schedule,
settings.summary,
ScanMailboxArgs( ScanMailboxArgs(
user, user,
settings.imapConnection, settings.imapConnection,
@ -139,6 +140,7 @@ object ScanMailboxRoutes {
} yield ScanMailboxSettings( } yield ScanMailboxSettings(
task.id, task.id,
task.enabled, task.enabled,
task.summary,
conn.getOrElse(Ident.unsafe("")), conn.getOrElse(Ident.unsafe("")),
task.args.folders, task.args.folders,
task.timer, task.timer,

View File

@ -0,0 +1,2 @@
ALTER TABLE "periodic_task"
ADD COLUMN "summary" varchar(254);

View File

@ -0,0 +1,2 @@
ALTER TABLE `periodic_task`
ADD COLUMN `summary` varchar(254);

View File

@ -0,0 +1,2 @@
ALTER TABLE "periodic_task"
ADD COLUMN "summary" varchar(254);

View File

@ -117,6 +117,9 @@ object ItemQueryGenerator {
if (flag) tables.item.state === ItemState.created if (flag) tables.item.state === ItemState.created
else tables.item.state === ItemState.confirmed else tables.item.state === ItemState.confirmed
case Expr.ValidItemStates =>
tables.item.state.in(ItemState.validStates)
case Expr.TagIdsMatch(op, tags) => case Expr.TagIdsMatch(op, tags) =>
val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption) val ids = tags.toList.flatMap(s => Ident.fromString(s).toOption)
Nel Nel

View File

@ -98,30 +98,7 @@ object QItem {
cv.itemId === itemId cv.itemId === itemId
).build.query[ItemFieldValue].to[Vector] ).build.query[ItemFieldValue].to[Vector]
private def findCustomFieldValuesForColl( private def findItemsBase(q: Query.Fix, today: LocalDate, noteMaxLen: Int): Select = {
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 = {
val attachs = AttachCountTable("cta") val attachs = AttachCountTable("cta")
val coll = q.account.collective val coll = q.account.collective
@ -169,9 +146,7 @@ object QItem {
.leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll) .leftJoin(pers1, pers1.pid === i.concPerson && pers1.cid === coll)
.leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll), .leftJoin(equip, equip.eid === i.concEquipment && equip.cid === coll),
where( where(
i.cid === coll &&? q.itemIds.map(s => i.cid === coll &&? q.query.map(qs => queryCondFromExpr(today, coll, qs))
Nel.fromList(s.toList).map(nel => i.id.in(nel)).getOrElse(i.id.isNull)
)
&& or( && or(
i.folder.isNull, i.folder.isNull,
i.folder.in(QFolder.findMemberFolderIds(q.account)) i.folder.in(QFolder.findMemberFolderIds(q.account))
@ -184,54 +159,17 @@ object QItem {
) )
} }
def queryCondFromForm(coll: Ident, q: Query.QueryForm): Condition = def queryCondFromExpr(today: LocalDate, coll: Ident, q: ItemQuery.Expr): 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 = {
val tables = Tables(i, org, pers0, pers1, equip, f, a, m, AttachCountTable("cta")) 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 = def queryCondition(today: LocalDate, coll: Ident, cond: Query.QueryCond): Condition =
cond match { cond match {
case fm: Query.QueryForm => case Query.QueryExpr(Some(expr)) =>
queryCondFromForm(coll, fm) queryCondFromExpr(today, coll, expr)
case expr: Query.QueryExpr => case Query.QueryExpr(None) =>
queryCondFromExpr(today, coll, expr.q) Condition.unit
} }
def findItems( def findItems(
@ -240,7 +178,7 @@ object QItem {
maxNoteLen: Int, maxNoteLen: Int,
batch: Batch batch: Batch
): Stream[ConnectionIO, ListItem] = { ): 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)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond))
.limit(batch) .limit(batch)
.build .build
@ -263,7 +201,7 @@ object QItem {
.innerJoin(i, i.id === ti.itemId) .innerJoin(i, i.id === ti.itemId)
val tagCloud = val tagCloud =
findItemsBase(q.fix, 0).unwrap findItemsBase(q.fix, today, 0).unwrap
.withSelect(select(tag.all).append(count(i.id).as("num"))) .withSelect(select(tag.all).append(count(i.id).as("num")))
.changeFrom(_.prepend(tagFrom)) .changeFrom(_.prepend(tagFrom))
.changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .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] = 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"))) .withSelect(Nel.of(count(i.id).as("num")))
.changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond))
.build .build
@ -290,7 +228,7 @@ object QItem {
def searchFolderSummary(today: LocalDate)(q: Query): ConnectionIO[List[FolderCount]] = { def searchFolderSummary(today: LocalDate)(q: Query): ConnectionIO[List[FolderCount]] = {
val fu = RUser.as("fu") 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"))) .withSelect(select(f.id, f.name, f.owner, fu.login).append(count(i.id).as("num")))
.changeFrom(_.innerJoin(fu, fu.uid === f.owner)) .changeFrom(_.innerJoin(fu, fu.uid === f.owner))
.changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond))
@ -307,7 +245,7 @@ object QItem {
.innerJoin(i, i.id === cv.itemId) .innerJoin(i, i.id === cv.itemId)
val base = val base =
findItemsBase(q.fix, 0).unwrap findItemsBase(q.fix, today, 0).unwrap
.changeFrom(_.prepend(fieldJoin)) .changeFrom(_.prepend(fieldJoin))
.changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond)) .changeWhere(c => c && queryCondition(today, q.fix.account.collective, q.cond))
.groupBy(GroupBy(cf.all)) .groupBy(GroupBy(cf.all))
@ -359,6 +297,7 @@ object QItem {
def findSelectedItems( def findSelectedItems(
q: Query, q: Query,
today: LocalDate,
maxNoteLen: Int, maxNoteLen: Int,
items: Set[SelectedItem] items: Set[SelectedItem]
): Stream[ConnectionIO, ListItem] = ): Stream[ConnectionIO, ListItem] =
@ -386,7 +325,7 @@ object QItem {
) )
) )
val from = findItemsBase(q.fix, maxNoteLen) val from = findItemsBase(q.fix, today, maxNoteLen)
.appendCte(cte) .appendCte(cte)
.appendSelect(Tids.weight.s) .appendSelect(Tids.weight.s)
.changeFrom(_.innerJoin(Tids, Tids.itemId === i.id)) .changeFrom(_.innerJoin(Tids, Tids.itemId === i.id))

View File

@ -85,6 +85,6 @@ object QUserTask {
) )
def makeUserTask(r: RPeriodicTask): UserTask[String] = 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)
} }

View File

@ -26,12 +26,12 @@ object Query {
case class Fix( case class Fix(
account: AccountId, account: AccountId,
itemIds: Option[Set[Ident]], query: Option[ItemQuery.Expr],
orderAsc: Option[RItem.Table => Column[_]] orderAsc: Option[RItem.Table => Column[_]]
) { ) {
def isEmpty: Boolean = def isEmpty: Boolean =
itemIds.isEmpty query.isEmpty
} }
sealed trait QueryCond { sealed trait QueryCond {
@ -41,64 +41,17 @@ object Query {
!isEmpty !isEmpty
} }
case class QueryForm( case class QueryExpr(q: Option[ItemQuery.Expr]) extends QueryCond {
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 {
def isEmpty: Boolean = def isEmpty: Boolean =
this == QueryForm.empty q.isEmpty
}
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
)
} }
case class QueryExpr(q: ItemQuery) extends QueryCond { object QueryExpr {
def isEmpty: Boolean = def apply(q: ItemQuery.Expr): QueryExpr =
q.expr == ItemQuery.all.expr QueryExpr(Some(q))
} }
def empty(account: AccountId): Query = def all(account: AccountId): Query =
Query(Fix(account, None, None), QueryForm.empty) Query(Fix(account, None, None), QueryExpr(None))
} }

View File

@ -30,7 +30,8 @@ case class RPeriodicTask(
marked: Option[Timestamp], marked: Option[Timestamp],
timer: CalEvent, timer: CalEvent,
nextrun: Timestamp, nextrun: Timestamp,
created: Timestamp created: Timestamp,
summary: Option[String]
) { ) {
def toJob[F[_]: Sync]: F[RJob] = def toJob[F[_]: Sync]: F[RJob] =
@ -66,7 +67,8 @@ object RPeriodicTask {
subject: String, subject: String,
submitter: Ident, submitter: Ident,
priority: Priority, priority: Priority,
timer: CalEvent timer: CalEvent,
summary: Option[String]
): F[RPeriodicTask] = ): F[RPeriodicTask] =
Ident Ident
.randomId[F] .randomId[F]
@ -91,7 +93,8 @@ object RPeriodicTask {
.map(_.toInstant) .map(_.toInstant)
.map(Timestamp.apply) .map(Timestamp.apply)
.getOrElse(Timestamp.Epoch), .getOrElse(Timestamp.Epoch),
now now,
summary
) )
} }
) )
@ -104,9 +107,20 @@ object RPeriodicTask {
subject: String, subject: String,
submitter: Ident, submitter: Ident,
priority: Priority, priority: Priority,
timer: CalEvent timer: CalEvent,
summary: Option[String]
)(implicit E: Encoder[A]): F[RPeriodicTask] = )(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 { final case class Table(alias: Option[String]) extends TableDef {
val tableName = "periodic_task" val tableName = "periodic_task"
@ -124,6 +138,7 @@ object RPeriodicTask {
val timer = Column[CalEvent]("timer", this) val timer = Column[CalEvent]("timer", this)
val nextrun = Column[Timestamp]("nextrun", this) val nextrun = Column[Timestamp]("nextrun", this)
val created = Column[Timestamp]("created", this) val created = Column[Timestamp]("created", this)
val summary = Column[String]("summary", this)
val all = NonEmptyList.of[Column[_]]( val all = NonEmptyList.of[Column[_]](
id, id,
enabled, enabled,
@ -137,7 +152,8 @@ object RPeriodicTask {
marked, marked,
timer, timer,
nextrun, nextrun,
created created,
summary
) )
} }
@ -151,7 +167,7 @@ object RPeriodicTask {
T.all, T.all,
fr"${v.id},${v.enabled},${v.task},${v.group},${v.args}," ++ fr"${v.id},${v.enabled},${v.task},${v.group},${v.args}," ++
fr"${v.subject},${v.submitter},${v.priority},${v.worker}," ++ 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] = def update(v: RPeriodicTask): ConnectionIO[Int] =
@ -168,7 +184,8 @@ object RPeriodicTask {
T.worker.setTo(v.worker), T.worker.setTo(v.worker),
T.marked.setTo(v.marked), T.marked.setTo(v.marked),
T.timer.setTo(v.timer), T.timer.setTo(v.timer),
T.nextrun.setTo(v.nextrun) T.nextrun.setTo(v.nextrun),
T.summary.setTo(v.summary)
) )
) )

View File

@ -16,6 +16,7 @@ case class UserTask[A](
name: Ident, name: Ident,
enabled: Boolean, enabled: Boolean,
timer: CalEvent, timer: CalEvent,
summary: Option[String],
args: A args: A
) { ) {
@ -47,7 +48,8 @@ object UserTask {
s"${account.user.id}: ${ut.name.id}", s"${account.user.id}: ${ut.name.id}",
account.user, account.user,
Priority.Low, Priority.Low,
ut.timer ut.timer,
ut.summary
) )
.map(r => r.copy(id = ut.id)) .map(r => r.copy(id = ut.id))
} }

View File

@ -162,7 +162,6 @@ import Api.Model.ItemInsights exposing (ItemInsights)
import Api.Model.ItemLightList exposing (ItemLightList) import Api.Model.ItemLightList exposing (ItemLightList)
import Api.Model.ItemProposals exposing (ItemProposals) import Api.Model.ItemProposals exposing (ItemProposals)
import Api.Model.ItemQuery exposing (ItemQuery) import Api.Model.ItemQuery exposing (ItemQuery)
import Api.Model.ItemSearch exposing (ItemSearch)
import Api.Model.ItemUploadMeta exposing (ItemUploadMeta) import Api.Model.ItemUploadMeta exposing (ItemUploadMeta)
import Api.Model.ItemsAndDate exposing (ItemsAndDate) import Api.Model.ItemsAndDate exposing (ItemsAndDate)
import Api.Model.ItemsAndDirection exposing (ItemsAndDirection) import Api.Model.ItemsAndDirection exposing (ItemsAndDirection)

View File

@ -28,6 +28,7 @@ import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..)) import Data.Validated exposing (Validated(..))
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Http import Http
import Styles as S import Styles as S
import Util.Http import Util.Http
@ -52,6 +53,7 @@ type alias Model =
, formMsg : Maybe BasicResult , formMsg : Maybe BasicResult
, loading : Int , loading : Int
, yesNoDelete : Comp.YesNoDimmer.Model , yesNoDelete : Comp.YesNoDimmer.Model
, summary : Maybe String
} }
@ -79,6 +81,7 @@ type Msg
| Cancel | Cancel
| RequestDelete | RequestDelete
| YesNoDeleteMsg Comp.YesNoDimmer.Msg | YesNoDeleteMsg Comp.YesNoDimmer.Msg
| SetSummary String
initWith : Flags -> NotificationSettings -> ( Model, Cmd Msg ) initWith : Flags -> NotificationSettings -> ( Model, Cmd Msg )
@ -121,6 +124,7 @@ initWith flags s =
, formMsg = Nothing , formMsg = Nothing
, loading = im.loading , loading = im.loading
, yesNoDelete = Comp.YesNoDimmer.emptyModel , yesNoDelete = Comp.YesNoDimmer.emptyModel
, summary = s.summary
} }
, Cmd.batch , Cmd.batch
[ nc [ nc
@ -158,6 +162,7 @@ init flags =
, formMsg = Nothing , formMsg = Nothing
, loading = 2 , loading = 2
, yesNoDelete = Comp.YesNoDimmer.emptyModel , yesNoDelete = Comp.YesNoDimmer.emptyModel
, summary = Nothing
} }
, Cmd.batch , Cmd.batch
[ Api.getMailSettings flags "" ConnResp [ Api.getMailSettings flags "" ConnResp
@ -203,6 +208,7 @@ makeSettings model =
, capOverdue = model.capOverdue , capOverdue = model.capOverdue
, enabled = model.enabled , enabled = model.enabled
, schedule = Data.CalEvent.makeEvent timer , schedule = Data.CalEvent.makeEvent timer
, summary = model.summary
} }
in in
Data.Validated.map4 make Data.Validated.map4 make
@ -450,6 +456,12 @@ update flags msg model =
, Cmd.none , Cmd.none
) )
SetSummary str ->
( { model | summary = Util.Maybe.fromString str }
, NoAction
, Cmd.none
)
--- View2 --- View2
@ -473,6 +485,14 @@ view2 extraClasses settings model =
let let
dimmerSettings = dimmerSettings =
Comp.YesNoDimmer.defaultSettings2 "Really delete this notification task?" 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 in
div div
[ class "flex flex-col md:relative" [ class "flex flex-col md:relative"
@ -501,7 +521,8 @@ view2 extraClasses settings model =
] ]
, end = , end =
if model.settings.id /= "" then if model.settings.id /= "" then
[ MB.DeleteButton [ startOnceBtn
, MB.DeleteButton
{ tagger = RequestDelete { tagger = RequestDelete
, label = "Delete" , label = "Delete"
, title = "Delete this task" , title = "Delete this task"
@ -510,7 +531,8 @@ view2 extraClasses settings model =
] ]
else else
[] [ startOnceBtn
]
, rootClasses = "mb-4" , rootClasses = "mb-4"
} }
, div , div
@ -534,6 +556,22 @@ view2 extraClasses settings model =
, id = "notify-enabled" , 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" ] , div [ class "mb-4" ]
[ label [ class S.inputLabel ] [ label [ class S.inputLabel ]
[ text "Send via" [ text "Send via"

View File

@ -54,13 +54,13 @@ view2 _ items =
, th [ class "text-center mr-2" ] , th [ class "text-center mr-2" ]
[ i [ class "fa fa-check" ] [] [ i [ class "fa fa-check" ] []
] ]
, th [ class "text-left " ] [ text "Summary" ]
, th [ class "text-left hidden sm:table-cell mr-2" ] , th [ class "text-left hidden sm:table-cell mr-2" ]
[ text "Schedule" ] [ text "Schedule" ]
, th [ class "text-left mr-2" ] , th [ class "text-left mr-2" ]
[ text "Connection" ] [ text "Connection" ]
, th [ class "text-left hidden sm:table-cell mr-2" ] , th [ class "text-left hidden sm:table-cell mr-2" ]
[ text "Recipients" ] [ text "Recipients" ]
, th [ class "text-center " ] [ text "Remind Days" ]
] ]
] ]
, tbody [] , tbody []
@ -76,6 +76,10 @@ viewItem2 item =
, td [ class "w-px whitespace-nowrap px-2 text-center" ] , td [ class "w-px whitespace-nowrap px-2 text-center" ]
[ Util.Html.checkbox2 item.enabled [ Util.Html.checkbox2 item.enabled
] ]
, td [ class "text-left" ]
[ Maybe.withDefault "" item.summary
|> text
]
, td [ class "text-left hidden sm:table-cell mr-2" ] , td [ class "text-left hidden sm:table-cell mr-2" ]
[ code [ class "font-mono text-sm" ] [ code [ class "font-mono text-sm" ]
[ text item.schedule [ text item.schedule
@ -87,8 +91,4 @@ viewItem2 item =
, td [ class "text-left hidden sm:table-cell mr-2" ] , td [ class "text-left hidden sm:table-cell mr-2" ]
[ String.join ", " item.recipients |> text [ String.join ", " item.recipients |> text
] ]
, td [ class "text-center" ]
[ String.fromInt item.remindDays
|> text
]
] ]

View File

@ -221,6 +221,7 @@ view2 settings model =
, ( S.successMessage, Maybe.map .success model.result == Just True ) , ( S.successMessage, Maybe.map .success model.result == Just True )
, ( "hidden", model.result == Nothing ) , ( "hidden", model.result == Nothing )
] ]
, class "mb-2"
] ]
[ Maybe.map .message model.result [ Maybe.map .message model.result
|> Maybe.withDefault "" |> Maybe.withDefault ""

View File

@ -75,6 +75,7 @@ type alias Model =
, languageModel : Comp.FixedDropdown.Model Language , languageModel : Comp.FixedDropdown.Model Language
, language : Maybe Language , language : Maybe Language
, postHandleAll : Bool , postHandleAll : Bool
, summary : Maybe String
, openTabs : Set String , openTabs : Set String
} }
@ -121,6 +122,7 @@ type Msg
| RemoveLanguage | RemoveLanguage
| TogglePostHandleAll | TogglePostHandleAll
| ToggleAkkordionTab String | ToggleAkkordionTab String
| SetSummary String
initWith : Flags -> ScanMailboxSettings -> ( Model, Cmd Msg ) initWith : Flags -> ScanMailboxSettings -> ( Model, Cmd Msg )
@ -167,6 +169,7 @@ initWith flags s =
Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all) Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all)
, language = Maybe.andThen Data.Language.fromString s.language , language = Maybe.andThen Data.Language.fromString s.language
, postHandleAll = Maybe.withDefault False s.postHandleAll , postHandleAll = Maybe.withDefault False s.postHandleAll
, summary = s.summary
} }
, Cmd.batch , Cmd.batch
[ Api.getImapSettings flags "" ConnResp [ Api.getImapSettings flags "" ConnResp
@ -221,6 +224,7 @@ init flags =
Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all) Comp.FixedDropdown.init (List.map mkLanguageItem Data.Language.all)
, language = Nothing , language = Nothing
, postHandleAll = False , postHandleAll = False
, summary = Nothing
, openTabs = Set.insert (tabTitle TabGeneral) Set.empty , openTabs = Set.insert (tabTitle TabGeneral) Set.empty
} }
, Cmd.batch , Cmd.batch
@ -283,6 +287,7 @@ makeSettings model =
|> Just |> Just
, language = Maybe.map Data.Language.toIso3 model.language , language = Maybe.map Data.Language.toIso3 model.language
, postHandleAll = Just model.postHandleAll , postHandleAll = Just model.postHandleAll
, summary = model.summary
} }
in in
Data.Validated.map3 make Data.Validated.map3 make
@ -689,6 +694,12 @@ update flags msg model =
, Cmd.none , Cmd.none
) )
SetSummary str ->
( { model | summary = Util.Maybe.fromString str }
, NoAction
, Cmd.none
)
--- View2 --- View2
@ -870,6 +881,22 @@ viewGeneral2 settings model =
[ text "Mailbox" [ text "Mailbox"
, B.inputRequired , 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 , Html.map ConnMsg
(Comp.Dropdown.view2 (Comp.Dropdown.view2
DS.mainStyle DS.mainStyle

View File

@ -54,12 +54,11 @@ view2 _ items =
, th [ class "" ] , th [ class "" ]
[ i [ class "fa fa-check" ] [] [ i [ class "fa fa-check" ] []
] ]
, th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Schedule" ] , th [ class "text-left" ] [ text "Summary" ]
, th [ class "text-left mr-2" ] [ text "Connection" ] , th [ class "text-left mr-2" ] [ text "Schedule" ]
, th [ class "text-left mr-2" ] [ text "Folders" ] , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Connection" ]
, th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Received Since" ] , th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Folders" ]
, th [ class "text-left mr-2 hidden md:table-cell" ] [ text "Target" ] , th [ class "text-left mr-2 hidden lg:table-cell" ] [ text "Received Since" ]
, th [ class "hidden md:table-cell" ] [ text "Delete" ]
] ]
] ]
, tbody [] , tbody []
@ -75,28 +74,24 @@ viewItem2 item =
, td [ class "w-px px-2" ] , td [ class "w-px px-2" ]
[ Util.Html.checkbox2 item.enabled [ 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" ] [ code [ class "font-mono text-sm" ]
[ text item.schedule [ text item.schedule
] ]
] ]
, td [ class "text-left mr-2" ] , td [ class "text-left mr-2 hidden md:table-cell" ]
[ text item.imapConnection [ text item.imapConnection
] ]
, td [ class "text-left mr-2" ] , td [ class "text-left mr-2 hidden md:table-cell" ]
[ String.join ", " item.folders |> text [ 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.map String.fromInt item.receivedSinceHours
|> Maybe.withDefault "-" |> Maybe.withDefault "-"
|> text |> text
, text " h" , 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
]
] ]

View File

@ -7,7 +7,6 @@ import Comp.MenuBar as MB
import Comp.PowerSearchInput import Comp.PowerSearchInput
import Comp.SearchMenu import Comp.SearchMenu
import Comp.SearchStatsView import Comp.SearchStatsView
import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemSelection import Data.ItemSelection
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
@ -53,19 +52,6 @@ viewContent flags settings model =
deleteSelectedDimmer : Model -> List (Html Msg) deleteSelectedDimmer : Model -> List (Html Msg)
deleteSelectedDimmer model = 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 case model.viewMode of
SelectView svm -> SelectView svm ->
case svm.confirmModal of case svm.confirmModal of

View File

@ -34,6 +34,7 @@ viewContent mid _ _ model =
[ id "content" [ id "content"
, class S.content , class S.content
] ]
[ div [ class "container mx-auto" ]
[ div [ class "px-0 flex flex-col" ] [ div [ class "px-0 flex flex-col" ]
[ div [ class "py-4" ] [ div [ class "py-4" ]
[ renderForm model [ renderForm model
@ -64,6 +65,7 @@ viewContent mid _ _ model =
, renderSuccessMsg (Util.Maybe.nonEmpty mid) model , renderSuccessMsg (Util.Maybe.nonEmpty mid) model
, renderUploads model , renderUploads model
] ]
]
renderForm : Model -> Html Msg renderForm : Model -> Html Msg

View File

@ -18,7 +18,7 @@ sidebarMenuItemActive =
content : String content : String
content = 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 sidebarLink : String

View File

@ -70,7 +70,7 @@ fi
set -o errexit -o pipefail -o noclobber -o nounset set -o errexit -o pipefail -o noclobber -o nounset
LOGIN_URL="$BASE_URL/api/v1/open/auth/login" 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" INSIGHT_URL="$BASE_URL/api/v1/sec/collective/insights"
DETAIL_URL="$BASE_URL/api/v1/sec/item" DETAIL_URL="$BASE_URL/api/v1/sec/item"
ATTACH_URL="$BASE_URL/api/v1/sec/attachment" ATTACH_URL="$BASE_URL/api/v1/sec/attachment"
@ -108,11 +108,11 @@ mcurl() {
errout "Login to Docspell." errout "Login to Docspell."
errout "Using url: $BASE_URL" errout "Using url: $BASE_URL"
if [ -z "$DS_USER" ]; then if [ -z "${DS_USER:-}" ]; then
errout -n "Account: " errout -n "Account: "
read DS_USER read DS_USER
fi fi
if [ -z "$DS_PASS" ]; then if [ -z "${DS_PASS:-}" ]; then
errout -n "Password: " errout -n "Password: "
read -s DS_PASS read -s DS_PASS
fi fi
@ -152,7 +152,7 @@ listItems() {
OFFSET="${1:-0}" OFFSET="${1:-0}"
LIMIT="${2:-50}" LIMIT="${2:-50}"
errout "Get next items with offset=$OFFSET, limit=$LIMIT" 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' mcurl -XPOST -H 'ContentType: application/json' -d "$REQ" "$SEARCH_URL" | "$JQ_CMD" -r '.groups[].items[]|.id'
} }