Allow to specify ordering when retrieving meta data

The query now searches in more fields. For example, when getting a
list of tags, the query is applied to the tag name *and* category.
When listing persons, the query now also looks in the associated
organization name.

This has been used to make some headers in the meta data tables
clickable to sort the list accordingly.

Refs: #965, #538
This commit is contained in:
eikek
2021-08-24 21:35:57 +02:00
parent 5926565267
commit cf88f5c2de
52 changed files with 1236 additions and 208 deletions

View File

@ -7,12 +7,13 @@
package docspell.backend.ops
import cats.data.EitherT
import cats.data.NonEmptyList
import cats.data.OptionT
import cats.data.{NonEmptyList => Nel}
import cats.effect._
import cats.implicits._
import docspell.backend.ops.OCustomFields.CustomFieldData
import docspell.backend.ops.OCustomFields.CustomFieldOrder
import docspell.backend.ops.OCustomFields.FieldValue
import docspell.backend.ops.OCustomFields.NewCustomField
import docspell.backend.ops.OCustomFields.RemoveValue
@ -33,7 +34,11 @@ import org.log4s.getLogger
trait OCustomFields[F[_]] {
/** Find all fields using an optional query on the name and label */
def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]]
def findAll(
coll: Ident,
nameQuery: Option[String],
order: CustomFieldOrder
): F[Vector[CustomFieldData]]
/** Find one field by its id */
def findById(coll: Ident, fieldId: Ident): F[Option[CustomFieldData]]
@ -50,13 +55,13 @@ trait OCustomFields[F[_]] {
/** Sets a value given a field an an item. Existing values are overwritten. */
def setValue(item: Ident, value: SetValue): F[SetValueResult]
def setValueMultiple(items: NonEmptyList[Ident], value: SetValue): F[SetValueResult]
def setValueMultiple(items: Nel[Ident], value: SetValue): F[SetValueResult]
/** Deletes a value for a given field an item. */
def deleteValue(in: RemoveValue): F[UpdateResult]
/** Finds all values to the given items */
def findAllValues(itemIds: NonEmptyList[Ident]): F[List[FieldValue]]
def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]]
}
object OCustomFields {
@ -96,10 +101,48 @@ object OCustomFields {
case class RemoveValue(
field: Ident,
item: NonEmptyList[Ident],
item: Nel[Ident],
collective: Ident
)
sealed trait CustomFieldOrder
object CustomFieldOrder {
import docspell.store.qb.DSL._
final case object NameAsc extends CustomFieldOrder
final case object NameDesc extends CustomFieldOrder
final case object LabelAsc extends CustomFieldOrder
final case object LabelDesc extends CustomFieldOrder
final case object TypeAsc extends CustomFieldOrder
final case object TypeDesc extends CustomFieldOrder
def parse(str: String): Either[String, CustomFieldOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case "label" => Right(LabelAsc)
case "-label" => Right(LabelDesc)
case "type" => Right(TypeAsc)
case "-type" => Right(TypeDesc)
case _ => Left(s"Unknown sort property for custom field: $str")
}
def parseOrDefault(str: String): CustomFieldOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(
order: CustomFieldOrder
)(field: RCustomField.Table) =
order match {
case NameAsc => Nel.of(field.name.asc)
case NameDesc => Nel.of(field.name.desc)
case LabelAsc => Nel.of(coalesce(field.label.s, field.name.s).asc)
case LabelDesc => Nel.of(coalesce(field.label.s, field.name.s).desc)
case TypeAsc => Nel.of(field.ftype.asc, field.name.asc)
case TypeDesc => Nel.of(field.ftype.desc, field.name.desc)
}
}
def apply[F[_]: Async](
store: Store[F]
): Resource[F, OCustomFields[F]] =
@ -107,14 +150,19 @@ object OCustomFields {
private[this] val logger = Logger.log4s[ConnectionIO](getLogger)
def findAllValues(itemIds: NonEmptyList[Ident]): F[List[FieldValue]] =
def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]] =
store.transact(QCustomField.findAllValues(itemIds))
def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]] =
def findAll(
coll: Ident,
nameQuery: Option[String],
order: CustomFieldOrder
): F[Vector[CustomFieldData]] =
store.transact(
QCustomField.findAllLike(
coll,
nameQuery.map(WildcardString.apply).flatMap(_.both)
nameQuery.map(WildcardString.apply).flatMap(_.both),
CustomFieldOrder(order)
)
)
@ -149,10 +197,10 @@ object OCustomFields {
}
def setValue(item: Ident, value: SetValue): F[SetValueResult] =
setValueMultiple(NonEmptyList.of(item), value)
setValueMultiple(Nel.of(item), value)
def setValueMultiple(
items: NonEmptyList[Ident],
items: Nel[Ident],
value: SetValue
): F[SetValueResult] =
(for {

View File

@ -6,6 +6,7 @@
package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource}
import cats.implicits._
@ -15,7 +16,11 @@ import docspell.store.{AddResult, Store}
trait OEquipment[F[_]] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]]
def findAll(
account: AccountId,
nameQuery: Option[String],
order: OEquipment.EquipmentOrder
): F[Vector[REquipment]]
def find(account: AccountId, id: Ident): F[Option[REquipment]]
@ -27,11 +32,39 @@ trait OEquipment[F[_]] {
}
object OEquipment {
import docspell.store.qb.DSL._
sealed trait EquipmentOrder
object EquipmentOrder {
final case object NameAsc extends EquipmentOrder
final case object NameDesc extends EquipmentOrder
def parse(str: String): Either[String, EquipmentOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case _ => Left(s"Unknown sort property for equipments: $str")
}
def parseOrDefault(str: String): EquipmentOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(order: EquipmentOrder)(table: REquipment.Table) = order match {
case NameAsc => NonEmptyList.of(table.name.asc)
case NameDesc => NonEmptyList.of(table.name.desc)
}
}
def apply[F[_]: Async](store: Store[F]): Resource[F, OEquipment[F]] =
Resource.pure[F, OEquipment[F]](new OEquipment[F] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] =
store.transact(REquipment.findAll(account.collective, nameQuery, _.name))
def findAll(
account: AccountId,
nameQuery: Option[String],
order: EquipmentOrder
): F[Vector[REquipment]] =
store.transact(
REquipment.findAll(account.collective, nameQuery, EquipmentOrder(order))
)
def find(account: AccountId, id: Ident): F[Option[REquipment]] =
store.transact(REquipment.findById(id)).map(_.filter(_.cid == account.collective))

View File

@ -6,6 +6,7 @@
package docspell.backend.ops
import cats.data.{NonEmptyList => Nel}
import cats.effect._
import docspell.common._
@ -18,7 +19,8 @@ trait OFolder[F[_]] {
def findAll(
account: AccountId,
ownerLogin: Option[Ident],
nameQuery: Option[String]
query: Option[String],
order: OFolder.FolderOrder
): F[Vector[OFolder.FolderItem]]
def findById(id: Ident, account: AccountId): F[Option[OFolder.FolderDetail]]
@ -50,6 +52,7 @@ trait OFolder[F[_]] {
}
object OFolder {
import docspell.store.qb.DSL._
type FolderChangeResult = QFolder.FolderChangeResult
val FolderChangeResult = QFolder.FolderChangeResult
@ -60,14 +63,45 @@ object OFolder {
type FolderDetail = QFolder.FolderDetail
val FolderDetail = QFolder.FolderDetail
sealed trait FolderOrder
object FolderOrder {
final case object NameAsc extends FolderOrder
final case object NameDesc extends FolderOrder
final case object OwnerAsc extends FolderOrder
final case object OwnerDesc extends FolderOrder
def parse(str: String): Either[String, FolderOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case "owner" => Right(OwnerAsc)
case "-owner" => Right(OwnerDesc)
case _ => Left(s"Unknown sort property for folder: $str")
}
def parseOrDefault(str: String): FolderOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(order: FolderOrder)(folder: RFolder.Table, user: RUser.Table) =
order match {
case NameAsc => Nel.of(folder.name.asc)
case NameDesc => Nel.of(folder.name.desc)
case OwnerAsc => Nel.of(user.login.asc, folder.name.asc)
case OwnerDesc => Nel.of(user.login.desc, folder.name.desc)
}
}
def apply[F[_]](store: Store[F]): Resource[F, OFolder[F]] =
Resource.pure[F, OFolder[F]](new OFolder[F] {
def findAll(
account: AccountId,
ownerLogin: Option[Ident],
nameQuery: Option[String]
query: Option[String],
order: FolderOrder
): F[Vector[FolderItem]] =
store.transact(QFolder.findAll(account, None, ownerLogin, nameQuery))
store.transact(
QFolder.findAll(account, None, ownerLogin, query, FolderOrder(order))
)
def findById(id: Ident, account: AccountId): F[Option[FolderDetail]] =
store.transact(QFolder.findById(id, account))

View File

@ -6,6 +6,7 @@
package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource}
import cats.implicits._
@ -16,10 +17,18 @@ import docspell.store.queries.QOrganization
import docspell.store.records._
trait OOrganization[F[_]] {
def findAllOrg(account: AccountId, query: Option[String]): F[Vector[OrgAndContacts]]
def findAllOrg(
account: AccountId,
query: Option[String],
order: OrganizationOrder
): F[Vector[OrgAndContacts]]
def findOrg(account: AccountId, orgId: Ident): F[Option[OrgAndContacts]]
def findAllOrgRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]]
def findAllOrgRefs(
account: AccountId,
nameQuery: Option[String],
order: OrganizationOrder
): F[Vector[IdRef]]
def addOrg(s: OrgAndContacts): F[AddResult]
@ -27,12 +36,17 @@ trait OOrganization[F[_]] {
def findAllPerson(
account: AccountId,
query: Option[String]
query: Option[String],
order: PersonOrder
): F[Vector[PersonAndContacts]]
def findPerson(account: AccountId, persId: Ident): F[Option[PersonAndContacts]]
def findAllPersonRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]]
def findAllPersonRefs(
account: AccountId,
nameQuery: Option[String],
order: PersonOrder
): F[Vector[IdRef]]
/** Add a new person with their contacts. The additional organization is ignored. */
def addPerson(s: PersonAndContacts): F[AddResult]
@ -46,6 +60,7 @@ trait OOrganization[F[_]] {
}
object OOrganization {
import docspell.store.qb.DSL._
case class OrgAndContacts(org: ROrganization, contacts: Seq[RContact])
@ -55,15 +70,79 @@ object OOrganization {
contacts: Seq[RContact]
)
sealed trait OrganizationOrder
object OrganizationOrder {
final case object NameAsc extends OrganizationOrder
final case object NameDesc extends OrganizationOrder
def parse(str: String): Either[String, OrganizationOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case _ => Left(s"Unknown sort property for organization: $str")
}
def parseOrDefault(str: String): OrganizationOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(order: OrganizationOrder)(table: ROrganization.Table) =
order match {
case NameAsc => NonEmptyList.of(table.name.asc)
case NameDesc => NonEmptyList.of(table.name.desc)
}
}
sealed trait PersonOrder
object PersonOrder {
final case object NameAsc extends PersonOrder
final case object NameDesc extends PersonOrder
final case object OrgAsc extends PersonOrder
final case object OrgDesc extends PersonOrder
def parse(str: String): Either[String, PersonOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case "org" => Right(OrgAsc)
case "-org" => Right(OrgDesc)
case _ => Left(s"Unknown sort property for person: $str")
}
def parseOrDefault(str: String): PersonOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(
order: PersonOrder
)(person: RPerson.Table, org: ROrganization.Table) =
order match {
case NameAsc => NonEmptyList.of(person.name.asc)
case NameDesc => NonEmptyList.of(person.name.desc)
case OrgAsc => NonEmptyList.of(org.name.asc)
case OrgDesc => NonEmptyList.of(org.name.desc)
}
private[ops] def nameOnly(order: PersonOrder)(person: RPerson.Table) =
order match {
case NameAsc => NonEmptyList.of(person.name.asc)
case NameDesc => NonEmptyList.of(person.name.desc)
case OrgAsc => NonEmptyList.of(person.name.asc)
case OrgDesc => NonEmptyList.of(person.name.asc)
}
}
def apply[F[_]: Async](store: Store[F]): Resource[F, OOrganization[F]] =
Resource.pure[F, OOrganization[F]](new OOrganization[F] {
def findAllOrg(
account: AccountId,
query: Option[String]
query: Option[String],
order: OrganizationOrder
): F[Vector[OrgAndContacts]] =
store
.transact(QOrganization.findOrgAndContact(account.collective, query, _.name))
.transact(
QOrganization
.findOrgAndContact(account.collective, query, OrganizationOrder(order))
)
.map { case (org, cont) => OrgAndContacts(org, cont) }
.compile
.toVector
@ -75,9 +154,16 @@ object OOrganization {
def findAllOrgRefs(
account: AccountId,
nameQuery: Option[String]
nameQuery: Option[String],
order: OrganizationOrder
): F[Vector[IdRef]] =
store.transact(ROrganization.findAllRef(account.collective, nameQuery, _.name))
store.transact(
ROrganization.findAllRef(
account.collective,
nameQuery,
OrganizationOrder(order)
)
)
def addOrg(s: OrgAndContacts): F[AddResult] =
QOrganization.addOrg(s.org, s.contacts, s.org.cid)(store)
@ -87,10 +173,14 @@ object OOrganization {
def findAllPerson(
account: AccountId,
query: Option[String]
query: Option[String],
order: PersonOrder
): F[Vector[PersonAndContacts]] =
store
.transact(QOrganization.findPersonAndContact(account.collective, query, _.name))
.transact(
QOrganization
.findPersonAndContact(account.collective, query, PersonOrder(order))
)
.map { case (person, org, cont) => PersonAndContacts(person, org, cont) }
.compile
.toVector
@ -102,9 +192,12 @@ object OOrganization {
def findAllPersonRefs(
account: AccountId,
nameQuery: Option[String]
nameQuery: Option[String],
order: PersonOrder
): F[Vector[IdRef]] =
store.transact(RPerson.findAllRef(account.collective, nameQuery, _.name))
store.transact(
RPerson.findAllRef(account.collective, nameQuery, PersonOrder.nameOnly(order))
)
def addPerson(s: PersonAndContacts): F[AddResult] =
QOrganization.addPerson(s.person, s.contacts, s.person.cid)(store)

View File

@ -6,6 +6,7 @@
package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource}
import cats.implicits._
@ -16,7 +17,11 @@ import docspell.store.{AddResult, Store}
trait OTag[F[_]] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]]
def findAll(
account: AccountId,
query: Option[String],
order: OTag.TagOrder
): F[Vector[RTag]]
def add(s: RTag): F[AddResult]
@ -30,11 +35,43 @@ trait OTag[F[_]] {
}
object OTag {
import docspell.store.qb.DSL._
sealed trait TagOrder
object TagOrder {
final case object NameAsc extends TagOrder
final case object NameDesc extends TagOrder
final case object CategoryAsc extends TagOrder
final case object CategoryDesc extends TagOrder
def parse(str: String): Either[String, TagOrder] =
str.toLowerCase match {
case "name" => Right(NameAsc)
case "-name" => Right(NameDesc)
case "category" => Right(CategoryAsc)
case "-category" => Right(CategoryDesc)
case _ => Left(s"Unknown sort property for tags: $str")
}
def parseOrDefault(str: String): TagOrder =
parse(str).toOption.getOrElse(NameAsc)
private[ops] def apply(order: TagOrder)(table: RTag.Table) = order match {
case NameAsc => NonEmptyList.of(table.name.asc)
case CategoryAsc => NonEmptyList.of(table.category.asc, table.name.asc)
case NameDesc => NonEmptyList.of(table.name.desc)
case CategoryDesc => NonEmptyList.of(table.category.desc, table.name.desc)
}
}
def apply[F[_]: Async](store: Store[F]): Resource[F, OTag[F]] =
Resource.pure[F, OTag[F]](new OTag[F] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] =
store.transact(RTag.findAll(account.collective, nameQuery, _.name))
def findAll(
account: AccountId,
query: Option[String],
order: TagOrder
): F[Vector[RTag]] =
store.transact(RTag.findAll(account.collective, query, TagOrder(order)))
def add(t: RTag): F[AddResult] = {
def insert = RTag.insert(t)