From cf88f5c2decbd2b68075254b8cc1f6f2841091be Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 24 Aug 2021 21:35:57 +0200 Subject: [PATCH] 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 --- .../docspell/backend/ops/OCustomFields.scala | 68 ++++++++-- .../docspell/backend/ops/OEquipment.scala | 39 +++++- .../scala/docspell/backend/ops/OFolder.scala | 40 +++++- .../docspell/backend/ops/OOrganization.scala | 117 ++++++++++++++++-- .../scala/docspell/backend/ops/OTag.scala | 43 ++++++- .../src/main/resources/docspell-openapi.yml | 37 +++++- .../restserver/http4s/QueryParam.scala | 41 ++++++ .../restserver/routes/CustomFieldRoutes.scala | 11 +- .../restserver/routes/EquipmentRoutes.scala | 9 +- .../restserver/routes/FolderRoutes.scala | 6 +- .../routes/OrganizationRoutes.scala | 13 +- .../restserver/routes/PersonRoutes.scala | 17 ++- .../restserver/routes/TagRoutes.scala | 9 +- .../main/scala/docspell/store/qb/Column.scala | 1 + .../main/scala/docspell/store/qb/DSL.scala | 6 + .../main/scala/docspell/store/qb/Select.scala | 3 + .../docspell/store/queries/QCustomField.scala | 10 +- .../docspell/store/queries/QFolder.scala | 13 +- .../store/queries/QOrganization.scala | 12 +- .../docspell/store/records/REquipment.scala | 2 +- .../store/records/ROrganization.scala | 8 +- .../docspell/store/records/RPerson.scala | 12 +- .../scala/docspell/store/records/RTag.scala | 7 +- modules/webapp/src/main/elm/Api.elm | 73 ++++++++--- .../main/elm/Comp/ClassifierSettingsForm.elm | 3 +- .../src/main/elm/Comp/CustomFieldManage.elm | 42 ++++++- .../main/elm/Comp/CustomFieldMultiInput.elm | 3 +- .../src/main/elm/Comp/CustomFieldTable.elm | 72 ++++++++++- .../src/main/elm/Comp/EquipmentManage.elm | 23 +++- .../src/main/elm/Comp/EquipmentTable.elm | 44 +++++-- .../webapp/src/main/elm/Comp/FolderManage.elm | 47 +++++-- .../webapp/src/main/elm/Comp/FolderTable.elm | 70 ++++++++++- .../elm/Comp/ItemDetail/MultiEditMenu.elm | 12 +- .../src/main/elm/Comp/ItemDetail/Update.elm | 14 ++- .../src/main/elm/Comp/NotificationForm.elm | 3 +- .../webapp/src/main/elm/Comp/OrgManage.elm | 27 +++- modules/webapp/src/main/elm/Comp/OrgTable.elm | 42 +++++-- .../webapp/src/main/elm/Comp/PersonManage.elm | 24 +++- .../webapp/src/main/elm/Comp/PersonTable.elm | 76 ++++++++++-- .../src/main/elm/Comp/ScanMailboxForm.elm | 10 +- .../webapp/src/main/elm/Comp/SearchMenu.elm | 38 +++--- .../webapp/src/main/elm/Comp/SourceForm.elm | 6 +- .../webapp/src/main/elm/Comp/TagManage.elm | 26 +++- modules/webapp/src/main/elm/Comp/TagTable.elm | 89 +++++++++++-- .../src/main/elm/Comp/UiSettingsForm.elm | 3 +- .../src/main/elm/Data/CustomFieldOrder.elm | 31 +++++ .../src/main/elm/Data/EquipmentOrder.elm | 23 ++++ .../webapp/src/main/elm/Data/FolderOrder.elm | 31 +++++ .../src/main/elm/Data/OrganizationOrder.elm | 23 ++++ .../webapp/src/main/elm/Data/PersonOrder.elm | 31 +++++ modules/webapp/src/main/elm/Data/TagOrder.elm | 31 +++++ .../main/elm/Messages/Comp/FolderTable.elm | 3 + 52 files changed, 1236 insertions(+), 208 deletions(-) create mode 100644 modules/webapp/src/main/elm/Data/CustomFieldOrder.elm create mode 100644 modules/webapp/src/main/elm/Data/EquipmentOrder.elm create mode 100644 modules/webapp/src/main/elm/Data/FolderOrder.elm create mode 100644 modules/webapp/src/main/elm/Data/OrganizationOrder.elm create mode 100644 modules/webapp/src/main/elm/Data/PersonOrder.elm create mode 100644 modules/webapp/src/main/elm/Data/TagOrder.elm diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala index c864b098..44485e56 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala @@ -7,12 +7,13 @@ package docspell.backend.ops import cats.data.EitherT -import cats.data.NonEmptyList import cats.data.OptionT +import cats.data.{NonEmptyList => Nel} import cats.effect._ import cats.implicits._ import docspell.backend.ops.OCustomFields.CustomFieldData +import docspell.backend.ops.OCustomFields.CustomFieldOrder import docspell.backend.ops.OCustomFields.FieldValue import docspell.backend.ops.OCustomFields.NewCustomField import docspell.backend.ops.OCustomFields.RemoveValue @@ -33,7 +34,11 @@ import org.log4s.getLogger trait OCustomFields[F[_]] { /** Find all fields using an optional query on the name and label */ - def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]] + def findAll( + coll: Ident, + nameQuery: Option[String], + order: CustomFieldOrder + ): F[Vector[CustomFieldData]] /** Find one field by its id */ def findById(coll: Ident, fieldId: Ident): F[Option[CustomFieldData]] @@ -50,13 +55,13 @@ trait OCustomFields[F[_]] { /** Sets a value given a field an an item. Existing values are overwritten. */ def setValue(item: Ident, value: SetValue): F[SetValueResult] - def setValueMultiple(items: NonEmptyList[Ident], value: SetValue): F[SetValueResult] + def setValueMultiple(items: Nel[Ident], value: SetValue): F[SetValueResult] /** Deletes a value for a given field an item. */ def deleteValue(in: RemoveValue): F[UpdateResult] /** Finds all values to the given items */ - def findAllValues(itemIds: NonEmptyList[Ident]): F[List[FieldValue]] + def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]] } object OCustomFields { @@ -96,10 +101,48 @@ object OCustomFields { case class RemoveValue( field: Ident, - item: NonEmptyList[Ident], + item: Nel[Ident], collective: Ident ) + sealed trait CustomFieldOrder + object CustomFieldOrder { + import docspell.store.qb.DSL._ + + final case object NameAsc extends CustomFieldOrder + final case object NameDesc extends CustomFieldOrder + final case object LabelAsc extends CustomFieldOrder + final case object LabelDesc extends CustomFieldOrder + final case object TypeAsc extends CustomFieldOrder + final case object TypeDesc extends CustomFieldOrder + + def parse(str: String): Either[String, CustomFieldOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case "label" => Right(LabelAsc) + case "-label" => Right(LabelDesc) + case "type" => Right(TypeAsc) + case "-type" => Right(TypeDesc) + case _ => Left(s"Unknown sort property for custom field: $str") + } + + def parseOrDefault(str: String): CustomFieldOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply( + order: CustomFieldOrder + )(field: RCustomField.Table) = + order match { + case NameAsc => Nel.of(field.name.asc) + case NameDesc => Nel.of(field.name.desc) + case LabelAsc => Nel.of(coalesce(field.label.s, field.name.s).asc) + case LabelDesc => Nel.of(coalesce(field.label.s, field.name.s).desc) + case TypeAsc => Nel.of(field.ftype.asc, field.name.asc) + case TypeDesc => Nel.of(field.ftype.desc, field.name.desc) + } + } + def apply[F[_]: Async]( store: Store[F] ): Resource[F, OCustomFields[F]] = @@ -107,14 +150,19 @@ object OCustomFields { private[this] val logger = Logger.log4s[ConnectionIO](getLogger) - def findAllValues(itemIds: NonEmptyList[Ident]): F[List[FieldValue]] = + def findAllValues(itemIds: Nel[Ident]): F[List[FieldValue]] = store.transact(QCustomField.findAllValues(itemIds)) - def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]] = + def findAll( + coll: Ident, + nameQuery: Option[String], + order: CustomFieldOrder + ): F[Vector[CustomFieldData]] = store.transact( QCustomField.findAllLike( coll, - nameQuery.map(WildcardString.apply).flatMap(_.both) + nameQuery.map(WildcardString.apply).flatMap(_.both), + CustomFieldOrder(order) ) ) @@ -149,10 +197,10 @@ object OCustomFields { } def setValue(item: Ident, value: SetValue): F[SetValueResult] = - setValueMultiple(NonEmptyList.of(item), value) + setValueMultiple(Nel.of(item), value) def setValueMultiple( - items: NonEmptyList[Ident], + items: Nel[Ident], value: SetValue ): F[SetValueResult] = (for { diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala index 984b351d..71e40201 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OEquipment.scala @@ -6,6 +6,7 @@ package docspell.backend.ops +import cats.data.NonEmptyList import cats.effect.{Async, Resource} import cats.implicits._ @@ -15,7 +16,11 @@ import docspell.store.{AddResult, Store} trait OEquipment[F[_]] { - def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] + def findAll( + account: AccountId, + nameQuery: Option[String], + order: OEquipment.EquipmentOrder + ): F[Vector[REquipment]] def find(account: AccountId, id: Ident): F[Option[REquipment]] @@ -27,11 +32,39 @@ trait OEquipment[F[_]] { } object OEquipment { + import docspell.store.qb.DSL._ + + sealed trait EquipmentOrder + object EquipmentOrder { + final case object NameAsc extends EquipmentOrder + final case object NameDesc extends EquipmentOrder + + def parse(str: String): Either[String, EquipmentOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case _ => Left(s"Unknown sort property for equipments: $str") + } + + def parseOrDefault(str: String): EquipmentOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply(order: EquipmentOrder)(table: REquipment.Table) = order match { + case NameAsc => NonEmptyList.of(table.name.asc) + case NameDesc => NonEmptyList.of(table.name.desc) + } + } def apply[F[_]: Async](store: Store[F]): Resource[F, OEquipment[F]] = Resource.pure[F, OEquipment[F]](new OEquipment[F] { - def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[REquipment]] = - store.transact(REquipment.findAll(account.collective, nameQuery, _.name)) + def findAll( + account: AccountId, + nameQuery: Option[String], + order: EquipmentOrder + ): F[Vector[REquipment]] = + store.transact( + REquipment.findAll(account.collective, nameQuery, EquipmentOrder(order)) + ) def find(account: AccountId, id: Ident): F[Option[REquipment]] = store.transact(REquipment.findById(id)).map(_.filter(_.cid == account.collective)) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala b/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala index b7e79766..f610d069 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OFolder.scala @@ -6,6 +6,7 @@ package docspell.backend.ops +import cats.data.{NonEmptyList => Nel} import cats.effect._ import docspell.common._ @@ -18,7 +19,8 @@ trait OFolder[F[_]] { def findAll( account: AccountId, ownerLogin: Option[Ident], - nameQuery: Option[String] + query: Option[String], + order: OFolder.FolderOrder ): F[Vector[OFolder.FolderItem]] def findById(id: Ident, account: AccountId): F[Option[OFolder.FolderDetail]] @@ -50,6 +52,7 @@ trait OFolder[F[_]] { } object OFolder { + import docspell.store.qb.DSL._ type FolderChangeResult = QFolder.FolderChangeResult val FolderChangeResult = QFolder.FolderChangeResult @@ -60,14 +63,45 @@ object OFolder { type FolderDetail = QFolder.FolderDetail val FolderDetail = QFolder.FolderDetail + sealed trait FolderOrder + object FolderOrder { + final case object NameAsc extends FolderOrder + final case object NameDesc extends FolderOrder + final case object OwnerAsc extends FolderOrder + final case object OwnerDesc extends FolderOrder + + def parse(str: String): Either[String, FolderOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case "owner" => Right(OwnerAsc) + case "-owner" => Right(OwnerDesc) + case _ => Left(s"Unknown sort property for folder: $str") + } + + def parseOrDefault(str: String): FolderOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply(order: FolderOrder)(folder: RFolder.Table, user: RUser.Table) = + order match { + case NameAsc => Nel.of(folder.name.asc) + case NameDesc => Nel.of(folder.name.desc) + case OwnerAsc => Nel.of(user.login.asc, folder.name.asc) + case OwnerDesc => Nel.of(user.login.desc, folder.name.desc) + } + } + def apply[F[_]](store: Store[F]): Resource[F, OFolder[F]] = Resource.pure[F, OFolder[F]](new OFolder[F] { def findAll( account: AccountId, ownerLogin: Option[Ident], - nameQuery: Option[String] + query: Option[String], + order: FolderOrder ): F[Vector[FolderItem]] = - store.transact(QFolder.findAll(account, None, ownerLogin, nameQuery)) + store.transact( + QFolder.findAll(account, None, ownerLogin, query, FolderOrder(order)) + ) def findById(id: Ident, account: AccountId): F[Option[FolderDetail]] = store.transact(QFolder.findById(id, account)) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala index 4bd077ed..b9fa1157 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala @@ -6,6 +6,7 @@ package docspell.backend.ops +import cats.data.NonEmptyList import cats.effect.{Async, Resource} import cats.implicits._ @@ -16,10 +17,18 @@ import docspell.store.queries.QOrganization import docspell.store.records._ trait OOrganization[F[_]] { - def findAllOrg(account: AccountId, query: Option[String]): F[Vector[OrgAndContacts]] + def findAllOrg( + account: AccountId, + query: Option[String], + order: OrganizationOrder + ): F[Vector[OrgAndContacts]] def findOrg(account: AccountId, orgId: Ident): F[Option[OrgAndContacts]] - def findAllOrgRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] + def findAllOrgRefs( + account: AccountId, + nameQuery: Option[String], + order: OrganizationOrder + ): F[Vector[IdRef]] def addOrg(s: OrgAndContacts): F[AddResult] @@ -27,12 +36,17 @@ trait OOrganization[F[_]] { def findAllPerson( account: AccountId, - query: Option[String] + query: Option[String], + order: PersonOrder ): F[Vector[PersonAndContacts]] def findPerson(account: AccountId, persId: Ident): F[Option[PersonAndContacts]] - def findAllPersonRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] + def findAllPersonRefs( + account: AccountId, + nameQuery: Option[String], + order: PersonOrder + ): F[Vector[IdRef]] /** Add a new person with their contacts. The additional organization is ignored. */ def addPerson(s: PersonAndContacts): F[AddResult] @@ -46,6 +60,7 @@ trait OOrganization[F[_]] { } object OOrganization { + import docspell.store.qb.DSL._ case class OrgAndContacts(org: ROrganization, contacts: Seq[RContact]) @@ -55,15 +70,79 @@ object OOrganization { contacts: Seq[RContact] ) + sealed trait OrganizationOrder + object OrganizationOrder { + final case object NameAsc extends OrganizationOrder + final case object NameDesc extends OrganizationOrder + + def parse(str: String): Either[String, OrganizationOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case _ => Left(s"Unknown sort property for organization: $str") + } + + def parseOrDefault(str: String): OrganizationOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply(order: OrganizationOrder)(table: ROrganization.Table) = + order match { + case NameAsc => NonEmptyList.of(table.name.asc) + case NameDesc => NonEmptyList.of(table.name.desc) + } + } + + sealed trait PersonOrder + object PersonOrder { + final case object NameAsc extends PersonOrder + final case object NameDesc extends PersonOrder + final case object OrgAsc extends PersonOrder + final case object OrgDesc extends PersonOrder + + def parse(str: String): Either[String, PersonOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case "org" => Right(OrgAsc) + case "-org" => Right(OrgDesc) + case _ => Left(s"Unknown sort property for person: $str") + } + + def parseOrDefault(str: String): PersonOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply( + order: PersonOrder + )(person: RPerson.Table, org: ROrganization.Table) = + order match { + case NameAsc => NonEmptyList.of(person.name.asc) + case NameDesc => NonEmptyList.of(person.name.desc) + case OrgAsc => NonEmptyList.of(org.name.asc) + case OrgDesc => NonEmptyList.of(org.name.desc) + } + + private[ops] def nameOnly(order: PersonOrder)(person: RPerson.Table) = + order match { + case NameAsc => NonEmptyList.of(person.name.asc) + case NameDesc => NonEmptyList.of(person.name.desc) + case OrgAsc => NonEmptyList.of(person.name.asc) + case OrgDesc => NonEmptyList.of(person.name.asc) + } + } + def apply[F[_]: Async](store: Store[F]): Resource[F, OOrganization[F]] = Resource.pure[F, OOrganization[F]](new OOrganization[F] { def findAllOrg( account: AccountId, - query: Option[String] + query: Option[String], + order: OrganizationOrder ): F[Vector[OrgAndContacts]] = store - .transact(QOrganization.findOrgAndContact(account.collective, query, _.name)) + .transact( + QOrganization + .findOrgAndContact(account.collective, query, OrganizationOrder(order)) + ) .map { case (org, cont) => OrgAndContacts(org, cont) } .compile .toVector @@ -75,9 +154,16 @@ object OOrganization { def findAllOrgRefs( account: AccountId, - nameQuery: Option[String] + nameQuery: Option[String], + order: OrganizationOrder ): F[Vector[IdRef]] = - store.transact(ROrganization.findAllRef(account.collective, nameQuery, _.name)) + store.transact( + ROrganization.findAllRef( + account.collective, + nameQuery, + OrganizationOrder(order) + ) + ) def addOrg(s: OrgAndContacts): F[AddResult] = QOrganization.addOrg(s.org, s.contacts, s.org.cid)(store) @@ -87,10 +173,14 @@ object OOrganization { def findAllPerson( account: AccountId, - query: Option[String] + query: Option[String], + order: PersonOrder ): F[Vector[PersonAndContacts]] = store - .transact(QOrganization.findPersonAndContact(account.collective, query, _.name)) + .transact( + QOrganization + .findPersonAndContact(account.collective, query, PersonOrder(order)) + ) .map { case (person, org, cont) => PersonAndContacts(person, org, cont) } .compile .toVector @@ -102,9 +192,12 @@ object OOrganization { def findAllPersonRefs( account: AccountId, - nameQuery: Option[String] + nameQuery: Option[String], + order: PersonOrder ): F[Vector[IdRef]] = - store.transact(RPerson.findAllRef(account.collective, nameQuery, _.name)) + store.transact( + RPerson.findAllRef(account.collective, nameQuery, PersonOrder.nameOnly(order)) + ) def addPerson(s: PersonAndContacts): F[AddResult] = QOrganization.addPerson(s.person, s.contacts, s.person.cid)(store) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala index ce500aec..7bf6128e 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OTag.scala @@ -6,6 +6,7 @@ package docspell.backend.ops +import cats.data.NonEmptyList import cats.effect.{Async, Resource} import cats.implicits._ @@ -16,7 +17,11 @@ import docspell.store.{AddResult, Store} trait OTag[F[_]] { - def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] + def findAll( + account: AccountId, + query: Option[String], + order: OTag.TagOrder + ): F[Vector[RTag]] def add(s: RTag): F[AddResult] @@ -30,11 +35,43 @@ trait OTag[F[_]] { } object OTag { + import docspell.store.qb.DSL._ + + sealed trait TagOrder + object TagOrder { + final case object NameAsc extends TagOrder + final case object NameDesc extends TagOrder + final case object CategoryAsc extends TagOrder + final case object CategoryDesc extends TagOrder + + def parse(str: String): Either[String, TagOrder] = + str.toLowerCase match { + case "name" => Right(NameAsc) + case "-name" => Right(NameDesc) + case "category" => Right(CategoryAsc) + case "-category" => Right(CategoryDesc) + case _ => Left(s"Unknown sort property for tags: $str") + } + + def parseOrDefault(str: String): TagOrder = + parse(str).toOption.getOrElse(NameAsc) + + private[ops] def apply(order: TagOrder)(table: RTag.Table) = order match { + case NameAsc => NonEmptyList.of(table.name.asc) + case CategoryAsc => NonEmptyList.of(table.category.asc, table.name.asc) + case NameDesc => NonEmptyList.of(table.name.desc) + case CategoryDesc => NonEmptyList.of(table.category.desc, table.name.desc) + } + } def apply[F[_]: Async](store: Store[F]): Resource[F, OTag[F]] = Resource.pure[F, OTag[F]](new OTag[F] { - def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[RTag]] = - store.transact(RTag.findAll(account.collective, nameQuery, _.name)) + def findAll( + account: AccountId, + query: Option[String], + order: TagOrder + ): F[Vector[RTag]] = + store.transact(RTag.findAll(account.collective, query, TagOrder(order))) def add(t: RTag): F[AddResult] = { def insert = RTag.insert(t) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index fc7d9e24..3ee2a5a9 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -501,11 +501,14 @@ paths: tags: [ Tags ] summary: Get a list of tags 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: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/sort" responses: 200: description: Ok @@ -579,12 +582,16 @@ paths: tags: [ Organization ] summary: Get a list of organizations. 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: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/full" - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/sort" responses: 200: description: Ok @@ -677,12 +684,17 @@ paths: tags: [ Person ] summary: Get a list of persons. 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: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/full" - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/sort" responses: 200: description: Ok @@ -775,11 +787,14 @@ paths: tags: [ Equipment ] summary: Get a list of equipments 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: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/sort" responses: 200: description: Ok @@ -3771,11 +3786,15 @@ paths: tags: [ Custom Fields ] summary: Get all defined custom fields. 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: - authTokenHeader: [] parameters: - $ref: "#/components/parameters/q" + - $ref: "#/components/parameters/sort" responses: 200: description: Ok @@ -5985,6 +6004,14 @@ components: schema: type: integer format: int32 + sort: + name: sort + in: query + required: false + description: | + How to sort the returned list + schema: + type: string withDetails: name: withDetails in: query diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala index 23619cd3..95b2afb8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/QueryParam.scala @@ -6,6 +6,11 @@ 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.SearchMode @@ -29,6 +34,36 @@ object QueryParam { 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 OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning") @@ -42,6 +77,12 @@ object QueryParam { object Offset extends OptionalQueryParamDecoderMatcher[Int]("offset") object WithDetails extends OptionalQueryParamDecoderMatcher[Boolean]("withDetails") 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") } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala index 7a5bb8a0..22729db1 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala @@ -13,7 +13,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OCustomFields -import docspell.backend.ops.OCustomFields.CustomFieldData +import docspell.backend.ops.OCustomFields.{CustomFieldData, CustomFieldOrder} import docspell.common._ import docspell.restapi.model._ import docspell.restserver.conv.Conversions @@ -34,9 +34,14 @@ object CustomFieldRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? QueryParam.QueryOpt(param) => + case GET -> Root :? QueryParam.QueryOpt(param) +& QueryParam.FieldSort(sort) => + val order = sort.getOrElse(CustomFieldOrder.NameAsc) 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)) } yield res diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala index 92e858ea..b52c3504 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/EquipmentRoutes.scala @@ -12,6 +12,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OEquipment import docspell.common.Ident import docspell.restapi.model._ import docspell.restserver.conv.Conversions._ @@ -29,9 +30,13 @@ object EquipmentRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? QueryParam.QueryOpt(q) => + case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.EquipSort(sort) => 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)) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala index 99475f0f..3c012681 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/FolderRoutes.scala @@ -31,11 +31,13 @@ object FolderRoutes { import dsl._ 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 = owning.filter(identity).map(_ => user.account.user) 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)) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala index 100f4ff3..b8f5c34d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/OrganizationRoutes.scala @@ -12,6 +12,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OOrganization.OrganizationOrder import docspell.common.Ident import docspell.restapi.model._ import docspell.restserver.conv.Conversions._ @@ -29,15 +30,21 @@ object OrganizationRoutes { import dsl._ 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)) 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)) } yield resp else 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)) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala index fa677125..810c04af 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PersonRoutes.scala @@ -12,6 +12,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OOrganization import docspell.common.Ident import docspell.common.syntax.all._ import docspell.restapi.model._ @@ -32,15 +33,25 @@ object PersonRoutes { import dsl._ 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)) 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)) } yield resp else 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)) } yield resp diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala index 85e4c5f3..4e3f85a8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/TagRoutes.scala @@ -11,6 +11,7 @@ import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken +import docspell.backend.ops.OTag.TagOrder import docspell.common.Ident import docspell.restapi.model._ import docspell.restserver.conv.Conversions._ @@ -28,9 +29,13 @@ object TagRoutes { import dsl._ HttpRoutes.of { - case GET -> Root :? QueryParam.QueryOpt(q) => + case GET -> Root :? QueryParam.QueryOpt(q) :? QueryParam.TagSort(sort) => 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)) } yield resp diff --git a/modules/store/src/main/scala/docspell/store/qb/Column.scala b/modules/store/src/main/scala/docspell/store/qb/Column.scala index fa065862..45748523 100644 --- a/modules/store/src/main/scala/docspell/store/qb/Column.scala +++ b/modules/store/src/main/scala/docspell/store/qb/Column.scala @@ -12,6 +12,7 @@ case class Column[A](name: String, table: TableDef) { def cast[B]: Column[B] = this.asInstanceOf[Column[B]] + } object Column {} diff --git a/modules/store/src/main/scala/docspell/store/qb/DSL.scala b/modules/store/src/main/scala/docspell/store/qb/DSL.scala index d9588357..b3c95dac 100644 --- a/modules/store/src/main/scala/docspell/store/qb/DSL.scala +++ b/modules/store/src/main/scala/docspell/store/qb/DSL.scala @@ -303,6 +303,12 @@ trait DSL extends DoobieMeta { def as(otherCol: Column[_]): SelectExpr = 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 = Condition.CompareFVal(dbf.s, Operator.Eq, value) diff --git a/modules/store/src/main/scala/docspell/store/qb/Select.scala b/modules/store/src/main/scala/docspell/store/qb/Select.scala index 67cb4cdd..42e9c3f9 100644 --- a/modules/store/src/main/scala/docspell/store/qb/Select.scala +++ b/modules/store/src/main/scala/docspell/store/qb/Select.scala @@ -135,6 +135,9 @@ object Select { def orderBy(ob: OrderBy, obs: OrderBy*): Ordered = 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 { diff --git a/modules/store/src/main/scala/docspell/store/queries/QCustomField.scala b/modules/store/src/main/scala/docspell/store/queries/QCustomField.scala index b2ce9836..c6cc4e73 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QCustomField.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QCustomField.scala @@ -24,9 +24,10 @@ object QCustomField { def findAllLike( coll: Ident, - nameQuery: Option[String] + nameQuery: Option[String], + order: RCustomField.Table => Nel[OrderBy] ): 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]] = findFragment(collective, None, field.some).build.query[CustomFieldData].option @@ -34,7 +35,8 @@ object QCustomField { private def findFragment( coll: Ident, nameQuery: Option[String], - fieldId: Option[Ident] + fieldId: Option[Ident], + order: RCustomField.Table => Nel[OrderBy] = t => Nel.of(t.name.asc) ): Select = { val nameFilter = nameQuery.map { q => f.name.likes(q) || (f.label.isNotNull && f.label.like(q)) @@ -46,7 +48,7 @@ object QCustomField { .leftJoin(v, f.id === v.field), f.cid === coll &&? nameFilter &&? fieldId.map(fid => f.id === fid), GroupBy(f.all) - ) + ).orderBy(order(f)) } final case class FieldValue( diff --git a/modules/store/src/main/scala/docspell/store/queries/QFolder.scala b/modules/store/src/main/scala/docspell/store/queries/QFolder.scala index 819b6004..dc4cf257 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QFolder.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QFolder.scala @@ -7,6 +7,7 @@ package docspell.store.queries import cats.data.OptionT +import cats.data.{NonEmptyList => Nel} import cats.implicits._ import docspell.common._ @@ -156,8 +157,11 @@ object QFolder { ).query[IdRef].to[Vector] (for { - folder <- OptionT(findAll(account, Some(id), None, None).map(_.headOption)) - memb <- OptionT.liftF(memberQ) + folder <- OptionT( + findAll(account, Some(id), None, None, (ft, _) => Nel.of(ft.name.asc)) + .map(_.headOption) + ) + memb <- OptionT.liftF(memberQ) } yield folder.withMembers(memb.toList)).value } @@ -165,7 +169,8 @@ object QFolder { account: AccountId, idQ: Option[Ident], ownerLogin: Option[Ident], - nameQ: Option[String] + nameQ: Option[String], + order: (RFolder.Table, RUser.Table) => Nel[OrderBy] ): ConnectionIO[Vector[FolderItem]] = { // with memberlogin as // (select m.folder_id,u.login @@ -239,7 +244,7 @@ object QFolder { nameQ.map(q => folder.name.like(s"%${q.toLowerCase}%")) &&? ownerLogin.map(login => user.login === login) ) - ).orderBy(folder.name.asc) + ).orderBy(order(folder, user)) ).build.query[FolderItem].to[Vector] } diff --git a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala index 09acc340..2286fac3 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala @@ -6,7 +6,7 @@ package docspell.store.queries -import cats.data.NonEmptyList +import cats.data.{NonEmptyList => Nel} import cats.implicits._ import fs2._ @@ -27,7 +27,7 @@ object QOrganization { def findOrgAndContact( coll: Ident, query: Option[String], - order: ROrganization.Table => Column[_] + order: ROrganization.Table => Nel[OrderBy] ): Stream[ConnectionIO, (ROrganization, Vector[RContact])] = { val valFilter = query.map { q => val v = s"%$q%" @@ -74,18 +74,18 @@ object QOrganization { def findPersonAndContact( coll: Ident, query: Option[String], - order: RPerson.Table => Column[_] + order: (RPerson.Table, ROrganization.Table) => Nel[OrderBy] ): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = { val valFilter = query .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( select(p.all, org.all, c.all), from(p) .leftJoin(org, org.oid === p.oid) .leftJoin(c, c.personId === p.pid), p.cid === coll &&? valFilter - ).orderBy(order(p)) + ).orderBy(order(p, org)) sql.build .query[(RPerson, Option[ROrganization], Option[RContact])] @@ -128,7 +128,7 @@ object QOrganization { coll: Ident, value: String, ck: Option[ContactKind], - use: Option[NonEmptyList[PersonUse]] + use: Option[Nel[PersonUse]] ): Stream[ConnectionIO, RPerson] = runDistinct( select(p.all), diff --git a/modules/store/src/main/scala/docspell/store/records/REquipment.scala b/modules/store/src/main/scala/docspell/store/records/REquipment.scala index 0aa6fc34..517b589e 100644 --- a/modules/store/src/main/scala/docspell/store/records/REquipment.scala +++ b/modules/store/src/main/scala/docspell/store/records/REquipment.scala @@ -87,7 +87,7 @@ object REquipment { def findAll( coll: Ident, nameQ: Option[String], - order: Table => Column[_] + order: Table => NonEmptyList[OrderBy] ): ConnectionIO[Vector[REquipment]] = { val t = Table(None) diff --git a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala index 4e2f4a01..ca86c4ec 100644 --- a/modules/store/src/main/scala/docspell/store/records/ROrganization.scala +++ b/modules/store/src/main/scala/docspell/store/records/ROrganization.scala @@ -7,7 +7,7 @@ package docspell.store.records import cats.Eq -import cats.data.NonEmptyList +import cats.data.{NonEmptyList => Nel} import fs2.Stream import docspell.common.{IdRef, _} @@ -52,7 +52,7 @@ object ROrganization { val shortName = Column[String]("short_name", this) val use = Column[OrgUse]("org_use", this) val all = - NonEmptyList.of[Column[_]]( + Nel.of[Column[_]]( oid, cid, name, @@ -122,7 +122,7 @@ object ROrganization { def findLike( coll: Ident, orgName: String, - use: NonEmptyList[OrgUse] + use: Nel[OrgUse] ): ConnectionIO[Vector[IdRef]] = run( select(T.oid, T.name), @@ -163,7 +163,7 @@ object ROrganization { def findAllRef( coll: Ident, nameQ: Option[String], - order: Table => Column[_] + order: Table => Nel[OrderBy] ): ConnectionIO[Vector[IdRef]] = { val nameFilter = nameQ.map(s => T.name.like(s"%${s.toLowerCase}%") || T.shortName.like(s"%${s.toLowerCase}%") diff --git a/modules/store/src/main/scala/docspell/store/records/RPerson.scala b/modules/store/src/main/scala/docspell/store/records/RPerson.scala index e14b0ecf..fe740d65 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPerson.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPerson.scala @@ -7,7 +7,7 @@ package docspell.store.records import cats.Eq -import cats.data.NonEmptyList +import cats.data.{NonEmptyList => Nel} import cats.effect._ import fs2.Stream @@ -52,7 +52,7 @@ object RPerson { val updated = Column[Timestamp]("updated", this) val oid = Column[Ident]("oid", this) val use = Column[PersonUse]("person_use", this) - val all = NonEmptyList.of[Column[_]]( + val all = Nel.of[Column[_]]( pid, cid, name, @@ -122,7 +122,7 @@ object RPerson { def findLike( coll: Ident, personName: String, - use: NonEmptyList[PersonUse] + use: Nel[PersonUse] ): ConnectionIO[Vector[IdRef]] = run( select(T.pid, T.name), @@ -134,7 +134,7 @@ object RPerson { coll: Ident, contactKind: ContactKind, value: String, - use: NonEmptyList[PersonUse] + use: Nel[PersonUse] ): ConnectionIO[Vector[IdRef]] = { val p = RPerson.as("p") val c = RContact.as("c") @@ -162,7 +162,7 @@ object RPerson { def findAllRef( coll: Ident, nameQ: Option[String], - order: Table => Column[_] + order: Table => Nel[OrderBy] ): ConnectionIO[Vector[IdRef]] = { 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) def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = - NonEmptyList.fromList(ids.toList) match { + Nel.fromList(ids.toList) match { case Some(nel) => run(select(T.pid, T.name, T.oid), from(T), T.pid.in(nel)) .query[PersonRef] diff --git a/modules/store/src/main/scala/docspell/store/records/RTag.scala b/modules/store/src/main/scala/docspell/store/records/RTag.scala index f9739b74..00ac43e8 100644 --- a/modules/store/src/main/scala/docspell/store/records/RTag.scala +++ b/modules/store/src/main/scala/docspell/store/records/RTag.scala @@ -75,10 +75,11 @@ object RTag { def findAll( coll: Ident, - nameQ: Option[String], - order: Table => Column[_] + query: Option[String], + order: Table => NonEmptyList[OrderBy] ): 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 = Select(select(T.all), from(T), T.cid === coll &&? nameFilter).orderBy(order(T)) sql.build.query[RTag].to[Vector] diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index d816f551..9d65af14 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -216,8 +216,14 @@ import Api.Model.UserList exposing (UserList) import Api.Model.UserPass exposing (UserPass) import Api.Model.VersionInfo exposing (VersionInfo) import Data.ContactType exposing (ContactType) +import Data.CustomFieldOrder exposing (CustomFieldOrder) +import Data.EquipmentOrder exposing (EquipmentOrder) 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.TagOrder exposing (TagOrder) import Data.UiSettings exposing (UiSettings) import File exposing (File) import Http @@ -291,13 +297,15 @@ putCustomValue flags item fieldValue receive = } -getCustomFields : Flags -> String -> (Result Http.Error CustomFieldList -> msg) -> Cmd msg -getCustomFields flags query receive = +getCustomFields : Flags -> String -> CustomFieldOrder -> (Result Http.Error CustomFieldList -> msg) -> Cmd msg +getCustomFields flags query order receive = Http2.authGet { url = flags.config.baseUrl ++ "/api/v1/sec/customfield?q=" ++ Url.percentEncode query + ++ "&sort=" + ++ Data.CustomFieldOrder.asString order , account = getAccount flags , 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 flags query owningOnly receive = +getFolders : + Flags + -> String + -> FolderOrder + -> Bool + -> (Result Http.Error FolderList -> msg) + -> Cmd msg +getFolders flags query order owningOnly receive = Http2.authGet { url = flags.config.baseUrl ++ "/api/v1/sec/folder?q=" ++ Url.percentEncode query + ++ "&sort=" + ++ Data.FolderOrder.asString order ++ (if owningOnly then "&owning=true" @@ -1109,10 +1125,15 @@ getContacts flags kind q receive = --- Tags -getTags : Flags -> String -> (Result Http.Error TagList -> msg) -> Cmd msg -getTags flags query receive = +getTags : Flags -> String -> TagOrder -> (Result Http.Error TagList -> msg) -> Cmd msg +getTags flags query order receive = 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 , expect = Http.expectJson receive Api.Model.TagList.decoder } @@ -1148,10 +1169,15 @@ deleteTag flags tag receive = --- Equipments -getEquipments : Flags -> String -> (Result Http.Error EquipmentList -> msg) -> Cmd msg -getEquipments flags query receive = +getEquipments : Flags -> String -> EquipmentOrder -> (Result Http.Error EquipmentList -> msg) -> Cmd msg +getEquipments flags query order receive = 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 , 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 flags query receive = +getOrganizations : + Flags + -> String + -> OrganizationOrder + -> (Result Http.Error OrganizationList -> msg) + -> Cmd msg +getOrganizations flags query order receive = 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 , 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 query receive = +getPersons : Flags -> String -> PersonOrder -> (Result Http.Error PersonList -> msg) -> Cmd msg +getPersons flags query order receive = 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 , expect = Http.expectJson receive Api.Model.PersonList.decoder } diff --git a/modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm b/modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm index 119e9dcd..7fd872d0 100644 --- a/modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/ClassifierSettingsForm.elm @@ -25,6 +25,7 @@ import Data.CalEvent exposing (CalEvent) import Data.DropdownStyle as DS import Data.Flags exposing (Flags) import Data.ListType exposing (ListType) +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -90,7 +91,7 @@ init flags sett = Comp.FixedDropdown.init Data.ListType.all } , Cmd.batch - [ Api.getTags flags "" GetTagsResp + [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp , Cmd.map ScheduleMsg cec ] ) diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldManage.elm b/modules/webapp/src/main/elm/Comp/CustomFieldManage.elm index e8ba37a7..6e1640c6 100644 --- a/modules/webapp/src/main/elm/Comp/CustomFieldManage.elm +++ b/modules/webapp/src/main/elm/Comp/CustomFieldManage.elm @@ -21,6 +21,7 @@ import Comp.Basic as B import Comp.CustomFieldForm import Comp.CustomFieldTable import Comp.MenuBar as MB +import Data.CustomFieldOrder exposing (CustomFieldOrder) import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) @@ -36,6 +37,7 @@ type alias Model = , fields : List CustomField , query : String , loading : Bool + , order : CustomFieldOrder } @@ -54,13 +56,14 @@ empty = , fields = [] , query = "" , loading = False + , order = Data.CustomFieldOrder.LabelAsc } init : Flags -> ( Model, Cmd Msg ) init flags = ( empty - , Api.getCustomFields flags empty.query CustomFieldListResp + , loadFields flags empty ) @@ -68,14 +71,22 @@ init flags = --- 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 = case msg of TableMsg lm -> let - ( tm, action ) = + ( tm, action, maybeOrder ) = Comp.CustomFieldTable.update lm model.tableModel + newOrder = + Maybe.withDefault model.order maybeOrder + detail = case action of Comp.CustomFieldTable.EditAction item -> @@ -83,8 +94,22 @@ update flags msg model = Comp.CustomFieldTable.NoAction -> 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 - ( { model | tableModel = tm, detailModel = detail }, Cmd.none ) + ( m1, c1 ) DetailMsg lm -> case model.detailModel of @@ -95,7 +120,7 @@ update flags msg model = cmd = if back then - Api.getCustomFields flags model.query CustomFieldListResp + loadFields flags model else Cmd.none @@ -118,8 +143,12 @@ update flags msg model = ( model, Cmd.none ) SetQuery str -> - ( { model | query = str } - , Api.getCustomFields flags str CustomFieldListResp + let + newModel = + { model | query = str } + in + ( newModel + , loadFields flags newModel ) CustomFieldListResp (Ok sl) -> @@ -207,6 +236,7 @@ viewTable2 texts model = } , Html.map TableMsg (Comp.CustomFieldTable.view2 texts.fieldTable + model.order model.tableModel model.fields ) diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm index b9ca6282..6686fc69 100644 --- a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm +++ b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm @@ -29,6 +29,7 @@ import Api.Model.ItemFieldValue exposing (ItemFieldValue) import Comp.CustomFieldInput import Comp.FixedDropdown import Data.CustomFieldChange exposing (CustomFieldChange(..)) +import Data.CustomFieldOrder import Data.CustomFieldType import Data.DropdownStyle as DS import Data.Flags exposing (Flags) @@ -116,7 +117,7 @@ init flags = initCmd : Flags -> Cmd Msg initCmd flags = - Api.getCustomFields flags "" CustomFieldResp + Api.getCustomFields flags "" Data.CustomFieldOrder.LabelAsc CustomFieldResp setValues : List ItemFieldValue -> Msg diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm b/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm index 1075a30e..7046a314 100644 --- a/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm +++ b/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm @@ -16,8 +16,10 @@ module Comp.CustomFieldTable exposing import Api.Model.CustomField exposing (CustomField) import Comp.Basic as B +import Data.CustomFieldOrder exposing (CustomFieldOrder) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.CustomFieldTable exposing (Texts) import Styles as S @@ -28,6 +30,7 @@ type alias Model = type Msg = EditItem CustomField + | ToggleOrder CustomFieldOrder type Action @@ -35,31 +38,88 @@ type Action | EditAction CustomField +type Header + = Label + | Format + + init : Model init = {} -update : Msg -> Model -> ( Model, Action ) +update : Msg -> Model -> ( Model, Action, Maybe CustomFieldOrder ) update msg model = case msg of 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 : Texts -> Model -> List CustomField -> Html Msg -view2 texts _ items = +view2 : Texts -> CustomFieldOrder -> Model -> List CustomField -> Html Msg +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 [] [ table [ class S.tableMain ] [ thead [] [ tr [] [ th [] [] - , th [ class "text-left" ] [ text texts.nameLabel ] - , th [ class "text-left" ] [ text texts.format ] + , th [ class "text-left" ] + [ 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.basics.created ] ] diff --git a/modules/webapp/src/main/elm/Comp/EquipmentManage.elm b/modules/webapp/src/main/elm/Comp/EquipmentManage.elm index 1d771639..645767db 100644 --- a/modules/webapp/src/main/elm/Comp/EquipmentManage.elm +++ b/modules/webapp/src/main/elm/Comp/EquipmentManage.elm @@ -22,6 +22,7 @@ import Comp.EquipmentForm import Comp.EquipmentTable import Comp.MenuBar as MB import Comp.YesNoDimmer +import Data.EquipmentOrder exposing (EquipmentOrder) import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) @@ -40,6 +41,7 @@ type alias Model = , loading : Bool , deleteConfirm : Comp.YesNoDimmer.Model , query : String + , order : EquipmentOrder } @@ -64,6 +66,7 @@ emptyModel = , loading = False , deleteConfirm = Comp.YesNoDimmer.emptyModel , query = "" + , order = Data.EquipmentOrder.NameAsc } @@ -86,9 +89,12 @@ update flags msg model = case msg of TableMsg m -> let - ( tm, tc ) = + ( tm, tc, maybeOrder ) = Comp.EquipmentTable.update flags m model.tableModel + newOrder = + Maybe.withDefault model.order maybeOrder + ( m2, c2 ) = ( { model | tableModel = tm @@ -99,6 +105,7 @@ update flags msg model = else model.formError + , order = newOrder } , Cmd.map TableMsg tc ) @@ -110,8 +117,15 @@ update flags msg model = Nothing -> ( m2, Cmd.none ) + + ( m4, c4 ) = + if model.order == newOrder then + ( m3, Cmd.none ) + + else + update flags LoadEquipments m3 in - ( m3, Cmd.batch [ c2, c3 ] ) + ( m4, Cmd.batch [ c2, c3, c4 ] ) FormMsg m -> let @@ -121,7 +135,7 @@ update flags msg model = ( { model | formModel = m2 }, Cmd.map FormMsg c2 ) LoadEquipments -> - ( { model | loading = True }, Api.getEquipments flags "" EquipmentResp ) + ( { model | loading = True }, Api.getEquipments flags model.query model.order EquipmentResp ) EquipmentResp (Ok equipments) -> let @@ -211,7 +225,7 @@ update flags msg model = m = { model | query = str } 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 (Comp.EquipmentTable.view2 texts.equipmentTable + model.order model.tableModel ) , div diff --git a/modules/webapp/src/main/elm/Comp/EquipmentTable.elm b/modules/webapp/src/main/elm/Comp/EquipmentTable.elm index 116c96af..2dadfae7 100644 --- a/modules/webapp/src/main/elm/Comp/EquipmentTable.elm +++ b/modules/webapp/src/main/elm/Comp/EquipmentTable.elm @@ -15,10 +15,12 @@ module Comp.EquipmentTable exposing import Api.Model.Equipment exposing (Equipment) import Comp.Basic as B +import Data.EquipmentOrder exposing (EquipmentOrder) import Data.EquipmentUse import Data.Flags exposing (Flags) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.EquipmentTable exposing (Texts) import Styles as S @@ -40,27 +42,50 @@ type Msg = SetEquipments (List Equipment) | Select Equipment | Deselect + | ToggleOrder EquipmentOrder -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe EquipmentOrder ) update _ msg model = case msg of SetEquipments list -> - ( { model | equips = list, selected = Nothing }, Cmd.none ) + ( { model | equips = list, selected = Nothing }, Cmd.none, Nothing ) Select equip -> - ( { model | selected = Just equip }, Cmd.none ) + ( { model | selected = Just equip }, Cmd.none, Nothing ) 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 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> EquipmentOrder -> Model -> Html Msg +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 ] [ thead [] [ tr [] @@ -68,7 +93,12 @@ view2 texts model = , th [ class "text-left pr-1 md:px-2 w-20" ] [ 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 [] diff --git a/modules/webapp/src/main/elm/Comp/FolderManage.elm b/modules/webapp/src/main/elm/Comp/FolderManage.elm index 9917d7bd..b41196c0 100644 --- a/modules/webapp/src/main/elm/Comp/FolderManage.elm +++ b/modules/webapp/src/main/elm/Comp/FolderManage.elm @@ -24,6 +24,7 @@ import Comp.FolderDetail import Comp.FolderTable import Comp.MenuBar as MB import Data.Flags exposing (Flags) +import Data.FolderOrder exposing (FolderOrder) import Html exposing (..) import Html.Attributes exposing (..) import Http @@ -39,6 +40,7 @@ type alias Model = , query : String , owningOnly : Bool , loading : Bool + , order : FolderOrder } @@ -62,6 +64,7 @@ empty = , query = "" , owningOnly = True , loading = False + , order = Data.FolderOrder.NameAsc } @@ -70,7 +73,7 @@ init flags = ( empty , Cmd.batch [ Api.getUsers flags UserListResp - , Api.getFolders flags empty.query empty.owningOnly FolderListResp + , loadFolders flags empty ] ) @@ -79,23 +82,41 @@ init flags = --- 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 = case msg of TableMsg lm -> let - ( tm, action ) = + ( tm, action, maybeOrder ) = Comp.FolderTable.update lm model.tableModel - cmd = + newOrder = + Maybe.withDefault model.order maybeOrder + + newModel = + { model | tableModel = tm, order = newOrder } + + detailCmd = case action of Comp.FolderTable.EditAction item -> Api.getFolderDetail flags item.id FolderDetailResp Comp.FolderTable.NoAction -> Cmd.none + + refreshCmd = + if model.order == newOrder then + Cmd.none + + else + loadFolders flags newModel in - ( { model | tableModel = tm }, cmd ) + ( newModel, Cmd.batch [ detailCmd, refreshCmd ] ) DetailMsg lm -> case model.detailModel of @@ -106,7 +127,7 @@ update flags msg model = cmd = if back then - Api.getFolders flags model.query model.owningOnly FolderListResp + loadFolders flags model else Cmd.none @@ -129,17 +150,24 @@ update flags msg model = ( model, Cmd.none ) SetQuery str -> - ( { model | query = str } - , Api.getFolders flags str model.owningOnly FolderListResp + let + nm = + { model | query = str } + in + ( nm + , loadFolders flags nm ) ToggleOwningOnly -> let newOwning = not model.owningOnly + + nm = + { model | owningOnly = newOwning } in - ( { model | owningOnly = newOwning } - , Api.getFolders flags model.query newOwning FolderListResp + ( nm + , loadFolders flags nm ) UserListResp (Ok ul) -> @@ -241,6 +269,7 @@ viewTable2 texts model = , Html.map TableMsg (Comp.FolderTable.view2 texts.folderTable + model.order model.tableModel model.folders ) diff --git a/modules/webapp/src/main/elm/Comp/FolderTable.elm b/modules/webapp/src/main/elm/Comp/FolderTable.elm index 1c9fea1d..6b7b9b97 100644 --- a/modules/webapp/src/main/elm/Comp/FolderTable.elm +++ b/modules/webapp/src/main/elm/Comp/FolderTable.elm @@ -16,8 +16,10 @@ module Comp.FolderTable exposing import Api.Model.FolderItem exposing (FolderItem) import Comp.Basic as B +import Data.FolderOrder exposing (FolderOrder) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.FolderTable exposing (Texts) import Styles as S @@ -28,6 +30,7 @@ type alias Model = type Msg = EditItem FolderItem + | ToggleOrder FolderOrder type Action @@ -35,32 +38,87 @@ type Action | EditAction FolderItem +type Header + = Name + | Owner + + init : Model init = {} -update : Msg -> Model -> ( Model, Action ) +update : Msg -> Model -> ( Model, Action, Maybe FolderOrder ) update msg model = case msg of 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 : Texts -> Model -> List FolderItem -> Html Msg -view2 texts _ items = +view2 : Texts -> FolderOrder -> Model -> List FolderItem -> Html Msg +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 ] [ thead [] [ tr [] [ th [ class "w-px whitespace-nowrap pr-1 md:pr-3" ] [] , 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" ] [ span [ class "hidden sm:inline" ] [ text texts.memberCount diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm index a9231590..84f18b59 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/MultiEditMenu.elm @@ -35,10 +35,14 @@ import Comp.Tabs as TB import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.Direction exposing (Direction) import Data.DropdownStyle +import Data.EquipmentOrder import Data.Fields import Data.Flags exposing (Flags) +import Data.FolderOrder import Data.Icons as Icons +import Data.PersonOrder import Data.PersonUse +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import DatePicker exposing (DatePicker) import Html exposing (..) @@ -157,11 +161,11 @@ loadModel flags = Comp.DatePicker.init in Cmd.batch - [ Api.getTags flags "" GetTagsResp + [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp , Api.getOrgLight flags GetOrgResp - , Api.getPersons flags "" GetPersonResp - , Api.getEquipments flags "" GetEquipResp - , Api.getFolders flags "" False GetFolderResp + , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp + , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp + , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , Cmd.map ItemDatePickerMsg dpc , Cmd.map DueDatePickerMsg dpc diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 1289a52f..691d0639 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -59,10 +59,14 @@ import Comp.PersonForm import Comp.SentMails import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.Direction +import Data.EquipmentOrder import Data.Fields exposing (Field) import Data.Flags exposing (Flags) +import Data.FolderOrder import Data.ItemNav exposing (ItemNav) +import Data.PersonOrder import Data.PersonUse +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import DatePicker import Dict @@ -265,7 +269,7 @@ update key flags inav settings msg model = , getOptions flags , proposalCmd , Api.getSentMails flags item.id SentMailsResp - , Api.getPersons flags "" GetPersonResp + , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) ] , sub = @@ -1642,11 +1646,11 @@ update key flags inav settings msg model = getOptions : Flags -> Cmd Msg getOptions flags = Cmd.batch - [ Api.getTags flags "" GetTagsResp + [ Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp , Api.getOrgLight flags GetOrgResp - , Api.getPersons flags "" GetPersonResp - , Api.getEquipments flags "" GetEquipResp - , Api.getFolders flags "" False GetFolderResp + , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp + , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp + , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp ] diff --git a/modules/webapp/src/main/elm/Comp/NotificationForm.elm b/modules/webapp/src/main/elm/Comp/NotificationForm.elm index 32ea132e..da4906ad 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationForm.elm @@ -30,6 +30,7 @@ import Comp.YesNoDimmer import Data.CalEvent exposing (CalEvent) import Data.DropdownStyle as DS import Data.Flags exposing (Flags) +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import Data.Validated exposing (Validated(..)) import Html exposing (..) @@ -182,7 +183,7 @@ init flags = } , Cmd.batch [ Api.getMailSettings flags "" ConnResp - , Api.getTags flags "" GetTagsResp + , Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp , Cmd.map CalEventMsg scmd ] ) diff --git a/modules/webapp/src/main/elm/Comp/OrgManage.elm b/modules/webapp/src/main/elm/Comp/OrgManage.elm index 0e46d760..5d8f4d47 100644 --- a/modules/webapp/src/main/elm/Comp/OrgManage.elm +++ b/modules/webapp/src/main/elm/Comp/OrgManage.elm @@ -23,6 +23,7 @@ import Comp.OrgForm import Comp.OrgTable import Comp.YesNoDimmer import Data.Flags exposing (Flags) +import Data.OrganizationOrder exposing (OrganizationOrder) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -41,6 +42,7 @@ type alias Model = , loading : Bool , deleteConfirm : Comp.YesNoDimmer.Model , query : String + , order : OrganizationOrder } @@ -65,6 +67,7 @@ emptyModel = , loading = False , deleteConfirm = Comp.YesNoDimmer.emptyModel , query = "" + , order = Data.OrganizationOrder.NameAsc } @@ -87,7 +90,7 @@ update flags msg model = case msg of TableMsg m -> let - ( tm, tc ) = + ( tm, tc, maybeOrder ) = Comp.OrgTable.update flags m model.tableModel ( m2, c2 ) = @@ -100,6 +103,7 @@ update flags msg model = else model.formError + , order = Maybe.withDefault model.order maybeOrder } , Cmd.map TableMsg tc ) @@ -111,8 +115,15 @@ update flags msg model = Nothing -> ( m2, Cmd.none ) + + ( m4, c4 ) = + if maybeOrder /= Nothing && maybeOrder /= Just model.order then + update flags LoadOrgs m3 + + else + ( m3, Cmd.none ) in - ( m3, Cmd.batch [ c2, c3 ] ) + ( m4, Cmd.batch [ c2, c3, c4 ] ) FormMsg m -> let @@ -122,7 +133,13 @@ update flags msg model = ( { model | formModel = m2 }, Cmd.map FormMsg c2 ) LoadOrgs -> - ( { model | loading = True }, Api.getOrganizations flags model.query OrgResp ) + ( { model | loading = True } + , Api.getOrganizations + flags + model.query + model.order + OrgResp + ) OrgResp (Ok orgs) -> let @@ -212,7 +229,7 @@ update flags msg model = m = { model | query = str } 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" } - , Html.map TableMsg (Comp.OrgTable.view2 texts.orgTable model.tableModel) + , Html.map TableMsg (Comp.OrgTable.view2 texts.orgTable model.order model.tableModel) , B.loadingDimmer { active = model.loading , label = texts.basics.loading diff --git a/modules/webapp/src/main/elm/Comp/OrgTable.elm b/modules/webapp/src/main/elm/Comp/OrgTable.elm index fd75b0b1..b2d102a2 100644 --- a/modules/webapp/src/main/elm/Comp/OrgTable.elm +++ b/modules/webapp/src/main/elm/Comp/OrgTable.elm @@ -17,8 +17,10 @@ import Api.Model.Organization exposing (Organization) import Comp.Basic as B import Data.Flags exposing (Flags) import Data.OrgUse +import Data.OrganizationOrder exposing (OrganizationOrder) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.OrgTable exposing (Texts) import Styles as S import Util.Address @@ -42,27 +44,50 @@ type Msg = SetOrgs (List Organization) | Select Organization | Deselect + | ToggleOrder OrganizationOrder -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe OrganizationOrder ) update _ msg model = case msg of SetOrgs list -> - ( { model | orgs = list, selected = Nothing }, Cmd.none ) + ( { model | orgs = list, selected = Nothing }, Cmd.none, Nothing ) Select equip -> - ( { model | selected = Just equip }, Cmd.none ) + ( { model | selected = Just equip }, Cmd.none, Nothing ) 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 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> OrganizationOrder -> Model -> Html Msg +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 ] [ thead [] [ tr [] @@ -71,7 +96,10 @@ view2 texts model = [ text texts.use ] , 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" ] [ text texts.address diff --git a/modules/webapp/src/main/elm/Comp/PersonManage.elm b/modules/webapp/src/main/elm/Comp/PersonManage.elm index a58e18fa..b70dd990 100644 --- a/modules/webapp/src/main/elm/Comp/PersonManage.elm +++ b/modules/webapp/src/main/elm/Comp/PersonManage.elm @@ -24,6 +24,7 @@ import Comp.PersonForm import Comp.PersonTable import Comp.YesNoDimmer import Data.Flags exposing (Flags) +import Data.PersonOrder exposing (PersonOrder) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -42,6 +43,7 @@ type alias Model = , loading : Int , deleteConfirm : Comp.YesNoDimmer.Model , query : String + , order : PersonOrder } @@ -66,6 +68,7 @@ emptyModel = , loading = 0 , deleteConfirm = Comp.YesNoDimmer.emptyModel , query = "" + , order = Data.PersonOrder.NameAsc } @@ -89,9 +92,12 @@ update flags msg model = case msg of TableMsg m -> let - ( tm, tc ) = + ( tm, tc, maybeOrder ) = Comp.PersonTable.update flags m model.tableModel + newOrder = + Maybe.withDefault model.order maybeOrder + ( m2, c2 ) = ( { model | tableModel = tm @@ -102,6 +108,7 @@ update flags msg model = else model.formError + , order = newOrder } , Cmd.map TableMsg tc ) @@ -113,8 +120,15 @@ update flags msg model = Nothing -> ( m2, Cmd.none ) + + ( m4, c4 ) = + if model.order == newOrder then + ( m3, Cmd.none ) + + else + update flags LoadPersons m3 in - ( m3, Cmd.batch [ c2, c3 ] ) + ( m4, Cmd.batch [ c2, c3, c4 ] ) FormMsg m -> let @@ -126,7 +140,7 @@ update flags msg model = LoadPersons -> ( { model | loading = model.loading + 2 } , Cmd.batch - [ Api.getPersons flags model.query PersonResp + [ Api.getPersons flags model.query model.order PersonResp , Api.getOrgLight flags GetOrgResp ] ) @@ -244,7 +258,7 @@ update flags msg model = m = { model | query = str } in - ( m, Api.getPersons flags str PersonResp ) + ( m, Api.getPersons flags str model.order PersonResp ) isLoading : Model -> Bool @@ -287,7 +301,7 @@ viewTable2 texts model = ] , 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 { active = isLoading model , label = texts.basics.loading diff --git a/modules/webapp/src/main/elm/Comp/PersonTable.elm b/modules/webapp/src/main/elm/Comp/PersonTable.elm index bd2eb177..167b93ba 100644 --- a/modules/webapp/src/main/elm/Comp/PersonTable.elm +++ b/modules/webapp/src/main/elm/Comp/PersonTable.elm @@ -16,9 +16,11 @@ module Comp.PersonTable exposing import Api.Model.Person exposing (Person) import Comp.Basic as B import Data.Flags exposing (Flags) +import Data.PersonOrder exposing (PersonOrder) import Data.PersonUse import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.PersonTable exposing (Texts) import Styles as S import Util.Contact @@ -41,27 +43,75 @@ type Msg = SetPersons (List Person) | Select Person | Deselect + | ToggleOrder PersonOrder -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe PersonOrder ) update _ msg model = case msg of SetPersons list -> - ( { model | equips = list, selected = Nothing }, Cmd.none ) + ( { model | equips = list, selected = Nothing }, Cmd.none, Nothing ) Select equip -> - ( { model | selected = Just equip }, Cmd.none ) + ( { model | selected = Just equip }, Cmd.none, Nothing ) 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 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> PersonOrder -> Model -> Html Msg +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 ] [ thead [] [ tr [] @@ -69,8 +119,18 @@ view2 texts model = , th [ class "text-left pr-1 md:px-2" ] [ text texts.use ] - , th [ class "text-left" ] [ text texts.basics.name ] - , th [ class "text-left hidden sm:table-cell" ] [ text texts.basics.organization ] + , th [ class "text-left" ] + [ 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 ] ] ] diff --git a/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm b/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm index d6e749e5..c6dfb4ec 100644 --- a/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm +++ b/modules/webapp/src/main/elm/Comp/ScanMailboxForm.elm @@ -37,7 +37,9 @@ import Data.CalEvent exposing (CalEvent) import Data.Direction exposing (Direction(..)) import Data.DropdownStyle as DS import Data.Flags exposing (Flags) +import Data.FolderOrder import Data.Language exposing (Language) +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import Data.Validated exposing (Validated(..)) import Html exposing (..) @@ -221,8 +223,8 @@ initWith flags s = [ Api.getImapSettings flags "" ConnResp , nc , Cmd.map CalEventMsg sc - , Api.getFolders flags "" False GetFolderResp - , Api.getTags flags "" GetTagResp + , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp + , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp ] ) @@ -268,8 +270,8 @@ init flags = } , Cmd.batch [ Api.getImapSettings flags "" ConnResp - , Api.getFolders flags "" False GetFolderResp - , Api.getTags flags "" GetTagResp + , Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp + , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp , Cmd.map CalEventMsg scmd ] ) diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index d74b532f..dc3e391b 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -40,10 +40,12 @@ import Comp.TagSelect import Data.CustomFieldChange exposing (CustomFieldValueCollect) import Data.Direction exposing (Direction) import Data.DropdownStyle as DS +import Data.EquipmentOrder import Data.EquipmentUse import Data.Fields import Data.Flags exposing (Flags) import Data.ItemQuery as Q exposing (ItemQuery) +import Data.PersonOrder import Data.PersonUse import Data.SearchMode exposing (SearchMode) import Data.UiSettings exposing (UiSettings) @@ -441,8 +443,8 @@ updateDrop ddm flags settings msg model = Cmd.batch [ Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp , Api.getOrgLight flags GetOrgResp - , Api.getEquipments flags "" GetEquipResp - , Api.getPersons flags "" GetPersonResp + , Api.getEquipments flags "" Data.EquipmentOrder.NameAsc GetEquipResp + , Api.getPersons flags "" Data.PersonOrder.NameAsc GetPersonResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , cdp ] @@ -1088,7 +1090,7 @@ findTab tab = Nothing -tabLook :UiSettings -> Model -> SearchTab -> Comp.Tabs.Look +tabLook : UiSettings -> Model -> SearchTab -> Comp.Tabs.Look tabLook settings model tab = let isHidden f = @@ -1097,6 +1099,7 @@ tabLook settings model tab = hiddenOr fields default = if List.all isHidden fields then Comp.Tabs.Hidden + else default @@ -1126,41 +1129,41 @@ tabLook settings model tab = activeWhen model.inboxCheckbox TabTags -> - hiddenOr [Data.Fields.Tag] + hiddenOr [ Data.Fields.Tag ] (activeWhenNotEmpty model.tagSelection.includeTags model.tagSelection.excludeTags) TabTagCategories -> - hiddenOr [Data.Fields.Tag] + hiddenOr [ Data.Fields.Tag ] (activeWhenNotEmpty model.tagSelection.includeCats model.tagSelection.excludeCats) TabFolder -> - hiddenOr [Data.Fields.Folder] + hiddenOr [ Data.Fields.Folder ] (activeWhenJust model.selectedFolder) TabCorrespondent -> - hiddenOr [Data.Fields.CorrOrg, Data.Fields.CorrPerson] <| + hiddenOr [ Data.Fields.CorrOrg, Data.Fields.CorrPerson ] <| activeWhenNotEmpty (Comp.Dropdown.getSelected model.orgModel) (Comp.Dropdown.getSelected model.corrPersonModel) TabConcerning -> - hiddenOr [Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| + hiddenOr [ Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| activeWhenNotEmpty (Comp.Dropdown.getSelected model.concPersonModel) (Comp.Dropdown.getSelected model.concEquipmentModel) TabDate -> - hiddenOr [Data.Fields.Date] <| - activeWhenJust (Util.Maybe.or [model.fromDate, model.untilDate]) + hiddenOr [ Data.Fields.Date ] <| + activeWhenJust (Util.Maybe.or [ model.fromDate, model.untilDate ]) TabDueDate -> - hiddenOr [Data.Fields.DueDate] <| - activeWhenJust (Util.Maybe.or [model.fromDueDate, model.untilDueDate]) + hiddenOr [ Data.Fields.DueDate ] <| + activeWhenJust (Util.Maybe.or [ model.fromDueDate, model.untilDueDate ]) TabSource -> - hiddenOr [Data.Fields.SourceName] <| + hiddenOr [ Data.Fields.SourceName ] <| activeWhenJust model.sourceModel TabDirection -> - hiddenOr [Data.Fields.Direction] <| + hiddenOr [ Data.Fields.Direction ] <| activeWhenNotEmpty (Comp.Dropdown.getSelected model.directionModel) [] TabTrashed -> @@ -1179,7 +1182,6 @@ searchTabState settings model tab = searchTab = findTab tab - folded = if Set.member tab.name model.openTabs then Comp.Tabs.Open @@ -1189,10 +1191,10 @@ searchTabState settings model tab = state = { folded = folded - , look = Maybe.map (tabLook settings model) searchTab - |> Maybe.withDefault Comp.Tabs.Normal + , look = + Maybe.map (tabLook settings model) searchTab + |> Maybe.withDefault Comp.Tabs.Normal } - in ( state, ToggleAkkordionTab tab.name ) diff --git a/modules/webapp/src/main/elm/Comp/SourceForm.elm b/modules/webapp/src/main/elm/Comp/SourceForm.elm index 3975018f..abf414d2 100644 --- a/modules/webapp/src/main/elm/Comp/SourceForm.elm +++ b/modules/webapp/src/main/elm/Comp/SourceForm.elm @@ -27,8 +27,10 @@ import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.FixedDropdown import Data.DropdownStyle as DS import Data.Flags exposing (Flags) +import Data.FolderOrder import Data.Language exposing (Language) import Data.Priority exposing (Priority) +import Data.TagOrder import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -89,8 +91,8 @@ init : Flags -> ( Model, Cmd Msg ) init flags = ( emptyModel , Cmd.batch - [ Api.getFolders flags "" False GetFolderResp - , Api.getTags flags "" GetTagResp + [ Api.getFolders flags "" Data.FolderOrder.NameAsc False GetFolderResp + , Api.getTags flags "" Data.TagOrder.NameAsc GetTagResp ] ) diff --git a/modules/webapp/src/main/elm/Comp/TagManage.elm b/modules/webapp/src/main/elm/Comp/TagManage.elm index 3ac89f82..e3782bde 100644 --- a/modules/webapp/src/main/elm/Comp/TagManage.elm +++ b/modules/webapp/src/main/elm/Comp/TagManage.elm @@ -23,6 +23,7 @@ import Comp.TagForm import Comp.TagTable import Comp.YesNoDimmer import Data.Flags exposing (Flags) +import Data.TagOrder exposing (TagOrder) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onSubmit) @@ -42,6 +43,7 @@ type alias Model = , loading : Bool , deleteConfirm : Comp.YesNoDimmer.Model , query : String + , order : TagOrder } @@ -66,6 +68,7 @@ emptyModel = , loading = False , deleteConfirm = Comp.YesNoDimmer.emptyModel , query = "" + , order = Data.TagOrder.NameAsc } @@ -88,12 +91,16 @@ update flags msg model = case msg of TableMsg m -> let - ( tm, tc ) = + ( tm, tc, maybeOrder ) = Comp.TagTable.update flags m model.tagTableModel + newOrder = + Maybe.withDefault model.order maybeOrder + ( m2, c2 ) = ( { model | tagTableModel = tm + , order = newOrder , viewMode = Maybe.map (\_ -> Form) tm.selected |> Maybe.withDefault Table , formError = if Util.Maybe.nonEmpty tm.selected then @@ -112,8 +119,15 @@ update flags msg model = Nothing -> ( m2, Cmd.none ) + + ( m4, c4 ) = + if model.order == newOrder then + ( m3, Cmd.none ) + + else + update flags LoadTags m3 in - ( m3, Cmd.batch [ c2, c3 ] ) + ( m4, Cmd.batch [ c2, c3, c4 ] ) FormMsg m -> let @@ -123,7 +137,9 @@ update flags msg model = ( { model | tagFormModel = m2 }, Cmd.map FormMsg c2 ) 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) -> let @@ -224,7 +240,7 @@ update flags msg model = m = { model | query = str } 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" } - , Html.map TableMsg (Comp.TagTable.view2 texts.tagTable model.tagTableModel) + , Html.map TableMsg (Comp.TagTable.view2 texts.tagTable model.order model.tagTableModel) , div [ classList [ ( "ui dimmer", True ) diff --git a/modules/webapp/src/main/elm/Comp/TagTable.elm b/modules/webapp/src/main/elm/Comp/TagTable.elm index 6cab6c31..8f9e5858 100644 --- a/modules/webapp/src/main/elm/Comp/TagTable.elm +++ b/modules/webapp/src/main/elm/Comp/TagTable.elm @@ -16,8 +16,10 @@ module Comp.TagTable exposing import Api.Model.Tag exposing (Tag) import Comp.Basic as B import Data.Flags exposing (Flags) +import Data.TagOrder exposing (TagOrder) import Html exposing (..) import Html.Attributes exposing (..) +import Html.Events exposing (onClick) import Messages.Comp.TagTable exposing (Texts) import Styles as S @@ -35,37 +37,108 @@ emptyModel = } +type Header + = Name + | Category + + type Msg = SetTags (List Tag) | Select Tag | Deselect + | SortClick TagOrder -update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) +update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe TagOrder ) update _ msg model = case msg of SetTags list -> - ( { model | tags = list, selected = Nothing }, Cmd.none ) + ( { model | tags = list, selected = Nothing }, Cmd.none, Nothing ) Select tag -> - ( { model | selected = Just tag }, Cmd.none ) + ( { model | selected = Just tag }, Cmd.none, Nothing ) 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 : Texts -> Model -> Html Msg -view2 texts model = +view2 : Texts -> TagOrder -> Model -> Html Msg +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 ] [ thead [] [ tr [] [ th [ class "" ] [] - , th [ class "text-left" ] [ text texts.basics.name ] - , th [ class "text-left" ] [ text texts.category ] + , th [ class "text-left" ] + [ 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 [] diff --git a/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm index c9e6fe4d..fd21d154 100644 --- a/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm +++ b/modules/webapp/src/main/elm/Comp/UiSettingsForm.elm @@ -28,6 +28,7 @@ import Data.DropdownStyle as DS import Data.Fields exposing (Field) import Data.Flags exposing (Flags) import Data.ItemTemplate as IT exposing (ItemTemplate) +import Data.TagOrder import Data.UiSettings exposing (ItemPattern, Pos(..), UiSettings) import Dict exposing (Dict) import Html exposing (..) @@ -160,7 +161,7 @@ init flags settings = Comp.FixedDropdown.init Messages.UiLanguage.all , openTabs = Set.empty } - , Api.getTags flags "" GetTagsResp + , Api.getTags flags "" Data.TagOrder.NameAsc GetTagsResp ) diff --git a/modules/webapp/src/main/elm/Data/CustomFieldOrder.elm b/modules/webapp/src/main/elm/Data/CustomFieldOrder.elm new file mode 100644 index 00000000..54a1cb05 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/CustomFieldOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Data/EquipmentOrder.elm b/modules/webapp/src/main/elm/Data/EquipmentOrder.elm new file mode 100644 index 00000000..82fd6cee --- /dev/null +++ b/modules/webapp/src/main/elm/Data/EquipmentOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Data/FolderOrder.elm b/modules/webapp/src/main/elm/Data/FolderOrder.elm new file mode 100644 index 00000000..feadbd06 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/FolderOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Data/OrganizationOrder.elm b/modules/webapp/src/main/elm/Data/OrganizationOrder.elm new file mode 100644 index 00000000..a34154ca --- /dev/null +++ b/modules/webapp/src/main/elm/Data/OrganizationOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Data/PersonOrder.elm b/modules/webapp/src/main/elm/Data/PersonOrder.elm new file mode 100644 index 00000000..c8c76a15 --- /dev/null +++ b/modules/webapp/src/main/elm/Data/PersonOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Data/TagOrder.elm b/modules/webapp/src/main/elm/Data/TagOrder.elm new file mode 100644 index 00000000..1bd2d48f --- /dev/null +++ b/modules/webapp/src/main/elm/Data/TagOrder.elm @@ -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" diff --git a/modules/webapp/src/main/elm/Messages/Comp/FolderTable.elm b/modules/webapp/src/main/elm/Messages/Comp/FolderTable.elm index 8e097a60..d742fae2 100644 --- a/modules/webapp/src/main/elm/Messages/Comp/FolderTable.elm +++ b/modules/webapp/src/main/elm/Messages/Comp/FolderTable.elm @@ -20,6 +20,7 @@ type alias Texts = { basics : Messages.Basics.Texts , memberCount : String , formatDateShort : Int -> String + , owner : String } @@ -28,6 +29,7 @@ gb = { basics = Messages.Basics.gb , memberCount = "#Member" , formatDateShort = DF.formatDateShort Messages.UiLanguage.English + , owner = "Owner" } @@ -36,4 +38,5 @@ de = { basics = Messages.Basics.de , memberCount = "#Mitglieder" , formatDateShort = DF.formatDateShort Messages.UiLanguage.German + , owner = "Besitzer" }