From 7a14b05ea70a949b11ae7ae3345097ddd7ec09fb Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 9 Nov 2020 17:04:56 +0100 Subject: [PATCH] Enhance item card displaying current file and number of pages --- modules/webapp/src/main/elm/Api.elm | 16 +- modules/webapp/src/main/elm/Comp/ItemCard.elm | 442 ++++++++++++++++++ .../webapp/src/main/elm/Comp/ItemCardList.elm | 332 ++----------- modules/webapp/src/main/webjar/docspell.css | 11 +- 4 files changed, 490 insertions(+), 311 deletions(-) create mode 100644 modules/webapp/src/main/elm/Comp/ItemCard.elm diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index 601f092e..c9935f49 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -6,6 +6,7 @@ module Api exposing , addMember , addTag , addTagsMultiple + , attachmentPreviewURL , cancelJob , changeFolderName , changePassword @@ -58,9 +59,9 @@ module Api exposing , getTagCloud , getTags , getUsers + , itemBasePreviewURL , itemDetail , itemIndexSearch - , itemPreviewURL , itemSearch , login , loginSession @@ -1504,16 +1505,9 @@ deleteAllItems flags ids receive = --- Item -itemPreviewURL : ItemLight -> String -itemPreviewURL item = - let - makeUrl a = - "/api/v1/sec/attachment/" ++ a.id ++ "/preview?withFallback=true" - in - List.sortBy .position item.attachments - |> List.head - |> Maybe.map makeUrl - |> Maybe.withDefault (itemBasePreviewURL item.id) +attachmentPreviewURL : String -> String +attachmentPreviewURL id = + "/api/v1/sec/attachment/" ++ id ++ "/preview?withFallback=true" itemBasePreviewURL : String -> String diff --git a/modules/webapp/src/main/elm/Comp/ItemCard.elm b/modules/webapp/src/main/elm/Comp/ItemCard.elm new file mode 100644 index 00000000..7c48245a --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemCard.elm @@ -0,0 +1,442 @@ +module Comp.ItemCard exposing (..) + +import Api +import Api.Model.AttachmentLight exposing (AttachmentLight) +import Api.Model.HighlightEntry exposing (HighlightEntry) +import Api.Model.ItemLight exposing (ItemLight) +import Data.Direction +import Data.Fields +import Data.Icons as Icons +import Data.ItemSelection exposing (ItemSelection) +import Data.UiSettings exposing (UiSettings) +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Markdown +import Page exposing (Page(..)) +import Set exposing (Set) +import Util.Html +import Util.ItemDragDrop as DD +import Util.List +import Util.Maybe +import Util.String +import Util.Time + + +type alias Model = + { previewAttach : Maybe AttachmentLight + } + + +type Msg + = CyclePreview ItemLight + | ToggleSelectItem (Set String) String + | ItemDDMsg DD.Msg + + +type alias ViewConfig = + { selection : ItemSelection + , extraClasses : String + } + + +type alias UpdateResult = + { model : Model + , dragModel : DD.Model + , selection : ItemSelection + } + + +init : Model +init = + { previewAttach = Nothing + } + + +currentAttachment : Model -> ItemLight -> Maybe AttachmentLight +currentAttachment model item = + Util.Maybe.or + [ model.previewAttach + , List.head item.attachments + ] + + +currentPosition : Model -> ItemLight -> Int +currentPosition model item = + let + filter cur el = + cur.id == el.id + in + case model.previewAttach of + Just a -> + case Util.List.findIndexed (filter a) item.attachments of + Just ( _, n ) -> + n + 1 + + Nothing -> + 1 + + Nothing -> + 1 + + +update : DD.Model -> Msg -> Model -> UpdateResult +update ddm msg model = + case msg of + ItemDDMsg lm -> + let + ddd = + DD.update lm ddm + in + UpdateResult model ddd.model Data.ItemSelection.Inactive + + ToggleSelectItem ids id -> + let + newSet = + if Set.member id ids then + Set.remove id ids + + else + Set.insert id ids + in + UpdateResult model ddm (Data.ItemSelection.Active newSet) + + CyclePreview item -> + let + mainAttach = + currentAttachment model item + + next = + Util.List.findNext (\e -> Just e.id == Maybe.map .id mainAttach) item.attachments + in + UpdateResult { model | previewAttach = next } + ddm + Data.ItemSelection.Inactive + + +view : ViewConfig -> UiSettings -> Model -> ItemLight -> Html Msg +view cfg settings model item = + let + dirIcon = + i [ class (Data.Direction.iconFromMaybe item.direction) ] [] + + corr = + List.filterMap identity [ item.corrOrg, item.corrPerson ] + |> List.map .name + |> List.intersperse ", " + |> String.concat + + conc = + List.filterMap identity [ item.concPerson, item.concEquip ] + |> List.map .name + |> List.intersperse ", " + |> String.concat + + folder = + Maybe.map .name item.folder + |> Maybe.withDefault "" + + dueDate = + Maybe.map Util.Time.formatDateShort item.dueDate + |> Maybe.withDefault "" + + isConfirmed = + item.state /= "created" + + cardColor = + if isSelected cfg item.id then + "purple" + + else if not isConfirmed then + "blue" + + else + "" + + fieldHidden f = + Data.UiSettings.fieldHidden settings f + + cardAction = + case cfg.selection of + Data.ItemSelection.Inactive -> + Page.href (ItemDetailPage item.id) + + Data.ItemSelection.Active ids -> + onClick (ToggleSelectItem ids item.id) + + mainAttach = + currentAttachment model item + + previewUrl = + Maybe.map .id mainAttach + |> Maybe.map Api.attachmentPreviewURL + |> Maybe.withDefault (Api.itemBasePreviewURL item.id) + + pageCount = + Maybe.andThen .pageCount mainAttach + |> Maybe.withDefault 0 + + pageCountLabel = + div + [ classList + [ ( "card-attachment-nav", True ) + , ( "invisible", pageCount == 0 ) + ] + ] + [ if item.fileCount == 1 then + div + [ class "ui secondary basic mini label" + , title "Number of pages" + ] + [ text "p." + , text (String.fromInt pageCount) + ] + + else + div [ class "ui left labeled mini button" ] + [ div [ class "ui basic right pointing mini label" ] + [ currentPosition model item + |> String.fromInt + |> text + , text "/" + , text (String.fromInt item.fileCount) + , text " p." + , text (String.fromInt pageCount) + ] + , a + [ class "ui mini icon secondary button" + , href "#" + , onClick (CyclePreview item) + ] + [ i [ class "arrow right icon" ] [] + ] + ] + ] + in + div + ([ classList + [ ( "ui fluid card", True ) + , ( cardColor, True ) + , ( cfg.extraClasses, True ) + ] + , id item.id + ] + ++ DD.draggable ItemDDMsg item.id + ) + [ if fieldHidden Data.Fields.PreviewImage then + span [ class "invisible" ] [] + + else + div [ class "image" ] + [ img + [ class "preview-image" + , src previewUrl + , Data.UiSettings.cardPreviewSize settings + ] + [] + , pageCountLabel + ] + , a + [ class "link content" + , href "#" + , cardAction + ] + [ case cfg.selection of + Data.ItemSelection.Active ids -> + div [ class "header" ] + [ Util.Html.checkbox (Set.member item.id ids) + , dirIcon + , Util.String.underscoreToSpace item.name + |> text + ] + + Data.ItemSelection.Inactive -> + if fieldHidden Data.Fields.Direction then + div [ class "header" ] + [ Util.String.underscoreToSpace item.name |> text + ] + + else + div + [ class "header" + , Data.Direction.labelFromMaybe item.direction + |> title + ] + [ dirIcon + , Util.String.underscoreToSpace item.name + |> text + ] + , div + [ classList + [ ( "ui right corner label", True ) + , ( cardColor, True ) + , ( "invisible", isConfirmed ) + ] + , title "New" + ] + [ i [ class "exclamation icon" ] [] + ] + , div + [ classList + [ ( "meta", True ) + , ( "invisible hidden", fieldHidden Data.Fields.Date ) + ] + ] + [ Util.Time.formatDate item.date |> text + ] + , div [ class "meta description" ] + [ div + [ classList + [ ( "ui right floated tiny labels", True ) + , ( "invisible hidden", item.tags == [] || fieldHidden Data.Fields.Tag ) + ] + ] + (List.map + (\tag -> + div + [ classList + [ ( "ui basic label", True ) + , ( Data.UiSettings.tagColorString tag settings, True ) + ] + ] + [ text tag.name ] + ) + item.tags + ) + ] + ] + , div + [ classList + [ ( "content", True ) + , ( "invisible hidden" + , settings.itemSearchNoteLength + <= 0 + || Util.String.isNothingOrBlank item.notes + ) + ] + ] + [ span [ class "small-info" ] + [ Maybe.withDefault "" item.notes + |> Util.String.ellipsis settings.itemSearchNoteLength + |> text + ] + ] + , div [ class "content" ] + [ div [ class "ui horizontal list" ] + [ div + [ classList + [ ( "item", True ) + , ( "invisible hidden" + , fieldHidden Data.Fields.CorrOrg + && fieldHidden Data.Fields.CorrPerson + ) + ] + , title "Correspondent" + ] + [ Icons.correspondentIcon "" + , text " " + , Util.String.withDefault "-" corr |> text + ] + , div + [ classList + [ ( "item", True ) + , ( "invisible hidden" + , fieldHidden Data.Fields.ConcPerson + && fieldHidden Data.Fields.ConcEquip + ) + ] + , title "Concerning" + ] + [ Icons.concernedIcon + , text " " + , Util.String.withDefault "-" conc |> text + ] + , div + [ classList + [ ( "item", True ) + , ( "invisible hidden", fieldHidden Data.Fields.Folder ) + ] + , title "Folder" + ] + [ Icons.folderIcon "" + , text " " + , Util.String.withDefault "-" folder |> text + ] + ] + , div [ class "right floated meta" ] + [ div [ class "ui horizontal list" ] + [ div + [ class "item" + , title "Source" + ] + [ text item.source + ] + , div + [ classList + [ ( "item", True ) + , ( "invisible hidden" + , item.dueDate + == Nothing + || fieldHidden Data.Fields.DueDate + ) + ] + , title ("Due on " ++ dueDate) + ] + [ div + [ class "ui basic grey label" + ] + [ Icons.dueDateIcon "" + , text (" " ++ dueDate) + ] + ] + ] + ] + ] + , div + [ classList + [ ( "content search-highlight", True ) + , ( "invisible hidden", item.highlighting == [] ) + ] + ] + [ div [ class "ui list" ] + (List.map renderHighlightEntry item.highlighting) + ] + ] + + +renderHighlightEntry : HighlightEntry -> Html Msg +renderHighlightEntry entry = + let + stripWhitespace str = + String.trim str + |> String.replace "```" "" + |> String.replace "\t" " " + |> String.replace "\n\n" "\n" + |> String.lines + |> List.map String.trim + |> String.join "\n" + in + div [ class "item" ] + [ div [ class "content" ] + (div [ class "header" ] + [ i [ class "caret right icon" ] [] + , text (entry.name ++ ":") + ] + :: List.map + (\str -> + Markdown.toHtml [ class "description" ] <| + (stripWhitespace str ++ "…") + ) + entry.lines + ) + ] + + +isSelected : ViewConfig -> String -> Bool +isSelected cfg id = + case cfg.selection of + Data.ItemSelection.Active ids -> + Set.member id ids + + Data.ItemSelection.Inactive -> + False diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 675847b3..24b942bb 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -10,46 +10,38 @@ module Comp.ItemCardList exposing , view ) -import Api -import Api.Model.HighlightEntry exposing (HighlightEntry) import Api.Model.ItemLight exposing (ItemLight) import Api.Model.ItemLightGroup exposing (ItemLightGroup) import Api.Model.ItemLightList exposing (ItemLightList) -import Data.Direction -import Data.Fields +import Comp.ItemCard import Data.Flags exposing (Flags) -import Data.Icons as Icons import Data.ItemSelection exposing (ItemSelection) import Data.Items import Data.UiSettings exposing (UiSettings) +import Dict exposing (Dict) import Html exposing (..) import Html.Attributes exposing (..) -import Html.Events exposing (onClick) -import Markdown import Page exposing (Page(..)) -import Set exposing (Set) -import Util.Html import Util.ItemDragDrop as DD import Util.List -import Util.String -import Util.Time type alias Model = { results : ItemLightList + , itemCards : Dict String Comp.ItemCard.Model } type Msg = SetResults ItemLightList | AddResults ItemLightList - | ItemDDMsg DD.Msg - | ToggleSelectItem (Set String) String + | ItemCardMsg ItemLight Comp.ItemCard.Msg init : Model init = { results = Api.Model.ItemLightList.empty + , itemCards = Dict.empty } @@ -112,23 +104,22 @@ updateDrag dm _ msg model = in UpdateResult newModel Cmd.none dm Data.ItemSelection.Inactive - ItemDDMsg lm -> + ItemCardMsg item lm -> let - ddd = - DD.update lm dm - in - UpdateResult model Cmd.none ddd.model Data.ItemSelection.Inactive + cardModel = + Dict.get item.id model.itemCards + |> Maybe.withDefault Comp.ItemCard.init - ToggleSelectItem ids id -> - let - newSet = - if Set.member id ids then - Set.remove id ids + result = + Comp.ItemCard.update dm lm cardModel - else - Set.insert id ids + cards = + Dict.insert item.id result.model model.itemCards in - UpdateResult model Cmd.none dm (Data.ItemSelection.Active newSet) + UpdateResult { model | itemCards = cards } + Cmd.none + result.dragModel + result.selection @@ -141,297 +132,42 @@ type alias ViewConfig = } -isSelected : ViewConfig -> String -> Bool -isSelected cfg id = - case cfg.selection of - Data.ItemSelection.Active ids -> - Set.member id ids - - Data.ItemSelection.Inactive -> - False - - view : ViewConfig -> UiSettings -> Model -> Html Msg view cfg settings model = div [ class "ui container" ] - (List.map (viewGroup cfg settings) model.results.groups) + (List.map (viewGroup model cfg settings) model.results.groups) -viewGroup : ViewConfig -> UiSettings -> ItemLightGroup -> Html Msg -viewGroup cfg settings group = +viewGroup : Model -> ViewConfig -> UiSettings -> ItemLightGroup -> Html Msg +viewGroup model cfg settings group = div [ class "item-group" ] [ div [ class "ui horizontal divider header item-list" ] [ i [ class "calendar alternate outline icon" ] [] , text group.name ] , div [ class "ui stackable three cards" ] - (List.map (viewItem cfg settings) group.items) + (List.map (viewItem model cfg settings) group.items) ] -viewItem : ViewConfig -> UiSettings -> ItemLight -> Html Msg -viewItem cfg settings item = +viewItem : Model -> ViewConfig -> UiSettings -> ItemLight -> Html Msg +viewItem model cfg settings item = let - dirIcon = - i [ class (Data.Direction.iconFromMaybe item.direction) ] [] - - corr = - List.filterMap identity [ item.corrOrg, item.corrPerson ] - |> List.map .name - |> List.intersperse ", " - |> String.concat - - conc = - List.filterMap identity [ item.concPerson, item.concEquip ] - |> List.map .name - |> List.intersperse ", " - |> String.concat - - folder = - Maybe.map .name item.folder - |> Maybe.withDefault "" - - dueDate = - Maybe.map Util.Time.formatDateShort item.dueDate - |> Maybe.withDefault "" - - isConfirmed = - item.state /= "created" - - cardColor = - if isSelected cfg item.id then - "purple" - - else if not isConfirmed then - "blue" + currentClass = + if cfg.current == Just item.id then + "current" else "" - fieldHidden f = - Data.UiSettings.fieldHidden settings f + vvcfg = + Comp.ItemCard.ViewConfig cfg.selection currentClass - cardAction = - case cfg.selection of - Data.ItemSelection.Inactive -> - Page.href (ItemDetailPage item.id) + cardModel = + Dict.get item.id model.itemCards + |> Maybe.withDefault Comp.ItemCard.init - Data.ItemSelection.Active ids -> - onClick (ToggleSelectItem ids item.id) + cardHtml = + Comp.ItemCard.view vvcfg settings cardModel item in - div - ([ classList - [ ( "ui fluid card", True ) - , ( cardColor, True ) - , ( "current", cfg.current == Just item.id ) - ] - , id item.id - ] - ++ DD.draggable ItemDDMsg item.id - ) - [ if fieldHidden Data.Fields.PreviewImage then - span [ class "invisible" ] [] - - else - div [ class "image" ] - [ img - [ class "preview-image" - , src (Api.itemPreviewURL item) - , Data.UiSettings.cardPreviewSize settings - ] - [] - ] - , a - [ class "content" - , href "#" - , cardAction - ] - [ case cfg.selection of - Data.ItemSelection.Active ids -> - div [ class "header" ] - [ Util.Html.checkbox (Set.member item.id ids) - , dirIcon - , Util.String.underscoreToSpace item.name - |> text - ] - - Data.ItemSelection.Inactive -> - if fieldHidden Data.Fields.Direction then - div [ class "header" ] - [ Util.String.underscoreToSpace item.name |> text - ] - - else - div - [ class "header" - , Data.Direction.labelFromMaybe item.direction - |> title - ] - [ dirIcon - , Util.String.underscoreToSpace item.name - |> text - ] - , div - [ classList - [ ( "ui right corner label", True ) - , ( cardColor, True ) - , ( "invisible", isConfirmed ) - ] - , title "New" - ] - [ i [ class "exclamation icon" ] [] - ] - , div - [ classList - [ ( "meta", True ) - , ( "invisible hidden", fieldHidden Data.Fields.Date ) - ] - ] - [ Util.Time.formatDate item.date |> text - ] - , div [ class "meta description" ] - [ div - [ classList - [ ( "ui right floated tiny labels", True ) - , ( "invisible hidden", item.tags == [] || fieldHidden Data.Fields.Tag ) - ] - ] - (List.map - (\tag -> - div - [ classList - [ ( "ui basic label", True ) - , ( Data.UiSettings.tagColorString tag settings, True ) - ] - ] - [ text tag.name ] - ) - item.tags - ) - ] - ] - , div - [ classList - [ ( "content", True ) - , ( "invisible hidden" - , settings.itemSearchNoteLength - <= 0 - || Util.String.isNothingOrBlank item.notes - ) - ] - ] - [ span [ class "small-info" ] - [ Maybe.withDefault "" item.notes - |> Util.String.ellipsis settings.itemSearchNoteLength - |> text - ] - ] - , div [ class "content" ] - [ div [ class "ui horizontal list" ] - [ div - [ classList - [ ( "item", True ) - , ( "invisible hidden" - , fieldHidden Data.Fields.CorrOrg - && fieldHidden Data.Fields.CorrPerson - ) - ] - , title "Correspondent" - ] - [ Icons.correspondentIcon "" - , text " " - , Util.String.withDefault "-" corr |> text - ] - , div - [ classList - [ ( "item", True ) - , ( "invisible hidden" - , fieldHidden Data.Fields.ConcPerson - && fieldHidden Data.Fields.ConcEquip - ) - ] - , title "Concerning" - ] - [ Icons.concernedIcon - , text " " - , Util.String.withDefault "-" conc |> text - ] - , div - [ classList - [ ( "item", True ) - , ( "invisible hidden", fieldHidden Data.Fields.Folder ) - ] - , title "Folder" - ] - [ Icons.folderIcon "" - , text " " - , Util.String.withDefault "-" folder |> text - ] - ] - , div [ class "right floated meta" ] - [ div [ class "ui horizontal list" ] - [ div - [ class "item" - , title "Source" - ] - [ text item.source - ] - , div - [ classList - [ ( "item", True ) - , ( "invisible hidden" - , item.dueDate - == Nothing - || fieldHidden Data.Fields.DueDate - ) - ] - , title ("Due on " ++ dueDate) - ] - [ div - [ class "ui basic grey label" - ] - [ Icons.dueDateIcon "" - , text (" " ++ dueDate) - ] - ] - ] - ] - ] - , div - [ classList - [ ( "content search-highlight", True ) - , ( "invisible hidden", item.highlighting == [] ) - ] - ] - [ div [ class "ui list" ] - (List.map renderHighlightEntry item.highlighting) - ] - ] - - -renderHighlightEntry : HighlightEntry -> Html Msg -renderHighlightEntry entry = - let - stripWhitespace str = - String.trim str - |> String.replace "```" "" - |> String.replace "\t" " " - |> String.replace "\n\n" "\n" - |> String.lines - |> List.map String.trim - |> String.join "\n" - in - div [ class "item" ] - [ div [ class "content" ] - (div [ class "header" ] - [ i [ class "caret right icon" ] [] - , text (entry.name ++ ":") - ] - :: List.map - (\str -> - Markdown.toHtml [ class "description" ] <| - (stripWhitespace str ++ "…") - ) - entry.lines - ) - ] + Html.map (ItemCardMsg item) cardHtml diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 04ec1eff..58387cac 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -92,7 +92,15 @@ background: floralwhite; padding: 0.8em; } - +.default-layout .ui.card .link.content:hover { + box-shadow: 0 0 0 1px #d4d4d5,0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.15); +} +.default-layout .image .card-attachment-nav { + position: absolute; + bottom: 2px; + right: 2px; + z-index: 10; +} .default-layout img.preview-image { margin-left: auto; margin-right: auto; @@ -119,7 +127,6 @@ background: rgba(220, 255, 71, 0.6); } .default-layout .ui.cards .ui.card.current { - /* semantic-ui purple */ box-shadow: 0 0 6px rgba(0,0,0,0.55); }