diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 4a94d772..5102aa97 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -3776,6 +3776,9 @@ components: concEquip: type: string format: ident + folder: + type: string + format: ident dateFrom: type: integer format: date-time @@ -3829,6 +3832,8 @@ components: $ref: "#/components/schemas/IdName" concEquip: $ref: "#/components/schemas/IdName" + folder: + $ref: "#/components/schemas/IdName" fileCount: type: integer format: int32 diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index 4f093c40..d899ea06 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -121,6 +121,7 @@ trait Conversions { m.corrOrg, m.concPerson, m.concEquip, + m.folder, m.tagsInclude.map(Ident.unsafe), m.tagsExclude.map(Ident.unsafe), m.dateFrom, @@ -193,6 +194,7 @@ trait Conversions { i.corrPerson.map(mkIdName), i.concPerson.map(mkIdName), i.concEquip.map(mkIdName), + i.folder.map(mkIdName), i.fileCount, Nil, Nil diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala index 657f10ec..a40f11d2 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala @@ -155,7 +155,8 @@ object QItem { corrOrg: Option[IdRef], corrPerson: Option[IdRef], concPerson: Option[IdRef], - concEquip: Option[IdRef] + concEquip: Option[IdRef], + folder: Option[IdRef] ) case class Query( @@ -167,6 +168,7 @@ object QItem { corrOrg: Option[Ident], concPerson: Option[Ident], concEquip: Option[Ident], + folder: Option[Ident], tagsInclude: List[Ident], tagsExclude: List[Ident], dateFrom: Option[Timestamp], @@ -189,6 +191,7 @@ object QItem { None, None, None, + None, Nil, Nil, None, @@ -233,10 +236,12 @@ object QItem { val PC = RPerson.Columns val OC = ROrganization.Columns val EC = REquipment.Columns + val FC = RFolder.Columns val itemCols = IC.all - val personCols = List(RPerson.Columns.pid, RPerson.Columns.name) - val orgCols = List(ROrganization.Columns.oid, ROrganization.Columns.name) - val equipCols = List(REquipment.Columns.eid, REquipment.Columns.name) + val personCols = List(PC.pid, PC.name) + val orgCols = List(OC.oid, OC.name) + val equipCols = List(EC.eid, EC.name) + val folderCols = List(FC.id, FC.name) val finalCols = commas( Seq( @@ -257,6 +262,8 @@ object QItem { PC.name.prefix("p1").f, EC.eid.prefix("e1").f, EC.name.prefix("e1").f, + FC.id.prefix("f1").f, + FC.name.prefix("f1").f, q.orderAsc match { case Some(co) => coalesce(co(IC).prefix("i").f, IC.created.prefix("i").f) @@ -270,6 +277,8 @@ object QItem { val withPerson = selectSimple(personCols, RPerson.table, PC.cid.is(q.collective)) val withOrgs = selectSimple(orgCols, ROrganization.table, OC.cid.is(q.collective)) val withEquips = selectSimple(equipCols, REquipment.table, EC.cid.is(q.collective)) + val withFolder = + selectSimple(folderCols, RFolder.table, FC.collective.is(q.collective)) val withAttach = fr"SELECT COUNT(" ++ AC.id.f ++ fr") as num, " ++ AC.itemId.f ++ fr"from" ++ RAttachment.table ++ fr"GROUP BY (" ++ AC.itemId.f ++ fr")" @@ -280,7 +289,8 @@ object QItem { "persons" -> withPerson, "orgs" -> withOrgs, "equips" -> withEquips, - "attachs" -> withAttach + "attachs" -> withAttach, + "folders" -> withFolder ) ++ ctes): _* ) ++ selectKW ++ finalCols ++ fr" FROM items i" ++ @@ -288,7 +298,10 @@ object QItem { fr"LEFT JOIN persons p0 ON" ++ IC.corrPerson.prefix("i").is(PC.pid.prefix("p0")) ++ fr"LEFT JOIN orgs o0 ON" ++ IC.corrOrg.prefix("i").is(OC.oid.prefix("o0")) ++ fr"LEFT JOIN persons p1 ON" ++ IC.concPerson.prefix("i").is(PC.pid.prefix("p1")) ++ - fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment.prefix("i").is(EC.eid.prefix("e1")) + fr"LEFT JOIN equips e1 ON" ++ IC.concEquipment + .prefix("i") + .is(EC.eid.prefix("e1")) ++ + fr"LEFT JOIN folders f1 ON" ++ IC.folder.prefix("i").is(FC.id.prefix("f1")) query } @@ -346,6 +359,7 @@ object QItem { ROrganization.Columns.oid.prefix("o0").isOrDiscard(q.corrOrg), 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 else IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 47d2c345..78d21a89 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -21,7 +21,6 @@ import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) import Markdown -import Ports import Util.List import Util.String import Util.Time @@ -125,6 +124,10 @@ viewItem settings item = |> List.intersperse ", " |> String.concat + folder = + Maybe.map .name item.folder + |> Maybe.withDefault "" + dueDate = Maybe.map Util.Time.formatDateShort item.dueDate |> Maybe.withDefault "" @@ -212,6 +215,14 @@ viewItem settings item = , text " " , Util.String.withDefault "-" conc |> text ] + , div + [ class "item" + , title "Folder" + ] + [ Icons.folderIcon "" + , text " " + , Util.String.withDefault "-" folder |> text + ] ] , div [ class "right floated meta" ] [ div [ class "ui horizontal list" ] diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index 5e1b93d9..f7539d61 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -11,6 +11,7 @@ module Comp.SearchMenu exposing import Api import Api.Model.Equipment exposing (Equipment) import Api.Model.EquipmentList exposing (EquipmentList) +import Api.Model.FolderList exposing (FolderList) import Api.Model.IdName exposing (IdName) import Api.Model.ItemSearch exposing (ItemSearch) import Api.Model.ReferenceList exposing (ReferenceList) @@ -45,6 +46,7 @@ type alias Model = , corrPersonModel : Comp.Dropdown.Model IdName , concPersonModel : Comp.Dropdown.Model IdName , concEquipmentModel : Comp.Dropdown.Model Equipment + , folderModel : Comp.Dropdown.Model IdName , inboxCheckbox : Bool , fromDateModel : DatePicker , fromDate : Maybe Int @@ -103,6 +105,14 @@ init = , labelColor = \_ -> \_ -> "" , placeholder = "Choose an equipment" } + , folderModel = + Comp.Dropdown.makeModel + { multiple = False + , searchable = \n -> n > 5 + , makeOption = \e -> { value = e.id, text = e.name } + , labelColor = \_ -> \_ -> "" + , placeholder = "Only items in folder" + } , inboxCheckbox = False , fromDateModel = Comp.DatePicker.emptyModel , fromDate = Nothing @@ -144,6 +154,8 @@ type Msg | ResetForm | KeyUpMsg (Maybe KeyCode) | ToggleNameHelp + | FolderMsg (Comp.Dropdown.Msg IdName) + | GetFolderResp (Result Http.Error FolderList) getDirection : Model -> Maybe Direction @@ -184,6 +196,7 @@ getItemSearch model = , corrOrg = Comp.Dropdown.getSelected model.orgModel |> List.map .id |> List.head , concPerson = Comp.Dropdown.getSelected model.concPersonModel |> List.map .id |> List.head , concEquip = Comp.Dropdown.getSelected model.concEquipmentModel |> List.map .id |> List.head + , folder = Comp.Dropdown.getSelected model.folderModel |> List.map .id |> List.head , direction = Comp.Dropdown.getSelected model.directionModel |> List.head |> Maybe.map Data.Direction.toString , inbox = model.inboxCheckbox , dateFrom = model.fromDate @@ -250,6 +263,7 @@ update flags settings msg model = , Api.getOrgLight flags GetOrgResp , Api.getEquipments flags "" GetEquipResp , Api.getPersonsLight flags GetPersonResp + , Api.getFolders flags "" False GetFolderResp , cdp ] ) @@ -513,6 +527,29 @@ update flags settings msg model = ToggleNameHelp -> NextState ( { model | showNameHelp = not model.showNameHelp }, Cmd.none ) False + GetFolderResp (Ok fs) -> + let + opts = + List.filter .isMember fs.items + |> List.map (\e -> IdName e.id e.name) + |> Comp.Dropdown.SetOptions + in + update flags settings (FolderMsg opts) model + + GetFolderResp (Err _) -> + noChange ( model, Cmd.none ) + + FolderMsg lm -> + let + ( m2, c2 ) = + Comp.Dropdown.update lm model.folderModel + in + NextState + ( { model | folderModel = m2 } + , Cmd.map FolderMsg c2 + ) + (isDropdownChangeMsg lm) + -- View @@ -629,6 +666,11 @@ view flags settings model = [ text "Looks in item name only." ] ] + , formHeader (Icons.folderIcon "") "Folder" + , div [ class "field" ] + [ label [] [ text "Folder" ] + , Html.map FolderMsg (Comp.Dropdown.view settings model.folderModel) + ] , formHeader (Icons.tagsIcon "") "Tags" , div [ class "field" ] [ label [] [ text "Include (and)" ]