diff --git a/app/Cargo.toml b/app/Cargo.toml index a861efa..8026ccb 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -24,6 +24,8 @@ features = [ "HtmlDocument", "HtmlInputElement", "HtmlOptionElement", + "HtmlOptionsCollection", + "HtmlSelectElement", "console", ] diff --git a/app/src/components/add_user_to_group.rs b/app/src/components/add_user_to_group.rs new file mode 100644 index 0000000..117ce67 --- /dev/null +++ b/app/src/components/add_user_to_group.rs @@ -0,0 +1,271 @@ +use crate::{ + components::user_details::{Group, User}, + infra::api::HostService, +}; +use anyhow::{Error, Result}; +use graphql_client::GraphQLQuery; +use std::collections::HashSet; +use yew::{ + html::ChangeData, + prelude::*, + services::{fetch::FetchTask, ConsoleService}, +}; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/add_user_to_group.graphql", + response_derives = "Debug", + variables_derives = "Clone", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct AddUserToGroup; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "../schema.graphql", + query_path = "queries/get_group_list.graphql", + response_derives = "Debug", + variables_derives = "Clone", + custom_scalars_module = "crate::infra::graphql" +)] +pub struct GetGroupList; +type GroupListGroup = get_group_list::GetGroupListGroups; + +impl From for Group { + fn from(group: GroupListGroup) -> Self { + Self { + id: group.id, + display_name: group.display_name, + } + } +} + +pub struct AddUserToGroupComponent { + link: ComponentLink, + user: User, + /// The list of existing groups, initially not loaded. + group_list: Option>, + /// Whether the "+" button has been clicked. + add_group: bool, + on_error: Callback, + on_user_added_to_group: Callback, + selected_group: Option, + // Used to keep the request alive long enough. + _task: Option, +} + +pub enum Msg { + AddGroupButtonClicked, + GroupListResponse(Result), + SubmitAddGroup, + AddGroupResponse(Result), + SelectionChanged(ChangeData), +} + +#[derive(yew::Properties, Clone, PartialEq)] +pub struct Props { + pub user: User, + pub on_user_added_to_group: Callback, + pub on_error: Callback, +} + +impl AddUserToGroupComponent { + fn get_group_list(&mut self) { + self._task = HostService::graphql_query::( + get_group_list::Variables, + self.link.callback(Msg::GroupListResponse), + "Error trying to fetch group list", + ) + .map_err(|e| { + ConsoleService::log(&e.to_string()); + e + }) + .ok(); + } + + fn submit_add_group(&mut self) -> Result { + let group_id = match &self.selected_group { + None => return Ok(false), + Some(group) => group.id, + }; + self._task = HostService::graphql_query::( + add_user_to_group::Variables { + user: self.user.id.clone(), + group: group_id, + }, + self.link.callback(Msg::AddGroupResponse), + "Error trying to initiate adding the user to a group", + ) + .map_err(|e| { + ConsoleService::log(&e.to_string()); + e + }) + .ok(); + Ok(true) + } + + fn handle_msg(&mut self, msg: ::Message) -> Result { + match msg { + Msg::AddGroupButtonClicked => { + if self.group_list.is_none() { + self.get_group_list(); + } else { + self.set_default_selection(); + } + self.add_group = true; + } + Msg::GroupListResponse(response) => { + self.group_list = Some(response?.groups.into_iter().map(Into::into).collect()); + self.set_default_selection(); + } + Msg::SubmitAddGroup => return self.submit_add_group(), + Msg::AddGroupResponse(response) => { + response?; + // Adding the user to the group succeeded, we're not in the process of adding a + // group anymore. + self.add_group = false; + let group = self + .selected_group + .as_ref() + .expect("Could not get selected group") + .clone(); + // Remove the group from the dropdown. + self.on_user_added_to_group.emit(group); + } + Msg::SelectionChanged(data) => match data { + ChangeData::Select(e) => { + self.update_selection(e); + } + _ => unreachable!(), + }, + } + Ok(true) + } + + fn update_selection(&mut self, e: web_sys::HtmlSelectElement) { + if e.selected_index() == -1 { + self.selected_group = None; + } else { + use wasm_bindgen::JsCast; + let option = e + .options() + .get_with_index(e.selected_index() as u32) + .unwrap() + .dyn_into::() + .unwrap(); + self.selected_group = Some(Group { + id: option.value().parse::().unwrap(), + display_name: option.text(), + }); + } + } + + fn get_selectable_group_list(&self, group_list: &Vec) -> Vec { + let user_groups = self.user.groups.iter().collect::>(); + group_list + .iter() + .filter(|g| !user_groups.contains(g)) + .map(Clone::clone) + .collect() + } + + fn set_default_selection(&mut self) { + self.selected_group = (|| { + let groups = self.get_selectable_group_list(self.group_list.as_ref()?); + groups.into_iter().next() + })(); + } +} + +impl Component for AddUserToGroupComponent { + type Message = Msg; + type Properties = Props; + fn create(props: Self::Properties, link: ComponentLink) -> Self { + Self { + link, + user: props.user, + group_list: None, + add_group: false, + on_error: props.on_error, + on_user_added_to_group: props.on_user_added_to_group, + selected_group: None, + _task: None, + } + } + fn update(&mut self, msg: Self::Message) -> ShouldRender { + match self.handle_msg(msg) { + Err(e) => { + ConsoleService::error(&e.to_string()); + self.on_error.emit(e); + true + } + Ok(b) => b, + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + if props.user.groups != self.user.groups { + self.user = props.user; + if self.selected_group.is_none() { + self.set_default_selection(); + } + true + } else { + false + } + } + + fn view(&self) -> Html { + if !self.add_group { + return html! { + <> + + + + + + }; + } + + if let Some(group_list) = &self.group_list { + let to_add_group_list = self.get_selectable_group_list(&group_list); + let make_select_option = |group: Group| { + html! { + + } + }; + html! { + <> + + + + + + + + } + } else { + html! { + <> + {"Loading groups"} + + + } + } + } +} diff --git a/app/src/components/mod.rs b/app/src/components/mod.rs index 446f45f..8fd65a8 100644 --- a/app/src/components/mod.rs +++ b/app/src/components/mod.rs @@ -1,3 +1,4 @@ +pub mod add_user_to_group; pub mod app; pub mod change_password; pub mod create_user; diff --git a/app/src/components/user_details.rs b/app/src/components/user_details.rs index 5959848..2ed7c8f 100644 --- a/app/src/components/user_details.rs +++ b/app/src/components/user_details.rs @@ -1,10 +1,12 @@ use crate::{ - components::router::{AppRoute, NavButton}, + components::{ + add_user_to_group::AddUserToGroupComponent, + router::{AppRoute, NavButton}, + }, infra::api::HostService, }; use anyhow::{anyhow, bail, Error, Result}; use graphql_client::GraphQLQuery; -use std::collections::HashSet; use yew::{ prelude::*, services::{fetch::FetchTask, ConsoleService}, @@ -14,23 +16,13 @@ use yew::{ #[graphql( schema_path = "../schema.graphql", query_path = "queries/get_user_details.graphql", - response_derives = "Debug, Hash, PartialEq, Eq", + response_derives = "Debug, Hash, PartialEq, Eq, Clone", custom_scalars_module = "crate::infra::graphql" )] pub struct GetUserDetails; -type User = get_user_details::GetUserDetailsUser; -type Group = get_user_details::GetUserDetailsUserGroups; -type GroupListGroup = get_group_list::GetGroupListGroups; - -impl From for Group { - fn from(group: GroupListGroup) -> Self { - Self { - id: group.id, - display_name: group.display_name, - } - } -} +pub type User = get_user_details::GetUserDetailsUser; +pub type Group = get_user_details::GetUserDetailsUserGroups; #[derive(GraphQLQuery)] #[graphql( @@ -42,16 +34,6 @@ impl From for Group { )] pub struct UpdateUser; -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "queries/add_user_to_group.graphql", - response_derives = "Debug", - variables_derives = "Clone", - custom_scalars_module = "crate::infra::graphql" -)] -pub struct AddUserToGroup; - #[derive(GraphQLQuery)] #[graphql( schema_path = "../schema.graphql", @@ -62,16 +44,6 @@ pub struct AddUserToGroup; )] pub struct RemoveUserFromGroup; -#[derive(GraphQLQuery)] -#[graphql( - schema_path = "../schema.graphql", - query_path = "queries/get_group_list.graphql", - response_derives = "Debug", - variables_derives = "Clone", - custom_scalars_module = "crate::infra::graphql" -)] -pub struct GetGroupList; - pub struct UserDetails { link: ComponentLink, /// Which user this is about. @@ -87,11 +59,7 @@ pub struct UserDetails { update_request: Option, /// True iff we just finished updating the user, to display a successful message. update_successful: bool, - /// Whether the "+" button has been clicked. - add_group: bool, is_admin: bool, - /// The list of existing groups, initially not loaded. - group_list: Option>, /// The group that we're requesting to remove, if any. group_to_remove: Option, // Used to keep the request alive long enough. @@ -107,12 +75,10 @@ pub enum Msg { SubmitUserUpdateForm, /// Response after updating the user's details. UpdateFinished(Result), - AddGroupButtonClicked, - GroupListResponse(Result), - SubmitAddGroup, - AddGroupResponse(Result), SubmitRemoveGroup(Group), RemoveGroupResponse(Result), + OnError(Error), + OnUserAddedToGroup(Group), } #[derive(yew::Properties, Clone, PartialEq)] @@ -214,42 +180,6 @@ impl UserDetails { Ok(true) } - fn get_group_list(&mut self) { - self._task = HostService::graphql_query::( - get_group_list::Variables, - self.link.callback(Msg::GroupListResponse), - "Error trying to fetch group list", - ) - .map_err(|e| { - ConsoleService::log(&e.to_string()); - e - }) - .ok(); - } - - fn submit_add_group(&mut self) -> Result { - if self.group_list.is_none() { - return Ok(false); - } - let group_id = get_selected_group() - .expect("could not get selected group") - .id; - self._task = HostService::graphql_query::( - add_user_to_group::Variables { - user: self.username.clone(), - group: group_id, - }, - self.link.callback(Msg::AddGroupResponse), - "Error trying to initiate adding the user to a group", - ) - .map_err(|e| { - ConsoleService::log(&e.to_string()); - e - }) - .ok(); - Ok(true) - } - fn submit_remove_group(&mut self, group: Group) -> Result { self._task = HostService::graphql_query::( remove_user_from_group::Variables { @@ -280,49 +210,16 @@ impl UserDetails { }, Msg::SubmitUserUpdateForm => return self.submit_user_update_form(), Msg::UpdateFinished(r) => return self.user_update_finished(r), - Msg::AddGroupButtonClicked => { - if self.group_list.is_none() { - self.get_group_list(); - } - self.add_group = true; - } - Msg::GroupListResponse(response) => { - let user_groups = self - .user - .as_ref() - .unwrap() - .groups - .iter() - .collect::>(); - self.group_list = Some( - response? - .groups - .into_iter() - .map(Into::into) - .filter(|g| !user_groups.contains(g)) - .collect(), - ); - } - Msg::SubmitAddGroup => return self.submit_add_group(), - Msg::AddGroupResponse(response) => { - response?; - // Adding the user to the group succeeded, we're not in the process of adding a - // group anymore. - self.add_group = false; - let group = get_selected_group().expect("Could not get selected group"); - // Remove the group from the dropdown. - self.group_list.as_mut().unwrap().retain(|g| g != &group); - self.user.as_mut().unwrap().groups.push(group); - } 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); - if let Some(groups) = self.group_list.as_mut() { - groups.push(group); - } + } + Msg::OnError(e) => return Err(e), + Msg::OnUserAddedToGroup(group) => { + self.user.as_mut().unwrap().groups.push(group); } } Ok(true) @@ -382,7 +279,7 @@ impl UserDetails { let id = group.id; let display_name = group.display_name.clone(); html! { - + {&group.display_name} { if self.is_admin { html! { @@ -395,88 +292,43 @@ impl UserDetails {
{"Group memberships"} - + { if self.is_admin { html!{ }} else { html!{} }} {u.groups.iter().map(make_group_row).collect::>()} - {self.view_add_group_button()} + + {self.view_add_group_button(u)} +
{"Group"}
} } - fn view_add_group_button(&self) -> Html { - let make_select_option = |group: &Group| { + fn view_add_group_button(&self, u: &User) -> Html { + if self.is_admin { html! { - - } - }; - if self.add_group { - html! { - - - { if let Some(groups) = self.group_list.as_ref() { - html! { - - } - } else { - html! { {"Loading groups"} } } - } - - { if self.is_admin { html!{ - - - - }} else { html! {} } - } - + } } else { - html! { - - - - - - - } + html! {} } } } -fn get_html_element(name: &str) -> Option { - use wasm_bindgen::JsCast; - web_sys::window()? - .document()? - .get_element_by_id(name)? - .dyn_into::() - .ok() -} - fn get_element(name: &str) -> Option { - Some(get_html_element::(name)?.value()) -} - -fn get_selected_group() -> Option { use wasm_bindgen::JsCast; - let select = get_html_element::("groupToAdd")?; - let id = select.value().parse::().expect("invalid group id"); - let display_name = select - .get(select.selected_index() as u32) - .unwrap() - .dyn_into::() - .unwrap() - .text(); - Some(Group { id, display_name }) + Some( + web_sys::window()? + .document()? + .get_element_by_id(name)? + .dyn_into::() + .ok()? + .value(), + ) } fn get_element_or_empty(name: &str) -> String { @@ -485,7 +337,6 @@ fn get_element_or_empty(name: &str) -> String { impl Component for UserDetails { type Message = Msg; - // The username. type Properties = Props; fn create(props: Self::Properties, link: ComponentLink) -> Self { @@ -495,11 +346,9 @@ impl Component for UserDetails { node_ref: NodeRef::default(), _task: None, user: None, - group_list: None, error: None, update_request: None, update_successful: false, - add_group: false, is_admin: props.is_admin, group_to_remove: None, };