mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
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:
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user