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::{
components::user_details::{Group, User},
components::{
select::{Select, SelectOption, SelectOptionProps},
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},
};
@ -43,24 +45,20 @@ impl From<GroupListGroup> for Group {
pub struct AddUserToGroupComponent {
link: ComponentLink<Self>,
user: User,
props: Props,
/// The list of existing groups, initially not loaded.
group_list: Option<Vec<Group>>,
/// Whether the "+" button has been clicked.
add_group: bool,
on_error: Callback<Error>,
on_user_added_to_group: Callback<Group>,
/// The currently selected group.
selected_group: Option<Group>,
// Used to keep the request alive long enough.
_task: Option<FetchTask>,
}
pub enum Msg {
AddGroupButtonClicked,
GroupListResponse(Result<get_group_list::ResponseData>),
SubmitAddGroup,
AddGroupResponse(Result<add_user_to_group::ResponseData>),
SelectionChanged(ChangeData),
SelectionChanged(Option<SelectOptionProps>),
}
#[derive(yew::Properties, Clone, PartialEq)]
@ -91,7 +89,7 @@ impl AddUserToGroupComponent {
};
self._task = HostService::graphql_query::<AddUserToGroup>(
add_user_to_group::Variables {
user: self.user.id.clone(),
user: self.props.user.id.clone(),
group: group_id,
},
self.link.callback(Msg::AddGroupResponse),
@ -107,97 +105,62 @@ impl AddUserToGroupComponent {
fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
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);
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)
}
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::<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<_>>();
fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> {
let user_groups = self.props.user.groups.iter().collect::<HashSet<_>>();
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 {
Self {
let mut res = Self {
link,
user: props.user,
props,
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,
}
};
res.get_group_list();
res
}
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);
self.props.on_error.emit(e);
true
}
Ok(b) => b,
@ -205,11 +168,8 @@ impl Component for AddUserToGroupComponent {
}
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();
}
if props.user.groups != self.props.user.groups {
self.props.user = props.user;
true
} else {
false
@ -217,39 +177,25 @@ impl Component for AddUserToGroupComponent {
}
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 {
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| {
html! {
<option value={group.id.to_string()}>{group.display_name}</option>
html_nested! {
<SelectOption value=group.id.to_string() text=group.display_name key=group.id />
}
};
html! {
<>
<td>
<select name="groupToAdd" id="groupToAdd"
onchange=self.link.callback(|e| Msg::SelectionChanged(e))>
<Select on_selection_change=self.link.callback(Msg::SelectionChanged)>
{
to_add_group_list
.into_iter()
.map(make_select_option)
.collect::<Vec<_>>()
}
</select>
</Select>
</td>
<td>
<button onclick=self.link.callback(

View File

@ -5,5 +5,6 @@ pub mod create_user;
pub mod login;
pub mod logout;
pub mod router;
pub mod select;
pub mod user_details;
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 display_name = group.display_name.clone();
html! {
<tr key="groupRow_".to_string() + &display_name.clone()>
<tr key="groupRow_".to_string() + &display_name>
<td>{&group.display_name}</td>
{ if self.is_admin { html! {
<td><button onclick=self.link.callback(move |_| Msg::SubmitRemoveGroup(Group{id, display_name: display_name.clone()}))>{"-"}</button></td>