diff --git a/client/src/elm/Item/Encoder.elm b/client/src/elm/Item/Encoder.elm new file mode 100644 index 0000000..0c146cc --- /dev/null +++ b/client/src/elm/Item/Encoder.elm @@ -0,0 +1,12 @@ +module Item.Encoder exposing (encodeItemName) + +import Json.Encode exposing (Value, object, string) +import Item.Model exposing (Item) + + +{-| Encode just the name of the item, so we can send a +PATCH request to update the name on the backend. +-} +encodeItemName : Item -> Value +encodeItemName item = + object [ ( "label", string item.name ) ] diff --git a/client/src/elm/ItemManager/Model.elm b/client/src/elm/ItemManager/Model.elm index 7c17ef4..349df21 100644 --- a/client/src/elm/ItemManager/Model.elm +++ b/client/src/elm/ItemManager/Model.elm @@ -23,6 +23,7 @@ just stay within the `WebData` container. -} type alias Model = { items : Dict ItemId (WebData Item) + , itemPage : Pages.Item.Model.Model , itemsPage : Pages.Items.Model.Model } @@ -43,11 +44,13 @@ type Msg | MsgPagesItems Pages.Items.Model.Msg | HandleFetchedItem ItemId (Result Http.Error Item) | HandleFetchedItems (Result Http.Error ItemsDict) + | HandlePatchResponse (Result Http.Error ()) | HandlePusherEvent (Result String PusherEvent) emptyModel : Model emptyModel = { items = Dict.empty + , itemPage = Pages.Item.Model.emptyModel , itemsPage = Pages.Items.Model.emptyModel } diff --git a/client/src/elm/ItemManager/Update.elm b/client/src/elm/ItemManager/Update.elm index 77e3837..e765bf7 100644 --- a/client/src/elm/ItemManager/Update.elm +++ b/client/src/elm/ItemManager/Update.elm @@ -5,13 +5,15 @@ import Config.Model exposing (BackendUrl) import Date exposing (Date) import Dict exposing (Dict) import Item.Model exposing (Item, ItemId) +import Item.Encoder exposing (encodeItemName) import ItemManager.Decoder exposing (decodeItemFromResponse, decodeItemsFromResponse) import ItemManager.Model exposing (..) import ItemManager.Utils exposing (..) import Json.Decode exposing (decodeValue) -import Json.Encode exposing (Value) -import HttpBuilder exposing (get, withQueryParams) +import Json.Encode exposing (Value, object) +import HttpBuilder exposing (get, withQueryParams, withJsonBody, send) import Pages.Item.Update +import Pages.Item.Model exposing (ItemUpdate(..), unwrapItemUpdate) import Pages.Items.Update import Pusher.Decoder exposing (decodePusherEvent) import Pusher.Model exposing (PusherEventData(..)) @@ -70,11 +72,32 @@ update currentDate backendUrl accessToken user msg model = case getItem id model of Success item -> let - ( subModel, subCmd, redirectPage ) = - Pages.Item.Update.update backendUrl accessToken user subMsg item + ( subModel, itemUpdate, subCmd, redirectPage ) = + Pages.Item.Update.update backendUrl accessToken user subMsg item model.itemPage + + updatedItems = + itemUpdate + |> unwrapItemUpdate model.items + (\item_ -> + Dict.insert id (Success item_) model.items + ) + + sendItemCmd = + case itemUpdate of + UpdateFromUser item_ -> + sendUpdatedItemToBackend backendUrl accessToken id item_ + + _ -> + Cmd.none in - ( { model | items = Dict.insert id (Success subModel) model.items } - , Cmd.map (MsgPagesItem id) subCmd + ( { model + | items = updatedItems + , itemPage = subModel + } + , Cmd.batch + [ Cmd.map (MsgPagesItem id) subCmd + , sendItemCmd + ] , redirectPage ) @@ -125,6 +148,16 @@ update currentDate backendUrl accessToken user msg model = , Nothing ) + HandlePatchResponse (Ok ()) -> + ( model, Cmd.none, Nothing ) + + HandlePatchResponse (Err err) -> + let + _ = + Debug.log "HandlePatchResponse" err + in + ( model, Cmd.none, Nothing ) + HandleFetchedItem itemId (Err err) -> ( { model | items = Dict.insert itemId (Failure err) model.items } , Cmd.none @@ -185,6 +218,18 @@ fetchAllItemsFromBackend backendUrl accessToken model = ) +sendUpdatedItemToBackend : BackendUrl -> String -> ItemId -> Item -> Cmd Msg +sendUpdatedItemToBackend backendUrl accessToken itemId item = + let + command = + HttpBuilder.patch (backendUrl ++ "/api/items/" ++ itemId) + |> withQueryParams [ ( "access_token", accessToken ) ] + |> withJsonBody (encodeItemName item) + |> send HandlePatchResponse + in + command + + subscriptions : Model -> Page -> Sub Msg subscriptions model activePage = pusherItemMessages (decodeValue decodePusherEvent >> HandlePusherEvent) diff --git a/client/src/elm/ItemManager/View.elm b/client/src/elm/ItemManager/View.elm index 20839c7..f040c73 100644 --- a/client/src/elm/ItemManager/View.elm +++ b/client/src/elm/ItemManager/View.elm @@ -59,4 +59,4 @@ viewPageItem currentDate id user model = Success item -> div [] - [ Html.map (MsgPagesItem id) <| Pages.Item.View.view currentDate user id item ] + [ Html.map (MsgPagesItem id) <| Pages.Item.View.view currentDate user id item model.itemPage ] diff --git a/client/src/elm/Pages/Item/Model.elm b/client/src/elm/Pages/Item/Model.elm index 4f240dc..3070321 100644 --- a/client/src/elm/Pages/Item/Model.elm +++ b/client/src/elm/Pages/Item/Model.elm @@ -1,12 +1,60 @@ -module Pages.Item.Model - exposing - ( Msg(..) - ) +module Pages.Item.Model exposing (..) import App.PageType exposing (Page(..)) +import Item.Model exposing (Item) import Pusher.Model exposing (PusherEventData) type Msg = HandlePusherEventData PusherEventData | SetRedirectPage Page + | EditingNameBegin + | EditingNameUpdate String + | EditingNameFinish + | EditingNameCancel + + +{-| This lets us keep track of who has updated the item that +we're returning from `Pages.Item.Update.update` to the +ItemManager. If the user changed it, then we should send the +updated item to the server; if, on the other hand, we got +this change from the server in the first place, all we have to +do is make a note of it ourselves. +-} +type ItemUpdate + = UpdateFromBackend Item + | UpdateFromUser Item + | NoUpdate + + +{-| This allows you to easily do something with the updated +item if there is one, wherever you got it from. Cf. +`Maybe.Extra.unwrap`. +-} +unwrapItemUpdate : a -> (Item -> a) -> ItemUpdate -> a +unwrapItemUpdate default f itemUpdate = + case itemUpdate of + UpdateFromBackend item -> + f item + + UpdateFromUser item -> + f item + + NoUpdate -> + default + + +{-| At the moment the only state peculiar to the Item page is +whether the user is currently editing the name of the item, +and if so, what have they typed in. (We wouldn't have to keep +track of the latter in the Model, but doing so allows us to +the contents of the input field if the users navigates to +another page and then comes back.) +-} +type alias Model = + { editingItemName : Maybe String } + + +emptyModel : Model +emptyModel = + { editingItemName = Nothing } diff --git a/client/src/elm/Pages/Item/Update.elm b/client/src/elm/Pages/Item/Update.elm index a7d6f86..e236078 100644 --- a/client/src/elm/Pages/Item/Update.elm +++ b/client/src/elm/Pages/Item/Update.elm @@ -2,14 +2,14 @@ module Pages.Item.Update exposing (update) import App.PageType exposing (Page(..)) import Config.Model exposing (BackendUrl) +import Item.Model exposing (Item) import User.Model exposing (..) -import Pages.Item.Model exposing (Msg(..)) +import Pages.Item.Model exposing (Model, Msg(..), ItemUpdate(..)) import Pusher.Model exposing (PusherEventData(..)) -import Item.Model exposing (Item) -update : BackendUrl -> String -> User -> Msg -> Item -> ( Item, Cmd Msg, Maybe Page ) -update backendUrl accessToken user msg item = +update : BackendUrl -> String -> User -> Msg -> Item -> Model -> ( Model, ItemUpdate, Cmd Msg, Maybe Page ) +update backendUrl accessToken user msg item model = case msg of HandlePusherEventData event -> case event of @@ -18,10 +18,52 @@ update backendUrl accessToken user msg item = -- which has already been saved at the server. Note that -- we may have just pushed this change ourselves, so it's -- already reflected here. - ( newItem + ( model + , UpdateFromBackend newItem , Cmd.none , Nothing ) SetRedirectPage page -> - ( item, Cmd.none, Just page ) + ( model, NoUpdate, Cmd.none, Just page ) + + EditingNameBegin -> + ( { model | editingItemName = Just item.name } + , NoUpdate + , Cmd.none + , Nothing + ) + + EditingNameFinish -> + let + newName = + case model.editingItemName of + Just name -> + name + + Nothing -> + -- if this happens, then the view + -- code is broken. We can't + -- finish editing the name unless + -- we've already started! + item.name + in + ( { model | editingItemName = Nothing } + , UpdateFromUser { item | name = newName } + , Cmd.none + , Nothing + ) + + EditingNameUpdate updatedName -> + ( { model | editingItemName = Just updatedName } + , NoUpdate + , Cmd.none + , Nothing + ) + + EditingNameCancel -> + ( { model | editingItemName = Nothing } + , NoUpdate + , Cmd.none + , Nothing + ) diff --git a/client/src/elm/Pages/Item/View.elm b/client/src/elm/Pages/Item/View.elm index 8881cd5..3b68d8c 100644 --- a/client/src/elm/Pages/Item/View.elm +++ b/client/src/elm/Pages/Item/View.elm @@ -3,26 +3,27 @@ module Pages.Item.View exposing (view) import Date exposing (Date) import Html exposing (..) import Html.Attributes exposing (..) -import Pages.Item.Model exposing (Msg(..)) +import Html.Events exposing (onClick, on, targetValue) +import Json.Decode +import Pages.Item.Model exposing (Model, Msg(..)) import Item.Model exposing (ItemId, Item) import User.Model exposing (User) -view : Date -> User -> ItemId -> Item -> Html Msg -view currentDate currentUser itemId item = +view : Date -> User -> ItemId -> Item -> Model -> Html Msg +view currentDate currentUser itemId item model = div [] [ div [ class "ui secondary pointing fluid menu" ] - [ h2 - [ class "ui header" ] - [ text item.name ] - , div - [ class "right menu" ] - [ a - [ class "ui active item" ] - [ text "Overview" ] - ] - ] + <| + itemHeader item model + ++ [ div + [ class "right menu" ] + [ a + [ class "ui active item" ] + [ text "Overview" ] + ] + ] , div [] [ img [ src item.image, alt item.name ] [] ] @@ -30,3 +31,46 @@ view currentDate currentUser itemId item = [ class "ui divider" ] [] ] + + +itemHeader : Item -> Model -> List (Html Msg) +itemHeader item model = + case model.editingItemName of + Just editedName -> + [ h2 [ class "ui header input" ] + [ input + [ value editedName + , onChange EditingNameUpdate + ] + [] + ] + , button + [ name "Done" + , type_ "button" + , onClick EditingNameFinish + ] + [ text "Done" ] + , button + [ name "Cancel" + , type_ "button" + , onClick EditingNameCancel + ] + [ text "Cancel" ] + ] + + Nothing -> + [ h2 + [ class "ui header" ] + [ text item.name ] + , button + [ name "Edit" + , type_ "button" + , onClick EditingNameBegin + ] + [ text "Edit" ] + ] + + +onChange : (String -> Msg) -> Attribute Msg +onChange tagger = + on "change" (Json.Decode.map tagger targetValue)