mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 21:42:52 +00:00
Merge pull request #214 from eikek/tag-category-search
Tag category search
This commit is contained in:
commit
c4d48d8709
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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 =>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user