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 package docspell.backend.ops
import cats.data.EitherT import cats.data.EitherT
import cats.data.NonEmptyList
import cats.data.OptionT import cats.data.OptionT
import cats.data.{NonEmptyList => Nel}
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.backend.ops.OCustomFields.CustomFieldData import docspell.backend.ops.OCustomFields.CustomFieldData
import docspell.backend.ops.OCustomFields.CustomFieldOrder
import docspell.backend.ops.OCustomFields.FieldValue import docspell.backend.ops.OCustomFields.FieldValue
import docspell.backend.ops.OCustomFields.NewCustomField import docspell.backend.ops.OCustomFields.NewCustomField
import docspell.backend.ops.OCustomFields.RemoveValue import docspell.backend.ops.OCustomFields.RemoveValue
@ -33,7 +34,11 @@ import org.log4s.getLogger
trait OCustomFields[F[_]] { trait OCustomFields[F[_]] {
/** Find all fields using an optional query on the name and label */ /** 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 */ /** Find one field by its id */
def findById(coll: Ident, fieldId: Ident): F[Option[CustomFieldData]] 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. */ /** Sets a value given a field an an item. Existing values are overwritten. */
def setValue(item: Ident, value: SetValue): F[SetValueResult] 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. */ /** Deletes a value for a given field an item. */
def deleteValue(in: RemoveValue): F[UpdateResult] def deleteValue(in: RemoveValue): F[UpdateResult]
/** Finds all values to the given items */ /** 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 { object OCustomFields {
@ -96,10 +101,48 @@ object OCustomFields {
case class RemoveValue( case class RemoveValue(
field: Ident, field: Ident,
item: NonEmptyList[Ident], item: Nel[Ident],
collective: 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]( def apply[F[_]: Async](
store: Store[F] store: Store[F]
): Resource[F, OCustomFields[F]] = ): Resource[F, OCustomFields[F]] =
@ -107,14 +150,19 @@ object OCustomFields {
private[this] val logger = Logger.log4s[ConnectionIO](getLogger) 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)) 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( store.transact(
QCustomField.findAllLike( QCustomField.findAllLike(
coll, 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] = def setValue(item: Ident, value: SetValue): F[SetValueResult] =
setValueMultiple(NonEmptyList.of(item), value) setValueMultiple(Nel.of(item), value)
def setValueMultiple( def setValueMultiple(
items: NonEmptyList[Ident], items: Nel[Ident],
value: SetValue value: SetValue
): F[SetValueResult] = ): F[SetValueResult] =
(for { (for {

View File

@ -6,6 +6,7 @@
package docspell.backend.ops package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource} import cats.effect.{Async, Resource}
import cats.implicits._ import cats.implicits._
@ -15,7 +16,11 @@ import docspell.store.{AddResult, Store}
trait OEquipment[F[_]] { 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]] def find(account: AccountId, id: Ident): F[Option[REquipment]]
@ -27,11 +32,39 @@ trait OEquipment[F[_]] {
} }
object OEquipment { 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]] = def apply[F[_]: Async](store: Store[F]): Resource[F, OEquipment[F]] =
Resource.pure[F, OEquipment[F]](new OEquipment[F] { Resource.pure[F, OEquipment[F]](new OEquipment[F] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] = def findAll(
store.transact(REquipment.findAll(account.collective, nameQuery, _.name)) 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]] = def find(account: AccountId, id: Ident): F[Option[REquipment]] =
store.transact(REquipment.findById(id)).map(_.filter(_.cid == account.collective)) store.transact(REquipment.findById(id)).map(_.filter(_.cid == account.collective))

View File

@ -6,6 +6,7 @@
package docspell.backend.ops package docspell.backend.ops
import cats.data.{NonEmptyList => Nel}
import cats.effect._ import cats.effect._
import docspell.common._ import docspell.common._
@ -18,7 +19,8 @@ trait OFolder[F[_]] {
def findAll( def findAll(
account: AccountId, account: AccountId,
ownerLogin: Option[Ident], ownerLogin: Option[Ident],
nameQuery: Option[String] query: Option[String],
order: OFolder.FolderOrder
): F[Vector[OFolder.FolderItem]] ): F[Vector[OFolder.FolderItem]]
def findById(id: Ident, account: AccountId): F[Option[OFolder.FolderDetail]] def findById(id: Ident, account: AccountId): F[Option[OFolder.FolderDetail]]
@ -50,6 +52,7 @@ trait OFolder[F[_]] {
} }
object OFolder { object OFolder {
import docspell.store.qb.DSL._
type FolderChangeResult = QFolder.FolderChangeResult type FolderChangeResult = QFolder.FolderChangeResult
val FolderChangeResult = QFolder.FolderChangeResult val FolderChangeResult = QFolder.FolderChangeResult
@ -60,14 +63,45 @@ object OFolder {
type FolderDetail = QFolder.FolderDetail type FolderDetail = QFolder.FolderDetail
val 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]] = def apply[F[_]](store: Store[F]): Resource[F, OFolder[F]] =
Resource.pure[F, OFolder[F]](new OFolder[F] { Resource.pure[F, OFolder[F]](new OFolder[F] {
def findAll( def findAll(
account: AccountId, account: AccountId,
ownerLogin: Option[Ident], ownerLogin: Option[Ident],
nameQuery: Option[String] query: Option[String],
order: FolderOrder
): F[Vector[FolderItem]] = ): 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]] = def findById(id: Ident, account: AccountId): F[Option[FolderDetail]] =
store.transact(QFolder.findById(id, account)) store.transact(QFolder.findById(id, account))

View File

@ -6,6 +6,7 @@
package docspell.backend.ops package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource} import cats.effect.{Async, Resource}
import cats.implicits._ import cats.implicits._
@ -16,10 +17,18 @@ import docspell.store.queries.QOrganization
import docspell.store.records._ import docspell.store.records._
trait OOrganization[F[_]] { 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 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] def addOrg(s: OrgAndContacts): F[AddResult]
@ -27,12 +36,17 @@ trait OOrganization[F[_]] {
def findAllPerson( def findAllPerson(
account: AccountId, account: AccountId,
query: Option[String] query: Option[String],
order: PersonOrder
): F[Vector[PersonAndContacts]] ): F[Vector[PersonAndContacts]]
def findPerson(account: AccountId, persId: Ident): F[Option[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. */ /** Add a new person with their contacts. The additional organization is ignored. */
def addPerson(s: PersonAndContacts): F[AddResult] def addPerson(s: PersonAndContacts): F[AddResult]
@ -46,6 +60,7 @@ trait OOrganization[F[_]] {
} }
object OOrganization { object OOrganization {
import docspell.store.qb.DSL._
case class OrgAndContacts(org: ROrganization, contacts: Seq[RContact]) case class OrgAndContacts(org: ROrganization, contacts: Seq[RContact])
@ -55,15 +70,79 @@ object OOrganization {
contacts: Seq[RContact] 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]] = def apply[F[_]: Async](store: Store[F]): Resource[F, OOrganization[F]] =
Resource.pure[F, OOrganization[F]](new OOrganization[F] { Resource.pure[F, OOrganization[F]](new OOrganization[F] {
def findAllOrg( def findAllOrg(
account: AccountId, account: AccountId,
query: Option[String] query: Option[String],
order: OrganizationOrder
): F[Vector[OrgAndContacts]] = ): F[Vector[OrgAndContacts]] =
store store
.transact(QOrganization.findOrgAndContact(account.collective, query, _.name)) .transact(
QOrganization
.findOrgAndContact(account.collective, query, OrganizationOrder(order))
)
.map { case (org, cont) => OrgAndContacts(org, cont) } .map { case (org, cont) => OrgAndContacts(org, cont) }
.compile .compile
.toVector .toVector
@ -75,9 +154,16 @@ object OOrganization {
def findAllOrgRefs( def findAllOrgRefs(
account: AccountId, account: AccountId,
nameQuery: Option[String] nameQuery: Option[String],
order: OrganizationOrder
): F[Vector[IdRef]] = ): 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] = def addOrg(s: OrgAndContacts): F[AddResult] =
QOrganization.addOrg(s.org, s.contacts, s.org.cid)(store) QOrganization.addOrg(s.org, s.contacts, s.org.cid)(store)
@ -87,10 +173,14 @@ object OOrganization {
def findAllPerson( def findAllPerson(
account: AccountId, account: AccountId,
query: Option[String] query: Option[String],
order: PersonOrder
): F[Vector[PersonAndContacts]] = ): F[Vector[PersonAndContacts]] =
store 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) } .map { case (person, org, cont) => PersonAndContacts(person, org, cont) }
.compile .compile
.toVector .toVector
@ -102,9 +192,12 @@ object OOrganization {
def findAllPersonRefs( def findAllPersonRefs(
account: AccountId, account: AccountId,
nameQuery: Option[String] nameQuery: Option[String],
order: PersonOrder
): F[Vector[IdRef]] = ): 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] = def addPerson(s: PersonAndContacts): F[AddResult] =
QOrganization.addPerson(s.person, s.contacts, s.person.cid)(store) QOrganization.addPerson(s.person, s.contacts, s.person.cid)(store)

View File

@ -6,6 +6,7 @@
package docspell.backend.ops package docspell.backend.ops
import cats.data.NonEmptyList
import cats.effect.{Async, Resource} import cats.effect.{Async, Resource}
import cats.implicits._ import cats.implicits._
@ -16,7 +17,11 @@ import docspell.store.{AddResult, Store}
trait OTag[F[_]] { 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] def add(s: RTag): F[AddResult]
@ -30,11 +35,43 @@ trait OTag[F[_]] {
} }
object OTag { 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]] = def apply[F[_]: Async](store: Store[F]): Resource[F, OTag[F]] =
Resource.pure[F, OTag[F]](new OTag[F] { Resource.pure[F, OTag[F]](new OTag[F] {
def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] = def findAll(
store.transact(RTag.findAll(account.collective, nameQuery, _.name)) 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 add(t: RTag): F[AddResult] = {
def insert = RTag.insert(t) def insert = RTag.insert(t)

View File

@ -501,11 +501,14 @@ paths:
tags: [ Tags ] tags: [ Tags ]
summary: Get a list of tags summary: Get a list of tags
description: | description: |
Return a list of all configured tags. Return a list of all configured tags. The `sort` query
parameter is optional and can specify how the list is sorted.
Possible values are: `name`, `-name`, `category`, `-category`.
security: security:
- authTokenHeader: [] - authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/sort"
responses: responses:
200: 200:
description: Ok description: Ok
@ -579,12 +582,16 @@ paths:
tags: [ Organization ] tags: [ Organization ]
summary: Get a list of organizations. summary: Get a list of organizations.
description: | description: |
Return a list of all organizations. Only name and id are returned. Return a list of all organizations. Only name and id are
returned. If `full` is specified, the list contains all
organization data. The `sort` parameter can be either `name`
or `-name` to specify the order.
security: security:
- authTokenHeader: [] - authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/full" - $ref: "#/components/parameters/full"
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/sort"
responses: responses:
200: 200:
description: Ok description: Ok
@ -677,12 +684,17 @@ paths:
tags: [ Person ] tags: [ Person ]
summary: Get a list of persons. summary: Get a list of persons.
description: | description: |
Return a list of all persons. Only name and id are returned. Return a list of all persons. Only name and id are returned
unless the `full` parameter is specified. The `sort` parameter
can be used to control the order of the result. Use one of:
`name`, `-name`, `org`, `-org`. Note that order by `org` only
works when retrieving the full list.
security: security:
- authTokenHeader: [] - authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/full" - $ref: "#/components/parameters/full"
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/sort"
responses: responses:
200: 200:
description: Ok description: Ok
@ -775,11 +787,14 @@ paths:
tags: [ Equipment ] tags: [ Equipment ]
summary: Get a list of equipments summary: Get a list of equipments
description: | description: |
Return a list of all configured equipments. Return a list of all configured equipments. The sort query
parameter is optional and can be one of `name` or `-name` to
sort the list of equipments.
security: security:
- authTokenHeader: [] - authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/sort"
responses: responses:
200: 200:
description: Ok description: Ok
@ -3771,11 +3786,15 @@ paths:
tags: [ Custom Fields ] tags: [ Custom Fields ]
summary: Get all defined custom fields. summary: Get all defined custom fields.
description: | description: |
Get all custom fields defined for the current collective. Get all custom fields defined for the current collective. The
`sort` parameter can be used to control the order of the
returned list. It can take a value from: `name`, `-name`,
`label`, `-label`, `type`, `-type`.
security: security:
- authTokenHeader: [] - authTokenHeader: []
parameters: parameters:
- $ref: "#/components/parameters/q" - $ref: "#/components/parameters/q"
- $ref: "#/components/parameters/sort"
responses: responses:
200: 200:
description: Ok description: Ok
@ -5985,6 +6004,14 @@ components:
schema: schema:
type: integer type: integer
format: int32 format: int32
sort:
name: sort
in: query
required: false
description: |
How to sort the returned list
schema:
type: string
withDetails: withDetails:
name: withDetails name: withDetails
in: query in: query

View File

@ -6,6 +6,11 @@
package docspell.restserver.http4s package docspell.restserver.http4s
import docspell.backend.ops.OCustomFields.CustomFieldOrder
import docspell.backend.ops.OEquipment.EquipmentOrder
import docspell.backend.ops.OFolder.FolderOrder
import docspell.backend.ops.OOrganization.{OrganizationOrder, PersonOrder}
import docspell.backend.ops.OTag.TagOrder
import docspell.common.ContactKind import docspell.common.ContactKind
import docspell.common.SearchMode import docspell.common.SearchMode
@ -29,6 +34,36 @@ object QueryParam {
SearchMode.fromString(str).left.map(s => ParseFailure(str, s)) SearchMode.fromString(str).left.map(s => ParseFailure(str, s))
) )
implicit val tagOrderDecoder: QueryParamDecoder[TagOrder] =
QueryParamDecoder[String].emap(str =>
TagOrder.parse(str).left.map(s => ParseFailure(str, s))
)
implicit val euqipOrderDecoder: QueryParamDecoder[EquipmentOrder] =
QueryParamDecoder[String].emap(str =>
EquipmentOrder.parse(str).left.map(s => ParseFailure(str, s))
)
implicit val orgOrderDecoder: QueryParamDecoder[OrganizationOrder] =
QueryParamDecoder[String].emap(str =>
OrganizationOrder.parse(str).left.map(s => ParseFailure(str, s))
)
implicit val personOrderDecoder: QueryParamDecoder[PersonOrder] =
QueryParamDecoder[String].emap(str =>
PersonOrder.parse(str).left.map(s => ParseFailure(str, s))
)
implicit val folderOrderDecoder: QueryParamDecoder[FolderOrder] =
QueryParamDecoder[String].emap(str =>
FolderOrder.parse(str).left.map(s => ParseFailure(str, s))
)
implicit val customFieldOrderDecoder: QueryParamDecoder[CustomFieldOrder] =
QueryParamDecoder[String].emap(str =>
CustomFieldOrder.parse(str).left.map(s => ParseFailure(str, s))
)
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full") object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning") object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
@ -42,6 +77,12 @@ object QueryParam {
object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset") object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset")
object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails") object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails")
object SearchKind extends OptionalQueryParamDecoderMatcher[SearchMode]("searchMode") object SearchKind extends OptionalQueryParamDecoderMatcher[SearchMode]("searchMode")
object TagSort extends OptionalQueryParamDecoderMatcher[TagOrder]("sort")
object EquipSort extends OptionalQueryParamDecoderMatcher[EquipmentOrder]("sort")
object OrgSort extends OptionalQueryParamDecoderMatcher[OrganizationOrder]("sort")
object PersonSort extends OptionalQueryParamDecoderMatcher[PersonOrder]("sort")
object FolderSort extends OptionalQueryParamDecoderMatcher[FolderOrder]("sort")
object FieldSort extends OptionalQueryParamDecoderMatcher[CustomFieldOrder]("sort")
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback") object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
} }

View File

@ -13,7 +13,7 @@ import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.backend.ops.OCustomFields import docspell.backend.ops.OCustomFields
import docspell.backend.ops.OCustomFields.CustomFieldData import docspell.backend.ops.OCustomFields.{CustomFieldData, CustomFieldOrder}
import docspell.common._ import docspell.common._
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.conv.Conversions import docspell.restserver.conv.Conversions
@ -34,9 +34,14 @@ object CustomFieldRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.QueryOpt(param) => case GET -> Root :? QueryParam.QueryOpt(param) +& QueryParam.FieldSort(sort) =>
val order = sort.getOrElse(CustomFieldOrder.NameAsc)
for { for {
fs <- backend.customFields.findAll(user.account.collective, param.map(_.q)) fs <- backend.customFields.findAll(
user.account.collective,
param.map(_.q),
order
)
res <- Ok(CustomFieldList(fs.map(convertField).toList)) res <- Ok(CustomFieldList(fs.map(convertField).toList))
} yield res } yield res

View File

@ -12,6 +12,7 @@ import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.backend.ops.OEquipment
import docspell.common.Ident import docspell.common.Ident
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.conv.Conversions._ import docspell.restserver.conv.Conversions._
@ -29,9 +30,13 @@ object EquipmentRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.QueryOpt(q) => case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.EquipSort(sort) =>
for { for {
data <- backend.equipment.findAll(user.account, q.map(_.q)) data <- backend.equipment.findAll(
user.account,
q.map(_.q),
sort.getOrElse(OEquipment.EquipmentOrder.NameAsc)
)
resp <- Ok(EquipmentList(data.map(mkEquipment).toList)) resp <- Ok(EquipmentList(data.map(mkEquipment).toList))
} yield resp } yield resp

View File

@ -31,11 +31,13 @@ object FolderRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.OwningOpt(owning) => case GET -> Root :? QueryParam.QueryOpt(q) :?
QueryParam.OwningOpt(owning) +& QueryParam.FolderSort(sort) =>
val order = sort.getOrElse(OFolder.FolderOrder.NameAsc)
val login = val login =
owning.filter(identity).map(_ => user.account.user) owning.filter(identity).map(_ => user.account.user)
for { for {
all <- backend.folder.findAll(user.account, login, q.map(_.q)) all <- backend.folder.findAll(user.account, login, q.map(_.q), order)
resp <- Ok(FolderList(all.map(mkFolder).toList)) resp <- Ok(FolderList(all.map(mkFolder).toList))
} yield resp } yield resp

View File

@ -12,6 +12,7 @@ import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.backend.ops.OOrganization.OrganizationOrder
import docspell.common.Ident import docspell.common.Ident
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.conv.Conversions._ import docspell.restserver.conv.Conversions._
@ -29,15 +30,21 @@ object OrganizationRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.FullOpt(full) +& QueryParam.QueryOpt(q) => case GET -> Root :? QueryParam.FullOpt(full) +&
QueryParam.QueryOpt(q) +& QueryParam.OrgSort(sort) =>
val order = sort.getOrElse(OrganizationOrder.NameAsc)
if (full.getOrElse(false)) if (full.getOrElse(false))
for { for {
data <- backend.organization.findAllOrg(user.account, q.map(_.q)) data <- backend.organization.findAllOrg(
user.account,
q.map(_.q),
order
)
resp <- Ok(OrganizationList(data.map(mkOrg).toList)) resp <- Ok(OrganizationList(data.map(mkOrg).toList))
} yield resp } yield resp
else else
for { for {
data <- backend.organization.findAllOrgRefs(user.account, q.map(_.q)) data <- backend.organization.findAllOrgRefs(user.account, q.map(_.q), order)
resp <- Ok(ReferenceList(data.map(mkIdName).toList)) resp <- Ok(ReferenceList(data.map(mkIdName).toList))
} yield resp } yield resp

View File

@ -12,6 +12,7 @@ import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.backend.ops.OOrganization
import docspell.common.Ident import docspell.common.Ident
import docspell.common.syntax.all._ import docspell.common.syntax.all._
import docspell.restapi.model._ import docspell.restapi.model._
@ -32,15 +33,25 @@ object PersonRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.FullOpt(full) +& QueryParam.QueryOpt(q) => case GET -> Root :? QueryParam.FullOpt(full) +&
QueryParam.QueryOpt(q) +& QueryParam.PersonSort(sort) =>
val order = sort.getOrElse(OOrganization.PersonOrder.NameAsc)
if (full.getOrElse(false)) if (full.getOrElse(false))
for { for {
data <- backend.organization.findAllPerson(user.account, q.map(_.q)) data <- backend.organization.findAllPerson(
user.account,
q.map(_.q),
order
)
resp <- Ok(PersonList(data.map(mkPerson).toList)) resp <- Ok(PersonList(data.map(mkPerson).toList))
} yield resp } yield resp
else else
for { for {
data <- backend.organization.findAllPersonRefs(user.account, q.map(_.q)) data <- backend.organization.findAllPersonRefs(
user.account,
q.map(_.q),
order
)
resp <- Ok(ReferenceList(data.map(mkIdName).toList)) resp <- Ok(ReferenceList(data.map(mkIdName).toList))
} yield resp } yield resp

View File

@ -11,6 +11,7 @@ import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken import docspell.backend.auth.AuthToken
import docspell.backend.ops.OTag.TagOrder
import docspell.common.Ident import docspell.common.Ident
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.conv.Conversions._ import docspell.restserver.conv.Conversions._
@ -28,9 +29,13 @@ object TagRoutes {
import dsl._ import dsl._
HttpRoutes.of { HttpRoutes.of {
case GET -> Root :? QueryParam.QueryOpt(q) => case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.TagSort(sort) =>
for { for {
all <- backend.tag.findAll(user.account, q.map(_.q)) all <- backend.tag.findAll(
user.account,
q.map(_.q),
sort.getOrElse(TagOrder.NameAsc)
)
resp <- Ok(TagList(all.size, all.map(mkTag).toList)) resp <- Ok(TagList(all.size, all.map(mkTag).toList))
} yield resp } yield resp

View File

@ -12,6 +12,7 @@ case class Column[A](name: String, table: TableDef) {
def cast[B]: Column[B] = def cast[B]: Column[B] =
this.asInstanceOf[Column[B]] this.asInstanceOf[Column[B]]
} }
object Column {} object Column {}

View File

@ -303,6 +303,12 @@ trait DSL extends DoobieMeta {
def as(otherCol: Column[_]): SelectExpr = def as(otherCol: Column[_]): SelectExpr =
SelectExpr.SelectFun(dbf, Some(otherCol.name)) SelectExpr.SelectFun(dbf, Some(otherCol.name))
def asc: OrderBy =
OrderBy(SelectExpr.SelectFun(dbf, None), OrderBy.OrderType.Asc)
def desc: OrderBy =
OrderBy(SelectExpr.SelectFun(dbf, None), OrderBy.OrderType.Desc)
def ===[A](value: A)(implicit P: Put[A]): Condition = def ===[A](value: A)(implicit P: Put[A]): Condition =
Condition.CompareFVal(dbf.s, Operator.Eq, value) Condition.CompareFVal(dbf.s, Operator.Eq, value)

View File

@ -135,6 +135,9 @@ object Select {
def orderBy(ob: OrderBy, obs: OrderBy*): Ordered = def orderBy(ob: OrderBy, obs: OrderBy*): Ordered =
Ordered(this, ob, obs.toVector) Ordered(this, ob, obs.toVector)
def orderBy(ob: Nel[OrderBy]): Ordered =
Ordered(this, ob.head, ob.tail.toVector)
} }
case class RawSelect(fragment: Fragment) extends Select { case class RawSelect(fragment: Fragment) extends Select {

View File

@ -24,9 +24,10 @@ object QCustomField {
def findAllLike( def findAllLike(
coll: Ident, coll: Ident,
nameQuery: Option[String] nameQuery: Option[String],
order: RCustomField.Table => Nel[OrderBy]
): ConnectionIO[Vector[CustomFieldData]] = ): ConnectionIO[Vector[CustomFieldData]] =
findFragment(coll, nameQuery, None).build.query[CustomFieldData].to[Vector] findFragment(coll, nameQuery, None, order).build.query[CustomFieldData].to[Vector]
def findById(field: Ident, collective: Ident): ConnectionIO[Option[CustomFieldData]] = def findById(field: Ident, collective: Ident): ConnectionIO[Option[CustomFieldData]] =
findFragment(collective, None, field.some).build.query[CustomFieldData].option findFragment(collective, None, field.some).build.query[CustomFieldData].option
@ -34,7 +35,8 @@ object QCustomField {
private def findFragment( private def findFragment(
coll: Ident, coll: Ident,
nameQuery: Option[String], nameQuery: Option[String],
fieldId: Option[Ident] fieldId: Option[Ident],
order: RCustomField.Table => Nel[OrderBy] = t => Nel.of(t.name.asc)
): Select = { ): Select = {
val nameFilter = nameQuery.map { q => val nameFilter = nameQuery.map { q =>
f.name.likes(q) || (f.label.isNotNull && f.label.like(q)) f.name.likes(q) || (f.label.isNotNull && f.label.like(q))
@ -46,7 +48,7 @@ object QCustomField {
.leftJoin(v, f.id === v.field), .leftJoin(v, f.id === v.field),
f.cid === coll &&? nameFilter &&? fieldId.map(fid => f.id === fid), f.cid === coll &&? nameFilter &&? fieldId.map(fid => f.id === fid),
GroupBy(f.all) GroupBy(f.all)
) ).orderBy(order(f))
} }
final case class FieldValue( final case class FieldValue(

View File

@ -7,6 +7,7 @@
package docspell.store.queries package docspell.store.queries
import cats.data.OptionT import cats.data.OptionT
import cats.data.{NonEmptyList => Nel}
import cats.implicits._ import cats.implicits._
import docspell.common._ import docspell.common._
@ -156,8 +157,11 @@ object QFolder {
).query[IdRef].to[Vector] ).query[IdRef].to[Vector]
(for { (for {
folder <- OptionT(findAll(account, Some(id), None, None).map(_.headOption)) folder <- OptionT(
memb <- OptionT.liftF(memberQ) findAll(account, Some(id), None, None, (ft, _) => Nel.of(ft.name.asc))
.map(_.headOption)
)
memb <- OptionT.liftF(memberQ)
} yield folder.withMembers(memb.toList)).value } yield folder.withMembers(memb.toList)).value
} }
@ -165,7 +169,8 @@ object QFolder {
account: AccountId, account: AccountId,
idQ: Option[Ident], idQ: Option[Ident],
ownerLogin: Option[Ident], ownerLogin: Option[Ident],
nameQ: Option[String] nameQ: Option[String],
order: (RFolder.Table, RUser.Table) => Nel[OrderBy]
): ConnectionIO[Vector[FolderItem]] = { ): ConnectionIO[Vector[FolderItem]] = {
// with memberlogin as // with memberlogin as
// (select m.folder_id,u.login // (select m.folder_id,u.login
@ -239,7 +244,7 @@ object QFolder {
nameQ.map(q => folder.name.like(s"%${q.toLowerCase}%")) &&? nameQ.map(q => folder.name.like(s"%${q.toLowerCase}%")) &&?
ownerLogin.map(login => user.login === login) ownerLogin.map(login => user.login === login)
) )
).orderBy(folder.name.asc) ).orderBy(order(folder, user))
).build.query[FolderItem].to[Vector] ).build.query[FolderItem].to[Vector]
} }

View File

@ -6,7 +6,7 @@
package docspell.store.queries package docspell.store.queries
import cats.data.NonEmptyList import cats.data.{NonEmptyList => Nel}
import cats.implicits._ import cats.implicits._
import fs2._ import fs2._
@ -27,7 +27,7 @@ object QOrganization {
def findOrgAndContact( def findOrgAndContact(
coll: Ident, coll: Ident,
query: Option[String], query: Option[String],
order: ROrganization.Table => Column[_] order: ROrganization.Table => Nel[OrderBy]
): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = { ): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = {
val valFilter = query.map { q => val valFilter = query.map { q =>
val v = s"%$q%" val v = s"%$q%"
@ -74,18 +74,18 @@ object QOrganization {
def findPersonAndContact( def findPersonAndContact(
coll: Ident, coll: Ident,
query: Option[String], query: Option[String],
order: RPerson.Table => Column[_] order: (RPerson.Table, ROrganization.Table) => Nel[OrderBy]
): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = { ): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = {
val valFilter = query val valFilter = query
.map(s => s"%$s%") .map(s => s"%$s%")
.map(v => c.value.like(v) || p.name.like(v) || p.notes.like(v)) .map(v => c.value.like(v) || p.name.like(v) || org.name.like(v) || p.notes.like(v))
val sql = Select( val sql = Select(
select(p.all, org.all, c.all), select(p.all, org.all, c.all),
from(p) from(p)
.leftJoin(org, org.oid === p.oid) .leftJoin(org, org.oid === p.oid)
.leftJoin(c, c.personId === p.pid), .leftJoin(c, c.personId === p.pid),
p.cid === coll &&? valFilter p.cid === coll &&? valFilter
).orderBy(order(p)) ).orderBy(order(p, org))
sql.build sql.build
.query[(RPerson, Option[ROrganization], Option[RContact])] .query[(RPerson, Option[ROrganization], Option[RContact])]
@ -128,7 +128,7 @@ object QOrganization {
coll: Ident, coll: Ident,
value: String, value: String,
ck: Option[ContactKind], ck: Option[ContactKind],
use: Option[NonEmptyList[PersonUse]] use: Option[Nel[PersonUse]]
): Stream[ConnectionIO, RPerson] = ): Stream[ConnectionIO, RPerson] =
runDistinct( runDistinct(
select(p.all), select(p.all),

View File

@ -87,7 +87,7 @@ object REquipment {
def findAll( def findAll(
coll: Ident, coll: Ident,
nameQ: Option[String], nameQ: Option[String],
order: Table => Column[_] order: Table => NonEmptyList[OrderBy]
): ConnectionIO[Vector[REquipment]] = { ): ConnectionIO[Vector[REquipment]] = {
val t = Table(None) val t = Table(None)

View File

@ -7,7 +7,7 @@
package docspell.store.records package docspell.store.records
import cats.Eq import cats.Eq
import cats.data.NonEmptyList import cats.data.{NonEmptyList => Nel}
import fs2.Stream import fs2.Stream
import docspell.common.{IdRef, _} import docspell.common.{IdRef, _}
@ -52,7 +52,7 @@ object ROrganization {
val shortName = Column[String]("short_name", this) val shortName = Column[String]("short_name", this)
val use = Column[OrgUse]("org_use", this) val use = Column[OrgUse]("org_use", this)
val all = val all =
NonEmptyList.of[Column[_]]( Nel.of[Column[_]](
oid, oid,
cid, cid,
name, name,
@ -122,7 +122,7 @@ object ROrganization {
def findLike( def findLike(
coll: Ident, coll: Ident,
orgName: String, orgName: String,
use: NonEmptyList[OrgUse] use: Nel[OrgUse]
): ConnectionIO[Vector[IdRef]] = ): ConnectionIO[Vector[IdRef]] =
run( run(
select(T.oid, T.name), select(T.oid, T.name),
@ -163,7 +163,7 @@ object ROrganization {
def findAllRef( def findAllRef(
coll: Ident, coll: Ident,
nameQ: Option[String], nameQ: Option[String],
order: Table => Column[_] order: Table => Nel[OrderBy]
): ConnectionIO[Vector[IdRef]] = { ): ConnectionIO[Vector[IdRef]] = {
val nameFilter = nameQ.map(s => val nameFilter = nameQ.map(s =>
T.name.like(s"%${s.toLowerCase}%") || T.shortName.like(s"%${s.toLowerCase}%") T.name.like(s"%${s.toLowerCase}%") || T.shortName.like(s"%${s.toLowerCase}%")

View File

@ -7,7 +7,7 @@
package docspell.store.records package docspell.store.records
import cats.Eq import cats.Eq
import cats.data.NonEmptyList import cats.data.{NonEmptyList => Nel}
import cats.effect._ import cats.effect._
import fs2.Stream import fs2.Stream
@ -52,7 +52,7 @@ object RPerson {
val updated = Column[Timestamp]("updated", this) val updated = Column[Timestamp]("updated", this)
val oid = Column[Ident]("oid", this) val oid = Column[Ident]("oid", this)
val use = Column[PersonUse]("person_use", this) val use = Column[PersonUse]("person_use", this)
val all = NonEmptyList.of[Column[_]]( val all = Nel.of[Column[_]](
pid, pid,
cid, cid,
name, name,
@ -122,7 +122,7 @@ object RPerson {
def findLike( def findLike(
coll: Ident, coll: Ident,
personName: String, personName: String,
use: NonEmptyList[PersonUse] use: Nel[PersonUse]
): ConnectionIO[Vector[IdRef]] = ): ConnectionIO[Vector[IdRef]] =
run( run(
select(T.pid, T.name), select(T.pid, T.name),
@ -134,7 +134,7 @@ object RPerson {
coll: Ident, coll: Ident,
contactKind: ContactKind, contactKind: ContactKind,
value: String, value: String,
use: NonEmptyList[PersonUse] use: Nel[PersonUse]
): ConnectionIO[Vector[IdRef]] = { ): ConnectionIO[Vector[IdRef]] = {
val p = RPerson.as("p") val p = RPerson.as("p")
val c = RContact.as("c") val c = RContact.as("c")
@ -162,7 +162,7 @@ object RPerson {
def findAllRef( def findAllRef(
coll: Ident, coll: Ident,
nameQ: Option[String], nameQ: Option[String],
order: Table => Column[_] order: Table => Nel[OrderBy]
): ConnectionIO[Vector[IdRef]] = { ): ConnectionIO[Vector[IdRef]] = {
val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%")) val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%"))
@ -176,7 +176,7 @@ object RPerson {
DML.delete(T, T.pid === personId && T.cid === coll) DML.delete(T, T.pid === personId && T.cid === coll)
def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] =
NonEmptyList.fromList(ids.toList) match { Nel.fromList(ids.toList) match {
case Some(nel) => case Some(nel) =>
run(select(T.pid, T.name, T.oid), from(T), T.pid.in(nel)) run(select(T.pid, T.name, T.oid), from(T), T.pid.in(nel))
.query[PersonRef] .query[PersonRef]

View File

@ -75,10 +75,11 @@ object RTag {
def findAll( def findAll(
coll: Ident, coll: Ident,
nameQ: Option[String], query: Option[String],
order: Table => Column[_] order: Table => NonEmptyList[OrderBy]
): ConnectionIO[Vector[RTag]] = { ): ConnectionIO[Vector[RTag]] = {
val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%")) val nameFilter =
query.map(_.toLowerCase).map(s => T.name.like(s"%$s%") || T.category.like(s"%$s%"))
val sql = val sql =
Select(select(T.all), from(T), T.cid === coll &&? nameFilter).orderBy(order(T)) Select(select(T.all), from(T), T.cid === coll &&? nameFilter).orderBy(order(T))
sql.build.query[RTag].to[Vector] sql.build.query[RTag].to[Vector]

View File

@ -216,8 +216,14 @@ import Api.Model.UserList exposing (UserList)
import Api.Model.UserPass exposing (UserPass) import Api.Model.UserPass exposing (UserPass)
import Api.Model.VersionInfo exposing (VersionInfo) import Api.Model.VersionInfo exposing (VersionInfo)
import Data.ContactType exposing (ContactType) import Data.ContactType exposing (ContactType)
import Data.CustomFieldOrder exposing (CustomFieldOrder)
import Data.EquipmentOrder exposing (EquipmentOrder)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder exposing (FolderOrder)
import Data.OrganizationOrder exposing (OrganizationOrder)
import Data.PersonOrder exposing (PersonOrder)
import Data.Priority exposing (Priority) import Data.Priority exposing (Priority)
import Data.TagOrder exposing (TagOrder)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import File exposing (File) import File exposing (File)
import Http import Http
@ -291,13 +297,15 @@ putCustomValue flags item fieldValue receive =
} }
getCustomFields : Flags -> String -> (Result Http.Error CustomFieldList -> msg) -> Cmd msg getCustomFields : Flags -> String -> CustomFieldOrder -> (Result Http.Error CustomFieldList -> msg) -> Cmd msg
getCustomFields flags query receive = getCustomFields flags query order receive =
Http2.authGet Http2.authGet
{ url = { url =
flags.config.baseUrl flags.config.baseUrl
++ "/api/v1/sec/customfield?q=" ++ "/api/v1/sec/customfield?q="
++ Url.percentEncode query ++ Url.percentEncode query
++ "&sort="
++ Data.CustomFieldOrder.asString order
, account = getAccount flags , account = getAccount flags
, expect = Http.expectJson receive Api.Model.CustomFieldList.decoder , expect = Http.expectJson receive Api.Model.CustomFieldList.decoder
} }
@ -402,13 +410,21 @@ getFolderDetail flags id receive =
} }
getFolders : Flags -> String -> Bool -> (Result Http.Error FolderList -> msg) -> Cmd msg getFolders :
getFolders flags query owningOnly receive = Flags
-> String
-> FolderOrder
-> Bool
-> (Result Http.Error FolderList -> msg)
-> Cmd msg
getFolders flags query order owningOnly receive =
Http2.authGet Http2.authGet
{ url = { url =
flags.config.baseUrl flags.config.baseUrl
++ "/api/v1/sec/folder?q=" ++ "/api/v1/sec/folder?q="
++ Url.percentEncode query ++ Url.percentEncode query
++ "&sort="
++ Data.FolderOrder.asString order
++ (if owningOnly then ++ (if owningOnly then
"&owning=true" "&owning=true"
@ -1109,10 +1125,15 @@ getContacts flags kind q receive =
--- Tags --- Tags
getTags : Flags -> String -> (Result Http.Error TagList -> msg) -> Cmd msg getTags : Flags -> String -> TagOrder -> (Result Http.Error TagList -> msg) -> Cmd msg
getTags flags query receive = getTags flags query order receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/tag?q=" ++ Url.percentEncode query { url =
flags.config.baseUrl
++ "/api/v1/sec/tag?sort="
++ Data.TagOrder.asString order
++ "&q="
++ Url.percentEncode query
, account = getAccount flags , account = getAccount flags
, expect = Http.expectJson receive Api.Model.TagList.decoder , expect = Http.expectJson receive Api.Model.TagList.decoder
} }
@ -1148,10 +1169,15 @@ deleteTag flags tag receive =
--- Equipments --- Equipments
getEquipments : Flags -> String -> (Result Http.Error EquipmentList -> msg) -> Cmd msg getEquipments : Flags -> String -> EquipmentOrder -> (Result Http.Error EquipmentList -> msg) -> Cmd msg
getEquipments flags query receive = getEquipments flags query order receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/equipment?q=" ++ Url.percentEncode query { url =
flags.config.baseUrl
++ "/api/v1/sec/equipment?q="
++ Url.percentEncode query
++ "&sort="
++ Data.EquipmentOrder.asString order
, account = getAccount flags , account = getAccount flags
, expect = Http.expectJson receive Api.Model.EquipmentList.decoder , expect = Http.expectJson receive Api.Model.EquipmentList.decoder
} }
@ -1214,10 +1240,20 @@ getOrgFull id flags receive =
} }
getOrganizations : Flags -> String -> (Result Http.Error OrganizationList -> msg) -> Cmd msg getOrganizations :
getOrganizations flags query receive = Flags
-> String
-> OrganizationOrder
-> (Result Http.Error OrganizationList -> msg)
-> Cmd msg
getOrganizations flags query order receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/organization?full=true&q=" ++ Url.percentEncode query { url =
flags.config.baseUrl
++ "/api/v1/sec/organization?full=true&q="
++ Url.percentEncode query
++ "&sort="
++ Data.OrganizationOrder.asString order
, account = getAccount flags , account = getAccount flags
, expect = Http.expectJson receive Api.Model.OrganizationList.decoder , expect = Http.expectJson receive Api.Model.OrganizationList.decoder
} }
@ -1271,10 +1307,15 @@ getPersonFull id flags receive =
} }
getPersons : Flags -> String -> (Result Http.Error PersonList -> msg) -> Cmd msg getPersons : Flags -> String -> PersonOrder -> (Result Http.Error PersonList -> msg) -> Cmd msg
getPersons flags query receive = getPersons flags query order receive =
Http2.authGet Http2.authGet
{ url = flags.config.baseUrl ++ "/api/v1/sec/person?full=true&q=" ++ Url.percentEncode query { url =
flags.config.baseUrl
++ "/api/v1/sec/person?full=true&q="
++ Url.percentEncode query
++ "&sort="
++ Data.PersonOrder.asString order
, account = getAccount flags , account = getAccount flags
, expect = Http.expectJson receive Api.Model.PersonList.decoder , expect = Http.expectJson receive Api.Model.PersonList.decoder
} }

View File

@ -25,6 +25,7 @@ import Data.CalEvent exposing (CalEvent)
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ListType exposing (ListType) import Data.ListType exposing (ListType)
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -90,7 +91,7 @@ init flags sett =
Comp.FixedDropdown.init Data.ListType.all Comp.FixedDropdown.init Data.ListType.all
} }
, Cmd.batch , Cmd.batch
[ Api.getTags flags "" GetTagsResp [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
, Cmd.map ScheduleMsg cec , Cmd.map ScheduleMsg cec
] ]
) )

View File

@ -21,6 +21,7 @@ import Comp.Basic as B
import Comp.CustomFieldForm import Comp.CustomFieldForm
import Comp.CustomFieldTable import Comp.CustomFieldTable
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Data.CustomFieldOrder exposing (CustomFieldOrder)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -36,6 +37,7 @@ type alias Model =
, fields : List CustomField , fields : List CustomField
, query : String , query : String
, loading : Bool , loading : Bool
, order : CustomFieldOrder
} }
@ -54,13 +56,14 @@ empty =
, fields = [] , fields = []
, query = "" , query = ""
, loading = False , loading = False
, order = Data.CustomFieldOrder.LabelAsc
} }
init : Flags -> ( Model, Cmd Msg ) init : Flags -> ( Model, Cmd Msg )
init flags = init flags =
( empty ( empty
, Api.getCustomFields flags empty.query CustomFieldListResp , loadFields flags empty
) )
@ -68,14 +71,22 @@ init flags =
--- Update --- Update
loadFields : Flags -> Model -> Cmd Msg
loadFields flags model =
Api.getCustomFields flags model.query model.order CustomFieldListResp
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model = update flags msg model =
case msg of case msg of
TableMsg lm -> TableMsg lm ->
let let
( tm, action ) = ( tm, action, maybeOrder ) =
Comp.CustomFieldTable.update lm model.tableModel Comp.CustomFieldTable.update lm model.tableModel
newOrder =
Maybe.withDefault model.order maybeOrder
detail = detail =
case action of case action of
Comp.CustomFieldTable.EditAction item -> Comp.CustomFieldTable.EditAction item ->
@ -83,8 +94,22 @@ update flags msg model =
Comp.CustomFieldTable.NoAction -> Comp.CustomFieldTable.NoAction ->
model.detailModel model.detailModel
newModel =
{ model
| tableModel = tm
, detailModel = detail
, order = newOrder
}
( m1, c1 ) =
if model.order == newOrder then
( newModel, Cmd.none )
else
( newModel, loadFields flags newModel )
in in
( { model | tableModel = tm, detailModel = detail }, Cmd.none ) ( m1, c1 )
DetailMsg lm -> DetailMsg lm ->
case model.detailModel of case model.detailModel of
@ -95,7 +120,7 @@ update flags msg model =
cmd = cmd =
if back then if back then
Api.getCustomFields flags model.query CustomFieldListResp loadFields flags model
else else
Cmd.none Cmd.none
@ -118,8 +143,12 @@ update flags msg model =
( model, Cmd.none ) ( model, Cmd.none )
SetQuery str -> SetQuery str ->
( { model | query = str } let
, Api.getCustomFields flags str CustomFieldListResp newModel =
{ model | query = str }
in
( newModel
, loadFields flags newModel
) )
CustomFieldListResp (Ok sl) -> CustomFieldListResp (Ok sl) ->
@ -207,6 +236,7 @@ viewTable2 texts model =
} }
, Html.map TableMsg , Html.map TableMsg
(Comp.CustomFieldTable.view2 texts.fieldTable (Comp.CustomFieldTable.view2 texts.fieldTable
model.order
model.tableModel model.tableModel
model.fields model.fields
) )

View File

@ -29,6 +29,7 @@ import Api.Model.ItemFieldValue exposing (ItemFieldValue)
import Comp.CustomFieldInput import Comp.CustomFieldInput
import Comp.FixedDropdown import Comp.FixedDropdown
import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.CustomFieldChange exposing (CustomFieldChange(..))
import Data.CustomFieldOrder
import Data.CustomFieldType import Data.CustomFieldType
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
@ -116,7 +117,7 @@ init flags =
initCmd : Flags -> Cmd Msg initCmd : Flags -> Cmd Msg
initCmd flags = initCmd flags =
Api.getCustomFields flags "" CustomFieldResp Api.getCustomFields flags "" Data.CustomFieldOrder.LabelAsc CustomFieldResp
setValues : List ItemFieldValue -> Msg setValues : List ItemFieldValue -> Msg

View File

@ -16,8 +16,10 @@ module Comp.CustomFieldTable exposing
import Api.Model.CustomField exposing (CustomField) import Api.Model.CustomField exposing (CustomField)
import Comp.Basic as B import Comp.Basic as B
import Data.CustomFieldOrder exposing (CustomFieldOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.CustomFieldTable exposing (Texts) import Messages.Comp.CustomFieldTable exposing (Texts)
import Styles as S import Styles as S
@ -28,6 +30,7 @@ type alias Model =
type Msg type Msg
= EditItem CustomField = EditItem CustomField
| ToggleOrder CustomFieldOrder
type Action type Action
@ -35,31 +38,88 @@ type Action
| EditAction CustomField | EditAction CustomField
type Header
= Label
| Format
init : Model init : Model
init = init =
{} {}
update : Msg -> Model -> ( Model, Action ) update : Msg -> Model -> ( Model, Action, Maybe CustomFieldOrder )
update msg model = update msg model =
case msg of case msg of
EditItem item -> EditItem item ->
( model, EditAction item ) ( model, EditAction item, Nothing )
ToggleOrder order ->
( model, NoAction, Just order )
newOrder : Header -> CustomFieldOrder -> CustomFieldOrder
newOrder header current =
case ( header, current ) of
( Label, Data.CustomFieldOrder.LabelAsc ) ->
Data.CustomFieldOrder.LabelDesc
( Label, _ ) ->
Data.CustomFieldOrder.LabelAsc
( Format, Data.CustomFieldOrder.FormatAsc ) ->
Data.CustomFieldOrder.FormatDesc
( Format, _ ) ->
Data.CustomFieldOrder.FormatAsc
--- View2 --- View2
view2 : Texts -> Model -> List CustomField -> Html Msg view2 : Texts -> CustomFieldOrder -> Model -> List CustomField -> Html Msg
view2 texts _ items = view2 texts order _ items =
let
labelSortIcon =
case order of
Data.CustomFieldOrder.LabelAsc ->
"fa fa-sort-alpha-up"
Data.CustomFieldOrder.LabelDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-up"
formatSortIcon =
case order of
Data.CustomFieldOrder.FormatAsc ->
"fa fa-sort-alpha-up"
Data.CustomFieldOrder.FormatDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-up"
in
div [] div []
[ table [ class S.tableMain ] [ table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
[ th [] [] [ th [] []
, th [ class "text-left" ] [ text texts.nameLabel ] , th [ class "text-left" ]
, th [ class "text-left" ] [ text texts.format ] [ a [ href "#", onClick (ToggleOrder <| newOrder Label order) ]
[ i [ class labelSortIcon, class "mr-1" ] []
, text texts.nameLabel
]
]
, th [ class "text-left" ]
[ a [ href "#", onClick (ToggleOrder <| newOrder Format order) ]
[ i [ class formatSortIcon, class "mr-1" ] []
, text texts.format
]
]
, th [ class "text-center hidden sm:table-cell" ] [ text texts.usageCount ] , th [ class "text-center hidden sm:table-cell" ] [ text texts.usageCount ]
, th [ class "text-center hidden sm:table-cell" ] [ text texts.basics.created ] , th [ class "text-center hidden sm:table-cell" ] [ text texts.basics.created ]
] ]

View File

@ -22,6 +22,7 @@ import Comp.EquipmentForm
import Comp.EquipmentTable import Comp.EquipmentTable
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.EquipmentOrder exposing (EquipmentOrder)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -40,6 +41,7 @@ type alias Model =
, loading : Bool , loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, query : String , query : String
, order : EquipmentOrder
} }
@ -64,6 +66,7 @@ emptyModel =
, loading = False , loading = False
, deleteConfirm = Comp.YesNoDimmer.emptyModel , deleteConfirm = Comp.YesNoDimmer.emptyModel
, query = "" , query = ""
, order = Data.EquipmentOrder.NameAsc
} }
@ -86,9 +89,12 @@ update flags msg model =
case msg of case msg of
TableMsg m -> TableMsg m ->
let let
( tm, tc ) = ( tm, tc, maybeOrder ) =
Comp.EquipmentTable.update flags m model.tableModel Comp.EquipmentTable.update flags m model.tableModel
newOrder =
Maybe.withDefault model.order maybeOrder
( m2, c2 ) = ( m2, c2 ) =
( { model ( { model
| tableModel = tm | tableModel = tm
@ -99,6 +105,7 @@ update flags msg model =
else else
model.formError model.formError
, order = newOrder
} }
, Cmd.map TableMsg tc , Cmd.map TableMsg tc
) )
@ -110,8 +117,15 @@ update flags msg model =
Nothing -> Nothing ->
( m2, Cmd.none ) ( m2, Cmd.none )
( m4, c4 ) =
if model.order == newOrder then
( m3, Cmd.none )
else
update flags LoadEquipments m3
in in
( m3, Cmd.batch [ c2, c3 ] ) ( m4, Cmd.batch [ c2, c3, c4 ] )
FormMsg m -> FormMsg m ->
let let
@ -121,7 +135,7 @@ update flags msg model =
( { model | formModel = m2 }, Cmd.map FormMsg c2 ) ( { model | formModel = m2 }, Cmd.map FormMsg c2 )
LoadEquipments -> LoadEquipments ->
( { model | loading = True }, Api.getEquipments flags "" EquipmentResp ) ( { model | loading = True }, Api.getEquipments flags model.query model.order EquipmentResp )
EquipmentResp (Ok equipments) -> EquipmentResp (Ok equipments) ->
let let
@ -211,7 +225,7 @@ update flags msg model =
m = m =
{ model | query = str } { model | query = str }
in in
( m, Api.getEquipments flags str EquipmentResp ) ( m, Api.getEquipments flags str model.order EquipmentResp )
@ -251,6 +265,7 @@ viewTable2 texts model =
} }
, Html.map TableMsg , Html.map TableMsg
(Comp.EquipmentTable.view2 texts.equipmentTable (Comp.EquipmentTable.view2 texts.equipmentTable
model.order
model.tableModel model.tableModel
) )
, div , div

View File

@ -15,10 +15,12 @@ module Comp.EquipmentTable exposing
import Api.Model.Equipment exposing (Equipment) import Api.Model.Equipment exposing (Equipment)
import Comp.Basic as B import Comp.Basic as B
import Data.EquipmentOrder exposing (EquipmentOrder)
import Data.EquipmentUse import Data.EquipmentUse
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.EquipmentTable exposing (Texts) import Messages.Comp.EquipmentTable exposing (Texts)
import Styles as S import Styles as S
@ -40,27 +42,50 @@ type Msg
= SetEquipments (List Equipment) = SetEquipments (List Equipment)
| Select Equipment | Select Equipment
| Deselect | Deselect
| ToggleOrder EquipmentOrder
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe EquipmentOrder )
update _ msg model = update _ msg model =
case msg of case msg of
SetEquipments list -> SetEquipments list ->
( { model | equips = list, selected = Nothing }, Cmd.none ) ( { model | equips = list, selected = Nothing }, Cmd.none, Nothing )
Select equip -> Select equip ->
( { model | selected = Just equip }, Cmd.none ) ( { model | selected = Just equip }, Cmd.none, Nothing )
Deselect -> Deselect ->
( { model | selected = Nothing }, Cmd.none ) ( { model | selected = Nothing }, Cmd.none, Nothing )
ToggleOrder order ->
( model, Cmd.none, Just order )
newOrder : EquipmentOrder -> EquipmentOrder
newOrder current =
case current of
Data.EquipmentOrder.NameAsc ->
Data.EquipmentOrder.NameDesc
Data.EquipmentOrder.NameDesc ->
Data.EquipmentOrder.NameAsc
--- View2 --- View2
view2 : Texts -> Model -> Html Msg view2 : Texts -> EquipmentOrder -> Model -> Html Msg
view2 texts model = view2 texts order model =
let
nameSortIcon =
case order of
Data.EquipmentOrder.NameAsc ->
"fa fa-sort-alpha-up"
Data.EquipmentOrder.NameDesc ->
"fa fa-sort-alpha-down-alt"
in
table [ class S.tableMain ] table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
@ -68,7 +93,12 @@ view2 texts model =
, th [ class "text-left pr-1 md:px-2 w-20" ] , th [ class "text-left pr-1 md:px-2 w-20" ]
[ text texts.use [ text texts.use
] ]
, th [ class "text-left" ] [ text texts.basics.name ] , th [ class "text-left" ]
[ a [ href "#", onClick (ToggleOrder <| newOrder order) ]
[ i [ class nameSortIcon, class "mr-1" ] []
, text texts.basics.name
]
]
] ]
] ]
, tbody [] , tbody []

View File

@ -24,6 +24,7 @@ import Comp.FolderDetail
import Comp.FolderTable import Comp.FolderTable
import Comp.MenuBar as MB import Comp.MenuBar as MB
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder exposing (FolderOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Http import Http
@ -39,6 +40,7 @@ type alias Model =
, query : String , query : String
, owningOnly : Bool , owningOnly : Bool
, loading : Bool , loading : Bool
, order : FolderOrder
} }
@ -62,6 +64,7 @@ empty =
, query = "" , query = ""
, owningOnly = True , owningOnly = True
, loading = False , loading = False
, order = Data.FolderOrder.NameAsc
} }
@ -70,7 +73,7 @@ init flags =
( empty ( empty
, Cmd.batch , Cmd.batch
[ Api.getUsers flags UserListResp [ Api.getUsers flags UserListResp
, Api.getFolders flags empty.query empty.owningOnly FolderListResp , loadFolders flags empty
] ]
) )
@ -79,23 +82,41 @@ init flags =
--- Update --- Update
loadFolders : Flags -> Model -> Cmd Msg
loadFolders flags model =
Api.getFolders flags model.query model.order model.owningOnly FolderListResp
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model = update flags msg model =
case msg of case msg of
TableMsg lm -> TableMsg lm ->
let let
( tm, action ) = ( tm, action, maybeOrder ) =
Comp.FolderTable.update lm model.tableModel Comp.FolderTable.update lm model.tableModel
cmd = newOrder =
Maybe.withDefault model.order maybeOrder
newModel =
{ model | tableModel = tm, order = newOrder }
detailCmd =
case action of case action of
Comp.FolderTable.EditAction item -> Comp.FolderTable.EditAction item ->
Api.getFolderDetail flags item.id FolderDetailResp Api.getFolderDetail flags item.id FolderDetailResp
Comp.FolderTable.NoAction -> Comp.FolderTable.NoAction ->
Cmd.none Cmd.none
refreshCmd =
if model.order == newOrder then
Cmd.none
else
loadFolders flags newModel
in in
( { model | tableModel = tm }, cmd ) ( newModel, Cmd.batch [ detailCmd, refreshCmd ] )
DetailMsg lm -> DetailMsg lm ->
case model.detailModel of case model.detailModel of
@ -106,7 +127,7 @@ update flags msg model =
cmd = cmd =
if back then if back then
Api.getFolders flags model.query model.owningOnly FolderListResp loadFolders flags model
else else
Cmd.none Cmd.none
@ -129,17 +150,24 @@ update flags msg model =
( model, Cmd.none ) ( model, Cmd.none )
SetQuery str -> SetQuery str ->
( { model | query = str } let
, Api.getFolders flags str model.owningOnly FolderListResp nm =
{ model | query = str }
in
( nm
, loadFolders flags nm
) )
ToggleOwningOnly -> ToggleOwningOnly ->
let let
newOwning = newOwning =
not model.owningOnly not model.owningOnly
nm =
{ model | owningOnly = newOwning }
in in
( { model | owningOnly = newOwning } ( nm
, Api.getFolders flags model.query newOwning FolderListResp , loadFolders flags nm
) )
UserListResp (Ok ul) -> UserListResp (Ok ul) ->
@ -241,6 +269,7 @@ viewTable2 texts model =
, Html.map TableMsg , Html.map TableMsg
(Comp.FolderTable.view2 (Comp.FolderTable.view2
texts.folderTable texts.folderTable
model.order
model.tableModel model.tableModel
model.folders model.folders
) )

View File

@ -16,8 +16,10 @@ module Comp.FolderTable exposing
import Api.Model.FolderItem exposing (FolderItem) import Api.Model.FolderItem exposing (FolderItem)
import Comp.Basic as B import Comp.Basic as B
import Data.FolderOrder exposing (FolderOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.FolderTable exposing (Texts) import Messages.Comp.FolderTable exposing (Texts)
import Styles as S import Styles as S
@ -28,6 +30,7 @@ type alias Model =
type Msg type Msg
= EditItem FolderItem = EditItem FolderItem
| ToggleOrder FolderOrder
type Action type Action
@ -35,32 +38,87 @@ type Action
| EditAction FolderItem | EditAction FolderItem
type Header
= Name
| Owner
init : Model init : Model
init = init =
{} {}
update : Msg -> Model -> ( Model, Action ) update : Msg -> Model -> ( Model, Action, Maybe FolderOrder )
update msg model = update msg model =
case msg of case msg of
EditItem item -> EditItem item ->
( model, EditAction item ) ( model, EditAction item, Nothing )
ToggleOrder order ->
( model, NoAction, Just order )
newOrder : Header -> FolderOrder -> FolderOrder
newOrder header current =
case ( header, current ) of
( Name, Data.FolderOrder.NameAsc ) ->
Data.FolderOrder.NameDesc
( Name, _ ) ->
Data.FolderOrder.NameAsc
( Owner, Data.FolderOrder.OwnerAsc ) ->
Data.FolderOrder.OwnerDesc
( Owner, _ ) ->
Data.FolderOrder.OwnerAsc
--- View2 --- View2
view2 : Texts -> Model -> List FolderItem -> Html Msg view2 : Texts -> FolderOrder -> Model -> List FolderItem -> Html Msg
view2 texts _ items = view2 texts order _ items =
let
nameSortIcon =
case order of
Data.FolderOrder.NameAsc ->
"fa fa-sort-alpha-up"
Data.FolderOrder.NameDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-up"
ownerSortIcon =
case order of
Data.FolderOrder.OwnerAsc ->
"fa fa-sort-alpha-up"
Data.FolderOrder.OwnerDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-up"
in
table [ class S.tableMain ] table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
[ th [ class "w-px whitespace-nowrap pr-1 md:pr-3" ] [] [ th [ class "w-px whitespace-nowrap pr-1 md:pr-3" ] []
, th [ class "text-left" ] , th [ class "text-left" ]
[ text texts.basics.name [ a [ href "#", onClick (ToggleOrder <| newOrder Name order) ]
[ i [ class nameSortIcon, class "mr-1" ] []
, text texts.basics.name
]
]
, th [ class "text-left hidden sm:table-cell" ]
[ a [ href "#", onClick (ToggleOrder <| newOrder Owner order) ]
[ i [ class ownerSortIcon, class "mr-1" ] []
, text texts.owner
]
] ]
, th [ class "text-left hidden sm:table-cell" ] [ text "Owner" ]
, th [ class "text-center" ] , th [ class "text-center" ]
[ span [ class "hidden sm:inline" ] [ span [ class "hidden sm:inline" ]
[ text texts.memberCount [ text texts.memberCount

View File

@ -35,10 +35,14 @@ import Comp.Tabs as TB
import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.CustomFieldChange exposing (CustomFieldChange(..))
import Data.Direction exposing (Direction) import Data.Direction exposing (Direction)
import Data.DropdownStyle import Data.DropdownStyle
import Data.EquipmentOrder
import Data.Fields import Data.Fields
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder
import Data.Icons as Icons import Data.Icons as Icons
import Data.PersonOrder
import Data.PersonUse import Data.PersonUse
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import DatePicker exposing (DatePicker) import DatePicker exposing (DatePicker)
import Html exposing (..) import Html exposing (..)
@ -157,11 +161,11 @@ loadModel flags =
Comp.DatePicker.init Comp.DatePicker.init
in in
Cmd.batch Cmd.batch
[ Api.getTags flags "" GetTagsResp [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
, Api.getOrgLight flags GetOrgResp , Api.getOrgLight flags GetOrgResp
, Api.getPersons flags "" GetPersonResp , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp
, Api.getEquipments flags "" GetEquipResp , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp
, Api.getFolders flags "" False GetFolderResp , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp
, Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags)
, Cmd.map ItemDatePickerMsg dpc , Cmd.map ItemDatePickerMsg dpc
, Cmd.map DueDatePickerMsg dpc , Cmd.map DueDatePickerMsg dpc

View File

@ -59,10 +59,14 @@ import Comp.PersonForm
import Comp.SentMails import Comp.SentMails
import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.CustomFieldChange exposing (CustomFieldChange(..))
import Data.Direction import Data.Direction
import Data.EquipmentOrder
import Data.Fields exposing (Field) import Data.Fields exposing (Field)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder
import Data.ItemNav exposing (ItemNav) import Data.ItemNav exposing (ItemNav)
import Data.PersonOrder
import Data.PersonUse import Data.PersonUse
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import DatePicker import DatePicker
import Dict import Dict
@ -265,7 +269,7 @@ update key flags inav settings msg model =
, getOptions flags , getOptions flags
, proposalCmd , proposalCmd
, Api.getSentMails flags item.id SentMailsResp , Api.getSentMails flags item.id SentMailsResp
, Api.getPersons flags "" GetPersonResp , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp
, Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags)
] ]
, sub = , sub =
@ -1642,11 +1646,11 @@ update key flags inav settings msg model =
getOptions : Flags -> Cmd Msg getOptions : Flags -> Cmd Msg
getOptions flags = getOptions flags =
Cmd.batch Cmd.batch
[ Api.getTags flags "" GetTagsResp [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
, Api.getOrgLight flags GetOrgResp , Api.getOrgLight flags GetOrgResp
, Api.getPersons flags "" GetPersonResp , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp
, Api.getEquipments flags "" GetEquipResp , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp
, Api.getFolders flags "" False GetFolderResp , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp
] ]

View File

@ -30,6 +30,7 @@ import Comp.YesNoDimmer
import Data.CalEvent exposing (CalEvent) import Data.CalEvent exposing (CalEvent)
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..)) import Data.Validated exposing (Validated(..))
import Html exposing (..) import Html exposing (..)
@ -182,7 +183,7 @@ init flags =
} }
, Cmd.batch , Cmd.batch
[ Api.getMailSettings flags "" ConnResp [ Api.getMailSettings flags "" ConnResp
, Api.getTags flags "" GetTagsResp , Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
, Cmd.map CalEventMsg scmd , Cmd.map CalEventMsg scmd
] ]
) )

View File

@ -23,6 +23,7 @@ import Comp.OrgForm
import Comp.OrgTable import Comp.OrgTable
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.OrganizationOrder exposing (OrganizationOrder)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -41,6 +42,7 @@ type alias Model =
, loading : Bool , loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, query : String , query : String
, order : OrganizationOrder
} }
@ -65,6 +67,7 @@ emptyModel =
, loading = False , loading = False
, deleteConfirm = Comp.YesNoDimmer.emptyModel , deleteConfirm = Comp.YesNoDimmer.emptyModel
, query = "" , query = ""
, order = Data.OrganizationOrder.NameAsc
} }
@ -87,7 +90,7 @@ update flags msg model =
case msg of case msg of
TableMsg m -> TableMsg m ->
let let
( tm, tc ) = ( tm, tc, maybeOrder ) =
Comp.OrgTable.update flags m model.tableModel Comp.OrgTable.update flags m model.tableModel
( m2, c2 ) = ( m2, c2 ) =
@ -100,6 +103,7 @@ update flags msg model =
else else
model.formError model.formError
, order = Maybe.withDefault model.order maybeOrder
} }
, Cmd.map TableMsg tc , Cmd.map TableMsg tc
) )
@ -111,8 +115,15 @@ update flags msg model =
Nothing -> Nothing ->
( m2, Cmd.none ) ( m2, Cmd.none )
( m4, c4 ) =
if maybeOrder /= Nothing && maybeOrder /= Just model.order then
update flags LoadOrgs m3
else
( m3, Cmd.none )
in in
( m3, Cmd.batch [ c2, c3 ] ) ( m4, Cmd.batch [ c2, c3, c4 ] )
FormMsg m -> FormMsg m ->
let let
@ -122,7 +133,13 @@ update flags msg model =
( { model | formModel = m2 }, Cmd.map FormMsg c2 ) ( { model | formModel = m2 }, Cmd.map FormMsg c2 )
LoadOrgs -> LoadOrgs ->
( { model | loading = True }, Api.getOrganizations flags model.query OrgResp ) ( { model | loading = True }
, Api.getOrganizations
flags
model.query
model.order
OrgResp
)
OrgResp (Ok orgs) -> OrgResp (Ok orgs) ->
let let
@ -212,7 +229,7 @@ update flags msg model =
m = m =
{ model | query = str } { model | query = str }
in in
( m, Api.getOrganizations flags str OrgResp ) ( m, Api.getOrganizations flags str model.order OrgResp )
@ -250,7 +267,7 @@ viewTable2 texts model =
] ]
, rootClasses = "mb-4" , rootClasses = "mb-4"
} }
, Html.map TableMsg (Comp.OrgTable.view2 texts.orgTable model.tableModel) , Html.map TableMsg (Comp.OrgTable.view2 texts.orgTable model.order model.tableModel)
, B.loadingDimmer , B.loadingDimmer
{ active = model.loading { active = model.loading
, label = texts.basics.loading , label = texts.basics.loading

View File

@ -17,8 +17,10 @@ import Api.Model.Organization exposing (Organization)
import Comp.Basic as B import Comp.Basic as B
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.OrgUse import Data.OrgUse
import Data.OrganizationOrder exposing (OrganizationOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.OrgTable exposing (Texts) import Messages.Comp.OrgTable exposing (Texts)
import Styles as S import Styles as S
import Util.Address import Util.Address
@ -42,27 +44,50 @@ type Msg
= SetOrgs (List Organization) = SetOrgs (List Organization)
| Select Organization | Select Organization
| Deselect | Deselect
| ToggleOrder OrganizationOrder
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe OrganizationOrder )
update _ msg model = update _ msg model =
case msg of case msg of
SetOrgs list -> SetOrgs list ->
( { model | orgs = list, selected = Nothing }, Cmd.none ) ( { model | orgs = list, selected = Nothing }, Cmd.none, Nothing )
Select equip -> Select equip ->
( { model | selected = Just equip }, Cmd.none ) ( { model | selected = Just equip }, Cmd.none, Nothing )
Deselect -> Deselect ->
( { model | selected = Nothing }, Cmd.none ) ( { model | selected = Nothing }, Cmd.none, Nothing )
ToggleOrder order ->
( model, Cmd.none, Just order )
newOrder : OrganizationOrder -> OrganizationOrder
newOrder current =
case current of
Data.OrganizationOrder.NameAsc ->
Data.OrganizationOrder.NameDesc
Data.OrganizationOrder.NameDesc ->
Data.OrganizationOrder.NameAsc
--- View2 --- View2
view2 : Texts -> Model -> Html Msg view2 : Texts -> OrganizationOrder -> Model -> Html Msg
view2 texts model = view2 texts order model =
let
nameSortIcon =
case order of
Data.OrganizationOrder.NameAsc ->
"fa fa-sort-alpha-up"
Data.OrganizationOrder.NameDesc ->
"fa fa-sort-alpha-down-alt"
in
table [ class S.tableMain ] table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
@ -71,7 +96,10 @@ view2 texts model =
[ text texts.use [ text texts.use
] ]
, th [ class "text-left" ] , th [ class "text-left" ]
[ text texts.basics.name [ a [ href "#", onClick (ToggleOrder <| newOrder order) ]
[ i [ class nameSortIcon, class "mr-1" ] []
, text texts.basics.name
]
] ]
, th [ class "text-left hidden md:table-cell" ] , th [ class "text-left hidden md:table-cell" ]
[ text texts.address [ text texts.address

View File

@ -24,6 +24,7 @@ import Comp.PersonForm
import Comp.PersonTable import Comp.PersonTable
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.PersonOrder exposing (PersonOrder)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -42,6 +43,7 @@ type alias Model =
, loading : Int , loading : Int
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, query : String , query : String
, order : PersonOrder
} }
@ -66,6 +68,7 @@ emptyModel =
, loading = 0 , loading = 0
, deleteConfirm = Comp.YesNoDimmer.emptyModel , deleteConfirm = Comp.YesNoDimmer.emptyModel
, query = "" , query = ""
, order = Data.PersonOrder.NameAsc
} }
@ -89,9 +92,12 @@ update flags msg model =
case msg of case msg of
TableMsg m -> TableMsg m ->
let let
( tm, tc ) = ( tm, tc, maybeOrder ) =
Comp.PersonTable.update flags m model.tableModel Comp.PersonTable.update flags m model.tableModel
newOrder =
Maybe.withDefault model.order maybeOrder
( m2, c2 ) = ( m2, c2 ) =
( { model ( { model
| tableModel = tm | tableModel = tm
@ -102,6 +108,7 @@ update flags msg model =
else else
model.formError model.formError
, order = newOrder
} }
, Cmd.map TableMsg tc , Cmd.map TableMsg tc
) )
@ -113,8 +120,15 @@ update flags msg model =
Nothing -> Nothing ->
( m2, Cmd.none ) ( m2, Cmd.none )
( m4, c4 ) =
if model.order == newOrder then
( m3, Cmd.none )
else
update flags LoadPersons m3
in in
( m3, Cmd.batch [ c2, c3 ] ) ( m4, Cmd.batch [ c2, c3, c4 ] )
FormMsg m -> FormMsg m ->
let let
@ -126,7 +140,7 @@ update flags msg model =
LoadPersons -> LoadPersons ->
( { model | loading = model.loading + 2 } ( { model | loading = model.loading + 2 }
, Cmd.batch , Cmd.batch
[ Api.getPersons flags model.query PersonResp [ Api.getPersons flags model.query model.order PersonResp
, Api.getOrgLight flags GetOrgResp , Api.getOrgLight flags GetOrgResp
] ]
) )
@ -244,7 +258,7 @@ update flags msg model =
m = m =
{ model | query = str } { model | query = str }
in in
( m, Api.getPersons flags str PersonResp ) ( m, Api.getPersons flags str model.order PersonResp )
isLoading : Model -> Bool isLoading : Model -> Bool
@ -287,7 +301,7 @@ viewTable2 texts model =
] ]
, rootClasses = "mb-4" , rootClasses = "mb-4"
} }
, Html.map TableMsg (Comp.PersonTable.view2 texts.personTable model.tableModel) , Html.map TableMsg (Comp.PersonTable.view2 texts.personTable model.order model.tableModel)
, B.loadingDimmer , B.loadingDimmer
{ active = isLoading model { active = isLoading model
, label = texts.basics.loading , label = texts.basics.loading

View File

@ -16,9 +16,11 @@ module Comp.PersonTable exposing
import Api.Model.Person exposing (Person) import Api.Model.Person exposing (Person)
import Comp.Basic as B import Comp.Basic as B
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.PersonOrder exposing (PersonOrder)
import Data.PersonUse import Data.PersonUse
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.PersonTable exposing (Texts) import Messages.Comp.PersonTable exposing (Texts)
import Styles as S import Styles as S
import Util.Contact import Util.Contact
@ -41,27 +43,75 @@ type Msg
= SetPersons (List Person) = SetPersons (List Person)
| Select Person | Select Person
| Deselect | Deselect
| ToggleOrder PersonOrder
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe PersonOrder )
update _ msg model = update _ msg model =
case msg of case msg of
SetPersons list -> SetPersons list ->
( { model | equips = list, selected = Nothing }, Cmd.none ) ( { model | equips = list, selected = Nothing }, Cmd.none, Nothing )
Select equip -> Select equip ->
( { model | selected = Just equip }, Cmd.none ) ( { model | selected = Just equip }, Cmd.none, Nothing )
Deselect -> Deselect ->
( { model | selected = Nothing }, Cmd.none ) ( { model | selected = Nothing }, Cmd.none, Nothing )
ToggleOrder order ->
( model, Cmd.none, Just order )
type Header
= Name
| Org
newOrder : Header -> PersonOrder -> PersonOrder
newOrder header current =
case ( header, current ) of
( Name, Data.PersonOrder.NameAsc ) ->
Data.PersonOrder.NameDesc
( Name, _ ) ->
Data.PersonOrder.NameAsc
( Org, Data.PersonOrder.OrgAsc ) ->
Data.PersonOrder.OrgDesc
( Org, _ ) ->
Data.PersonOrder.OrgAsc
--- View2 --- View2
view2 : Texts -> Model -> Html Msg view2 : Texts -> PersonOrder -> Model -> Html Msg
view2 texts model = view2 texts order model =
let
nameSortIcon =
case order of
Data.PersonOrder.NameAsc ->
"fa fa-sort-alpha-up"
Data.PersonOrder.NameDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-down-alt"
orgSortIcon =
case order of
Data.PersonOrder.OrgAsc ->
"fa fa-sort-alpha-up"
Data.PersonOrder.OrgDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-down-alt"
in
table [ class S.tableMain ] table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
@ -69,8 +119,18 @@ view2 texts model =
, th [ class "text-left pr-1 md:px-2" ] , th [ class "text-left pr-1 md:px-2" ]
[ text texts.use [ text texts.use
] ]
, th [ class "text-left" ] [ text texts.basics.name ] , th [ class "text-left" ]
, th [ class "text-left hidden sm:table-cell" ] [ text texts.basics.organization ] [ a [ href "#", onClick (ToggleOrder <| newOrder Name order) ]
[ i [ class nameSortIcon, class "mr-1" ] []
, text texts.basics.name
]
]
, th [ class "text-left hidden sm:table-cell" ]
[ a [ href "#", onClick (ToggleOrder <| newOrder Org order) ]
[ i [ class orgSortIcon, class "mr-1" ] []
, text texts.basics.organization
]
]
, th [ class "text-left hidden md:table-cell" ] [ text texts.contact ] , th [ class "text-left hidden md:table-cell" ] [ text texts.contact ]
] ]
] ]

View File

@ -37,7 +37,9 @@ import Data.CalEvent exposing (CalEvent)
import Data.Direction exposing (Direction(..)) import Data.Direction exposing (Direction(..))
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder
import Data.Language exposing (Language) import Data.Language exposing (Language)
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Data.Validated exposing (Validated(..)) import Data.Validated exposing (Validated(..))
import Html exposing (..) import Html exposing (..)
@ -221,8 +223,8 @@ initWith flags s =
[ Api.getImapSettings flags "" ConnResp [ Api.getImapSettings flags "" ConnResp
, nc , nc
, Cmd.map CalEventMsg sc , Cmd.map CalEventMsg sc
, Api.getFolders flags "" False GetFolderResp , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp
, Api.getTags flags "" GetTagResp , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp
] ]
) )
@ -268,8 +270,8 @@ init flags =
} }
, Cmd.batch , Cmd.batch
[ Api.getImapSettings flags "" ConnResp [ Api.getImapSettings flags "" ConnResp
, Api.getFolders flags "" False GetFolderResp , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp
, Api.getTags flags "" GetTagResp , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp
, Cmd.map CalEventMsg scmd , Cmd.map CalEventMsg scmd
] ]
) )

View File

@ -40,10 +40,12 @@ import Comp.TagSelect
import Data.CustomFieldChange exposing (CustomFieldValueCollect) import Data.CustomFieldChange exposing (CustomFieldValueCollect)
import Data.Direction exposing (Direction) import Data.Direction exposing (Direction)
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.EquipmentOrder
import Data.EquipmentUse import Data.EquipmentUse
import Data.Fields import Data.Fields
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemQuery as Q exposing (ItemQuery) import Data.ItemQuery as Q exposing (ItemQuery)
import Data.PersonOrder
import Data.PersonUse import Data.PersonUse
import Data.SearchMode exposing (SearchMode) import Data.SearchMode exposing (SearchMode)
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
@ -441,8 +443,8 @@ updateDrop ddm flags settings msg model =
Cmd.batch Cmd.batch
[ Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp [ Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp
, Api.getOrgLight flags GetOrgResp , Api.getOrgLight flags GetOrgResp
, Api.getEquipments flags "" GetEquipResp , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp
, Api.getPersons flags "" GetPersonResp , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp
, Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags)
, cdp , cdp
] ]
@ -1088,7 +1090,7 @@ findTab tab =
Nothing Nothing
tabLook :UiSettings -> Model -> SearchTab -> Comp.Tabs.Look tabLook : UiSettings -> Model -> SearchTab -> Comp.Tabs.Look
tabLook settings model tab = tabLook settings model tab =
let let
isHidden f = isHidden f =
@ -1097,6 +1099,7 @@ tabLook settings model tab =
hiddenOr fields default = hiddenOr fields default =
if List.all isHidden fields then if List.all isHidden fields then
Comp.Tabs.Hidden Comp.Tabs.Hidden
else else
default default
@ -1126,41 +1129,41 @@ tabLook settings model tab =
activeWhen model.inboxCheckbox activeWhen model.inboxCheckbox
TabTags -> TabTags ->
hiddenOr [Data.Fields.Tag] hiddenOr [ Data.Fields.Tag ]
(activeWhenNotEmpty model.tagSelection.includeTags model.tagSelection.excludeTags) (activeWhenNotEmpty model.tagSelection.includeTags model.tagSelection.excludeTags)
TabTagCategories -> TabTagCategories ->
hiddenOr [Data.Fields.Tag] hiddenOr [ Data.Fields.Tag ]
(activeWhenNotEmpty model.tagSelection.includeCats model.tagSelection.excludeCats) (activeWhenNotEmpty model.tagSelection.includeCats model.tagSelection.excludeCats)
TabFolder -> TabFolder ->
hiddenOr [Data.Fields.Folder] hiddenOr [ Data.Fields.Folder ]
(activeWhenJust model.selectedFolder) (activeWhenJust model.selectedFolder)
TabCorrespondent -> TabCorrespondent ->
hiddenOr [Data.Fields.CorrOrg, Data.Fields.CorrPerson] <| hiddenOr [ Data.Fields.CorrOrg, Data.Fields.CorrPerson ] <|
activeWhenNotEmpty (Comp.Dropdown.getSelected model.orgModel) activeWhenNotEmpty (Comp.Dropdown.getSelected model.orgModel)
(Comp.Dropdown.getSelected model.corrPersonModel) (Comp.Dropdown.getSelected model.corrPersonModel)
TabConcerning -> TabConcerning ->
hiddenOr [Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| hiddenOr [ Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <|
activeWhenNotEmpty (Comp.Dropdown.getSelected model.concPersonModel) activeWhenNotEmpty (Comp.Dropdown.getSelected model.concPersonModel)
(Comp.Dropdown.getSelected model.concEquipmentModel) (Comp.Dropdown.getSelected model.concEquipmentModel)
TabDate -> TabDate ->
hiddenOr [Data.Fields.Date] <| hiddenOr [ Data.Fields.Date ] <|
activeWhenJust (Util.Maybe.or [model.fromDate, model.untilDate]) activeWhenJust (Util.Maybe.or [ model.fromDate, model.untilDate ])
TabDueDate -> TabDueDate ->
hiddenOr [Data.Fields.DueDate] <| hiddenOr [ Data.Fields.DueDate ] <|
activeWhenJust (Util.Maybe.or [model.fromDueDate, model.untilDueDate]) activeWhenJust (Util.Maybe.or [ model.fromDueDate, model.untilDueDate ])
TabSource -> TabSource ->
hiddenOr [Data.Fields.SourceName] <| hiddenOr [ Data.Fields.SourceName ] <|
activeWhenJust model.sourceModel activeWhenJust model.sourceModel
TabDirection -> TabDirection ->
hiddenOr [Data.Fields.Direction] <| hiddenOr [ Data.Fields.Direction ] <|
activeWhenNotEmpty (Comp.Dropdown.getSelected model.directionModel) [] activeWhenNotEmpty (Comp.Dropdown.getSelected model.directionModel) []
TabTrashed -> TabTrashed ->
@ -1179,7 +1182,6 @@ searchTabState settings model tab =
searchTab = searchTab =
findTab tab findTab tab
folded = folded =
if Set.member tab.name model.openTabs then if Set.member tab.name model.openTabs then
Comp.Tabs.Open Comp.Tabs.Open
@ -1189,10 +1191,10 @@ searchTabState settings model tab =
state = state =
{ folded = folded { folded = folded
, look = Maybe.map (tabLook settings model) searchTab , look =
|> Maybe.withDefault Comp.Tabs.Normal Maybe.map (tabLook settings model) searchTab
|> Maybe.withDefault Comp.Tabs.Normal
} }
in in
( state, ToggleAkkordionTab tab.name ) ( state, ToggleAkkordionTab tab.name )

View File

@ -27,8 +27,10 @@ import Comp.Dropdown exposing (isDropdownChangeMsg)
import Comp.FixedDropdown import Comp.FixedDropdown
import Data.DropdownStyle as DS import Data.DropdownStyle as DS
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.FolderOrder
import Data.Language exposing (Language) import Data.Language exposing (Language)
import Data.Priority exposing (Priority) import Data.Priority exposing (Priority)
import Data.TagOrder
import Data.UiSettings exposing (UiSettings) import Data.UiSettings exposing (UiSettings)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@ -89,8 +91,8 @@ init : Flags -> ( Model, Cmd Msg )
init flags = init flags =
( emptyModel ( emptyModel
, Cmd.batch , Cmd.batch
[ Api.getFolders flags "" False GetFolderResp [ Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp
, Api.getTags flags "" GetTagResp , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp
] ]
) )

View File

@ -23,6 +23,7 @@ import Comp.TagForm
import Comp.TagTable import Comp.TagTable
import Comp.YesNoDimmer import Comp.YesNoDimmer
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.TagOrder exposing (TagOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onSubmit) import Html.Events exposing (onSubmit)
@ -42,6 +43,7 @@ type alias Model =
, loading : Bool , loading : Bool
, deleteConfirm : Comp.YesNoDimmer.Model , deleteConfirm : Comp.YesNoDimmer.Model
, query : String , query : String
, order : TagOrder
} }
@ -66,6 +68,7 @@ emptyModel =
, loading = False , loading = False
, deleteConfirm = Comp.YesNoDimmer.emptyModel , deleteConfirm = Comp.YesNoDimmer.emptyModel
, query = "" , query = ""
, order = Data.TagOrder.NameAsc
} }
@ -88,12 +91,16 @@ update flags msg model =
case msg of case msg of
TableMsg m -> TableMsg m ->
let let
( tm, tc ) = ( tm, tc, maybeOrder ) =
Comp.TagTable.update flags m model.tagTableModel Comp.TagTable.update flags m model.tagTableModel
newOrder =
Maybe.withDefault model.order maybeOrder
( m2, c2 ) = ( m2, c2 ) =
( { model ( { model
| tagTableModel = tm | tagTableModel = tm
, order = newOrder
, viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table , viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table
, formError = , formError =
if Util.Maybe.nonEmpty tm.selected then if Util.Maybe.nonEmpty tm.selected then
@ -112,8 +119,15 @@ update flags msg model =
Nothing -> Nothing ->
( m2, Cmd.none ) ( m2, Cmd.none )
( m4, c4 ) =
if model.order == newOrder then
( m3, Cmd.none )
else
update flags LoadTags m3
in in
( m3, Cmd.batch [ c2, c3 ] ) ( m4, Cmd.batch [ c2, c3, c4 ] )
FormMsg m -> FormMsg m ->
let let
@ -123,7 +137,9 @@ update flags msg model =
( { model | tagFormModel = m2 }, Cmd.map FormMsg c2 ) ( { model | tagFormModel = m2 }, Cmd.map FormMsg c2 )
LoadTags -> LoadTags ->
( { model | loading = True }, Api.getTags flags model.query (TagResp model.query) ) ( { model | loading = True }
, Api.getTags flags model.query model.order (TagResp model.query)
)
TagResp query (Ok tags) -> TagResp query (Ok tags) ->
let let
@ -224,7 +240,7 @@ update flags msg model =
m = m =
{ model | query = str } { model | query = str }
in in
( m, Api.getTags flags str (TagResp str) ) ( m, Api.getTags flags str model.order (TagResp str) )
@ -262,7 +278,7 @@ viewTable2 texts model =
] ]
, rootClasses = "mb-4" , rootClasses = "mb-4"
} }
, Html.map TableMsg (Comp.TagTable.view2 texts.tagTable model.tagTableModel) , Html.map TableMsg (Comp.TagTable.view2 texts.tagTable model.order model.tagTableModel)
, div , div
[ classList [ classList
[ ( "ui dimmer", True ) [ ( "ui dimmer", True )

View File

@ -16,8 +16,10 @@ module Comp.TagTable exposing
import Api.Model.Tag exposing (Tag) import Api.Model.Tag exposing (Tag)
import Comp.Basic as B import Comp.Basic as B
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.TagOrder exposing (TagOrder)
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Messages.Comp.TagTable exposing (Texts) import Messages.Comp.TagTable exposing (Texts)
import Styles as S import Styles as S
@ -35,37 +37,108 @@ emptyModel =
} }
type Header
= Name
| Category
type Msg type Msg
= SetTags (List Tag) = SetTags (List Tag)
| Select Tag | Select Tag
| Deselect | Deselect
| SortClick TagOrder
update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe TagOrder )
update _ msg model = update _ msg model =
case msg of case msg of
SetTags list -> SetTags list ->
( { model | tags = list, selected = Nothing }, Cmd.none ) ( { model | tags = list, selected = Nothing }, Cmd.none, Nothing )
Select tag -> Select tag ->
( { model | selected = Just tag }, Cmd.none ) ( { model | selected = Just tag }, Cmd.none, Nothing )
Deselect -> Deselect ->
( { model | selected = Nothing }, Cmd.none ) ( { model | selected = Nothing }, Cmd.none, Nothing )
SortClick order ->
( model, Cmd.none, Just order )
newOrder : Header -> TagOrder -> TagOrder
newOrder header current =
case ( header, current ) of
( Name, Data.TagOrder.NameAsc ) ->
Data.TagOrder.NameDesc
( Name, Data.TagOrder.NameDesc ) ->
Data.TagOrder.NameAsc
( Name, Data.TagOrder.CategoryAsc ) ->
Data.TagOrder.NameAsc
( Name, Data.TagOrder.CategoryDesc ) ->
Data.TagOrder.NameAsc
( Category, Data.TagOrder.NameAsc ) ->
Data.TagOrder.CategoryAsc
( Category, Data.TagOrder.NameDesc ) ->
Data.TagOrder.CategoryAsc
( Category, Data.TagOrder.CategoryAsc ) ->
Data.TagOrder.CategoryDesc
( Category, Data.TagOrder.CategoryDesc ) ->
Data.TagOrder.CategoryAsc
--- View2 --- View2
view2 : Texts -> Model -> Html Msg view2 : Texts -> TagOrder -> Model -> Html Msg
view2 texts model = view2 texts order model =
let
nameSortIcon =
case order of
Data.TagOrder.NameAsc ->
"fa fa-sort-alpha-up"
Data.TagOrder.NameDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-down"
catSortIcon =
case order of
Data.TagOrder.CategoryAsc ->
"fa fa-sort-alpha-up"
Data.TagOrder.CategoryDesc ->
"fa fa-sort-alpha-down-alt"
_ ->
"invisible fa fa-sort-alpha-down"
in
table [ class S.tableMain ] table [ class S.tableMain ]
[ thead [] [ thead []
[ tr [] [ tr []
[ th [ class "" ] [] [ th [ class "" ] []
, th [ class "text-left" ] [ text texts.basics.name ] , th [ class "text-left" ]
, th [ class "text-left" ] [ text texts.category ] [ a [ href "#", onClick (SortClick <| newOrder Name order) ]
[ i [ class nameSortIcon, class "mr-1" ] []
, text texts.basics.name
]
]
, th [ class "text-left" ]
[ a [ href "#", onClick (SortClick <| newOrder Category order) ]
[ i [ class catSortIcon, class "mr-1" ]
[]
, text texts.category
]
]
] ]
] ]
, tbody [] , tbody []

View File

@ -28,6 +28,7 @@ import Data.DropdownStyle as DS
import Data.Fields exposing (Field) import Data.Fields exposing (Field)
import Data.Flags exposing (Flags) import Data.Flags exposing (Flags)
import Data.ItemTemplate as IT exposing (ItemTemplate) import Data.ItemTemplate as IT exposing (ItemTemplate)
import Data.TagOrder
import Data.UiSettings exposing (ItemPattern, Pos(..), UiSettings) import Data.UiSettings exposing (ItemPattern, Pos(..), UiSettings)
import Dict exposing (Dict) import Dict exposing (Dict)
import Html exposing (..) import Html exposing (..)
@ -160,7 +161,7 @@ init flags settings =
Comp.FixedDropdown.init Messages.UiLanguage.all Comp.FixedDropdown.init Messages.UiLanguage.all
, openTabs = Set.empty , openTabs = Set.empty
} }
, Api.getTags flags "" GetTagsResp , Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp
) )

View File

@ -0,0 +1,31 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.CustomFieldOrder exposing (CustomFieldOrder(..), asString)
type CustomFieldOrder
= LabelAsc
| LabelDesc
| FormatAsc
| FormatDesc
asString : CustomFieldOrder -> String
asString order =
case order of
LabelAsc ->
"label"
LabelDesc ->
"-label"
FormatAsc ->
"type"
FormatDesc ->
"-type"

View File

@ -0,0 +1,23 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.EquipmentOrder exposing (EquipmentOrder(..), asString)
type EquipmentOrder
= NameAsc
| NameDesc
asString : EquipmentOrder -> String
asString order =
case order of
NameAsc ->
"name"
NameDesc ->
"-name"

View File

@ -0,0 +1,31 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.FolderOrder exposing (FolderOrder(..), asString)
type FolderOrder
= NameAsc
| NameDesc
| OwnerAsc
| OwnerDesc
asString : FolderOrder -> String
asString order =
case order of
NameAsc ->
"name"
NameDesc ->
"-name"
OwnerAsc ->
"owner"
OwnerDesc ->
"-owner"

View File

@ -0,0 +1,23 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.OrganizationOrder exposing (OrganizationOrder(..), asString)
type OrganizationOrder
= NameAsc
| NameDesc
asString : OrganizationOrder -> String
asString order =
case order of
NameAsc ->
"name"
NameDesc ->
"-name"

View File

@ -0,0 +1,31 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.PersonOrder exposing (PersonOrder(..), asString)
type PersonOrder
= NameAsc
| NameDesc
| OrgAsc
| OrgDesc
asString : PersonOrder -> String
asString order =
case order of
NameAsc ->
"name"
NameDesc ->
"-name"
OrgAsc ->
"org"
OrgDesc ->
"-org"

View File

@ -0,0 +1,31 @@
{-
Copyright 2020 Docspell Contributors
SPDX-License-Identifier: GPL-3.0-or-later
-}
module Data.TagOrder exposing (TagOrder(..), asString)
type TagOrder
= NameAsc
| NameDesc
| CategoryAsc
| CategoryDesc
asString : TagOrder -> String
asString order =
case order of
NameAsc ->
"name"
NameDesc ->
"-name"
CategoryAsc ->
"category"
CategoryDesc ->
"-category"

View File

@ -20,6 +20,7 @@ type alias Texts =
{ basics : Messages.Basics.Texts { basics : Messages.Basics.Texts
, memberCount : String , memberCount : String
, formatDateShort : Int -> String , formatDateShort : Int -> String
, owner : String
} }
@ -28,6 +29,7 @@ gb =
{ basics = Messages.Basics.gb { basics = Messages.Basics.gb
, memberCount = "#Member" , memberCount = "#Member"
, formatDateShort = DF.formatDateShort Messages.UiLanguage.English , formatDateShort = DF.formatDateShort Messages.UiLanguage.English
, owner = "Owner"
} }
@ -36,4 +38,5 @@ de =
{ basics = Messages.Basics.de { basics = Messages.Basics.de
, memberCount = "#Mitglieder" , memberCount = "#Mitglieder"
, formatDateShort = DF.formatDateShort Messages.UiLanguage.German , formatDateShort = DF.formatDateShort Messages.UiLanguage.German
, owner = "Besitzer"
} }