mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-04 14:15:59 +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:
|
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:
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
@ -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 =>
|
||||||
|
@ -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 =
|
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
|
||||||
|
@ -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 =
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user