diff --git a/modules/webapp/src/main/elm/Comp/ItemCardList.elm b/modules/webapp/src/main/elm/Comp/ItemCardList.elm index 945febf5..7b0614c4 100644 --- a/modules/webapp/src/main/elm/Comp/ItemCardList.elm +++ b/modules/webapp/src/main/elm/Comp/ItemCardList.elm @@ -1,6 +1,7 @@ module Comp.ItemCardList exposing ( Model , Msg(..) + , ViewConfig , init , nextItem , prevItem @@ -17,12 +18,16 @@ import Data.Direction import Data.Fields import Data.Flags exposing (Flags) import Data.Icons as Icons +import Data.ItemSelection exposing (ItemSelection) import Data.Items 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.String @@ -38,6 +43,7 @@ type Msg = SetResults ItemLightList | AddResults ItemLightList | ItemDDMsg DD.Msg + | ToggleSelectItem (Set String) String init : Model @@ -75,6 +81,7 @@ type alias UpdateResult = { model : Model , cmd : Cmd Msg , dragModel : DD.Model + , selection : ItemSelection } @@ -91,51 +98,78 @@ updateDrag dm _ msg model = newModel = { model | results = list } in - UpdateResult newModel Cmd.none dm + UpdateResult newModel Cmd.none dm Data.ItemSelection.Inactive AddResults list -> if list.groups == [] then - UpdateResult model Cmd.none dm + UpdateResult model Cmd.none dm Data.ItemSelection.Inactive else let newModel = { model | results = Data.Items.concat model.results list } in - UpdateResult newModel Cmd.none dm + UpdateResult newModel Cmd.none dm Data.ItemSelection.Inactive ItemDDMsg lm -> let ddd = DD.update lm dm in - UpdateResult model Cmd.none ddd.model + UpdateResult model Cmd.none 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 Cmd.none dm (Data.ItemSelection.Active newSet) --- View -view : Maybe String -> UiSettings -> Model -> Html Msg -view current settings model = +type alias ViewConfig = + { current : Maybe String + , selection : ItemSelection + } + + +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 current settings) model.results.groups) + (List.map (viewGroup cfg settings) model.results.groups) -viewGroup : Maybe String -> UiSettings -> ItemLightGroup -> Html Msg -viewGroup current settings group = +viewGroup : ViewConfig -> UiSettings -> ItemLightGroup -> Html Msg +viewGroup 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 current settings) group.items) + (List.map (viewItem cfg settings) group.items) ] -viewItem : Maybe String -> UiSettings -> ItemLight -> Html Msg -viewItem current settings item = +viewItem : ViewConfig -> UiSettings -> ItemLight -> Html Msg +viewItem cfg settings item = let dirIcon = i [ class (Data.Direction.iconFromMaybe item.direction) ] [] @@ -163,43 +197,68 @@ viewItem current settings item = isConfirmed = item.state /= "created" - newColor = - "blue" + 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) in a ([ classList [ ( "ui fluid card", True ) - , ( newColor, not isConfirmed ) - , ( "current", current == Just item.id ) + , ( cardColor, True ) + , ( "current", cfg.current == Just item.id ) ] , id item.id - , Page.href (ItemDetailPage item.id) + , href "#" + , cardAction ] ++ DD.draggable ItemDDMsg item.id ) [ div [ class "content" ] - [ if fieldHidden Data.Fields.Direction then - div [ class "header" ] - [ Util.String.underscoreToSpace item.name |> text - ] + [ case cfg.selection of + Data.ItemSelection.Active ids -> + div [ class "header" ] + [ Util.Html.checkbox (Set.member item.id ids) + , Util.String.underscoreToSpace item.name + |> text + ] - else - div - [ class "header" - , Data.Direction.labelFromMaybe item.direction - |> title - ] - [ 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 ) - , ( newColor, True ) + , ( cardColor, True ) , ( "invisible", isConfirmed ) ] , title "New" diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm new file mode 100644 index 00000000..561f1ef8 --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/EditMenu.elm @@ -0,0 +1,693 @@ +module Comp.ItemDetail.EditMenu exposing + ( Model + , Msg + , SaveNameState(..) + , defaultViewConfig + , init + , loadModel + , update + , view + ) + +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.ItemProposals exposing (ItemProposals) +import Api.Model.ReferenceList exposing (ReferenceList) +import Api.Model.Tag exposing (Tag) +import Api.Model.TagList exposing (TagList) +import Comp.DatePicker +import Comp.DetailEdit +import Comp.Dropdown exposing (isDropdownChangeMsg) +import Comp.ItemDetail.FormChange exposing (FormChange(..)) +import Data.Direction exposing (Direction) +import Data.Fields +import Data.Flags exposing (Flags) +import Data.Icons as Icons +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 Page exposing (Page(..)) +import Task +import Throttle exposing (Throttle) +import Time +import Util.Folder exposing (mkFolderOption) +import Util.List +import Util.Maybe +import Util.Tag + + + +--- Model + + +type SaveNameState + = Saving + | SaveSuccess + | SaveFailed + + +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 + , itemProposals : ItemProposals + , 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 + } + + +type Msg + = ItemDatePickerMsg Comp.DatePicker.Msg + | DueDatePickerMsg Comp.DatePicker.Msg + | SetName String + | SaveName + | UpdateThrottle + | RemoveDueDate + | RemoveDate + | 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 ReferenceList) + | GetEquipResp (Result Http.Error EquipmentList) + | GetFolderResp (Result Http.Error FolderList) + + +init : Model +init = + { tagModel = + Util.Tag.makeDropdownModel + , directionModel = + Comp.Dropdown.makeSingleList + { makeOption = + \entry -> + { value = Data.Direction.toString entry + , text = Data.Direction.toString entry + , additional = "" + } + , options = Data.Direction.all + , placeholder = "Choose a direction…" + , selected = Nothing + } + , corrOrgModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , placeholder = "" + } + , corrPersonModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , placeholder = "" + } + , concPersonModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , placeholder = "" + } + , concEquipModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , placeholder = "" + } + , folderModel = + Comp.Dropdown.makeSingle + { makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , placeholder = "" + } + , allFolders = [] + , nameModel = "" + , nameSaveThrottle = Throttle.create 1 + , itemDatePicker = Comp.DatePicker.emptyModel + , itemDate = Nothing + , itemProposals = Api.Model.ItemProposals.empty + , dueDate = Nothing + , dueDatePicker = Comp.DatePicker.emptyModel + , modalEdit = Nothing + } + + +loadModel : Flags -> Cmd Msg +loadModel flags = + let + ( _, dpc ) = + Comp.DatePicker.init + in + Cmd.batch + [ Api.getTags flags "" GetTagsResp + , Api.getOrgLight flags GetOrgResp + , Api.getPersonsLight flags GetPersonResp + , Api.getEquipments flags "" GetEquipResp + , Api.getFolders flags "" False GetFolderResp + , 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 + TagDropdownMsg m -> + let + ( m2, _ ) = + Comp.Dropdown.update m model.tagModel + + newModel = + { model | tagModel = m2 } + + change = + if isDropdownChangeMsg m then + Comp.Dropdown.getSelected newModel.tagModel + |> Util.List.distinct + |> List.map (\t -> IdName t.id t.name) + |> ReferenceList + |> TagChange + + else + NoFormChange + in + resultNoCmd change newModel + + 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 + , folderModel = + Comp.Dropdown.setMkOption + (mkFolderOption flags fs.items) + model.folderModel + } + + 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 + opts = + Comp.Dropdown.SetOptions ps.items + + res1 = + update flags (CorrPersonMsg opts) model + + res2 = + update flags (ConcPersonMsg opts) 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 + + +nameThrottleSub : Model -> Sub Msg +nameThrottleSub model = + Throttle.ifNeeded + (Time.every 400 (\_ -> UpdateThrottle)) + model.nameSaveThrottle + + + +--- View + + +type alias ViewConfig = + { menuClass : String + , nameState : SaveNameState + } + + +defaultViewConfig : ViewConfig +defaultViewConfig = + { menuClass = "ui vertical segment" + , nameState = SaveSuccess + } + + +view : ViewConfig -> UiSettings -> Model -> Html Msg +view = + renderEditForm + + +renderEditForm : ViewConfig -> UiSettings -> Model -> Html Msg +renderEditForm 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 "invisible hidden" ] [] + in + div [ class cfg.menuClass ] + [ div [ class "ui form warning" ] + [ optional [ Data.Fields.Tag ] <| + div [ class "field" ] + [ label [] + [ Icons.tagsIcon "grey" + , text "Tags" + ] + , Html.map TagDropdownMsg (Comp.Dropdown.view settings model.tagModel) + ] + , div [ class " field" ] + [ label [] [ text "Name" ] + , div [ class "ui icon input" ] + [ input [ type_ "text", value model.nameModel, onInput SetName ] [] + , i + [ classList + [ ( "green check icon", cfg.nameState == SaveSuccess ) + , ( "red exclamation triangle icon", cfg.nameState == SaveFailed ) + , ( "sync loading icon", cfg.nameState == Saving ) + ] + ] + [] + ] + ] + , optional [ Data.Fields.Folder ] <| + div [ class "field" ] + [ label [] + [ Icons.folderIcon "grey" + , text "Folder" + ] + , Html.map FolderDropdownMsg (Comp.Dropdown.view settings model.folderModel) + , div + [ classList + [ ( "ui warning message", True ) + , ( "hidden", isFolderMember model ) + ] + ] + [ Markdown.toHtml [] """ +You are **not a member** of this folder. This item will be **hidden** +from any search now. Use a folder where you are a member of to make this +item visible. This message will disappear then. + """ + ] + ] + , optional [ Data.Fields.Direction ] <| + div [ class "field" ] + [ label [] + [ Icons.directionIcon "grey" + , text "Direction" + ] + , Html.map DirDropdownMsg (Comp.Dropdown.view settings model.directionModel) + ] + , optional [ Data.Fields.Date ] <| + div [ class "field" ] + [ label [] + [ Icons.dateIcon "grey" + , text "Date" + ] + , div [ class "ui action input" ] + [ Html.map ItemDatePickerMsg + (Comp.DatePicker.viewTime + model.itemDate + actionInputDatePicker + model.itemDatePicker + ) + , a [ class "ui icon button", href "", onClick RemoveDate ] + [ i [ class "trash alternate outline icon" ] [] + ] + ] + ] + , optional [ Data.Fields.DueDate ] <| + div [ class " field" ] + [ label [] + [ Icons.dueDateIcon "grey" + , text "Due Date" + ] + , div [ class "ui action input" ] + [ Html.map DueDatePickerMsg + (Comp.DatePicker.viewTime + model.dueDate + actionInputDatePicker + model.dueDatePicker + ) + , a [ class "ui icon button", href "", onClick RemoveDueDate ] + [ i [ class "trash alternate outline icon" ] [] ] + ] + ] + , optional [ Data.Fields.CorrOrg, Data.Fields.CorrPerson ] <| + h4 [ class "ui dividing header" ] + [ Icons.correspondentIcon "" + , text "Correspondent" + ] + , optional [ Data.Fields.CorrOrg ] <| + div [ class "field" ] + [ label [] + [ Icons.organizationIcon "grey" + , text "Organization" + ] + , Html.map OrgDropdownMsg (Comp.Dropdown.view settings model.corrOrgModel) + ] + , optional [ Data.Fields.CorrPerson ] <| + div [ class "field" ] + [ label [] + [ Icons.personIcon "grey" + , text "Person" + ] + , Html.map CorrPersonMsg (Comp.Dropdown.view settings model.corrPersonModel) + ] + , optional [ Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| + h4 [ class "ui dividing header" ] + [ Icons.concernedIcon + , text "Concerning" + ] + , optional [ Data.Fields.ConcPerson ] <| + div [ class "field" ] + [ label [] + [ Icons.personIcon "grey" + , text "Person" + ] + , Html.map ConcPersonMsg (Comp.Dropdown.view settings model.concPersonModel) + ] + , optional [ Data.Fields.ConcEquip ] <| + div [ class "field" ] + [ label [] + [ Icons.equipmentIcon "grey" + , text "Equipment" + ] + , Html.map ConcEquipMsg (Comp.Dropdown.view settings model.concEquipModel) + ] + ] + ] + + +actionInputDatePicker : DatePicker.Settings +actionInputDatePicker = + let + ds = + Comp.DatePicker.defaultSettings + in + { ds | containerClassList = [ ( "ui action input", True ) ] } diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm new file mode 100644 index 00000000..4cb0c91a --- /dev/null +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/FormChange.elm @@ -0,0 +1,44 @@ +module Comp.ItemDetail.FormChange exposing + ( FormChange(..) + , multiUpdate + ) + +import Api +import Api.Model.BasicResult exposing (BasicResult) +import Api.Model.IdName exposing (IdName) +import Api.Model.ItemsAndRefs exposing (ItemsAndRefs) +import Api.Model.ReferenceList exposing (ReferenceList) +import Data.Direction exposing (Direction) +import Data.Flags exposing (Flags) +import Http +import Set exposing (Set) + + +type FormChange + = NoFormChange + | TagChange ReferenceList + | FolderChange (Maybe IdName) + | DirectionChange Direction + | OrgChange (Maybe IdName) + | CorrPersonChange (Maybe IdName) + | ConcPersonChange (Maybe IdName) + | EquipChange (Maybe IdName) + | ItemDateChange (Maybe Int) + | DueDateChange (Maybe Int) + | NameChange String + + +multiUpdate : + Flags + -> Set String + -> FormChange + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +multiUpdate flags ids change receive = + let + items = + Set.toList ids + in + case change of + _ -> + Cmd.none diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm index ba2b76b9..40a5aeaa 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm @@ -24,7 +24,6 @@ import File exposing (File) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onCheck, onClick, onInput) -import Html5.DragDrop as DD import Markdown import Page exposing (Page(..)) import Set diff --git a/modules/webapp/src/main/elm/Comp/YesNoDimmer.elm b/modules/webapp/src/main/elm/Comp/YesNoDimmer.elm index 927d6463..8421bdb5 100644 --- a/modules/webapp/src/main/elm/Comp/YesNoDimmer.elm +++ b/modules/webapp/src/main/elm/Comp/YesNoDimmer.elm @@ -6,6 +6,8 @@ module Comp.YesNoDimmer exposing , defaultSettings , disable , emptyModel + , initActive + , initInactive , update , view , view2 @@ -27,6 +29,18 @@ emptyModel = } +initInactive : Model +initInactive = + { active = False + } + + +initActive : Model +initActive = + { active = True + } + + type Msg = Activate | Disable @@ -40,6 +54,7 @@ type alias Settings = , confirmButton : String , cancelButton : String , invertedDimmer : Bool + , extraClass : String } @@ -51,6 +66,7 @@ defaultSettings = , confirmButton = "Yes, do it!" , cancelButton = "No" , invertedDimmer = False + , extraClass = "" } @@ -87,6 +103,7 @@ view2 active settings model = div [ classList [ ( "ui dimmer", True ) + , ( settings.extraClass, True ) , ( "inverted", settings.invertedDimmer ) , ( "active", active && model.active ) ] diff --git a/modules/webapp/src/main/elm/Data/ItemSelection.elm b/modules/webapp/src/main/elm/Data/ItemSelection.elm new file mode 100644 index 00000000..96ea8e5e --- /dev/null +++ b/modules/webapp/src/main/elm/Data/ItemSelection.elm @@ -0,0 +1,32 @@ +module Data.ItemSelection exposing + ( ItemSelection(..) + , isActive + , isSelected + ) + +import Set exposing (Set) + + +type ItemSelection + = Inactive + | Active (Set String) + + +isSelected : String -> ItemSelection -> Bool +isSelected id set = + case set of + Inactive -> + False + + Active ids -> + Set.member id ids + + +isActive : ItemSelection -> Bool +isActive sel = + case sel of + Active _ -> + True + + Inactive -> + False diff --git a/modules/webapp/src/main/elm/Data/Items.elm b/modules/webapp/src/main/elm/Data/Items.elm index 04b9aaad..9c393b3e 100644 --- a/modules/webapp/src/main/elm/Data/Items.elm +++ b/modules/webapp/src/main/elm/Data/Items.elm @@ -1,12 +1,14 @@ module Data.Items exposing ( concat , first + , idSet , length ) import Api.Model.ItemLight exposing (ItemLight) import Api.Model.ItemLightGroup exposing (ItemLightGroup) import Api.Model.ItemLightList exposing (ItemLightList) +import Set exposing (Set) import Util.List @@ -65,3 +67,15 @@ lastGroup : ItemLightList -> Maybe ItemLightGroup lastGroup list = List.reverse list.groups |> List.head + + +idSet : ItemLightList -> Set String +idSet items = + List.map idSetGroup items.groups + |> List.foldl Set.union Set.empty + + +idSetGroup : ItemLightGroup -> Set String +idSetGroup group = + List.map .id group.items + |> Set.fromList diff --git a/modules/webapp/src/main/elm/Page/Home/Data.elm b/modules/webapp/src/main/elm/Page/Home/Data.elm index 0c995564..22709a6f 100644 --- a/modules/webapp/src/main/elm/Page/Home/Data.elm +++ b/modules/webapp/src/main/elm/Page/Home/Data.elm @@ -2,12 +2,18 @@ module Page.Home.Data exposing ( Model , Msg(..) , SearchType(..) + , SelectActionMode(..) + , SelectViewModel + , ViewMode(..) , defaultSearchType , doSearchCmd , init + , initSelectViewModel , itemNav + , menuCollapsed , resultsBelowLimit , searchTypeString + , selectActive ) import Api @@ -16,12 +22,16 @@ import Api.Model.ItemSearch import Browser.Dom as Dom import Comp.FixedDropdown import Comp.ItemCardList +import Comp.ItemDetail.EditMenu import Comp.SearchMenu +import Comp.YesNoDimmer import Data.Flags exposing (Flags) import Data.ItemNav exposing (ItemNav) +import Data.ItemSelection exposing (ItemSelection) import Data.Items import Data.UiSettings exposing (UiSettings) import Http +import Set exposing (Set) import Throttle exposing (Throttle) import Util.Html exposing (KeyCode(..)) import Util.ItemDragDrop as DD @@ -31,7 +41,7 @@ type alias Model = { searchMenuModel : Comp.SearchMenu.Model , itemListModel : Comp.ItemCardList.Model , searchInProgress : Bool - , menuCollapsed : Bool + , viewMode : ViewMode , searchOffset : Int , moreAvailable : Bool , moreInProgress : Bool @@ -45,6 +55,29 @@ type alias Model = } +type alias SelectViewModel = + { ids : Set String + , action : SelectActionMode + , deleteAllConfirm : Comp.YesNoDimmer.Model + , editModel : Comp.ItemDetail.EditMenu.Model + } + + +initSelectViewModel : SelectViewModel +initSelectViewModel = + { ids = Set.empty + , action = NoneAction + , deleteAllConfirm = Comp.YesNoDimmer.initActive + , editModel = Comp.ItemDetail.EditMenu.init + } + + +type ViewMode + = SimpleView + | SearchView + | SelectView SelectViewModel + + init : Flags -> Model init flags = let @@ -58,7 +91,6 @@ init flags = { searchMenuModel = Comp.SearchMenu.init , itemListModel = Comp.ItemCardList.init , searchInProgress = False - , menuCollapsed = True , searchOffset = 0 , moreAvailable = True , moreInProgress = False @@ -72,6 +104,7 @@ init flags = , dragDropData = DD.DragDropData DD.init Nothing , scrollToCard = Nothing + , viewMode = SimpleView } @@ -84,6 +117,32 @@ defaultSearchType flags = BasicSearch +menuCollapsed : Model -> Bool +menuCollapsed model = + case model.viewMode of + SimpleView -> + True + + SearchView -> + False + + SelectView _ -> + False + + +selectActive : Model -> Bool +selectActive model = + case model.viewMode of + SimpleView -> + False + + SearchView -> + False + + SelectView _ -> + True + + type Msg = Init | SearchMenuMsg Comp.SearchMenu.Msg @@ -93,6 +152,7 @@ type Msg | ItemSearchAddResp (Result Http.Error ItemLightList) | DoSearch | ToggleSearchMenu + | ToggleSelectView | LoadMore | UpdateThrottle | SetBasicSearch String @@ -101,6 +161,12 @@ type Msg | SetContentOnly String | ScrollResult (Result Dom.Error ()) | ClearItemDetailId + | SelectAllItems + | SelectNoItems + | RequestDeleteSelected + | DeleteSelectedConfirmMsg Comp.YesNoDimmer.Msg + | EditSelectedItems + | EditMenuMsg Comp.ItemDetail.EditMenu.Msg type SearchType @@ -109,6 +175,12 @@ type SearchType | ContentOnlySearch +type SelectActionMode + = NoneAction + | DeleteSelected + | EditSelected + + searchTypeString : SearchType -> String searchTypeString st = case st of diff --git a/modules/webapp/src/main/elm/Page/Home/Update.elm b/modules/webapp/src/main/elm/Page/Home/Update.elm index 9e01cb06..61a21cee 100644 --- a/modules/webapp/src/main/elm/Page/Home/Update.elm +++ b/modules/webapp/src/main/elm/Page/Home/Update.elm @@ -3,13 +3,18 @@ module Page.Home.Update exposing (update) import Browser.Navigation as Nav import Comp.FixedDropdown import Comp.ItemCardList +import Comp.ItemDetail.EditMenu import Comp.SearchMenu +import Comp.YesNoDimmer import Data.Flags exposing (Flags) +import Data.ItemSelection +import Data.Items import Data.UiSettings exposing (UiSettings) import Page exposing (Page(..)) import Page.Home.Data exposing (..) import Process import Scroll +import Set import Task import Throttle import Time @@ -82,10 +87,19 @@ update mId key flags settings msg model = flags m model.itemListModel + + nextView = + case ( model.viewMode, result.selection ) of + ( SelectView svm, Data.ItemSelection.Active ids ) -> + SelectView { svm | ids = ids } + + ( v, _ ) -> + v in withSub ( { model | itemListModel = result.model + , viewMode = nextView , dragDropData = DD.DragDropData result.dragModel Nothing } , Cmd.batch [ Cmd.map ItemCardListMsg result.cmd ] @@ -159,11 +173,43 @@ update mId key flags settings msg model = doSearch flags settings False nm ToggleSearchMenu -> + let + nextView = + case model.viewMode of + SimpleView -> + SearchView + + SearchView -> + SimpleView + + SelectView _ -> + SimpleView + in withSub - ( { model | menuCollapsed = not model.menuCollapsed } + ( { model | viewMode = nextView } , Cmd.none ) + ToggleSelectView -> + let + ( nextView, cmd ) = + case model.viewMode of + SimpleView -> + ( SelectView initSelectViewModel, loadEditModel flags ) + + SearchView -> + ( SelectView initSelectViewModel, loadEditModel flags ) + + SelectView _ -> + ( SearchView, Cmd.none ) + in + withSub + ( { model + | viewMode = nextView + } + , cmd + ) + LoadMore -> if model.moreAvailable then doSearchMore flags settings model |> withSub @@ -253,6 +299,139 @@ update mId key flags settings msg model = ClearItemDetailId -> noSub ( { model | scrollToCard = Nothing }, Cmd.none ) + SelectAllItems -> + case model.viewMode of + SelectView svm -> + let + visible = + Data.Items.idSet model.itemListModel.results + + svm_ = + { svm | ids = Set.union svm.ids visible } + in + noSub + ( { model | viewMode = SelectView svm_ } + , Cmd.none + ) + + _ -> + noSub ( model, Cmd.none ) + + SelectNoItems -> + case model.viewMode of + SelectView svm -> + let + svm_ = + { svm | ids = Set.empty } + in + noSub + ( { model | viewMode = SelectView svm_ } + , Cmd.none + ) + + _ -> + noSub ( model, Cmd.none ) + + DeleteSelectedConfirmMsg lmsg -> + case model.viewMode of + SelectView svm -> + let + ( confirmModel, confirmed ) = + Comp.YesNoDimmer.update lmsg svm.deleteAllConfirm + + cmd = + if confirmed then + Cmd.none + + else + Cmd.none + + act = + if confirmModel.active || confirmed then + DeleteSelected + + else + NoneAction + in + noSub + ( { model + | viewMode = + SelectView + { svm + | deleteAllConfirm = confirmModel + , action = act + } + } + , cmd + ) + + _ -> + noSub ( model, Cmd.none ) + + RequestDeleteSelected -> + case model.viewMode of + SelectView svm -> + if svm.ids == Set.empty then + noSub ( model, Cmd.none ) + + else + let + lmsg = + DeleteSelectedConfirmMsg Comp.YesNoDimmer.activate + + model_ = + { model | viewMode = SelectView { svm | action = DeleteSelected } } + in + update mId key flags settings lmsg model_ + + _ -> + noSub ( model, Cmd.none ) + + EditSelectedItems -> + case model.viewMode of + SelectView svm -> + if svm.action == EditSelected then + noSub + ( { model | viewMode = SelectView { svm | action = NoneAction } } + , Cmd.none + ) + + else if svm.ids == Set.empty then + noSub ( model, Cmd.none ) + + else + noSub + ( { model | viewMode = SelectView { svm | action = EditSelected } } + , Cmd.none + ) + + _ -> + noSub ( model, Cmd.none ) + + EditMenuMsg lmsg -> + case model.viewMode of + SelectView svm -> + let + res = + Comp.ItemDetail.EditMenu.update flags lmsg svm.editModel + + svm_ = + { svm | editModel = res.model } + + cmd_ = + Cmd.map EditMenuMsg res.cmd + + sub_ = + Sub.map EditMenuMsg res.sub + + _ = + Debug.log "change" res.change + in + ( { model | viewMode = SelectView svm_ }, cmd_, sub_ ) + + _ -> + noSub ( model, Cmd.none ) + --- Helpers @@ -275,12 +454,17 @@ scrollToCard mId model = ( model, Cmd.none, Sub.none ) +loadEditModel : Flags -> Cmd Msg +loadEditModel flags = + Cmd.map EditMenuMsg (Comp.ItemDetail.EditMenu.loadModel flags) + + doSearch : Flags -> UiSettings -> Bool -> Model -> ( Model, Cmd Msg, Sub Msg ) doSearch flags settings scroll model = let stype = if - not model.menuCollapsed + not (menuCollapsed model) || Util.String.isNothingOrBlank model.contentOnlySearch then BasicSearch diff --git a/modules/webapp/src/main/elm/Page/Home/View.elm b/modules/webapp/src/main/elm/Page/Home/View.elm index ce4d8969..84e36c89 100644 --- a/modules/webapp/src/main/elm/Page/Home/View.elm +++ b/modules/webapp/src/main/elm/Page/Home/View.elm @@ -3,26 +3,51 @@ module Page.Home.View exposing (view) import Api.Model.ItemSearch import Comp.FixedDropdown import Comp.ItemCardList +import Comp.ItemDetail.EditMenu import Comp.SearchMenu +import Comp.YesNoDimmer import Data.Flags exposing (Flags) +import Data.ItemSelection import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) import Page exposing (Page(..)) import Page.Home.Data exposing (..) +import Set import Util.Html view : Flags -> UiSettings -> Model -> Html Msg view flags settings model = + let + itemViewCfg = + case model.viewMode of + SelectView svm -> + Comp.ItemCardList.ViewConfig + model.scrollToCard + (Data.ItemSelection.Active svm.ids) + + _ -> + Comp.ItemCardList.ViewConfig + model.scrollToCard + Data.ItemSelection.Inactive + + selectAction = + case model.viewMode of + SelectView svm -> + svm.action + + _ -> + NoneAction + in div [ class "home-page ui padded grid" ] [ div [ classList [ ( "sixteen wide mobile six wide tablet four wide computer search-menu column" , True ) - , ( "invisible hidden", model.menuCollapsed ) + , ( "invisible hidden", menuCollapsed model ) ] ] [ div @@ -38,6 +63,17 @@ view flags settings model = ] , div [ class "right floated menu" ] [ a + [ classList + [ ( "borderless item", True ) + , ( "active", selectActive model ) + ] + , href "#" + , title "Select items" + , onClick ToggleSelectView + ] + [ i [ class "tasks icon" ] [] + ] + , a [ class "borderless item" , onClick ResetSearch , title "Reset form" @@ -63,26 +99,30 @@ view flags settings model = ] ] , div [ class "" ] - [ Html.map SearchMenuMsg - (Comp.SearchMenu.viewDrop model.dragDropData - flags - settings - model.searchMenuModel - ) - ] + (viewLeftMenu flags settings model) ] , div [ classList [ ( "sixteen wide mobile ten wide tablet twelve wide computer column" - , not model.menuCollapsed + , not (menuCollapsed model) ) - , ( "sixteen wide column", model.menuCollapsed ) + , ( "sixteen wide column", menuCollapsed model ) , ( "item-card-list", True ) ] ] - [ viewSearchBar flags model + [ viewBar flags model + , case model.viewMode of + SelectView svm -> + Html.map DeleteSelectedConfirmMsg + (Comp.YesNoDimmer.view2 (selectAction == DeleteSelected) + deleteAllDimmer + svm.deleteAllConfirm + ) + + _ -> + span [ class "invisible" ] [] , Html.map ItemCardListMsg - (Comp.ItemCardList.view model.scrollToCard settings model.itemListModel) + (Comp.ItemCardList.view itemViewCfg settings model.itemListModel) ] , div [ classList @@ -117,6 +157,113 @@ view flags settings model = ] +viewLeftMenu : Flags -> UiSettings -> Model -> List (Html Msg) +viewLeftMenu flags settings model = + let + searchMenu = + [ Html.map SearchMenuMsg + (Comp.SearchMenu.viewDrop model.dragDropData + flags + settings + model.searchMenuModel + ) + ] + in + case model.viewMode of + SelectView svm -> + case svm.action of + EditSelected -> + let + cfg = + Comp.ItemDetail.EditMenu.defaultViewConfig + in + [ div [ class "ui dividing header" ] + [ text "Multi-Edit" + ] + , div [ class "ui info message" ] + [ text "Note that a change here immediatly affects all selected items on the right!" + ] + , Html.map EditMenuMsg + (Comp.ItemDetail.EditMenu.view cfg settings svm.editModel) + ] + + _ -> + searchMenu + + _ -> + searchMenu + + +viewBar : Flags -> Model -> Html Msg +viewBar flags model = + case model.viewMode of + SimpleView -> + viewSearchBar flags model + + SearchView -> + div [ class "hidden invisible" ] [] + + SelectView svm -> + viewActionBar flags svm model + + +viewActionBar : Flags -> SelectViewModel -> Model -> Html Msg +viewActionBar _ svm model = + let + selectCount = + Set.size svm.ids |> String.fromInt + in + div + [ class "ui ablue-comp icon menu" + ] + [ a + [ classList + [ ( "borderless item", True ) + , ( "active", svm.action == EditSelected ) + ] + , href "#" + , title <| "Edit " ++ selectCount ++ " selected items" + , onClick EditSelectedItems + ] + [ i [ class "ui edit icon" ] [] + ] + , a + [ classList + [ ( "borderless item", True ) + , ( "active", svm.action == DeleteSelected ) + ] + , href "#" + , title <| "Delete " ++ selectCount ++ " selected items" + , onClick RequestDeleteSelected + ] + [ i [ class "trash icon" ] [] + ] + , div [ class "right menu" ] + [ a + [ class "item" + , href "#" + , onClick SelectAllItems + , title "Select all" + ] + [ i [ class "check square outline icon" ] [] + ] + , a + [ class "borderless item" + , href "#" + , title "Select none" + , onClick SelectNoItems + ] + [ i [ class "square outline icon" ] [] + ] + , div [ class "borderless label item" ] + [ div [ class "ui circular purple icon label" ] + [ text selectCount + ] + ] + ] + ] + + viewSearchBar : Flags -> Model -> Html Msg viewSearchBar flags model = let @@ -145,7 +292,7 @@ viewSearchBar flags model = in div [ classList - [ ( "invisible hidden", not model.menuCollapsed ) + [ ( "invisible hidden", not (menuCollapsed model) ) , ( "ui secondary stackable menu container", True ) ] ] @@ -221,3 +368,15 @@ hasMoreSearch model = Api.Model.ItemSearch.empty in is_ /= Api.Model.ItemSearch.empty + + +deleteAllDimmer : Comp.YesNoDimmer.Settings +deleteAllDimmer = + { message = "Really delete all selected items?" + , headerIcon = "exclamation icon" + , headerClass = "ui inverted icon header" + , confirmButton = "Yes" + , cancelButton = "No" + , invertedDimmer = False + , extraClass = "top aligned" + } diff --git a/modules/webapp/src/main/webjar/docspell.css b/modules/webapp/src/main/webjar/docspell.css index 31ee7eda..e24781e6 100644 --- a/modules/webapp/src/main/webjar/docspell.css +++ b/modules/webapp/src/main/webjar/docspell.css @@ -31,6 +31,9 @@ margin: 0 1em; } +.default-layout .ui.icon.menu .label.item { + padding: 0 0.5em 0 0; +} .default-layout .right-float { float: right;