mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-05 02:49:32 +00:00
502 lines
13 KiB
Elm
502 lines
13 KiB
Elm
{-
|
|
Copyright 2020 Docspell Contributors
|
|
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
-}
|
|
|
|
|
|
module Comp.ItemMerge exposing
|
|
( Model
|
|
, Msg
|
|
, Outcome(..)
|
|
, init
|
|
, initQuery
|
|
, update
|
|
, view
|
|
)
|
|
|
|
import Api
|
|
import Api.Model.BasicResult exposing (BasicResult)
|
|
import Api.Model.ItemLight exposing (ItemLight)
|
|
import Api.Model.ItemLightList exposing (ItemLightList)
|
|
import Comp.Basic
|
|
import Comp.MenuBar as MB
|
|
import Data.Direction
|
|
import Data.Fields
|
|
import Data.Flags exposing (Flags)
|
|
import Data.Icons as Icons
|
|
import Data.ItemQuery exposing (ItemQuery)
|
|
import Data.ItemTemplate as IT
|
|
import Data.SearchMode exposing (SearchMode)
|
|
import Data.UiSettings exposing (UiSettings)
|
|
import Html exposing (..)
|
|
import Html.Attributes exposing (..)
|
|
import Html.Events exposing (onClick)
|
|
import Html5.DragDrop as DD
|
|
import Http
|
|
import Messages.Comp.ItemMerge exposing (Texts)
|
|
import Styles as S
|
|
import Util.CustomField
|
|
import Util.Item
|
|
import Util.List
|
|
|
|
|
|
type alias Model =
|
|
{ items : List ItemLight
|
|
, showInfoText : Bool
|
|
, dragDrop : DDModel
|
|
, formState : FormState
|
|
}
|
|
|
|
|
|
init : List ItemLight -> Model
|
|
init items =
|
|
{ items = items
|
|
, showInfoText = False
|
|
, dragDrop = DD.init
|
|
, formState = FormStateInitial
|
|
}
|
|
|
|
|
|
initQuery : Flags -> SearchMode -> ItemQuery -> ( Model, Cmd Msg )
|
|
initQuery flags searchMode query =
|
|
let
|
|
itemQuery =
|
|
{ offset = Just 0
|
|
, limit = Just 50
|
|
, withDetails = Just True
|
|
, searchMode = Just (Data.SearchMode.asString searchMode)
|
|
, query = Data.ItemQuery.render query
|
|
}
|
|
in
|
|
( init [], Api.itemSearch flags itemQuery ItemResp )
|
|
|
|
|
|
type alias Dropped =
|
|
{ sourceIdx : Int
|
|
, targetIdx : Int
|
|
}
|
|
|
|
|
|
type alias DDModel =
|
|
DD.Model Int Int
|
|
|
|
|
|
type alias DDMsg =
|
|
DD.Msg Int Int
|
|
|
|
|
|
type FormState
|
|
= FormStateInitial
|
|
| FormStateHttp Http.Error
|
|
| FormStateMergeSuccessful
|
|
| FormStateError String
|
|
| FormStateMergeInProcess
|
|
|
|
|
|
|
|
--- Update
|
|
|
|
|
|
type Outcome
|
|
= OutcomeCancel
|
|
| OutcomeMerged
|
|
| OutcomeNotYet
|
|
|
|
|
|
type alias UpdateResult =
|
|
{ model : Model
|
|
, cmd : Cmd Msg
|
|
, outcome : Outcome
|
|
}
|
|
|
|
|
|
notDoneResult : ( Model, Cmd Msg ) -> UpdateResult
|
|
notDoneResult t =
|
|
{ model = Tuple.first t
|
|
, cmd = Tuple.second t
|
|
, outcome = OutcomeNotYet
|
|
}
|
|
|
|
|
|
type Msg
|
|
= ItemResp (Result Http.Error ItemLightList)
|
|
| ToggleInfoText
|
|
| DragDrop (DD.Msg Int Int)
|
|
| SubmitMerge
|
|
| CancelMerge
|
|
| MergeResp (Result Http.Error BasicResult)
|
|
|
|
|
|
update : Flags -> Msg -> Model -> UpdateResult
|
|
update flags msg model =
|
|
case msg of
|
|
ItemResp (Ok list) ->
|
|
notDoneResult ( init (flatten list), Cmd.none )
|
|
|
|
ItemResp (Err err) ->
|
|
notDoneResult ( { model | formState = FormStateHttp err }, Cmd.none )
|
|
|
|
MergeResp (Ok result) ->
|
|
if result.success then
|
|
{ model = { model | formState = FormStateMergeSuccessful }
|
|
, cmd = Cmd.none
|
|
, outcome = OutcomeMerged
|
|
}
|
|
|
|
else
|
|
{ model = { model | formState = FormStateError result.message }
|
|
, cmd = Cmd.none
|
|
, outcome = OutcomeNotYet
|
|
}
|
|
|
|
MergeResp (Err err) ->
|
|
{ model = { model | formState = FormStateHttp err }
|
|
, cmd = Cmd.none
|
|
, outcome = OutcomeNotYet
|
|
}
|
|
|
|
ToggleInfoText ->
|
|
notDoneResult
|
|
( { model | showInfoText = not model.showInfoText }
|
|
, Cmd.none
|
|
)
|
|
|
|
DragDrop lmsg ->
|
|
let
|
|
( m, res ) =
|
|
DD.update lmsg model.dragDrop
|
|
|
|
dropped =
|
|
Maybe.map (\( idx1, idx2, _ ) -> Dropped idx1 idx2) res
|
|
|
|
model_ =
|
|
{ model | dragDrop = m }
|
|
in
|
|
case dropped of
|
|
Just data ->
|
|
let
|
|
items =
|
|
Util.List.changePosition data.sourceIdx data.targetIdx model.items
|
|
in
|
|
notDoneResult ( { model_ | items = items }, Cmd.none )
|
|
|
|
Nothing ->
|
|
notDoneResult ( model_, Cmd.none )
|
|
|
|
SubmitMerge ->
|
|
let
|
|
ids =
|
|
List.map .id model.items
|
|
in
|
|
notDoneResult
|
|
( { model | formState = FormStateMergeInProcess }
|
|
, Api.mergeItems flags ids MergeResp
|
|
)
|
|
|
|
CancelMerge ->
|
|
{ model = model
|
|
, cmd = Cmd.none
|
|
, outcome = OutcomeCancel
|
|
}
|
|
|
|
|
|
flatten : ItemLightList -> List ItemLight
|
|
flatten list =
|
|
list.groups |> List.concatMap .items
|
|
|
|
|
|
|
|
--- View
|
|
|
|
|
|
view : Texts -> UiSettings -> Model -> Html Msg
|
|
view texts settings model =
|
|
div [ class "px-2 mb-4" ]
|
|
[ h1 [ class S.header1 ]
|
|
[ text texts.title
|
|
, a
|
|
[ class "ml-2"
|
|
, class S.link
|
|
, href "#"
|
|
, onClick ToggleInfoText
|
|
]
|
|
[ i [ class "fa fa-info-circle" ] []
|
|
]
|
|
]
|
|
, p
|
|
[ class S.infoMessage
|
|
, classList [ ( "hidden", not model.showInfoText ) ]
|
|
]
|
|
[ text texts.infoText
|
|
]
|
|
, p
|
|
[ class S.warnMessage
|
|
, class "mt-2"
|
|
]
|
|
[ text texts.deleteWarn
|
|
]
|
|
, MB.view <|
|
|
{ start =
|
|
[ MB.PrimaryButton
|
|
{ tagger = SubmitMerge
|
|
, title = texts.submitMergeTitle
|
|
, icon = Just "fa fa-less-than"
|
|
, label = texts.submitMerge
|
|
}
|
|
, MB.SecondaryButton
|
|
{ tagger = CancelMerge
|
|
, title = texts.cancelMergeTitle
|
|
, icon = Just "fa fa-times"
|
|
, label = texts.cancelMerge
|
|
}
|
|
]
|
|
, end = []
|
|
, rootClasses = "my-4"
|
|
}
|
|
, renderFormState texts model
|
|
, div [ class "flex-col px-2" ]
|
|
(List.indexedMap (itemCard texts settings model) model.items)
|
|
]
|
|
|
|
|
|
itemCard : Texts -> UiSettings -> Model -> Int -> ItemLight -> Html Msg
|
|
itemCard texts settings model index item =
|
|
let
|
|
previewUrl =
|
|
Api.itemBasePreviewURL item.id
|
|
|
|
fieldHidden f =
|
|
Data.UiSettings.fieldHidden settings f
|
|
|
|
dirIcon =
|
|
i
|
|
[ class (Data.Direction.iconFromMaybe2 item.direction)
|
|
, class "mr-2 w-4 text-center"
|
|
, classList [ ( "hidden", fieldHidden Data.Fields.Direction ) ]
|
|
, IT.render IT.direction (templateCtx texts) item |> title
|
|
]
|
|
[]
|
|
|
|
titlePattern =
|
|
settings.cardTitleTemplate.template
|
|
|
|
subtitlePattern =
|
|
settings.cardSubtitleTemplate.template
|
|
|
|
dropActive =
|
|
let
|
|
currentDrop =
|
|
getDropId model
|
|
|
|
currentDrag =
|
|
getDragId model
|
|
in
|
|
currentDrop == Just index && currentDrag /= Just index && currentDrag /= Just (index - 1)
|
|
in
|
|
div
|
|
([ classList [ ( "pt-12 mx-2", dropActive ) ]
|
|
]
|
|
++ droppable DragDrop index
|
|
)
|
|
[ div
|
|
([ class "flex flex-col sm:flex-row rounded"
|
|
, class "cursor-pointer items-center"
|
|
, classList
|
|
[ ( "border-2 border-blue-500 dark:border-blue-500", index == 0 )
|
|
, ( "bg-blue-100 dark:bg-lightblue-900", index == 0 )
|
|
, ( "border border-gray-400 dark:border-bluegray-600 dark:hover:border-bluegray-500 bg-white dark:bg-bluegray-700 mt-2", index /= 0 )
|
|
, ( "bg-yellow-50 dark:bg-lime-900 mt-4", dropActive )
|
|
]
|
|
, id ("merge-" ++ item.id)
|
|
]
|
|
++ draggable DragDrop index
|
|
)
|
|
[ div
|
|
[ class "mr-2 sm:rounded-l w-16 bg-white"
|
|
, classList [ ( "hidden", fieldHidden Data.Fields.PreviewImage ) ]
|
|
]
|
|
[ img
|
|
[ class "preview-image mx-auto pt-1"
|
|
, classList
|
|
[ ( "sm:rounded-l", True )
|
|
]
|
|
, src previewUrl
|
|
]
|
|
[]
|
|
]
|
|
, div [ class "flex-grow flex flex-col py-1 px-2" ]
|
|
[ div [ class "flex flex-col sm:flex-row items-center" ]
|
|
[ div
|
|
[ class "font-bold text-lg"
|
|
, classList [ ( "hidden", IT.render titlePattern (templateCtx texts) item == "" ) ]
|
|
]
|
|
[ dirIcon
|
|
, IT.render titlePattern (templateCtx texts) item |> text
|
|
]
|
|
, div
|
|
[ classList
|
|
[ ( "opacity-75 sm:ml-2", True )
|
|
, ( "hidden", IT.render subtitlePattern (templateCtx texts) item == "" )
|
|
]
|
|
]
|
|
[ IT.render subtitlePattern (templateCtx texts) item |> text
|
|
]
|
|
]
|
|
, mainData texts settings item
|
|
, mainTagsAndFields2 settings item
|
|
]
|
|
]
|
|
]
|
|
|
|
|
|
mainData : Texts -> UiSettings -> ItemLight -> Html Msg
|
|
mainData texts settings item =
|
|
let
|
|
ctx =
|
|
templateCtx texts
|
|
|
|
corr =
|
|
IT.render (Util.Item.corrTemplate settings) ctx item
|
|
|
|
conc =
|
|
IT.render (Util.Item.concTemplate settings) ctx item
|
|
in
|
|
div [ class "flex flex-row space-x-2" ]
|
|
[ div
|
|
[ classList
|
|
[ ( "hidden", corr == "" )
|
|
]
|
|
]
|
|
[ Icons.correspondentIcon2 "mr-1"
|
|
, text corr
|
|
]
|
|
, div
|
|
[ classList
|
|
[ ( "hidden", conc == "" )
|
|
]
|
|
, class "ml-2"
|
|
]
|
|
[ Icons.concernedIcon2 "mr-1"
|
|
, text conc
|
|
]
|
|
]
|
|
|
|
|
|
mainTagsAndFields2 : UiSettings -> ItemLight -> Html Msg
|
|
mainTagsAndFields2 settings item =
|
|
let
|
|
fieldHidden f =
|
|
Data.UiSettings.fieldHidden settings f
|
|
|
|
hideTags =
|
|
item.tags == [] || fieldHidden Data.Fields.Tag
|
|
|
|
hideFields =
|
|
item.customfields == [] || fieldHidden Data.Fields.CustomFields
|
|
|
|
showTag tag =
|
|
div
|
|
[ class "label mt-1 font-semibold"
|
|
, class (Data.UiSettings.tagColorString2 tag settings)
|
|
]
|
|
[ i [ class "fa fa-tag mr-2" ] []
|
|
, span [] [ text tag.name ]
|
|
]
|
|
|
|
showField fv =
|
|
Util.CustomField.renderValue2
|
|
[ ( S.basicLabel, True )
|
|
, ( "mt-1 font-semibold", True )
|
|
]
|
|
Nothing
|
|
fv
|
|
|
|
renderFields =
|
|
if hideFields then
|
|
[]
|
|
|
|
else
|
|
List.sortBy Util.CustomField.nameOrLabel item.customfields
|
|
|> List.map showField
|
|
|
|
renderTags =
|
|
if hideTags then
|
|
[]
|
|
|
|
else
|
|
List.map showTag item.tags
|
|
in
|
|
div
|
|
[ classList
|
|
[ ( "flex flex-row items-center flex-wrap text-xs font-medium my-1 space-x-2", True )
|
|
, ( "hidden", hideTags && hideFields )
|
|
]
|
|
]
|
|
(renderFields ++ renderTags)
|
|
|
|
|
|
renderFormState : Texts -> Model -> Html Msg
|
|
renderFormState texts model =
|
|
case model.formState of
|
|
FormStateInitial ->
|
|
span [ class "hidden" ] []
|
|
|
|
FormStateError msg ->
|
|
div
|
|
[ class S.errorMessage
|
|
, class "my-2"
|
|
]
|
|
[ text msg
|
|
]
|
|
|
|
FormStateHttp err ->
|
|
div
|
|
[ class S.errorMessage
|
|
, class "my-2"
|
|
]
|
|
[ text (texts.httpError err)
|
|
]
|
|
|
|
FormStateMergeSuccessful ->
|
|
div
|
|
[ class S.successMessage
|
|
, class "my-2"
|
|
]
|
|
[ text texts.mergeSuccessful
|
|
]
|
|
|
|
FormStateMergeInProcess ->
|
|
Comp.Basic.loadingDimmer
|
|
{ active = True
|
|
, label = texts.mergeInProcess
|
|
}
|
|
|
|
|
|
templateCtx : Texts -> IT.TemplateContext
|
|
templateCtx texts =
|
|
{ dateFormatLong = texts.formatDateLong
|
|
, dateFormatShort = texts.formatDateShort
|
|
, directionLabel = \_ -> ""
|
|
}
|
|
|
|
|
|
droppable : (DDMsg -> msg) -> Int -> List (Attribute msg)
|
|
droppable tagger dropId =
|
|
DD.droppable tagger dropId
|
|
|
|
|
|
draggable : (DDMsg -> msg) -> Int -> List (Attribute msg)
|
|
draggable tagger itemId =
|
|
DD.draggable tagger itemId
|
|
|
|
|
|
getDropId : Model -> Maybe Int
|
|
getDropId model =
|
|
DD.getDropId model.dragDrop
|
|
|
|
|
|
getDragId : Model -> Maybe Int
|
|
getDragId model =
|
|
DD.getDragId model.dragDrop
|