mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-01 15:35:06 +00:00
Remove deprecated search routes and some refactoring
This commit is contained in:
parent
bd5dba9f8e
commit
cc38b850a6
modules
backend/src/main/scala/docspell/backend/ops
joex/src/main/scala/docspell/joex/notify
query/shared/src/main/scala/docspell/query
restapi/src/main/resources
restserver/src/main/scala/docspell/restserver
store/src/main/scala/docspell/store
webapp/src/main/elm
tools/export-files
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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._
|
||||||
|
|
||||||
@ -10,6 +10,9 @@ import docspell.joex.mail.EmilHeader
|
|||||||
import docspell.joex.scheduler.{Context, Task}
|
import docspell.joex.scheduler.{Context, Task}
|
||||||
import docspell.store.queries.QItem
|
import docspell.store.queries.QItem
|
||||||
import docspell.store.records._
|
import docspell.store.records._
|
||||||
|
import docspell.query.Date
|
||||||
|
import docspell.query.ItemQuery._
|
||||||
|
import docspell.query.ItemQueryDsl._
|
||||||
|
|
||||||
import emil._
|
import emil._
|
||||||
import emil.builder._
|
import emil.builder._
|
||||||
@ -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 <-
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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] =
|
||||||
|
@ -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 ]
|
||||||
@ -5268,104 +5178,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.
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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._
|
||||||
@ -102,85 +101,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 +124,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 +460,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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -473,6 +473,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 +509,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 +519,8 @@ view2 extraClasses settings model =
|
|||||||
]
|
]
|
||||||
|
|
||||||
else
|
else
|
||||||
[]
|
[ startOnceBtn
|
||||||
|
]
|
||||||
, rootClasses = "mb-4"
|
, rootClasses = "mb-4"
|
||||||
}
|
}
|
||||||
, div
|
, div
|
||||||
|
@ -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 ""
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user