Merge pull request #214 from eikek/tag-category-search

Tag category search
This commit is contained in:
mergify[bot] 2020-08-06 20:39:55 +00:00 committed by GitHub
commit c4d48d8709
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 153 additions and 15 deletions

View File

@ -3752,6 +3752,14 @@ components:
items:
type: string
format: ident
tagCategoriesInclude:
type: array
items:
type: string
tagCategoriesExclude:
type: array
items:
type: string
inbox:
type: boolean
offset:

View File

@ -124,6 +124,8 @@ trait Conversions {
m.folder,
m.tagsInclude.map(Ident.unsafe),
m.tagsExclude.map(Ident.unsafe),
m.tagCategoriesInclude,
m.tagCategoriesExclude,
m.dateFrom,
m.dateUntil,
m.dueDateFrom,

View File

@ -26,6 +26,9 @@ case class Column(name: String, ns: String = "", alias: String = "") {
def is[A: Put](value: A): Fragment =
f ++ fr" = $value"
def lowerIs[A: Put](value: A): Fragment =
fr"lower(" ++ f ++ fr") = $value"
def is[A: Put](ov: Option[A]): Fragment =
ov match {
case Some(v) => f ++ fr" = $v"

View File

@ -172,6 +172,8 @@ object QItem {
folder: Option[Ident],
tagsInclude: List[Ident],
tagsExclude: List[Ident],
tagCategoryIncl: List[String],
tagCategoryExcl: List[String],
dateFrom: Option[Timestamp],
dateTo: Option[Timestamp],
dueDateFrom: Option[Timestamp],
@ -195,6 +197,8 @@ object QItem {
None,
Nil,
Nil,
Nil,
Nil,
None,
None,
None,
@ -323,25 +327,21 @@ object QItem {
val EC = REquipment.Columns
// inclusive tags are AND-ed
val tagSelectsIncl = q.tagsInclude
val tagSelectsIncl = (q.tagsInclude
.map(tid =>
selectSimple(
List(RTagItem.Columns.itemId),
RTagItem.table,
RTagItem.Columns.tagId.is(tid)
)
)
) ++ q.tagCategoryIncl.map(cat =>
TagItemName.itemsInCategory(NonEmptyList.of(cat))
))
.map(f => sql"(" ++ f ++ sql") ")
// exclusive tags are OR-ed
val tagSelectsExcl =
if (q.tagsExclude.isEmpty) Fragment.empty
else
selectSimple(
List(RTagItem.Columns.itemId),
RTagItem.table,
RTagItem.Columns.tagId.isOneOf(q.tagsExclude)
)
TagItemName.itemsWithTagOrCategory(q.tagsExclude, q.tagCategoryExcl)
val iFolder = IC.folder.prefix("i")
val name = q.name.map(_.toLowerCase).map(queryWildcard)
@ -370,11 +370,11 @@ object QItem {
RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson),
REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip),
RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder),
if (q.tagsInclude.isEmpty) Fragment.empty
if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty
else
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl
.reduce(_ ++ fr"INTERSECT" ++ _) ++ sql")",
if (q.tagsExclude.isEmpty) Fragment.empty
if (q.tagsExclude.isEmpty && q.tagCategoryExcl.isEmpty) Fragment.empty
else IC.id.prefix("i").f ++ sql" NOT IN (" ++ tagSelectsExcl ++ sql")",
q.dateFrom
.map(d =>

View File

@ -0,0 +1,60 @@
package docspell.store.records
import docspell.common._
import docspell.store.impl.Implicits._
import cats.data.NonEmptyList
import doobie._
import doobie.implicits._
/** A helper class combining information from `RTag` and `RTagItem`.
* This is not a "record", there is no corresponding table.
*/
case class TagItemName(
tagId: Ident,
collective: Ident,
name: String,
category: Option[String],
tagItemId: Ident,
itemId: Ident
)
object TagItemName {
def itemsInCategory(cats: NonEmptyList[String]): Fragment = {
val catsLower = cats.map(_.toLowerCase)
val tiItem = RTagItem.Columns.itemId.prefix("ti")
val tiTag = RTagItem.Columns.tagId.prefix("ti")
val tCat = RTag.Columns.category.prefix("t")
val tId = RTag.Columns.tid.prefix("t")
val from = RTag.table ++ fr"t INNER JOIN" ++
RTagItem.table ++ fr"ti ON" ++ tiTag.is(tId)
if (cats.tail.isEmpty)
selectSimple(List(tiItem), from, tCat.lowerIs(catsLower.head))
else
selectSimple(List(tiItem), from, tCat.isLowerIn(catsLower))
}
def itemsWithTagOrCategory(tags: List[Ident], cats: List[String]): Fragment = {
val catsLower = cats.map(_.toLowerCase)
val tiItem = RTagItem.Columns.itemId.prefix("ti")
val tiTag = RTagItem.Columns.tagId.prefix("ti")
val tCat = RTag.Columns.category.prefix("t")
val tId = RTag.Columns.tid.prefix("t")
val from = RTag.table ++ fr"t INNER JOIN" ++
RTagItem.table ++ fr"ti ON" ++ tiTag.is(tId)
(NonEmptyList.fromList(tags), NonEmptyList.fromList(catsLower)) match {
case (Some(tagNel), Some(catNel)) =>
selectSimple(List(tiItem), from, or(tId.isIn(tagNel), tCat.isLowerIn(catNel)))
case (Some(tagNel), None) =>
selectSimple(List(tiItem), from, tId.isIn(tagNel))
case (None, Some(catNel)) =>
selectSimple(List(tiItem), from, tCat.isLowerIn(catNel))
case (None, None) =>
Fragment.empty
}
}
}

View File

@ -41,6 +41,8 @@ import Util.Update
type alias Model =
{ tagInclModel : Comp.Dropdown.Model Tag
, tagExclModel : Comp.Dropdown.Model Tag
, tagCatInclModel : Comp.Dropdown.Model String
, tagCatExclModel : Comp.Dropdown.Model String
, directionModel : Comp.Dropdown.Model Direction
, orgModel : Comp.Dropdown.Model IdName
, corrPersonModel : Comp.Dropdown.Model IdName
@ -68,6 +70,8 @@ init : Model
init =
{ tagInclModel = Util.Tag.makeDropdownModel
, tagExclModel = Util.Tag.makeDropdownModel
, tagCatInclModel = Util.Tag.makeCatDropdownModel
, tagCatExclModel = Util.Tag.makeCatDropdownModel
, directionModel =
Comp.Dropdown.makeSingleList
{ makeOption =
@ -157,6 +161,8 @@ type Msg
| ToggleNameHelp
| FolderMsg (Comp.Dropdown.Msg IdName)
| GetFolderResp (Result Http.Error FolderList)
| TagCatIncMsg (Comp.Dropdown.Msg String)
| TagCatExcMsg (Comp.Dropdown.Msg String)
getDirection : Model -> Maybe Direction
@ -211,6 +217,8 @@ getItemSearch model =
model.allNameModel
|> Maybe.map amendWildcards
, fullText = model.fulltextModel
, tagCategoriesInclude = Comp.Dropdown.getSelected model.tagCatInclModel
, tagCategoriesExclude = Comp.Dropdown.getSelected model.tagCatExclModel
}
@ -280,11 +288,17 @@ update flags settings msg model =
let
tagList =
Comp.Dropdown.SetOptions tags.items
catList =
Util.Tag.getCategories tags.items
|> Comp.Dropdown.SetOptions
in
noChange <|
Util.Update.andThen1
[ update flags settings (TagIncMsg tagList) >> .modelCmd
, update flags settings (TagExcMsg tagList) >> .modelCmd
, update flags settings (TagCatIncMsg catList) >> .modelCmd
, update flags settings (TagCatExcMsg catList) >> .modelCmd
]
model
@ -551,6 +565,28 @@ update flags settings msg model =
)
(isDropdownChangeMsg lm)
TagCatIncMsg m ->
let
( m2, c2 ) =
Comp.Dropdown.update m model.tagCatInclModel
in
NextState
( { model | tagCatInclModel = m2 }
, Cmd.map TagCatIncMsg c2
)
(isDropdownChangeMsg m)
TagCatExcMsg m ->
let
( m2, c2 ) =
Comp.Dropdown.update m model.tagCatExclModel
in
NextState
( { model | tagCatExclModel = m2 }
, Cmd.map TagCatExcMsg c2
)
(isDropdownChangeMsg m)
-- View
@ -645,6 +681,14 @@ view flags settings model =
[ label [] [ text "Exclude (or)" ]
, Html.map TagExcMsg (Comp.Dropdown.view settings model.tagExclModel)
]
, div [ class "field" ]
[ label [] [ text "Category Include (and)" ]
, Html.map TagCatIncMsg (Comp.Dropdown.view settings model.tagCatInclModel)
]
, div [ class "field" ]
[ label [] [ text "Category Exclude (or)" ]
, Html.map TagCatExcMsg (Comp.Dropdown.view settings model.tagCatExclModel)
]
, formHeader (Icons.searchIcon "") "Content"
, div
[ classList

View File

@ -18,7 +18,7 @@ import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onCheck)
import Http
import Util.List
import Util.Tag
type alias Model =
@ -148,8 +148,7 @@ update sett msg model =
GetTagsResp (Ok tl) ->
let
categories =
List.filterMap .category tl.items
|> Util.List.distinct
Util.Tag.getCategories tl.items
in
( { model
| tagColorModel =

View File

@ -1,8 +1,13 @@
module Util.Tag exposing (makeDropdownModel)
module Util.Tag exposing
( getCategories
, makeCatDropdownModel
, makeDropdownModel
)
import Api.Model.Tag exposing (Tag)
import Comp.Dropdown
import Data.UiSettings
import Util.List
makeDropdownModel : Comp.Dropdown.Model Tag
@ -17,3 +22,20 @@ makeDropdownModel =
"basic " ++ Data.UiSettings.tagColorString tag settings
, placeholder = "Choose a tag"
}
makeCatDropdownModel : Comp.Dropdown.Model String
makeCatDropdownModel =
Comp.Dropdown.makeModel
{ multiple = True
, searchable = \n -> n > 5
, makeOption = \cat -> { value = cat, text = cat, additional = "" }
, labelColor = \_ -> \_ -> ""
, placeholder = "Choose a tag category"
}
getCategories : List Tag -> List String
getCategories tags =
List.filterMap .category tags
|> Util.List.distinct