From 402ef2f83af31aec7adc93c0aee63eefdb841999 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Fri, 24 Sep 2021 11:12:50 +0200 Subject: [PATCH] app: Add a component to delete a user Also adds a way to hook to the bootstrap modals to show or hide them. --- app/index.html | 4 + app/queries/delete_user.graphql | 5 + app/src/components/delete_user.rs | 156 ++++++++++++++++++++++++++++++ app/src/components/mod.rs | 1 + app/src/components/user_table.rs | 94 +++++++++++++----- app/src/infra/mod.rs | 1 + app/src/infra/modal.rs | 16 +++ 7 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 app/queries/delete_user.graphql create mode 100644 app/src/components/delete_user.rs create mode 100644 app/src/infra/modal.rs diff --git a/app/index.html b/app/index.html index b83fc53..c6c9d5f 100644 --- a/app/index.html +++ b/app/index.html @@ -11,6 +11,10 @@ integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous" as="style" /> + , + props: DeleteUserProps, + node_ref: NodeRef, + modal: Option, + _task: Option, +} + +#[derive(yew::Properties, Clone, PartialEq, Debug)] +pub struct DeleteUserProps { + pub username: String, + pub on_user_deleted: Callback, + pub on_error: Callback, +} + +pub enum Msg { + ClickedDeleteUser, + ConfirmDeleteUser, + DismissModal, + DeleteUserResponse(Result), +} + +impl Component for DeleteUser { + type Message = Msg; + type Properties = DeleteUserProps; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + Self { + link, + props, + node_ref: NodeRef::default(), + modal: None, + _task: None, + } + } + + fn rendered(&mut self, first_render: bool) { + if first_render { + self.modal = Some(Modal::new( + self.node_ref + .cast::() + .expect("Modal node is not an element"), + )); + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match msg { + Msg::ClickedDeleteUser => { + self.modal.as_ref().expect("modal not initialized").show(); + } + Msg::ConfirmDeleteUser => { + self.update(Msg::DismissModal); + self._task = HostService::graphql_query::( + delete_user_query::Variables { + user: self.props.username.clone(), + }, + self.link.callback(Msg::DeleteUserResponse), + "Error trying to delete user", + ) + .map_err(|e| self.props.on_error.emit(e)) + .ok(); + } + Msg::DismissModal => { + self.modal.as_ref().expect("modal not initialized").hide(); + } + Msg::DeleteUserResponse(response) => { + if let Err(e) = response { + self.props.on_error.emit(e); + } else { + self.props.on_user_deleted.emit(self.props.username.clone()); + } + } + } + true + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + self.props.neq_assign(props) + } + + fn view(&self) -> Html { + html! { + <> + + {self.show_modal()} + + } + } +} + +impl DeleteUser { + fn show_modal(&self) -> Html { + html! { + + } + } +} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs index 77f0244..e0b7a94 100644 --- a/app/src/components/mod.rs +++ b/app/src/components/mod.rs @@ -2,6 +2,7 @@ pub mod add_user_to_group; pub mod app; pub mod change_password; pub mod create_user; +pub mod delete_user; pub mod login; pub mod logout; pub mod remove_user_from_group; diff --git a/app/src/components/user_table.rs b/app/src/components/user_table.rs index 80df507..e896a5e 100644 --- a/app/src/components/user_table.rs +++ b/app/src/components/user_table.rs @@ -1,8 +1,11 @@ use crate::{ - components::router::{AppRoute, Link}, + components::{ + delete_user::DeleteUser, + router::{AppRoute, Link}, + }, infra::api::HostService, }; -use anyhow::{anyhow, Result}; +use anyhow::{Error, Result}; use graphql_client::GraphQLQuery; use yew::prelude::*; use yew::services::{fetch::FetchTask, ConsoleService}; @@ -22,13 +25,16 @@ type User = list_users_query::ListUsersQueryUsers; pub struct UserTable { link: ComponentLink, - users: Option>>, + users: Option>, + error: Option, // Used to keep the request alive long enough. _task: Option, } pub enum Msg { ListUsersResponse(Result), + OnUserDeleted(String), + OnError(Error), } impl UserTable { @@ -55,21 +61,21 @@ impl Component for UserTable { link, _task: None, users: None, + error: None, }; table.get_users(None); table } fn update(&mut self, msg: Self::Message) -> ShouldRender { - match msg { - Msg::ListUsersResponse(Ok(users)) => { - self.users = Some(Ok(users.users.into_iter().collect())); - true - } - Msg::ListUsersResponse(Err(e)) => { - self.users = Some(Err(anyhow!("Error listing users: {}", e))); + self.error = None; + match self.handle_msg(msg) { + Err(e) => { + ConsoleService::error(&e.to_string()); + self.error = Some(e); true } + Ok(b) => b, } } @@ -78,18 +84,32 @@ impl Component for UserTable { } fn view(&self) -> Html { - let make_user_row = |user: &User| { - html! { - - {&user.id} - {&user.email} - {&user.display_name} - {&user.first_name} - {&user.last_name} - {&user.creation_date.with_timezone(&chrono::Local)} - + html! { +
+ {self.view_users()} + {self.view_errors()} +
+ } + } +} + +impl UserTable { + fn handle_msg(&mut self, msg: ::Message) -> Result { + match msg { + Msg::ListUsersResponse(users) => { + self.users = Some(users?.users.into_iter().collect()); + Ok(true) } - }; + Msg::OnError(e) => Err(e), + Msg::OnUserDeleted(user_id) => { + debug_assert!(self.users.is_some()); + self.users.as_mut().unwrap().retain(|u| u.id != user_id); + Ok(true) + } + } + } + + fn view_users(&self) -> Html { let make_table = |users: &Vec| { html! {
@@ -102,10 +122,11 @@ impl Component for UserTable { {"First name"} {"Last name"} {"Creation date"} + {"Delete"} - {users.iter().map(make_user_row).collect::>()} + {users.iter().map(|u| self.view_user(u)).collect::>()}
@@ -113,8 +134,33 @@ impl Component for UserTable { }; match &self.users { None => html! {{"Loading..."}}, - Some(Err(e)) => html! {
{"Error: "}{e.to_string()}
}, - Some(Ok(users)) => make_table(users), + Some(users) => make_table(users), + } + } + + fn view_user(&self, user: &User) -> Html { + html! { + + {&user.id} + {&user.email} + {&user.display_name} + {&user.first_name} + {&user.last_name} + {&user.creation_date.with_timezone(&chrono::Local)} + + + + + } + } + + fn view_errors(&self) -> Html { + match &self.error { + None => html! {}, + Some(e) => html! {
{"Error: "}{e.to_string()}
}, } } } diff --git a/app/src/infra/mod.rs b/app/src/infra/mod.rs index f8e9611..3615653 100644 --- a/app/src/infra/mod.rs +++ b/app/src/infra/mod.rs @@ -1,3 +1,4 @@ pub mod api; pub mod cookies; pub mod graphql; +pub mod modal; diff --git a/app/src/infra/modal.rs b/app/src/infra/modal.rs new file mode 100644 index 0000000..9aa9565 --- /dev/null +++ b/app/src/infra/modal.rs @@ -0,0 +1,16 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(module = "bootstrap")] +extern "C" { + #[wasm_bindgen] + pub type Modal; + + #[wasm_bindgen(constructor)] + pub fn new(e: web_sys::Element) -> Modal; + + #[wasm_bindgen(method)] + pub fn show(this: &Modal); + + #[wasm_bindgen(method)] + pub fn hide(this: &Modal); +}