app: Extract a RemoveUserFromGroup component

This commit is contained in:
Valentin Tolmer 2021-09-19 16:01:44 +02:00 committed by nitnelave
parent 14be1170f2
commit 00efdb42af
7 changed files with 189 additions and 108 deletions

16
Cargo.lock generated
View File

@ -1770,6 +1770,7 @@ dependencies = [
"yew-router", "yew-router",
"yew_form", "yew_form",
"yew_form_derive", "yew_form_derive",
"yewtil",
] ]
[[package]] [[package]]
@ -3537,6 +3538,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"serde",
"serde_json",
"wasm-bindgen-macro", "wasm-bindgen-macro",
] ]
@ -3757,6 +3760,19 @@ dependencies = [
"yew_form", "yew_form",
] ]
[[package]]
name = "yewtil"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8543663ac49cd613df079282a1d8bdbdebdad6e02bac229f870fd4237b5d9aaa"
dependencies = [
"log",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"yew",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.4.1" version = "1.4.1"

View File

@ -18,6 +18,7 @@ validator = "*"
validator_derive = "*" validator_derive = "*"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
yew = "0.18" yew = "0.18"
yewtil = "*"
yew-router = "0.15" yew-router = "0.15"
yew_form = "0.1.8" yew_form = "0.1.8"
yew_form_derive = "*" yew_form_derive = "*"

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
components::{ components::{
select::{Select, SelectOption, SelectOptionProps}, select::{Select, SelectOption, SelectOptionProps},
user_details::{Group, User}, user_details::Group,
}, },
infra::api::HostService, infra::api::HostService,
}; };
@ -12,6 +12,7 @@ use yew::{
prelude::*, prelude::*,
services::{fetch::FetchTask, ConsoleService}, services::{fetch::FetchTask, ConsoleService},
}; };
use yewtil::NeqAssign;
#[derive(GraphQLQuery)] #[derive(GraphQLQuery)]
#[graphql( #[graphql(
@ -63,7 +64,8 @@ pub enum Msg {
#[derive(yew::Properties, Clone, PartialEq)] #[derive(yew::Properties, Clone, PartialEq)]
pub struct Props { pub struct Props {
pub user: User, pub username: String,
pub groups: Vec<Group>,
pub on_user_added_to_group: Callback<Group>, pub on_user_added_to_group: Callback<Group>,
pub on_error: Callback<Error>, pub on_error: Callback<Error>,
} }
@ -89,7 +91,7 @@ impl AddUserToGroupComponent {
}; };
self._task = HostService::graphql_query::<AddUserToGroup>( self._task = HostService::graphql_query::<AddUserToGroup>(
add_user_to_group::Variables { add_user_to_group::Variables {
user: self.props.user.id.clone(), user: self.props.username.clone(),
group: group_id, group: group_id,
}, },
self.link.callback(Msg::AddGroupResponse), self.link.callback(Msg::AddGroupResponse),
@ -133,7 +135,7 @@ impl AddUserToGroupComponent {
} }
fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> { fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> {
let user_groups = self.props.user.groups.iter().collect::<HashSet<_>>(); let user_groups = self.props.groups.iter().collect::<HashSet<_>>();
group_list group_list
.iter() .iter()
.filter(|g| !user_groups.contains(g)) .filter(|g| !user_groups.contains(g))
@ -168,12 +170,7 @@ impl Component for AddUserToGroupComponent {
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn change(&mut self, props: Self::Properties) -> ShouldRender {
if props.user.groups != self.props.user.groups { self.props.neq_assign(props)
self.props.user = props.user;
true
} else {
false
}
} }
fn view(&self) -> Html { fn view(&self) -> Html {

View File

@ -4,6 +4,7 @@ pub mod change_password;
pub mod create_user; pub mod create_user;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod remove_user_from_group;
pub mod router; pub mod router;
pub mod select; pub mod select;
pub mod user_details; pub mod user_details;

View File

@ -0,0 +1,111 @@
use crate::{components::user_details::Group, infra::api::HostService};
use anyhow::{Error, Result};
use graphql_client::GraphQLQuery;
use yew::{
prelude::*,
services::{fetch::FetchTask, ConsoleService},
};
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/remove_user_from_group.graphql",
response_derives = "Debug",
variables_derives = "Clone",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct RemoveUserFromGroup;
pub struct RemoveUserFromGroupComponent {
link: ComponentLink<Self>,
props: Props,
// Used to keep the request alive long enough.
_task: Option<FetchTask>,
}
#[derive(yew::Properties, Clone, PartialEq)]
pub struct Props {
pub username: String,
pub group: Group,
pub is_admin: bool,
pub on_user_removed_from_group: Callback<Group>,
pub on_error: Callback<Error>,
}
pub enum Msg {
SubmitRemoveGroup,
RemoveGroupResponse(Result<remove_user_from_group::ResponseData>),
}
impl RemoveUserFromGroupComponent {
fn submit_remove_group(&mut self) -> Result<bool> {
let group = self.props.group.id;
self._task = HostService::graphql_query::<RemoveUserFromGroup>(
remove_user_from_group::Variables {
user: self.props.username.clone(),
group,
},
self.link.callback(Msg::RemoveGroupResponse),
"Error trying to initiate removing the user from a group",
)
.map_err(|e| {
ConsoleService::log(&e.to_string());
e
})
.ok();
Ok(true)
}
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
match msg {
Msg::SubmitRemoveGroup => return self.submit_remove_group(),
Msg::RemoveGroupResponse(response) => {
response?;
self.props
.on_user_removed_from_group
.emit(self.props.group.clone());
}
}
Ok(true)
}
}
impl Component for RemoveUserFromGroupComponent {
type Message = Msg;
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self {
link,
props,
_task: None,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match self.handle_msg(msg) {
Err(e) => {
self.props.on_error.emit(e);
true
}
Ok(b) => b,
}
}
fn change(&mut self, _: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
let group = &self.props.group;
html! {
<>
<td>{&group.display_name}</td>
{ if self.props.is_admin { html! {
<td><button onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>{"-"}</button></td>
}} else { html!{} }
}
</>
}
}
}

View File

@ -1,8 +1,10 @@
use yew::{html::ChangeData, prelude::*}; use yew::{html::ChangeData, prelude::*};
use yewtil::NeqAssign;
pub struct Select { pub struct Select {
link: ComponentLink<Self>, link: ComponentLink<Self>,
props: SelectProps, props: SelectProps,
node_ref: NodeRef,
} }
#[derive(yew::Properties, Clone, PartialEq, Debug)] #[derive(yew::Properties, Clone, PartialEq, Debug)]
@ -26,48 +28,47 @@ impl Select {
.nth(nth as usize) .nth(nth as usize)
.map(|child| child.props) .map(|child| child.props)
} }
fn send_selection_update(&self) {
let select_node = self.node_ref.cast::<web_sys::HtmlSelectElement>().unwrap();
self.props
.on_selection_change
.emit(self.get_nth_child_props(select_node.selected_index()))
}
} }
impl Component for Select { impl Component for Select {
type Message = SelectMsg; type Message = SelectMsg;
type Properties = SelectProps; type Properties = SelectProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let res = Self { link, props }; Self {
res.props link,
.on_selection_change props,
.emit(res.get_nth_child_props(0)); node_ref: NodeRef::default(),
res
} }
}
fn rendered(&mut self, _first_render: bool) {
self.send_selection_update();
}
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg { let SelectMsg::OnSelectChange(data) = msg;
SelectMsg::OnSelectChange(data) => match data { match data {
ChangeData::Select(e) => { ChangeData::Select(_) => self.send_selection_update(),
self.props
.on_selection_change
.emit(self.get_nth_child_props(e.selected_index()));
}
_ => unreachable!(), _ => unreachable!(),
},
} }
false false
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props.children.len() != props.children.len() { self.props.children.neq_assign(props.children)
let was_empty = self.props.children.is_empty();
self.props = props;
if self.props.children.is_empty() || was_empty {
self.props
.on_selection_change
.emit(self.get_nth_child_props(0));
}
true
} else {
false
}
} }
fn view(&self) -> Html { fn view(&self) -> Html {
html! { html! {
<select <select
ref=self.node_ref.clone()
onchange=self.link.callback(SelectMsg::OnSelectChange)> onchange=self.link.callback(SelectMsg::OnSelectChange)>
{ self.props.children.clone() } { self.props.children.clone() }
</select> </select>
@ -79,7 +80,7 @@ pub struct SelectOption {
props: SelectOptionProps, props: SelectOptionProps,
} }
#[derive(yew::Properties, Clone, PartialEq)] #[derive(yew::Properties, Clone, PartialEq, Debug)]
pub struct SelectOptionProps { pub struct SelectOptionProps {
pub value: String, pub value: String,
pub text: String, pub text: String,
@ -88,20 +89,19 @@ pub struct SelectOptionProps {
impl Component for SelectOption { impl Component for SelectOption {
type Message = (); type Message = ();
type Properties = SelectOptionProps; type Properties = SelectOptionProps;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self { fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props } Self { props }
} }
fn update(&mut self, _: Self::Message) -> ShouldRender { fn update(&mut self, _: Self::Message) -> ShouldRender {
false false
} }
fn change(&mut self, props: Self::Properties) -> ShouldRender { fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props != props { self.props.neq_assign(props)
self.props = props;
true
} else {
false
}
} }
fn view(&self) -> Html { fn view(&self) -> Html {
html! { html! {
<option value=self.props.value.clone()> <option value=self.props.value.clone()>

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
components::{ components::{
add_user_to_group::AddUserToGroupComponent, add_user_to_group::AddUserToGroupComponent,
remove_user_from_group::RemoveUserFromGroupComponent,
router::{AppRoute, NavButton}, router::{AppRoute, NavButton},
user_details_form::UserDetailsForm, user_details_form::UserDetailsForm,
}, },
@ -25,30 +26,14 @@ pub struct GetUserDetails;
pub type User = get_user_details::GetUserDetailsUser; pub type User = get_user_details::GetUserDetailsUser;
pub type Group = get_user_details::GetUserDetailsUserGroups; pub type Group = get_user_details::GetUserDetailsUserGroups;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "../schema.graphql",
query_path = "queries/remove_user_from_group.graphql",
response_derives = "Debug",
variables_derives = "Clone",
custom_scalars_module = "crate::infra::graphql"
)]
pub struct RemoveUserFromGroup;
pub struct UserDetails { pub struct UserDetails {
link: ComponentLink<Self>, link: ComponentLink<Self>,
/// Which user this is about. props: Props,
username: String,
/// The user info. If none, the error is in `error`. If `error` is None, then we haven't /// The user info. If none, the error is in `error`. If `error` is None, then we haven't
/// received the server response yet. /// received the server response yet.
user: Option<User>, user: Option<User>,
/// Error message displayed to the user. /// Error message displayed to the user.
error: Option<Error>, error: Option<Error>,
/// True iff we just finished updating the user, to display a successful message.
update_successful: bool,
is_admin: bool,
/// The group that we're requesting to remove, if any.
group_to_remove: Option<Group>,
// Used to keep the request alive long enough. // Used to keep the request alive long enough.
_task: Option<FetchTask>, _task: Option<FetchTask>,
} }
@ -58,10 +43,9 @@ pub struct UserDetails {
pub enum Msg { pub enum Msg {
/// Received the user details response, either the user data or an error. /// Received the user details response, either the user data or an error.
UserDetailsResponse(Result<get_user_details::ResponseData>), UserDetailsResponse(Result<get_user_details::ResponseData>),
SubmitRemoveGroup(Group),
RemoveGroupResponse(Result<remove_user_from_group::ResponseData>),
OnError(Error), OnError(Error),
OnUserAddedToGroup(Group), OnUserAddedToGroup(Group),
OnUserRemovedFromGroup(Group),
} }
#[derive(yew::Properties, Clone, PartialEq)] #[derive(yew::Properties, Clone, PartialEq)]
@ -74,7 +58,7 @@ impl UserDetails {
fn get_user_details(&mut self) { fn get_user_details(&mut self) {
self._task = HostService::graphql_query::<GetUserDetails>( self._task = HostService::graphql_query::<GetUserDetails>(
get_user_details::Variables { get_user_details::Variables {
id: self.username.clone(), id: self.props.username.clone(),
}, },
self.link.callback(Msg::UserDetailsResponse), self.link.callback(Msg::UserDetailsResponse),
"Error trying to fetch user details", "Error trying to fetch user details",
@ -86,26 +70,7 @@ impl UserDetails {
.ok(); .ok();
} }
fn submit_remove_group(&mut self, group: Group) -> Result<bool> {
self._task = HostService::graphql_query::<RemoveUserFromGroup>(
remove_user_from_group::Variables {
user: self.username.clone(),
group: group.id,
},
self.link.callback(Msg::RemoveGroupResponse),
"Error trying to initiate removing the user from a group",
)
.map_err(|e| {
ConsoleService::log(&e.to_string());
e
})
.ok();
self.group_to_remove = Some(group);
Ok(true)
}
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
self.update_successful = false;
match msg { match msg {
Msg::UserDetailsResponse(response) => match response { Msg::UserDetailsResponse(response) => match response {
Ok(user) => self.user = Some(user.user), Ok(user) => self.user = Some(user.user),
@ -114,27 +79,19 @@ impl UserDetails {
bail!("Error getting user details: {}", e); bail!("Error getting user details: {}", e);
} }
}, },
Msg::SubmitRemoveGroup(group) => return self.submit_remove_group(group),
Msg::RemoveGroupResponse(response) => {
response?;
let group = self.group_to_remove.take().unwrap();
// Remove the group from the user and add it to the dropdown.
self.user.as_mut().unwrap().groups.retain(|g| g != &group);
}
Msg::OnError(e) => return Err(e), Msg::OnError(e) => return Err(e),
Msg::OnUserAddedToGroup(group) => { Msg::OnUserAddedToGroup(group) => {
self.user.as_mut().unwrap().groups.push(group); self.user.as_mut().unwrap().groups.push(group);
} }
Msg::OnUserRemovedFromGroup(group) => {
self.user.as_mut().unwrap().groups.retain(|g| g != &group);
}
} }
Ok(true) Ok(true)
} }
fn view_messages(&self, error: &Option<Error>) -> Html { fn view_messages(&self, error: &Option<Error>) -> Html {
if self.update_successful { if let Some(e) = error {
html! {
<span>{"Update successful!"}</span>
}
} else if let Some(e) = error {
html! { html! {
<div> <div>
<span>{"Error: "}{e.to_string()}</span> <span>{"Error: "}{e.to_string()}</span>
@ -147,15 +104,15 @@ impl UserDetails {
fn view_group_memberships(&self, u: &User) -> Html { fn view_group_memberships(&self, u: &User) -> Html {
let make_group_row = |group: &Group| { let make_group_row = |group: &Group| {
let id = group.id;
let display_name = group.display_name.clone(); let display_name = group.display_name.clone();
html! { html! {
<tr key="groupRow_".to_string() + &display_name> <tr key="groupRow_".to_string() + &display_name>
<td>{&group.display_name}</td> <RemoveUserFromGroupComponent
{ if self.is_admin { html! { username=u.id.clone()
<td><button onclick=self.link.callback(move |_| Msg::SubmitRemoveGroup(Group{id, display_name: display_name.clone()}))>{"-"}</button></td> group=group.clone()
}} else { html!{} } is_admin=self.props.is_admin
} on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
on_error=self.link.callback(Msg::OnError)/>
</tr> </tr>
} }
}; };
@ -165,7 +122,7 @@ impl UserDetails {
<table> <table>
<tr key="headerRow"> <tr key="headerRow">
<th>{"Group"}</th> <th>{"Group"}</th>
{ if self.is_admin { html!{ <th></th> }} else { html!{} }} { if self.props.is_admin { html!{ <th></th> }} else { html!{} }}
</tr> </tr>
{u.groups.iter().map(make_group_row).collect::<Vec<_>>()} {u.groups.iter().map(make_group_row).collect::<Vec<_>>()}
<tr key="groupToAddRow"> <tr key="groupToAddRow">
@ -177,10 +134,11 @@ impl UserDetails {
} }
fn view_add_group_button(&self, u: &User) -> Html { fn view_add_group_button(&self, u: &User) -> Html {
if self.is_admin { if self.props.is_admin {
html! { html! {
<AddUserToGroupComponent <AddUserToGroupComponent
user=u.clone() username=u.id.clone()
groups=u.groups.clone()
on_error=self.link.callback(Msg::OnError) on_error=self.link.callback(Msg::OnError)
on_user_added_to_group=self.link.callback(Msg::OnUserAddedToGroup)/> on_user_added_to_group=self.link.callback(Msg::OnUserAddedToGroup)/>
} }
@ -197,13 +155,10 @@ impl Component for UserDetails {
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let mut table = Self { let mut table = Self {
link, link,
username: props.username, props,
_task: None, _task: None,
user: None, user: None,
error: None, error: None,
update_successful: false,
is_admin: props.is_admin,
group_to_remove: None,
}; };
table.get_user_details(); table.get_user_details();
table table