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: items:
type: string type: string
format: ident format: ident
tagCategoriesInclude:
type: array
items:
type: string
tagCategoriesExclude:
type: array
items:
type: string
inbox: inbox:
type: boolean type: boolean
offset: offset:

View File

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

View File

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

View File

@ -172,6 +172,8 @@ object QItem {
folder: Option[Ident], folder: Option[Ident],
tagsInclude: List[Ident], tagsInclude: List[Ident],
tagsExclude: List[Ident], tagsExclude: List[Ident],
tagCategoryIncl: List[String],
tagCategoryExcl: List[String],
dateFrom: Option[Timestamp], dateFrom: Option[Timestamp],
dateTo: Option[Timestamp], dateTo: Option[Timestamp],
dueDateFrom: Option[Timestamp], dueDateFrom: Option[Timestamp],
@ -195,6 +197,8 @@ object QItem {
None, None,
Nil, Nil,
Nil, Nil,
Nil,
Nil,
None, None,
None, None,
None, None,
@ -323,25 +327,21 @@ object QItem {
val EC = REquipment.Columns val EC = REquipment.Columns
// inclusive tags are AND-ed // inclusive tags are AND-ed
val tagSelectsIncl = q.tagsInclude val tagSelectsIncl = (q.tagsInclude
.map(tid => .map(tid =>
selectSimple( selectSimple(
List(RTagItem.Columns.itemId), List(RTagItem.Columns.itemId),
RTagItem.table, RTagItem.table,
RTagItem.Columns.tagId.is(tid) RTagItem.Columns.tagId.is(tid)
) )
) ) ++ q.tagCategoryIncl.map(cat =>
TagItemName.itemsInCategory(NonEmptyList.of(cat))
))
.map(f => sql"(" ++ f ++ sql") ") .map(f => sql"(" ++ f ++ sql") ")
// exclusive tags are OR-ed // exclusive tags are OR-ed
val tagSelectsExcl = val tagSelectsExcl =
if (q.tagsExclude.isEmpty) Fragment.empty TagItemName.itemsWithTagOrCategory(q.tagsExclude, q.tagCategoryExcl)
else
selectSimple(
List(RTagItem.Columns.itemId),
RTagItem.table,
RTagItem.Columns.tagId.isOneOf(q.tagsExclude)
)
val iFolder = IC.folder.prefix("i") val iFolder = IC.folder.prefix("i")
val name = q.name.map(_.toLowerCase).map(queryWildcard) val name = q.name.map(_.toLowerCase).map(queryWildcard)
@ -370,11 +370,11 @@ object QItem {
RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson), RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson),
REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip), REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip),
RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder), RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder),
if (q.tagsInclude.isEmpty) Fragment.empty if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty
else else
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl
.reduce(_ ++ fr"INTERSECT" ++ _) ++ sql")", .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")", else IC.id.prefix("i").f ++ sql" NOT IN (" ++ tagSelectsExcl ++ sql")",
q.dateFrom q.dateFrom
.map(d => .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 = type alias Model =
{ tagInclModel : Comp.Dropdown.Model Tag { tagInclModel : Comp.Dropdown.Model Tag
, tagExclModel : Comp.Dropdown.Model Tag , tagExclModel : Comp.Dropdown.Model Tag
, tagCatInclModel : Comp.Dropdown.Model String
, tagCatExclModel : Comp.Dropdown.Model String
, directionModel : Comp.Dropdown.Model Direction , directionModel : Comp.Dropdown.Model Direction
, orgModel : Comp.Dropdown.Model IdName , orgModel : Comp.Dropdown.Model IdName
, corrPersonModel : Comp.Dropdown.Model IdName , corrPersonModel : Comp.Dropdown.Model IdName
@ -68,6 +70,8 @@ init : Model
init = init =
{ tagInclModel = Util.Tag.makeDropdownModel { tagInclModel = Util.Tag.makeDropdownModel
, tagExclModel = Util.Tag.makeDropdownModel , tagExclModel = Util.Tag.makeDropdownModel
, tagCatInclModel = Util.Tag.makeCatDropdownModel
, tagCatExclModel = Util.Tag.makeCatDropdownModel
, directionModel = , directionModel =
Comp.Dropdown.makeSingleList Comp.Dropdown.makeSingleList
{ makeOption = { makeOption =
@ -157,6 +161,8 @@ type Msg
| ToggleNameHelp | ToggleNameHelp
| FolderMsg (Comp.Dropdown.Msg IdName) | FolderMsg (Comp.Dropdown.Msg IdName)
| GetFolderResp (Result Http.Error FolderList) | GetFolderResp (Result Http.Error FolderList)
| TagCatIncMsg (Comp.Dropdown.Msg String)
| TagCatExcMsg (Comp.Dropdown.Msg String)
getDirection : Model -> Maybe Direction getDirection : Model -> Maybe Direction
@ -211,6 +217,8 @@ getItemSearch model =
model.allNameModel model.allNameModel
|> Maybe.map amendWildcards |> Maybe.map amendWildcards
, fullText = model.fulltextModel , 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 let
tagList = tagList =
Comp.Dropdown.SetOptions tags.items Comp.Dropdown.SetOptions tags.items
catList =
Util.Tag.getCategories tags.items
|> Comp.Dropdown.SetOptions
in in
noChange <| noChange <|
Util.Update.andThen1 Util.Update.andThen1
[ update flags settings (TagIncMsg tagList) >> .modelCmd [ update flags settings (TagIncMsg tagList) >> .modelCmd
, update flags settings (TagExcMsg tagList) >> .modelCmd , update flags settings (TagExcMsg tagList) >> .modelCmd
, update flags settings (TagCatIncMsg catList) >> .modelCmd
, update flags settings (TagCatExcMsg catList) >> .modelCmd
] ]
model model
@ -551,6 +565,28 @@ update flags settings msg model =
) )
(isDropdownChangeMsg lm) (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 -- View
@ -645,6 +681,14 @@ view flags settings model =
[ label [] [ text "Exclude (or)" ] [ label [] [ text "Exclude (or)" ]
, Html.map TagExcMsg (Comp.Dropdown.view settings model.tagExclModel) , 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" , formHeader (Icons.searchIcon "") "Content"
, div , div
[ classList [ classList

View File

@ -18,7 +18,7 @@ import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (onCheck) import Html.Events exposing (onCheck)
import Http import Http
import Util.List import Util.Tag
type alias Model = type alias Model =
@ -148,8 +148,7 @@ update sett msg model =
GetTagsResp (Ok tl) -> GetTagsResp (Ok tl) ->
let let
categories = categories =
List.filterMap .category tl.items Util.Tag.getCategories tl.items
|> Util.List.distinct
in in
( { model ( { model
| tagColorModel = | 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 Api.Model.Tag exposing (Tag)
import Comp.Dropdown import Comp.Dropdown
import Data.UiSettings import Data.UiSettings
import Util.List
makeDropdownModel : Comp.Dropdown.Model Tag makeDropdownModel : Comp.Dropdown.Model Tag
@ -17,3 +22,20 @@ makeDropdownModel =
"basic " ++ Data.UiSettings.tagColorString tag settings "basic " ++ Data.UiSettings.tagColorString tag settings
, placeholder = "Choose a tag" , 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