app: Extract a Select component

This commit is contained in:
Valentin Tolmer 2021-09-17 21:27:00 +02:00 committed by nitnelave
parent 37c6e8ef30
commit 5943df6443
4 changed files with 146 additions and 87 deletions

View File

@ -1,12 +1,14 @@
use crate::{ use crate::{
components::user_details::{Group, User}, components::{
select::{Select, SelectOption, SelectOptionProps},
user_details::{Group, User},
},
infra::api::HostService, infra::api::HostService,
}; };
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use std::collections::HashSet; use std::collections::HashSet;
use yew::{ use yew::{
html::ChangeData,
prelude::*, prelude::*,
services::{fetch::FetchTask, ConsoleService}, services::{fetch::FetchTask, ConsoleService},
}; };
@ -43,24 +45,20 @@ impl From<GroupListGroup> for Group {
pub struct AddUserToGroupComponent { pub struct AddUserToGroupComponent {
link: ComponentLink<Self>, link: ComponentLink<Self>,
user: User, props: Props,
/// The list of existing groups, initially not loaded. /// The list of existing groups, initially not loaded.
group_list: Option<Vec<Group>>, group_list: Option<Vec<Group>>,
/// Whether the "+" button has been clicked. /// The currently selected group.
add_group: bool,
on_error: Callback<Error>,
on_user_added_to_group: Callback<Group>,
selected_group: Option<Group>, selected_group: Option<Group>,
// Used to keep the request alive long enough. // Used to keep the request alive long enough.
_task: Option<FetchTask>, _task: Option<FetchTask>,
} }
pub enum Msg { pub enum Msg {
AddGroupButtonClicked,
GroupListResponse(Result<get_group_list::ResponseData>), GroupListResponse(Result<get_group_list::ResponseData>),
SubmitAddGroup, SubmitAddGroup,
AddGroupResponse(Result<add_user_to_group::ResponseData>), AddGroupResponse(Result<add_user_to_group::ResponseData>),
SelectionChanged(ChangeData), SelectionChanged(Option<SelectOptionProps>),
} }
#[derive(yew::Properties, Clone, PartialEq)] #[derive(yew::Properties, Clone, PartialEq)]
@ -91,7 +89,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.user.id.clone(), user: self.props.user.id.clone(),
group: group_id, group: group_id,
}, },
self.link.callback(Msg::AddGroupResponse), self.link.callback(Msg::AddGroupResponse),
@ -107,97 +105,62 @@ impl AddUserToGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> { fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
match msg { 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) => { Msg::GroupListResponse(response) => {
self.group_list = Some(response?.groups.into_iter().map(Into::into).collect()); self.group_list = Some(response?.groups.into_iter().map(Into::into).collect());
self.set_default_selection();
} }
Msg::SubmitAddGroup => return self.submit_add_group(), Msg::SubmitAddGroup => return self.submit_add_group(),
Msg::AddGroupResponse(response) => { Msg::AddGroupResponse(response) => {
response?; response?;
// Adding the user to the group succeeded, we're not in the process of adding a // Adding the user to the group succeeded, we're not in the process of adding a
// group anymore. // group anymore.
self.add_group = false;
let group = self let group = self
.selected_group .selected_group
.as_ref() .as_ref()
.expect("Could not get selected group") .expect("Could not get selected group")
.clone(); .clone();
// Remove the group from the dropdown. // Remove the group from the dropdown.
self.on_user_added_to_group.emit(group); self.props.on_user_added_to_group.emit(group);
}
Msg::SelectionChanged(option_props) => {
self.selected_group = option_props.map(|props| Group {
id: props.value.parse::<i64>().unwrap(),
display_name: props.text,
});
return Ok(false);
} }
Msg::SelectionChanged(data) => match data {
ChangeData::Select(e) => {
self.update_selection(e);
}
_ => unreachable!(),
},
} }
Ok(true) Ok(true)
} }
fn update_selection(&mut self, e: web_sys::HtmlSelectElement) { fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> {
if e.selected_index() == -1 { let user_groups = self.props.user.groups.iter().collect::<HashSet<_>>();
self.selected_group = None;
} else {
use wasm_bindgen::JsCast;
let option = e
.options()
.get_with_index(e.selected_index() as u32)
.unwrap()
.dyn_into::<web_sys::HtmlOptionElement>()
.unwrap();
self.selected_group = Some(Group {
id: option.value().parse::<i64>().unwrap(),
display_name: option.text(),
});
}
}
fn get_selectable_group_list(&self, group_list: &Vec<Group>) -> Vec<Group> {
let user_groups = self.user.groups.iter().collect::<HashSet<_>>();
group_list group_list
.iter() .iter()
.filter(|g| !user_groups.contains(g)) .filter(|g| !user_groups.contains(g))
.map(Clone::clone) .map(Clone::clone)
.collect() .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 { impl Component for AddUserToGroupComponent {
type Message = Msg; type Message = Msg;
type Properties = Props; type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self { fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
Self { let mut res = Self {
link, link,
user: props.user, props,
group_list: None, 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, selected_group: None,
_task: None, _task: None,
} };
res.get_group_list();
res
} }
fn update(&mut self, msg: Self::Message) -> ShouldRender { fn update(&mut self, msg: Self::Message) -> ShouldRender {
match self.handle_msg(msg) { match self.handle_msg(msg) {
Err(e) => { Err(e) => {
ConsoleService::error(&e.to_string()); ConsoleService::error(&e.to_string());
self.on_error.emit(e); self.props.on_error.emit(e);
true true
} }
Ok(b) => b, Ok(b) => b,
@ -205,11 +168,8 @@ 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.user.groups { if props.user.groups != self.props.user.groups {
self.user = props.user; self.props.user = props.user;
if self.selected_group.is_none() {
self.set_default_selection();
}
true true
} else { } else {
false false
@ -217,39 +177,25 @@ impl Component for AddUserToGroupComponent {
} }
fn view(&self) -> Html { fn view(&self) -> Html {
if !self.add_group {
return html! {
<>
<td></td>
<td>
<button onclick=self.link.callback(
|_| Msg::AddGroupButtonClicked)>
{"+"}
</button>
</td>
</>
};
}
if let Some(group_list) = &self.group_list { if let Some(group_list) = &self.group_list {
let to_add_group_list = self.get_selectable_group_list(&group_list); let to_add_group_list = self.get_selectable_group_list(group_list);
#[allow(unused_braces)]
let make_select_option = |group: Group| { let make_select_option = |group: Group| {
html! { html_nested! {
<option value={group.id.to_string()}>{group.display_name}</option> <SelectOption value=group.id.to_string() text=group.display_name key=group.id />
} }
}; };
html! { html! {
<> <>
<td> <td>
<select name="groupToAdd" id="groupToAdd" <Select on_selection_change=self.link.callback(Msg::SelectionChanged)>
onchange=self.link.callback(|e| Msg::SelectionChanged(e))>
{ {
to_add_group_list to_add_group_list
.into_iter() .into_iter()
.map(make_select_option) .map(make_select_option)
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
</select> </Select>
</td> </td>
<td> <td>
<button onclick=self.link.callback( <button onclick=self.link.callback(

View File

@ -5,5 +5,6 @@ pub mod create_user;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod router; pub mod router;
pub mod select;
pub mod user_details; pub mod user_details;
pub mod user_table; pub mod user_table;

View File

@ -0,0 +1,112 @@
use yew::{html::ChangeData, prelude::*};
pub struct Select {
link: ComponentLink<Self>,
props: SelectProps,
}
#[derive(yew::Properties, Clone, PartialEq, Debug)]
pub struct SelectProps {
pub children: ChildrenWithProps<SelectOption>,
pub on_selection_change: Callback<Option<SelectOptionProps>>,
}
pub enum SelectMsg {
OnSelectChange(ChangeData),
}
impl Select {
fn get_nth_child_props(&self, nth: i32) -> Option<SelectOptionProps> {
if nth == -1 {
return None;
}
self.props
.children
.iter()
.nth(nth as usize)
.map(|child| child.props)
}
}
impl Component for Select {
type Message = SelectMsg;
type Properties = SelectProps;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
let res = Self { link, props };
res.props
.on_selection_change
.emit(res.get_nth_child_props(0));
res
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
SelectMsg::OnSelectChange(data) => match data {
ChangeData::Select(e) => {
self.props
.on_selection_change
.emit(self.get_nth_child_props(e.selected_index()));
}
_ => unreachable!(),
},
}
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props.children.len() != props.children.len() {
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 {
html! {
<select
onchange=self.link.callback(SelectMsg::OnSelectChange)>
{ self.props.children.clone() }
</select>
}
}
}
pub struct SelectOption {
props: SelectOptionProps,
}
#[derive(yew::Properties, Clone, PartialEq)]
pub struct SelectOptionProps {
pub value: String,
pub text: String,
}
impl Component for SelectOption {
type Message = ();
type Properties = SelectOptionProps;
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
Self { props }
}
fn update(&mut self, _: Self::Message) -> ShouldRender {
false
}
fn change(&mut self, props: Self::Properties) -> ShouldRender {
if self.props != props {
self.props = props;
true
} else {
false
}
}
fn view(&self) -> Html {
html! {
<option value=self.props.value.clone()>
{&self.props.text}
</option>
}
}
}

View File

@ -279,7 +279,7 @@ impl UserDetails {
let id = group.id; 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.clone()> <tr key="groupRow_".to_string() + &display_name>
<td>{&group.display_name}</td> <td>{&group.display_name}</td>
{ if self.is_admin { html! { { if self.is_admin { html! {
<td><button onclick=self.link.callback(move |_| Msg::SubmitRemoveGroup(Group{id, display_name: display_name.clone()}))>{"-"}</button></td> <td><button onclick=self.link.callback(move |_| Msg::SubmitRemoveGroup(Group{id, display_name: display_name.clone()}))>{"-"}</button></td>