Select linked items from search page

- Quickly select related items from the search view
- Include related items with item details to spare another request
This commit is contained in:
eikek
2022-03-19 18:31:45 +01:00
parent 71f88486c2
commit 37b5a4dfef
13 changed files with 119 additions and 22 deletions

View File

@ -134,8 +134,7 @@ object OItemSearch {
def findItem(id: Ident, collective: Ident): F[Option[ItemData]] = def findItem(id: Ident, collective: Ident): F[Option[ItemData]] =
store store
.transact(QItem.findItem(id)) .transact(QItem.findItem(id, collective))
.map(opt => opt.flatMap(_.filterCollective(collective)))
def findItems(maxNoteLen: Int)(q: Query, batch: Batch): F[Vector[ListItem]] = def findItems(maxNoteLen: Int)(q: Query, batch: Batch): F[Vector[ListItem]] =
Timestamp Timestamp

View File

@ -2533,6 +2533,8 @@ paths:
schema: schema:
$ref: "#/components/schemas/FileIntegrityCheckRequest" $ref: "#/components/schemas/FileIntegrityCheckRequest"
responses: responses:
422:
description: BadRequest
200: 200:
description: Ok description: Ok
content: content:
@ -7194,6 +7196,7 @@ components:
- archives - archives
- tags - tags
- customfields - customfields
- relatedItems
properties: properties:
id: id:
type: string type: string
@ -7257,6 +7260,13 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ItemFieldValue" $ref: "#/components/schemas/ItemFieldValue"
relatedItems:
description: |
All related items to this item. The list contains items
without more details being resolved.
type: array
items:
$ref: "#/components/schemas/ItemLight"
Attachment: Attachment:
description: | description: |
Information about an attachment to an item. Information about an attachment to an item.
@ -8001,6 +8011,11 @@ components:
type: array type: array
items: items:
$ref: "#/components/schemas/ItemFieldValue" $ref: "#/components/schemas/ItemFieldValue"
relatedItems:
type: array
items:
type: string
format: ident
notes: notes:
description: | description: |
Some prefix of the item notes. Some prefix of the item notes.

View File

@ -139,7 +139,8 @@ trait Conversions {
data.sources.map((mkAttachmentSource _).tupled).toList, data.sources.map((mkAttachmentSource _).tupled).toList,
data.archives.map((mkAttachmentArchive _).tupled).toList, data.archives.map((mkAttachmentArchive _).tupled).toList,
data.tags.map(mkTag).toList, data.tags.map(mkTag).toList,
data.customFields.map(mkItemFieldValue).toList data.customFields.map(mkItemFieldValue).toList,
data.relatedItems.map(mkItemLight).toList
) )
def mkItemFieldValue(v: OItemSearch.ItemFieldValue): ItemFieldValue = def mkItemFieldValue(v: OItemSearch.ItemFieldValue): ItemFieldValue =
@ -239,6 +240,7 @@ trait Conversions {
Nil, // attachments Nil, // attachments
Nil, // tags Nil, // tags
Nil, // customfields Nil, // customfields
Nil, // related items
i.notes, i.notes,
Nil // highlight Nil // highlight
) )
@ -254,7 +256,8 @@ trait Conversions {
.copy( .copy(
tags = i.tags.map(mkTag), tags = i.tags.map(mkTag),
attachments = i.attachments.map(mkAttachmentLight), attachments = i.attachments.map(mkAttachmentLight),
customfields = i.customfields.map(mkItemFieldValue) customfields = i.customfields.map(mkItemFieldValue),
relatedItems = i.relatedItems
) )
private def mkAttachmentLight(qa: QAttachmentLight): AttachmentLight = private def mkAttachmentLight(qa: QAttachmentLight): AttachmentLight =

View File

@ -21,7 +21,8 @@ case class ItemData(
attachments: Vector[(RAttachment, RFileMeta)], attachments: Vector[(RAttachment, RFileMeta)],
sources: Vector[(RAttachmentSource, RFileMeta)], sources: Vector[(RAttachmentSource, RFileMeta)],
archives: Vector[(RAttachmentArchive, RFileMeta)], archives: Vector[(RAttachmentArchive, RFileMeta)],
customFields: Vector[ItemFieldValue] customFields: Vector[ItemFieldValue],
relatedItems: Vector[ListItem]
) { ) {
def filterCollective(coll: Ident): Option[ItemData] = def filterCollective(coll: Ident): Option[ItemData] =

View File

@ -6,11 +6,13 @@
package docspell.store.queries package docspell.store.queries
import docspell.common.Ident
import docspell.store.records.RTag import docspell.store.records.RTag
case class ListItemWithTags( case class ListItemWithTags(
item: ListItem, item: ListItem,
tags: List[RTag], tags: List[RTag],
attachments: List[AttachmentLight], attachments: List[AttachmentLight],
customfields: List[ItemFieldValue] customfields: List[ItemFieldValue],
relatedItems: List[Ident]
) )

View File

@ -15,7 +15,8 @@ import cats.implicits._
import fs2.Stream import fs2.Stream
import docspell.common.{FileKey, IdRef, _} import docspell.common.{FileKey, IdRef, _}
import docspell.query.ItemQuery import docspell.query.ItemQuery.Expr.ValidItemStates
import docspell.query.{ItemQuery, ItemQueryDsl}
import docspell.store.Store import docspell.store.Store
import docspell.store.qb.DSL._ import docspell.store.qb.DSL._
import docspell.store.qb._ import docspell.store.qb._
@ -47,7 +48,7 @@ object QItem {
.unique .unique
.map(_ + items.size) .map(_ + items.size)
def findItem(id: Ident): ConnectionIO[Option[ItemData]] = { def findItem(id: Ident, collective: Ident): ConnectionIO[Option[ItemData]] = {
val ref = RItem.as("ref") val ref = RItem.as("ref")
val cq = val cq =
Select( Select(
@ -85,6 +86,7 @@ object QItem {
val archives = RAttachmentArchive.findByItemWithMeta(id) val archives = RAttachmentArchive.findByItemWithMeta(id)
val tags = RTag.findByItem(id) val tags = RTag.findByItem(id)
val customfields = findCustomFieldValuesForItem(id) val customfields = findCustomFieldValuesForItem(id)
val related = findRelatedItems(id, collective)
for { for {
data <- q data <- q
@ -93,11 +95,29 @@ object QItem {
arch <- archives arch <- archives
ts <- tags ts <- tags
cfs <- customfields cfs <- customfields
rel <- related
} yield data.map(d => } yield data.map(d =>
ItemData(d._1, d._2, d._3, d._4, d._5, d._6, d._7, ts, att, srcs, arch, cfs) ItemData(d._1, d._2, d._3, d._4, d._5, d._6, d._7, ts, att, srcs, arch, cfs, rel)
) )
} }
def findRelatedItems(id: Ident, collective: Ident): ConnectionIO[Vector[ListItem]] =
RItemLink
.findLinked(collective, id)
.map(v => Nel.fromList(v.toList))
.flatMap {
case None =>
Vector.empty[ListItem].pure[ConnectionIO]
case Some(nel) =>
val expr =
ItemQuery.Expr.and(ValidItemStates, ItemQueryDsl.Q.itemIdsIn(nel.map(_.id)))
val account = AccountId(collective, Ident.unsafe(""))
findItemsBase(Query.Fix(account, Some(expr), None), LocalDate.EPOCH, 0).build
.query[ListItem]
.to[Vector]
}
def findCustomFieldValuesForItem( def findCustomFieldValuesForItem(
itemId: Ident itemId: Ident
): ConnectionIO[Vector[ItemFieldValue]] = ): ConnectionIO[Vector[ItemFieldValue]] =
@ -440,11 +460,13 @@ object QItem {
attachs <- Stream.eval(findAttachmentLight(item.id)) attachs <- Stream.eval(findAttachmentLight(item.id))
ftags = tags.flatten.filter(t => t.collective == collective) ftags = tags.flatten.filter(t => t.collective == collective)
cfields <- Stream.eval(findCustomFieldValuesForItem(item.id)) cfields <- Stream.eval(findCustomFieldValuesForItem(item.id))
related <- Stream.eval(RItemLink.findLinked(collective, item.id))
} yield ListItemWithTags( } yield ListItemWithTags(
item, item,
RTag.sort(ftags.toList), RTag.sort(ftags.toList),
attachs.sortBy(_.position), attachs.sortBy(_.position),
cfields.toList cfields.toList,
related.toList
) )
} }

View File

@ -34,7 +34,6 @@ import Html.Events exposing (onClick)
import Markdown import Markdown
import Messages.Comp.ItemCard exposing (Texts) import Messages.Comp.ItemCard exposing (Texts)
import Page exposing (Page(..)) import Page exposing (Page(..))
import Set exposing (Set)
import Styles as S import Styles as S
import Util.CustomField import Util.CustomField
import Util.ItemDragDrop as DD import Util.ItemDragDrop as DD
@ -336,7 +335,7 @@ viewRow texts cfg settings flags model item =
, IT.render subtitleTemplate (templateCtx texts) item |> text , IT.render subtitleTemplate (templateCtx texts) item |> text
] ]
, div [ class "opacity-90" ] , div [ class "opacity-90" ]
[ mainTagsAndFields2 settings "flex truncate overflow-hidden flex-nowrap text-xs justify-start hidden md:flex" item [ mainTagsAndFields2 texts settings "flex truncate overflow-hidden flex-nowrap text-xs justify-start hidden md:flex" item
] ]
] ]
] ]
@ -449,7 +448,7 @@ viewRow texts cfg settings flags model item =
(IT.render IT.source (templateCtx texts) item) (IT.render IT.source (templateCtx texts) item)
] ]
] ]
, mainTagsAndFields2 settings "justify-start text-sm" item , mainTagsAndFields2 texts settings "justify-start text-sm" item
, notesContent2 settings item , notesContent2 settings item
] ]
] ]
@ -718,13 +717,13 @@ mainContent2 texts _ cardColor isCreated isDeleted settings _ item =
, IT.render subtitlePattern (templateCtx texts) item |> text , IT.render subtitlePattern (templateCtx texts) item |> text
] ]
, div [ class "" ] , div [ class "" ]
[ mainTagsAndFields2 settings "justify-end text-xs" item [ mainTagsAndFields2 texts settings "justify-end text-xs" item
] ]
] ]
mainTagsAndFields2 : UiSettings -> String -> ItemLight -> Html Msg mainTagsAndFields2 : Texts -> UiSettings -> String -> ItemLight -> Html Msg
mainTagsAndFields2 settings extraCss item = mainTagsAndFields2 texts settings extraCss item =
let let
fieldHidden f = fieldHidden f =
Data.UiSettings.fieldHidden settings f Data.UiSettings.fieldHidden settings f
@ -765,6 +764,22 @@ mainTagsAndFields2 settings extraCss item =
else else
List.map showTag item.tags List.map showTag item.tags
renderRelated =
if List.isEmpty item.relatedItems then
[]
else
[ a
[ class "label ml-1 mt-1 font-semibold hover:opacity-75 py-1"
, class "border-gray-500 dark:border-slate-300"
, href "#"
, onClick (SetLinkTarget <| Comp.LinkTarget.LinkRelatedItems (item.id :: item.relatedItems))
, title texts.showRelatedItems
]
[ i [ class "fa fa-link" ] []
]
]
in in
div div
[ classList [ classList
@ -773,7 +788,7 @@ mainTagsAndFields2 settings extraCss item =
] ]
, class extraCss , class extraCss
] ]
(renderFields ++ renderTags) (renderFields ++ renderTags ++ renderRelated)
previewImage2 : Texts -> ViewConfig -> UiSettings -> Model -> ItemLight -> Html Msg previewImage2 : Texts -> ViewConfig -> UiSettings -> Model -> ItemLight -> Html Msg

View File

@ -97,12 +97,12 @@ update inav env msg model =
( cm, cc ) = ( cm, cc ) =
Comp.CustomFieldMultiInput.init env.flags Comp.CustomFieldMultiInput.init env.flags
( ilm, ilc ) = ilm =
if model.item.id == "" then if model.item.id == "" then
( model.itemLinkModel, Cmd.none ) model.itemLinkModel
else else
Comp.ItemLinkForm.init env.flags model.item.id Comp.ItemLinkForm.initWith model.item.id model.item.relatedItems
in in
resultModelCmd resultModelCmd
( { model ( { model
@ -120,7 +120,6 @@ update inav env msg model =
, Cmd.map DueDatePickerMsg dpc , Cmd.map DueDatePickerMsg dpc
, Cmd.map ItemMailMsg ic , Cmd.map ItemMailMsg ic
, Cmd.map CustomFieldMsg cc , Cmd.map CustomFieldMsg cc
, Cmd.map ItemLinkFormMsg ilc
, Api.getSentMails env.flags model.item.id SentMailsResp , Api.getSentMails env.flags model.item.id SentMailsResp
] ]
) )

View File

@ -5,7 +5,7 @@
-} -}
module Comp.ItemLinkForm exposing (Model, Msg, emptyModel, init, update, view) module Comp.ItemLinkForm exposing (Model, Msg, emptyModel, init, initWith, update, view)
import Api import Api
import Api.Model.BasicResult exposing (BasicResult) import Api.Model.BasicResult exposing (BasicResult)
@ -67,6 +67,20 @@ type Msg
| ToggleEditMode | ToggleEditMode
initWith : String -> List ItemLight -> Model
initWith target related =
let
cfg =
Comp.ItemSearchInput.defaultConfig
in
{ itemSearchModel = Comp.ItemSearchInput.init cfg
, relatedItems = related
, targetItemId = target
, editMode = AddRelated
, formState = FormOk
}
init : Flags -> String -> ( Model, Cmd Msg ) init : Flags -> String -> ( Model, Cmd Msg )
init flags itemId = init flags itemId =
let let

View File

@ -36,6 +36,7 @@ type LinkTarget
| LinkCustomField ItemFieldValue | LinkCustomField ItemFieldValue
| LinkSource String | LinkSource String
| LinkBookmark String | LinkBookmark String
| LinkRelatedItems (List String)
| LinkNone | LinkNone

View File

@ -104,6 +104,7 @@ type alias Model =
, sourceModel : Maybe String , sourceModel : Maybe String
, allBookmarks : Comp.BookmarkChooser.Model , allBookmarks : Comp.BookmarkChooser.Model
, selectedBookmarks : Comp.BookmarkChooser.Selection , selectedBookmarks : Comp.BookmarkChooser.Selection
, selectedItems : List String
, includeSelection : Bool , includeSelection : Bool
, openTabs : Set String , openTabs : Set String
, searchMode : SearchMode , searchMode : SearchMode
@ -152,6 +153,7 @@ init flags =
, sourceModel = Nothing , sourceModel = Nothing
, allBookmarks = Comp.BookmarkChooser.init Data.Bookmarks.empty , allBookmarks = Comp.BookmarkChooser.init Data.Bookmarks.empty
, selectedBookmarks = Comp.BookmarkChooser.emptySelection , selectedBookmarks = Comp.BookmarkChooser.emptySelection
, selectedItems = []
, includeSelection = False , includeSelection = False
, openTabs = Set.fromList [ "Tags", "Inbox" ] , openTabs = Set.fromList [ "Tags", "Inbox" ]
, searchMode = Data.SearchMode.Normal , searchMode = Data.SearchMode.Normal
@ -311,6 +313,7 @@ getItemQuery selectedItems model =
, textSearch.fullText , textSearch.fullText
|> Maybe.map Q.Contents |> Maybe.map Q.Contents
, whenNotEmpty bookmarks Q.And , whenNotEmpty bookmarks Q.And
, whenNotEmpty model.selectedItems Q.ItemIdIn
] ]
@ -356,6 +359,7 @@ resetModel model =
, customValues = Data.CustomFieldChange.emptyCollect , customValues = Data.CustomFieldChange.emptyCollect
, sourceModel = Nothing , sourceModel = Nothing
, selectedBookmarks = Comp.BookmarkChooser.emptySelection , selectedBookmarks = Comp.BookmarkChooser.emptySelection
, selectedItems = []
, includeSelection = False , includeSelection = False
, searchMode = Data.SearchMode.Normal , searchMode = Data.SearchMode.Normal
} }
@ -397,6 +401,7 @@ type Msg
| SetFolder IdName | SetFolder IdName
| SetTag String | SetTag String
| SetBookmark String | SetBookmark String
| SetSelectedItems (List String)
| SetCustomField ItemFieldValue | SetCustomField ItemFieldValue
| CustomFieldMsg Comp.CustomFieldMultiInput.Msg | CustomFieldMsg Comp.CustomFieldMultiInput.Msg
| SetSource String | SetSource String
@ -459,6 +464,9 @@ linkTargetMsg linkTarget =
Comp.LinkTarget.LinkBookmark id -> Comp.LinkTarget.LinkBookmark id ->
Just <| SetBookmark id Just <| SetBookmark id
Comp.LinkTarget.LinkRelatedItems ids ->
Just <| SetSelectedItems ids
type alias NextState = type alias NextState =
{ model : Model { model : Model
@ -611,6 +619,19 @@ updateDrop ddm flags settings msg model =
, selectionChange = Data.ItemIds.noChange , selectionChange = Data.ItemIds.noChange
} }
SetSelectedItems ids ->
let
nextModel =
resetModel model
in
{ model = { nextModel | selectedItems = ids }
, cmd = Cmd.none
, sub = Sub.none
, stateChange = ids /= model.selectedItems
, dragDrop = DD.DragDropData ddm Nothing
, selectionChange = Data.ItemIds.noChange
}
GetAllTagsResp (Ok stats) -> GetAllTagsResp (Ok stats) ->
let let
tagSel = tagSel =

View File

@ -30,6 +30,7 @@ type alias Texts =
, formatDateLong : Int -> String , formatDateLong : Int -> String
, formatDateShort : Int -> String , formatDateShort : Int -> String
, directionLabel : Direction -> String , directionLabel : Direction -> String
, showRelatedItems : String
} }
@ -44,6 +45,7 @@ gb tz =
, formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.English tz , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.English tz
, formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.English tz , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.English tz
, directionLabel = Messages.Data.Direction.gb , directionLabel = Messages.Data.Direction.gb
, showRelatedItems = "Show linked items"
} }
@ -58,6 +60,7 @@ de tz =
, formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.German tz , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.German tz
, formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.German tz , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.German tz
, directionLabel = Messages.Data.Direction.de , directionLabel = Messages.Data.Direction.de
, showRelatedItems = "Verknüpfte Dokumente anzeigen"
} }
@ -72,4 +75,5 @@ fr tz =
, formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.French tz , formatDateLong = Messages.DateFormat.formatDateLong Messages.UiLanguage.French tz
, formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.French tz , formatDateShort = Messages.DateFormat.formatDateShort Messages.UiLanguage.French tz
, directionLabel = Messages.Data.Direction.fr , directionLabel = Messages.Data.Direction.fr
, showRelatedItems = "Afficher les documents liés"
} }

View File

@ -38,6 +38,7 @@ toItemLight detail =
, tags = detail.tags , tags = detail.tags
, customfields = detail.customfields , customfields = detail.customfields
, notes = detail.notes , notes = detail.notes
, relatedItems = List.map .id detail.relatedItems
, highlighting = [] , highlighting = []
} }