{- Copyright 2020 Docspell Contributors SPDX-License-Identifier: GPL-3.0-or-later -} module Comp.ItemDetail.MultiEditMenu exposing ( Model , Msg , SaveNameState(..) , defaultViewConfig , init , loadModel , update , view2 ) import Api import Api.Model.EquipmentList exposing (EquipmentList) import Api.Model.FolderItem exposing (FolderItem) import Api.Model.FolderList exposing (FolderList) import Api.Model.IdName exposing (IdName) import Api.Model.PersonList exposing (PersonList) import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.Tag exposing (Tag) import Api.Model.TagList exposing (TagList) import Comp.CustomFieldMultiInput import Comp.DatePicker import Comp.DetailEdit import Comp.Dropdown exposing (isDropdownChangeMsg) import Comp.ItemDetail.FieldTabState as FTabState exposing (EditTab(..), tabName) import Comp.ItemDetail.FormChange exposing (FormChange(..)) import Comp.Tabs as TB import Data.CustomFieldChange exposing (CustomFieldChange(..)) import Data.Direction exposing (Direction) import Data.DropdownStyle import Data.Fields import Data.Flags exposing (Flags) import Data.Icons as Icons import Data.PersonUse import Data.UiSettings exposing (UiSettings) import DatePicker exposing (DatePicker) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) import Http import Markdown import Messages.Comp.ItemDetail.MultiEditMenu exposing (Texts) import Page exposing (Page(..)) import Set exposing (Set) import Styles as S import Task import Throttle exposing (Throttle) import Time import Util.Folder import Util.List import Util.Maybe import Util.Tag --- Model type SaveNameState = Saving | SaveSuccess | SaveFailed type TagEditMode = AddTags | RemoveTags | ReplaceTags type alias Model = { tagModel : Comp.Dropdown.Model Tag , nameModel : String , nameSaveThrottle : Throttle Msg , folderModel : Comp.Dropdown.Model IdName , allFolders : List FolderItem , directionModel : Comp.Dropdown.Model Direction , itemDatePicker : DatePicker , itemDate : Maybe Int , dueDate : Maybe Int , dueDatePicker : DatePicker , corrOrgModel : Comp.Dropdown.Model IdName , corrPersonModel : Comp.Dropdown.Model IdName , concPersonModel : Comp.Dropdown.Model IdName , concEquipModel : Comp.Dropdown.Model IdName , modalEdit : Maybe Comp.DetailEdit.Model , tagEditMode : TagEditMode , customFieldModel : Comp.CustomFieldMultiInput.Model , openTabs : Set String } type Msg = ItemDatePickerMsg Comp.DatePicker.Msg | DueDatePickerMsg Comp.DatePicker.Msg | SetName String | SaveName | UpdateThrottle | RemoveDueDate | RemoveDate | ConfirmMsg Bool | ToggleTagEditMode | FolderDropdownMsg (Comp.Dropdown.Msg IdName) | TagDropdownMsg (Comp.Dropdown.Msg Tag) | DirDropdownMsg (Comp.Dropdown.Msg Direction) | OrgDropdownMsg (Comp.Dropdown.Msg IdName) | CorrPersonMsg (Comp.Dropdown.Msg IdName) | ConcPersonMsg (Comp.Dropdown.Msg IdName) | ConcEquipMsg (Comp.Dropdown.Msg IdName) | GetTagsResp (Result Http.Error TagList) | GetOrgResp (Result Http.Error ReferenceList) | GetPersonResp (Result Http.Error PersonList) | GetEquipResp (Result Http.Error EquipmentList) | GetFolderResp (Result Http.Error FolderList) | CustomFieldMsg Comp.CustomFieldMultiInput.Msg | ToggleAkkordionTab String init : Model init = { tagModel = Util.Tag.makeDropdownModel , directionModel = Comp.Dropdown.makeSingleList { options = Data.Direction.all , selected = Nothing } , corrOrgModel = Comp.Dropdown.makeSingle , corrPersonModel = Comp.Dropdown.makeSingle , concPersonModel = Comp.Dropdown.makeSingle , concEquipModel = Comp.Dropdown.makeSingle , folderModel = Comp.Dropdown.makeSingle , allFolders = [] , nameModel = "" , nameSaveThrottle = Throttle.create 1 , itemDatePicker = Comp.DatePicker.emptyModel , itemDate = Nothing , dueDate = Nothing , dueDatePicker = Comp.DatePicker.emptyModel , modalEdit = Nothing , tagEditMode = AddTags , customFieldModel = Comp.CustomFieldMultiInput.initWith [] , openTabs = Set.empty } loadModel : Flags -> Cmd Msg loadModel flags = let ( _, dpc ) = Comp.DatePicker.init in Cmd.batch [ Api.getTags flags "" GetTagsResp , Api.getOrgLight flags GetOrgResp , Api.getPersons flags "" GetPersonResp , Api.getEquipments flags "" GetEquipResp , Api.getFolders flags "" False GetFolderResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) , Cmd.map ItemDatePickerMsg dpc , Cmd.map DueDatePickerMsg dpc ] isFolderMember : Model -> Bool isFolderMember model = let selected = Comp.Dropdown.getSelected model.folderModel |> List.head |> Maybe.map .id in Util.Folder.isFolderMember model.allFolders selected --- Update type alias UpdateResult = { model : Model , cmd : Cmd Msg , sub : Sub Msg , change : FormChange } resultNoCmd : FormChange -> Model -> UpdateResult resultNoCmd change model = UpdateResult model Cmd.none Sub.none change resultNone : Model -> UpdateResult resultNone model = resultNoCmd NoFormChange model update : Flags -> Msg -> Model -> UpdateResult update flags msg model = case msg of ConfirmMsg flag -> resultNoCmd (ConfirmChange flag) model TagDropdownMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.tagModel newModel = { model | tagModel = m2 } mkChange list = case model.tagEditMode of AddTags -> AddTagChange list RemoveTags -> RemoveTagChange list ReplaceTags -> ReplaceTagChange list change = if isDropdownChangeMsg m then Comp.Dropdown.getSelected newModel.tagModel |> Util.List.distinct |> List.map (\t -> IdName t.id t.name) |> ReferenceList |> mkChange else NoFormChange in resultNoCmd change newModel ToggleTagEditMode -> let ( m2, _ ) = Comp.Dropdown.update (Comp.Dropdown.SetSelection []) model.tagModel newModel = { model | tagModel = m2 } in case model.tagEditMode of AddTags -> resultNone { newModel | tagEditMode = RemoveTags } RemoveTags -> resultNone { newModel | tagEditMode = ReplaceTags } ReplaceTags -> resultNone { newModel | tagEditMode = AddTags } GetTagsResp (Ok tags) -> let tagList = Comp.Dropdown.SetOptions tags.items in update flags (TagDropdownMsg tagList) model GetTagsResp (Err _) -> resultNone model FolderDropdownMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.folderModel newModel = { model | folderModel = m2 } idref = Comp.Dropdown.getSelected m2 |> List.head change = if isDropdownChangeMsg m then FolderChange idref else NoFormChange in resultNoCmd change newModel GetFolderResp (Ok fs) -> let model_ = { model | allFolders = fs.items } mkIdName fitem = IdName fitem.id fitem.name opts = fs.items |> List.map mkIdName |> Comp.Dropdown.SetOptions in update flags (FolderDropdownMsg opts) model_ GetFolderResp (Err _) -> resultNone model DirDropdownMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.directionModel newModel = { model | directionModel = m2 } change = if isDropdownChangeMsg m then let dir = Comp.Dropdown.getSelected m2 |> List.head in case dir of Just d -> DirectionChange d Nothing -> NoFormChange else NoFormChange in resultNoCmd change newModel OrgDropdownMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.corrOrgModel newModel = { model | corrOrgModel = m2 } idref = Comp.Dropdown.getSelected m2 |> List.head change = if isDropdownChangeMsg m then OrgChange idref else NoFormChange in resultNoCmd change newModel GetOrgResp (Ok orgs) -> let opts = Comp.Dropdown.SetOptions orgs.items in update flags (OrgDropdownMsg opts) model GetOrgResp (Err _) -> resultNone model CorrPersonMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.corrPersonModel newModel = { model | corrPersonModel = m2 } idref = Comp.Dropdown.getSelected m2 |> List.head change = if isDropdownChangeMsg m then CorrPersonChange idref else NoFormChange in resultNoCmd change newModel ConcPersonMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.concPersonModel newModel = { model | concPersonModel = m2 } idref = Comp.Dropdown.getSelected m2 |> List.head change = if isDropdownChangeMsg m then ConcPersonChange idref else NoFormChange in resultNoCmd change newModel GetPersonResp (Ok ps) -> let { concerning, correspondent } = Data.PersonUse.spanPersonList ps.items concRefs = List.map (\e -> IdName e.id e.name) concerning corrRefs = List.map (\e -> IdName e.id e.name) correspondent res1 = update flags (CorrPersonMsg (Comp.Dropdown.SetOptions corrRefs)) model res2 = update flags (ConcPersonMsg (Comp.Dropdown.SetOptions concRefs)) res1.model in res2 GetPersonResp (Err _) -> resultNone model ConcEquipMsg m -> let ( m2, _ ) = Comp.Dropdown.update m model.concEquipModel newModel = { model | concEquipModel = m2 } idref = Comp.Dropdown.getSelected m2 |> List.head change = if isDropdownChangeMsg m then EquipChange idref else NoFormChange in resultNoCmd change newModel GetEquipResp (Ok equips) -> let opts = Comp.Dropdown.SetOptions (List.map (\e -> IdName e.id e.name) equips.items ) in update flags (ConcEquipMsg opts) model GetEquipResp (Err _) -> resultNone model ItemDatePickerMsg m -> let ( dp, event ) = Comp.DatePicker.updateDefault m model.itemDatePicker in case event of DatePicker.Picked date -> let newModel = { model | itemDatePicker = dp, itemDate = Just (Comp.DatePicker.midOfDay date) } in resultNoCmd (ItemDateChange newModel.itemDate) newModel _ -> resultNone { model | itemDatePicker = dp } RemoveDate -> resultNoCmd (ItemDateChange Nothing) { model | itemDate = Nothing } DueDatePickerMsg m -> let ( dp, event ) = Comp.DatePicker.updateDefault m model.dueDatePicker in case event of DatePicker.Picked date -> let newModel = { model | dueDatePicker = dp, dueDate = Just (Comp.DatePicker.midOfDay date) } in resultNoCmd (DueDateChange newModel.dueDate) newModel _ -> resultNone { model | dueDatePicker = dp } RemoveDueDate -> resultNoCmd (DueDateChange Nothing) { model | dueDate = Nothing } SetName str -> case Util.Maybe.fromString str of Just newName -> let cmd_ = Task.succeed () |> Task.perform (\_ -> SaveName) ( newThrottle, cmd ) = Throttle.try cmd_ model.nameSaveThrottle newModel = { model | nameSaveThrottle = newThrottle , nameModel = newName } sub = nameThrottleSub newModel in UpdateResult newModel cmd sub NoFormChange Nothing -> resultNone { model | nameModel = str } SaveName -> case Util.Maybe.fromString model.nameModel of Just n -> resultNoCmd (NameChange n) model Nothing -> resultNone model UpdateThrottle -> let ( newThrottle, cmd ) = Throttle.update model.nameSaveThrottle newModel = { model | nameSaveThrottle = newThrottle } sub = nameThrottleSub newModel in UpdateResult newModel cmd sub NoFormChange CustomFieldMsg lm -> let res = Comp.CustomFieldMultiInput.update flags lm model.customFieldModel model_ = { model | customFieldModel = res.model } cmd_ = Cmd.map CustomFieldMsg res.cmd change = case res.result of NoFieldChange -> NoFormChange FieldValueRemove cf -> RemoveCustomValue cf FieldValueChange cf value -> CustomValueChange cf value FieldCreateNew -> NoFormChange in UpdateResult model_ cmd_ Sub.none change ToggleAkkordionTab name -> let tabs = if Set.member name model.openTabs then Set.remove name model.openTabs else Set.insert name model.openTabs in UpdateResult { model | openTabs = tabs } Cmd.none Sub.none NoFormChange nameThrottleSub : Model -> Sub Msg nameThrottleSub model = Throttle.ifNeeded (Time.every 400 (\_ -> UpdateThrottle)) model.nameSaveThrottle --- View type alias ViewConfig = { menuClass : String , nameState : SaveNameState , customFieldState : String -> SaveNameState } defaultViewConfig : ViewConfig defaultViewConfig = { menuClass = "" , nameState = SaveSuccess , customFieldState = \_ -> SaveSuccess } --- View2 view2 : Texts -> Flags -> ViewConfig -> UiSettings -> Model -> Html Msg view2 = renderEditForm2 renderEditForm2 : Texts -> Flags -> ViewConfig -> UiSettings -> Model -> Html Msg renderEditForm2 texts flags cfg settings model = let fieldVisible field = Data.UiSettings.fieldVisible settings field optional fields html = if List.map fieldVisible fields |> List.foldl (||) False then html else span [ class "hidden" ] [] tagModeIcon = case model.tagEditMode of AddTags -> i [ class "fa fa-plus" ] [] RemoveTags -> i [ class "fa fa-eraser" ] [] ReplaceTags -> i [ class "fa fa-redo-alt" ] [] tagModeMsg = case model.tagEditMode of AddTags -> texts.tagModeAddInfo RemoveTags -> texts.tagModeRemoveInfo ReplaceTags -> texts.tagModeReplaceInfo customFieldIcon field = case cfg.customFieldState field.id of SaveSuccess -> Nothing SaveFailed -> Just "text-red-500 fa fa-exclamation-triangle" Saving -> Just "fa fa-sync-alt animate-spin" customFieldSettings = { showAddButton = False , classes = "mb-4" , fieldIcon = customFieldIcon , style = dds , createCustomFieldTitle = "" , selectPlaceholder = texts.basics.selectPlaceholder } dds = Data.DropdownStyle.sidebarStyle tabStyle = TB.searchMenuStyle folderCfg = { makeOption = Util.Folder.mkFolderOption flags model.allFolders , placeholder = texts.basics.selectPlaceholder , labelColor = \_ -> \_ -> "" , style = dds } idNameCfg = { makeOption = \e -> { text = e.name, additional = "" } , labelColor = \_ -> \_ -> "" , placeholder = texts.basics.selectPlaceholder , style = dds } directionCfg = { makeOption = \entry -> { text = texts.direction entry , additional = "" } , placeholder = texts.chooseDirection , labelColor = \_ -> \_ -> "" , style = dds } in div [ class cfg.menuClass, class "mt-2" ] [ TB.akkordion tabStyle (tabState settings model) [ { name = tabName TabConfirmUnconfirm , title = texts.confirmUnconfirm , titleRight = [] , info = Nothing , body = [ div [ class "flex flex-row space-x-4" ] [ button [ class S.primaryButton , class "flex-grow" , onClick (ConfirmMsg True) ] [ text texts.confirm ] , button [ class S.secondaryButton , class "flex-grow" , onClick (ConfirmMsg False) ] [ text texts.unconfirm ] ] ] } , { name = tabName TabTags , title = texts.basics.tags , titleRight = [] , info = Nothing , body = [ div [ class "field" ] [ label [ class S.inputLabel ] [ Icons.tagsIcon2 "" , text texts.basics.tags , a [ class "float-right" , class S.link , href "#" , title texts.changeTagMode , onClick ToggleTagEditMode ] [ tagModeIcon ] ] , Html.map TagDropdownMsg (Comp.Dropdown.view2 (Util.Tag.tagSettings texts.basics.chooseTag dds) settings model.tagModel ) , Markdown.toHtml [ class "opacity-50 text-sm" ] tagModeMsg ] ] } , { name = tabName TabFolder , title = texts.basics.folder , titleRight = [] , info = Nothing , body = [ Html.map FolderDropdownMsg (Comp.Dropdown.view2 folderCfg settings model.folderModel) , div [ classList [ ( S.message, True ) , ( "hidden", isFolderMember model ) ] ] [ Markdown.toHtml [] texts.basics.folderNotOwnerWarning ] ] } , { name = tabName TabCustomFields , title = texts.basics.customFields , titleRight = [] , info = Nothing , body = [ Html.map CustomFieldMsg (Comp.CustomFieldMultiInput.view2 texts.customFieldMultiInput customFieldSettings model.customFieldModel ) ] } , { name = tabName TabDate , title = texts.basics.date , titleRight = [] , info = Nothing , body = [ div [ class "relative" ] [ Html.map ItemDatePickerMsg (Comp.DatePicker.viewTime model.itemDate actionInputDatePicker2 model.itemDatePicker ) , a [ class S.inputLeftIconLinkSidebar , href "#" , onClick RemoveDate ] [ i [ class "fa fa-trash-alt font-thin" ] [] ] , Icons.dateIcon2 S.dateInputIcon ] ] } , { name = tabName TabDueDate , title = texts.dueDateTab , titleRight = [] , info = Nothing , body = [ div [ class "relative" ] [ Html.map DueDatePickerMsg (Comp.DatePicker.viewTime model.dueDate actionInputDatePicker2 model.dueDatePicker ) , a [ class S.inputLeftIconLinkSidebar , href "#" , onClick RemoveDueDate ] [ i [ class "fa fa-trash-alt font-thin" ] [] ] , Icons.dueDateIcon2 S.dateInputIcon ] ] } , { name = tabName TabCorrespondent , title = texts.basics.correspondent , titleRight = [] , info = Nothing , body = [ optional [ Data.Fields.CorrOrg ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] [ Icons.organizationIcon2 "" , span [ class "ml-2" ] [ text texts.basics.organization ] ] , Html.map OrgDropdownMsg (Comp.Dropdown.view2 idNameCfg settings model.corrOrgModel ) ] , optional [ Data.Fields.CorrPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] [ Icons.personIcon2 "" , span [ class "ml-2" ] [ text texts.basics.person ] ] , Html.map CorrPersonMsg (Comp.Dropdown.view2 idNameCfg settings model.corrPersonModel ) ] ] } , { name = tabName TabConcerning , title = texts.basics.concerning , titleRight = [] , info = Nothing , body = [ optional [ Data.Fields.ConcPerson ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] [ Icons.personIcon2 "" , span [ class "ml-2" ] [ text texts.basics.person ] ] , Html.map ConcPersonMsg (Comp.Dropdown.view2 idNameCfg settings model.concPersonModel) ] , optional [ Data.Fields.ConcEquip ] <| div [ class "mb-4" ] [ label [ class S.inputLabel ] [ Icons.equipmentIcon2 "" , span [ class "ml-2" ] [ text texts.basics.equipment ] ] , Html.map ConcEquipMsg (Comp.Dropdown.view2 idNameCfg settings model.concEquipModel ) ] ] } , { name = tabName TabDirection , title = texts.basics.direction , titleRight = [] , info = Nothing , body = [ Html.map DirDropdownMsg (Comp.Dropdown.view2 directionCfg settings model.directionModel) ] } , { name = tabName TabName , title = texts.basics.name , titleRight = [] , info = Nothing , body = [ div [ class "relative" ] [ input [ type_ "text" , value model.nameModel , onInput SetName , class S.textInputSidebar ] [] , span [ class S.inputLeftIconOnly ] [ i [ classList [ ( "text-green-500 fa fa-check" , cfg.nameState == SaveSuccess ) , ( "text-red-500 fa fa-exclamation-triangle" , cfg.nameState == SaveFailed ) , ( "sync fa fa-circle-notch animate-spin" , cfg.nameState == Saving ) ] ] [] ] ] ] } ] ] tabState : UiSettings -> Model -> TB.Tab Msg -> ( TB.State, Msg ) tabState settings model tab = FTabState.tabState settings model.openTabs (Just model.customFieldModel) (.name >> ToggleAkkordionTab) tab actionInputDatePicker2 : DatePicker.Settings actionInputDatePicker2 = Comp.DatePicker.defaultSettings