From 1d5cf2e4b57aa62e1867b6866e205bba8fdcc9a7 Mon Sep 17 00:00:00 2001 From: William Carroll Date: Sun, 2 Aug 2020 18:02:15 +0100 Subject: [PATCH] Support Admins inviting users from the client The title says it all. --- client/src/Admin.elm | 59 +++++++++++++++++++++++++++++++- client/src/State.elm | 81 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/client/src/Admin.elm b/client/src/Admin.elm index 17155c1d8..d95609ee1 100644 --- a/client/src/Admin.elm +++ b/client/src/Admin.elm @@ -5,6 +5,7 @@ import Date import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) +import Maybe.Extra as ME import RemoteData import State import Tailwind @@ -12,6 +13,59 @@ import UI import Utils +roleToggle : State.Model -> State.Role -> Html State.Msg +roleToggle model role = + div [ [ "px-1", "inline" ] |> Tailwind.use |> class ] + [ UI.toggleButton + { toggled = model.inviteRole == Just role + , label = State.roleToString role + , handleEnable = State.UpdateInviteRole (Just role) + , handleDisable = State.UpdateInviteRole Nothing + } + ] + + +inviteUser : State.Model -> Html State.Msg +inviteUser model = + div [ [ "pb-6" ] |> Tailwind.use |> class ] + [ UI.header 3 "Invite a user" + , UI.textField + { handleInput = State.UpdateInviteEmail + , inputId = "invite-email" + , inputValue = model.inviteEmail + , pholder = "Email..." + } + , div [ [ "pt-4" ] |> Tailwind.use |> class ] + [ roleToggle model State.User + , roleToggle model State.Manager + , roleToggle model State.Admin + ] + , UI.baseButton + { enabled = + List.all + identity + [ String.length model.inviteEmail > 0 + , ME.isJust model.inviteRole + ] + , extraClasses = [ "my-4" ] + , label = + case model.inviteResponseStatus of + RemoteData.Loading -> + "Sending..." + + _ -> + "Send invitation" + , handleClick = + case model.inviteRole of + Nothing -> + State.DoNothing + + Just role -> + State.AttemptInviteUser role + } + ] + + allTrips : State.Model -> Html State.Msg allTrips model = case model.trips of @@ -124,7 +178,10 @@ render model = ] , case model.adminTab of State.Accounts -> - allUsers model + div [] + [ inviteUser model + , allUsers model + ] State.Trips -> allTrips model diff --git a/client/src/State.elm b/client/src/State.elm index 8898918cc..a38895a6c 100644 --- a/client/src/State.elm +++ b/client/src/State.elm @@ -40,6 +40,9 @@ type Msg | ClearErrors | ToggleLoginForm | PrintPage + | UpdateInviteEmail String + | UpdateInviteRole (Maybe Role) + | ReceiveTodaysDate Date.Date -- SPA | LinkClicked Browser.UrlRequest | UrlChanged Url.Url @@ -52,6 +55,7 @@ type Msg | AttemptDeleteAccount String | AttemptCreateTrip Date.Date Date.Date | AttemptDeleteTrip Trip + | AttemptInviteUser Role -- Inbound network | GotAccounts (WebData (List Account)) | GotTrips (WebData (List Trip)) @@ -61,6 +65,7 @@ type Msg | GotDeleteAccount (Result Http.Error String) | GotCreateTrip (Result Http.Error ()) | GotDeleteTrip (Result Http.Error ()) + | GotInviteUser (Result Http.Error ()) type Route @@ -121,6 +126,7 @@ type alias Model = , url : Url.Url , key : Nav.Key , session : Maybe Session + , todaysDate : Maybe Date.Date , username : String , email : String , password : String @@ -135,12 +141,16 @@ type alias Model = , trips : WebData (List Trip) , adminTab : AdminTab , loginTab : LoginTab + , inviteEmail : String + , inviteRole : Maybe Role + , inviteResponseStatus : WebData () , loginError : Maybe Http.Error , logoutError : Maybe Http.Error , signUpError : Maybe Http.Error , deleteUserError : Maybe Http.Error , createTripError : Maybe Http.Error , deleteTripError : Maybe Http.Error + , inviteUserError : Maybe Http.Error } @@ -151,6 +161,7 @@ allErrors model = , ( model.signUpError, "Error attempting to create your account" ) , ( model.deleteUserError, "Error attempting to delete a user" ) , ( model.createTripError, "Error attempting to create a trip" ) + , ( model.inviteUserError, "Error attempting to invite a user" ) ] @@ -178,6 +189,19 @@ endpoint = UrlBuilder.crossOrigin Shared.serverOrigin +encodeRole : Role -> JE.Value +encodeRole x = + case x of + User -> + JE.string "user" + + Manager -> + JE.string "manager" + + Admin -> + JE.string "admin" + + decodeRole : JD.Decoder Role decodeRole = let @@ -254,6 +278,21 @@ signUp { username, email, password } = } +inviteUser : { email : String, role : Role } -> Cmd Msg +inviteUser { email, role } = + Utils.postWithCredentials + { url = endpoint [ "invite" ] [] + , body = + Http.jsonBody + (JE.object + [ ( "email", JE.string email ) + , ( "role", encodeRole role ) + ] + ) + , expect = Http.expectWhatever GotInviteUser + } + + createTrip : { username : String , destination : String @@ -424,6 +463,7 @@ prod _ url key = , url = url , key = key , session = Nothing + , todaysDate = Nothing , username = "" , email = "" , password = "" @@ -438,16 +478,21 @@ prod _ url key = , endDatePicker = endDatePicker , adminTab = Accounts , loginTab = LoginForm + , inviteEmail = "" + , inviteRole = Nothing + , inviteResponseStatus = RemoteData.NotAsked , loginError = Nothing , logoutError = Nothing , signUpError = Nothing , deleteUserError = Nothing , createTripError = Nothing , deleteTripError = Nothing + , inviteUserError = Nothing } , Cmd.batch [ Cmd.map UpdateTripStartDate startDatePickerCmd , Cmd.map UpdateTripEndDate endDatePickerCmd + , Date.today |> Task.perform ReceiveTodaysDate ] ) @@ -632,6 +677,15 @@ update msg model = PrintPage -> ( model, printPage () ) + UpdateInviteEmail x -> + ( { model | inviteEmail = x }, Cmd.none ) + + UpdateInviteRole mRole -> + ( { model | inviteRole = mRole }, Cmd.none ) + + ReceiveTodaysDate date -> + ( { model | todaysDate = Just date }, Cmd.none ) + LinkClicked urlRequest -> case urlRequest of Browser.Internal url -> @@ -766,6 +820,33 @@ update msg model = , sleepAndClearErrors ) + AttemptInviteUser role -> + ( { model | inviteResponseStatus = RemoteData.Loading } + , inviteUser + { email = model.inviteEmail + , role = role + } + ) + + GotInviteUser result -> + case result of + Ok _ -> + ( { model + | inviteEmail = "" + , inviteRole = Nothing + , inviteResponseStatus = RemoteData.Success () + } + , Cmd.none + ) + + Err x -> + ( { model + | inviteUserError = Just x + , inviteResponseStatus = RemoteData.Failure x + } + , sleepAndClearErrors + ) + -- POST /accounts AttemptSignUp -> ( model