mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	app: Implement group management
Except group creation
This commit is contained in:
		
							parent
							
								
									3dc0bce421
								
							
						
					
					
						commit
						2a97a2aedf
					
				
							
								
								
									
										5
									
								
								app/queries/delete_group.graphql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/queries/delete_group.graphql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
mutation DeleteGroupQuery($groupId: Int!) {
 | 
			
		||||
  deleteGroup(groupId: $groupId) {
 | 
			
		||||
    ok
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								app/queries/get_group_details.graphql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/queries/get_group_details.graphql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
query GetGroupDetails($id: Int!) {
 | 
			
		||||
  group(groupId: $id) {
 | 
			
		||||
    id
 | 
			
		||||
    displayName
 | 
			
		||||
    users {
 | 
			
		||||
      id
 | 
			
		||||
      displayName
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -8,3 +8,9 @@ query ListUsersQuery($filters: RequestFilter) {
 | 
			
		||||
    creationDate
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
query ListUserNames($filters: RequestFilter) {
 | 
			
		||||
  users(filters: $filters) {
 | 
			
		||||
    id
 | 
			
		||||
    displayName
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										202
									
								
								app/src/components/add_group_member.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								app/src/components/add_group_member.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::select::{Select, SelectOption, SelectOptionProps},
 | 
			
		||||
    infra::api::HostService,
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use std::collections::HashSet;
 | 
			
		||||
use yew::{
 | 
			
		||||
    prelude::*,
 | 
			
		||||
    services::{fetch::FetchTask, ConsoleService},
 | 
			
		||||
};
 | 
			
		||||
use yewtil::NeqAssign;
 | 
			
		||||
 | 
			
		||||
#[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/list_users.graphql",
 | 
			
		||||
    response_derives = "Debug,Clone,PartialEq,Eq,Hash",
 | 
			
		||||
    variables_derives = "Clone",
 | 
			
		||||
    custom_scalars_module = "crate::infra::graphql"
 | 
			
		||||
)]
 | 
			
		||||
pub struct ListUserNames;
 | 
			
		||||
pub type User = list_user_names::ListUserNamesUsers;
 | 
			
		||||
 | 
			
		||||
pub struct AddGroupMemberComponent {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    props: Props,
 | 
			
		||||
    /// The list of existing users, initially not loaded.
 | 
			
		||||
    user_list: Option<Vec<User>>,
 | 
			
		||||
    /// The currently selected user.
 | 
			
		||||
    selected_user: Option<User>,
 | 
			
		||||
    // Used to keep the request alive long enough.
 | 
			
		||||
    _task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    UserListResponse(Result<list_user_names::ResponseData>),
 | 
			
		||||
    SubmitAddMember,
 | 
			
		||||
    AddMemberResponse(Result<add_user_to_group::ResponseData>),
 | 
			
		||||
    SelectionChanged(Option<SelectOptionProps>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq)]
 | 
			
		||||
pub struct Props {
 | 
			
		||||
    pub group_id: i64,
 | 
			
		||||
    pub users: Vec<User>,
 | 
			
		||||
    pub on_user_added_to_group: Callback<User>,
 | 
			
		||||
    pub on_error: Callback<Error>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AddGroupMemberComponent {
 | 
			
		||||
    fn get_user_list(&mut self) {
 | 
			
		||||
        self._task = HostService::graphql_query::<ListUserNames>(
 | 
			
		||||
            list_user_names::Variables { filters: None },
 | 
			
		||||
            self.link.callback(Msg::UserListResponse),
 | 
			
		||||
            "Error trying to fetch user list",
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|e| {
 | 
			
		||||
            ConsoleService::log(&e.to_string());
 | 
			
		||||
            e
 | 
			
		||||
        })
 | 
			
		||||
        .ok();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn submit_add_member(&mut self) -> Result<bool> {
 | 
			
		||||
        let user_id = match self.selected_user.clone() {
 | 
			
		||||
            None => return Ok(false),
 | 
			
		||||
            Some(user) => user.id,
 | 
			
		||||
        };
 | 
			
		||||
        self._task = HostService::graphql_query::<AddUserToGroup>(
 | 
			
		||||
            add_user_to_group::Variables {
 | 
			
		||||
                user: user_id,
 | 
			
		||||
                group: self.props.group_id,
 | 
			
		||||
            },
 | 
			
		||||
            self.link.callback(Msg::AddMemberResponse),
 | 
			
		||||
            "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: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::UserListResponse(response) => {
 | 
			
		||||
                self.user_list = Some(response?.users);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SubmitAddMember => return self.submit_add_member(),
 | 
			
		||||
            Msg::AddMemberResponse(response) => {
 | 
			
		||||
                response?;
 | 
			
		||||
                let user = self
 | 
			
		||||
                    .selected_user
 | 
			
		||||
                    .as_ref()
 | 
			
		||||
                    .expect("Could not get selected user")
 | 
			
		||||
                    .clone();
 | 
			
		||||
                // Remove the user from the dropdown.
 | 
			
		||||
                self.props.on_user_added_to_group.emit(user);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SelectionChanged(option_props) => {
 | 
			
		||||
                self.selected_user = option_props.map(|u| User {
 | 
			
		||||
                    id: u.value,
 | 
			
		||||
                    display_name: u.text,
 | 
			
		||||
                });
 | 
			
		||||
                return Ok(false);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_selectable_user_list(&self, user_list: &[User]) -> Vec<User> {
 | 
			
		||||
        let user_groups = self.props.users.iter().collect::<HashSet<_>>();
 | 
			
		||||
        user_list
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|u| !user_groups.contains(u))
 | 
			
		||||
            .map(Clone::clone)
 | 
			
		||||
            .collect()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for AddGroupMemberComponent {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
        let mut res = Self {
 | 
			
		||||
            link,
 | 
			
		||||
            props,
 | 
			
		||||
            user_list: None,
 | 
			
		||||
            selected_user: None,
 | 
			
		||||
            _task: None,
 | 
			
		||||
        };
 | 
			
		||||
        res.get_user_list();
 | 
			
		||||
        res
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        match self.handle_msg(msg) {
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                ConsoleService::error(&e.to_string());
 | 
			
		||||
                self.props.on_error.emit(e);
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            Ok(b) => b,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.props.neq_assign(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        if let Some(user_list) = &self.user_list {
 | 
			
		||||
            let to_add_user_list = self.get_selectable_user_list(user_list);
 | 
			
		||||
            #[allow(unused_braces)]
 | 
			
		||||
            let make_select_option = |user: User| {
 | 
			
		||||
                html_nested! {
 | 
			
		||||
                    <SelectOption value=user.id.clone() text=user.display_name.clone() key=user.id />
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            html! {
 | 
			
		||||
            <>
 | 
			
		||||
              <td>
 | 
			
		||||
                <Select on_selection_change=self.link.callback(Msg::SelectionChanged)>
 | 
			
		||||
                  {
 | 
			
		||||
                    to_add_user_list
 | 
			
		||||
                        .into_iter()
 | 
			
		||||
                        .map(make_select_option)
 | 
			
		||||
                        .collect::<Vec<_>>()
 | 
			
		||||
                  }
 | 
			
		||||
                </Select>
 | 
			
		||||
              </td>
 | 
			
		||||
                  <td>
 | 
			
		||||
                    <button
 | 
			
		||||
                      class="btn btn-success"
 | 
			
		||||
                      onclick=self.link.callback(|_| Msg::SubmitAddMember)>
 | 
			
		||||
                      {"Add"}
 | 
			
		||||
                    </button>
 | 
			
		||||
                  </td>
 | 
			
		||||
            </>
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            html! {
 | 
			
		||||
              <>
 | 
			
		||||
                <td>{"Loading groups"}</td>
 | 
			
		||||
                <td></td>
 | 
			
		||||
              </>
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,12 +2,13 @@ use crate::{
 | 
			
		||||
    components::{
 | 
			
		||||
        change_password::ChangePasswordForm,
 | 
			
		||||
        create_user::CreateUserForm,
 | 
			
		||||
        group_details::GroupDetails,
 | 
			
		||||
        group_table::GroupTable,
 | 
			
		||||
        login::LoginForm,
 | 
			
		||||
        logout::LogoutButton,
 | 
			
		||||
        router::{AppRoute, NavButton},
 | 
			
		||||
        user_details::UserDetails,
 | 
			
		||||
        user_table::UserTable,
 | 
			
		||||
        group_table::GroupTable,
 | 
			
		||||
    },
 | 
			
		||||
    infra::cookies::get_cookie,
 | 
			
		||||
};
 | 
			
		||||
@ -104,36 +105,28 @@ impl Component for App {
 | 
			
		||||
                              <LoginForm on_logged_in=link.callback(Msg::Login)/>
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::CreateUser => html! {
 | 
			
		||||
                              <div>
 | 
			
		||||
                                <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
 | 
			
		||||
                              <CreateUserForm/>
 | 
			
		||||
                              </div>
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::Index | AppRoute::ListUsers => html! {
 | 
			
		||||
                              <div>
 | 
			
		||||
                                <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
 | 
			
		||||
                                <UserTable />
 | 
			
		||||
                                <NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
 | 
			
		||||
                              </div>
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::ListGroups => html! {
 | 
			
		||||
                              <div>
 | 
			
		||||
                                <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
 | 
			
		||||
                                <GroupTable />
 | 
			
		||||
                                //<NavButton classes="btn btn-primary" route=AppRoute::CreateUser>{"Create a user"}</NavButton>
 | 
			
		||||
                              </div>
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::GroupDetails(group_id) => html! {
 | 
			
		||||
                              <GroupDetails group_id=group_id />
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::UserDetails(username) => html! {
 | 
			
		||||
                              <div>
 | 
			
		||||
                                <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
 | 
			
		||||
                              <UserDetails username=username.clone() is_admin=is_admin />
 | 
			
		||||
                              </div>
 | 
			
		||||
                          },
 | 
			
		||||
                          AppRoute::ChangePassword(username) => html! {
 | 
			
		||||
                              <div>
 | 
			
		||||
                                <LogoutButton on_logged_out=link.callback(|_| Msg::Logout) />
 | 
			
		||||
                              <ChangePasswordForm username=username.clone() is_admin=is_admin />
 | 
			
		||||
                              </div>
 | 
			
		||||
                          }
 | 
			
		||||
                      }
 | 
			
		||||
                  })
 | 
			
		||||
@ -182,10 +175,9 @@ impl App {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_banner(&self) -> Html {
 | 
			
		||||
        if !self.is_admin() {
 | 
			
		||||
            html!{}
 | 
			
		||||
        } else {
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
            {if self.is_admin() { html! {
 | 
			
		||||
              <>
 | 
			
		||||
                <div>
 | 
			
		||||
                  <NavButton
 | 
			
		||||
@ -202,7 +194,11 @@ impl App {
 | 
			
		||||
                  </NavButton>
 | 
			
		||||
                </div>
 | 
			
		||||
              </>
 | 
			
		||||
            }
 | 
			
		||||
            } } else { html!{} } }
 | 
			
		||||
            {if self.user_info.is_some() { html! {
 | 
			
		||||
              <LogoutButton on_logged_out=self.link.callback(|_| Msg::Logout) />
 | 
			
		||||
            }} else { html! {} }}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										158
									
								
								app/src/components/delete_group.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								app/src/components/delete_group.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,158 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::group_table::Group,
 | 
			
		||||
    infra::{api::HostService, modal::Modal},
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew::services::fetch::FetchTask;
 | 
			
		||||
use yewtil::NeqAssign;
 | 
			
		||||
 | 
			
		||||
#[derive(GraphQLQuery)]
 | 
			
		||||
#[graphql(
 | 
			
		||||
    schema_path = "../schema.graphql",
 | 
			
		||||
    query_path = "queries/delete_group.graphql",
 | 
			
		||||
    response_derives = "Debug",
 | 
			
		||||
    custom_scalars_module = "crate::infra::graphql"
 | 
			
		||||
)]
 | 
			
		||||
pub struct DeleteGroupQuery;
 | 
			
		||||
 | 
			
		||||
pub struct DeleteGroup {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    props: DeleteGroupProps,
 | 
			
		||||
    node_ref: NodeRef,
 | 
			
		||||
    modal: Option<Modal>,
 | 
			
		||||
    _task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq, Debug)]
 | 
			
		||||
pub struct DeleteGroupProps {
 | 
			
		||||
    pub group: Group,
 | 
			
		||||
    pub on_group_deleted: Callback<i64>,
 | 
			
		||||
    pub on_error: Callback<Error>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    ClickedDeleteGroup,
 | 
			
		||||
    ConfirmDeleteGroup,
 | 
			
		||||
    DismissModal,
 | 
			
		||||
    DeleteGroupResponse(Result<delete_group_query::ResponseData>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for DeleteGroup {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = DeleteGroupProps;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> 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::<web_sys::Element>()
 | 
			
		||||
                    .expect("Modal node is not an element"),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ClickedDeleteGroup => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").show();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::ConfirmDeleteGroup => {
 | 
			
		||||
                self.update(Msg::DismissModal);
 | 
			
		||||
                self._task = HostService::graphql_query::<DeleteGroupQuery>(
 | 
			
		||||
                    delete_group_query::Variables {
 | 
			
		||||
                        group_id: self.props.group.id,
 | 
			
		||||
                    },
 | 
			
		||||
                    self.link.callback(Msg::DeleteGroupResponse),
 | 
			
		||||
                    "Error trying to delete group",
 | 
			
		||||
                )
 | 
			
		||||
                .map_err(|e| self.props.on_error.emit(e))
 | 
			
		||||
                .ok();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DismissModal => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").hide();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DeleteGroupResponse(response) => {
 | 
			
		||||
                if let Err(e) = response {
 | 
			
		||||
                    self.props.on_error.emit(e);
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.props.on_group_deleted.emit(self.props.group.id);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.props.neq_assign(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            onclick=self.link.callback(|_| Msg::ClickedDeleteGroup)>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Delete group" />
 | 
			
		||||
          </button>
 | 
			
		||||
          {self.show_modal()}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DeleteGroup {
 | 
			
		||||
    fn show_modal(&self) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <div
 | 
			
		||||
            class="modal fade"
 | 
			
		||||
            id="exampleModal".to_string() + &self.props.group.id.to_string()
 | 
			
		||||
            tabindex="-1"
 | 
			
		||||
            aria-labelledby="exampleModalLabel"
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
            ref=self.node_ref.clone()>
 | 
			
		||||
            <div class="modal-dialog">
 | 
			
		||||
              <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
                  <h5 class="modal-title" id="exampleModalLabel">{"Delete group?"}</h5>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn-close"
 | 
			
		||||
                    aria-label="Close"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::DismissModal) />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                <span>
 | 
			
		||||
                  {"Are you sure you want to delete group "}
 | 
			
		||||
                  <b>{&self.props.group.display_name}</b>{"?"}
 | 
			
		||||
                </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn btn-secondary"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::DismissModal)>
 | 
			
		||||
                      {"Cancel"}
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onclick=self.link.callback(|_| Msg::ConfirmDeleteGroup)
 | 
			
		||||
                    class="btn btn-danger">{"Yes, I'm sure"}</button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										236
									
								
								app/src/components/group_details.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								app/src/components/group_details.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,236 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::{
 | 
			
		||||
        add_group_member::{self, AddGroupMemberComponent},
 | 
			
		||||
        remove_user_from_group::RemoveUserFromGroupComponent,
 | 
			
		||||
        router::{AppRoute, Link},
 | 
			
		||||
    },
 | 
			
		||||
    infra::api::HostService,
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{bail, Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::{
 | 
			
		||||
    prelude::*,
 | 
			
		||||
    services::{fetch::FetchTask, ConsoleService},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(GraphQLQuery)]
 | 
			
		||||
#[graphql(
 | 
			
		||||
    schema_path = "../schema.graphql",
 | 
			
		||||
    query_path = "queries/get_group_details.graphql",
 | 
			
		||||
    response_derives = "Debug, Hash, PartialEq, Eq, Clone",
 | 
			
		||||
    custom_scalars_module = "crate::infra::graphql"
 | 
			
		||||
)]
 | 
			
		||||
pub struct GetGroupDetails;
 | 
			
		||||
 | 
			
		||||
pub type Group = get_group_details::GetGroupDetailsGroup;
 | 
			
		||||
pub type User = get_group_details::GetGroupDetailsGroupUsers;
 | 
			
		||||
pub type AddGroupMemberUser = add_group_member::User;
 | 
			
		||||
 | 
			
		||||
pub struct GroupDetails {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    props: Props,
 | 
			
		||||
    /// The group info. If none, the error is in `error`. If `error` is None, then we haven't
 | 
			
		||||
    /// received the server response yet.
 | 
			
		||||
    group: Option<Group>,
 | 
			
		||||
    /// Error message displayed to the user.
 | 
			
		||||
    error: Option<Error>,
 | 
			
		||||
    // Used to keep the request alive long enough.
 | 
			
		||||
    _task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// State machine describing the possible transitions of the component state.
 | 
			
		||||
/// It starts out by fetching the user's details from the backend when loading.
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    /// Received the group details response, either the group data or an error.
 | 
			
		||||
    GroupDetailsResponse(Result<get_group_details::ResponseData>),
 | 
			
		||||
    OnError(Error),
 | 
			
		||||
    OnUserAddedToGroup(AddGroupMemberUser),
 | 
			
		||||
    OnUserRemovedFromGroup((String, i64)),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq)]
 | 
			
		||||
pub struct Props {
 | 
			
		||||
    pub group_id: i64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GroupDetails {
 | 
			
		||||
    fn get_group_details(&mut self) {
 | 
			
		||||
        self._task = HostService::graphql_query::<GetGroupDetails>(
 | 
			
		||||
            get_group_details::Variables {
 | 
			
		||||
                id: self.props.group_id,
 | 
			
		||||
            },
 | 
			
		||||
            self.link.callback(Msg::GroupDetailsResponse),
 | 
			
		||||
            "Error trying to fetch group details",
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|e| {
 | 
			
		||||
            ConsoleService::log(&e.to_string());
 | 
			
		||||
            e
 | 
			
		||||
        })
 | 
			
		||||
        .ok();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::GroupDetailsResponse(response) => match response {
 | 
			
		||||
                Ok(group) => self.group = Some(group.group),
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    self.group = None;
 | 
			
		||||
                    bail!("Error getting user details: {}", e);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Msg::OnError(e) => return Err(e),
 | 
			
		||||
            Msg::OnUserAddedToGroup(user) => {
 | 
			
		||||
                self.group.as_mut().unwrap().users.push(User {
 | 
			
		||||
                    id: user.id,
 | 
			
		||||
                    display_name: user.display_name,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            Msg::OnUserRemovedFromGroup((user_id, _)) => {
 | 
			
		||||
                self.group
 | 
			
		||||
                    .as_mut()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .users
 | 
			
		||||
                    .retain(|u| u.id != user_id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_messages(&self, error: &Option<Error>) -> Html {
 | 
			
		||||
        if let Some(e) = error {
 | 
			
		||||
            html! {
 | 
			
		||||
              <div class="alert alert-danger">
 | 
			
		||||
                <span>{"Error: "}{e.to_string()}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            html! {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_user_list(&self, g: &Group) -> Html {
 | 
			
		||||
        let make_user_row = |user: &User| {
 | 
			
		||||
            let user_id = user.id.clone();
 | 
			
		||||
            let display_name = user.display_name.clone();
 | 
			
		||||
            html! {
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <Link route=AppRoute::UserDetails(user_id.clone())>
 | 
			
		||||
                    {user_id.clone()}
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{display_name}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <RemoveUserFromGroupComponent
 | 
			
		||||
                    username=user_id
 | 
			
		||||
                    group_id=g.id
 | 
			
		||||
                    on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
 | 
			
		||||
                    on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        html! {
 | 
			
		||||
        <div>
 | 
			
		||||
          <h3>{"Members"}</h3>
 | 
			
		||||
          <div class="table-responsive">
 | 
			
		||||
            <table class="table table-striped">
 | 
			
		||||
              <thead>
 | 
			
		||||
                <tr key="headerRow">
 | 
			
		||||
                  <th>{"User Id"}</th>
 | 
			
		||||
                  <th>{"Display name"}</th>
 | 
			
		||||
                  <th></th>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </thead>
 | 
			
		||||
              <tbody>
 | 
			
		||||
                {if g.users.is_empty() {
 | 
			
		||||
                  html! {
 | 
			
		||||
                    <tr key="EmptyRow">
 | 
			
		||||
                      <td>{"No members"}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  html! {<>{g.users.iter().map(make_user_row).collect::<Vec<_>>()}</>}
 | 
			
		||||
                }}
 | 
			
		||||
                <hr/>
 | 
			
		||||
                <tr key="groupToAddRow">
 | 
			
		||||
                  {self.view_add_user_button(g)}
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_add_user_button(&self, g: &Group) -> Html {
 | 
			
		||||
        let users: Vec<_> = g
 | 
			
		||||
            .users
 | 
			
		||||
            .iter()
 | 
			
		||||
            .map(|u| AddGroupMemberUser {
 | 
			
		||||
                id: u.id.clone(),
 | 
			
		||||
                display_name: u.display_name.clone(),
 | 
			
		||||
            })
 | 
			
		||||
            .collect();
 | 
			
		||||
        html! {
 | 
			
		||||
            <AddGroupMemberComponent
 | 
			
		||||
                group_id=g.id
 | 
			
		||||
                users=users
 | 
			
		||||
                on_error=self.link.callback(Msg::OnError)
 | 
			
		||||
                on_user_added_to_group=self.link.callback(Msg::OnUserAddedToGroup)/>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for GroupDetails {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
        let mut table = Self {
 | 
			
		||||
            link,
 | 
			
		||||
            props,
 | 
			
		||||
            _task: None,
 | 
			
		||||
            group: None,
 | 
			
		||||
            error: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.get_group_details();
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        self.error = None;
 | 
			
		||||
        match self.handle_msg(msg) {
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                ConsoleService::error(&e.to_string());
 | 
			
		||||
                self.error = Some(e);
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            Ok(b) => b,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, _: Self::Properties) -> ShouldRender {
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        match (&self.group, &self.error) {
 | 
			
		||||
            (None, None) => html! {{"Loading..."}},
 | 
			
		||||
            (None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
 | 
			
		||||
            (Some(u), error) => {
 | 
			
		||||
                html! {
 | 
			
		||||
                    <div>
 | 
			
		||||
                        /*
 | 
			
		||||
                      <GroupDetailsForm
 | 
			
		||||
                        user=u.clone()
 | 
			
		||||
                        on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
                        */
 | 
			
		||||
                      {self.view_user_list(u)}
 | 
			
		||||
                      {self.view_messages(error)}
 | 
			
		||||
                    </div>
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    //components::{
 | 
			
		||||
    //    delete_group::DeleteGroup,
 | 
			
		||||
    //    router::{AppRoute, Link},
 | 
			
		||||
    //},
 | 
			
		||||
    components::{
 | 
			
		||||
        delete_group::DeleteGroup,
 | 
			
		||||
        router::{AppRoute, Link},
 | 
			
		||||
    },
 | 
			
		||||
    infra::api::HostService,
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
@ -14,14 +14,14 @@ use yew::services::{fetch::FetchTask, ConsoleService};
 | 
			
		||||
#[graphql(
 | 
			
		||||
    schema_path = "../schema.graphql",
 | 
			
		||||
    query_path = "queries/get_group_list.graphql",
 | 
			
		||||
    response_derives = "Debug",
 | 
			
		||||
    response_derives = "Debug,Clone,PartialEq",
 | 
			
		||||
    custom_scalars_module = "crate::infra::graphql"
 | 
			
		||||
)]
 | 
			
		||||
pub struct GetGroupList;
 | 
			
		||||
 | 
			
		||||
use get_group_list::ResponseData;
 | 
			
		||||
 | 
			
		||||
type Group = get_group_list::GetGroupListGroups;
 | 
			
		||||
pub type Group = get_group_list::GetGroupListGroups;
 | 
			
		||||
 | 
			
		||||
pub struct GroupTable {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
@ -116,9 +116,8 @@ impl GroupTable {
 | 
			
		||||
                  <table class="table table-striped">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th>{"Group ID"}</th>
 | 
			
		||||
                        <th>{"Display name"}</th>
 | 
			
		||||
                        //<th>{"Delete"}</th>
 | 
			
		||||
                        <th>{"Groups"}</th>
 | 
			
		||||
                        <th>{"Delete"}</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
@ -136,16 +135,18 @@ impl GroupTable {
 | 
			
		||||
 | 
			
		||||
    fn view_group(&self, group: &Group) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <tr key=group.id.clone()>
 | 
			
		||||
              //<td><Link route=AppRoute::GroupDetails(group.id.clone())>{&group.id}</Link></td>
 | 
			
		||||
              <td>{&group.id}</td>
 | 
			
		||||
              <td>{&group.display_name}</td>
 | 
			
		||||
              //<td>
 | 
			
		||||
              //  <DeleteGroup
 | 
			
		||||
              //    groupname=group.id.clone()
 | 
			
		||||
              //    on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
 | 
			
		||||
              //    on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
              //</td>
 | 
			
		||||
          <tr key=group.id>
 | 
			
		||||
              <td>
 | 
			
		||||
                <Link route=AppRoute::GroupDetails(group.id)>
 | 
			
		||||
                  {&group.display_name}
 | 
			
		||||
                </Link>
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <DeleteGroup
 | 
			
		||||
                  group=group.clone()
 | 
			
		||||
                  on_group_deleted=self.link.callback(Msg::OnGroupDeleted)
 | 
			
		||||
                  on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
              </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,11 @@
 | 
			
		||||
pub mod add_group_member;
 | 
			
		||||
pub mod add_user_to_group;
 | 
			
		||||
pub mod app;
 | 
			
		||||
pub mod change_password;
 | 
			
		||||
pub mod create_user;
 | 
			
		||||
pub mod delete_group;
 | 
			
		||||
pub mod delete_user;
 | 
			
		||||
pub mod group_details;
 | 
			
		||||
pub mod group_table;
 | 
			
		||||
pub mod login;
 | 
			
		||||
pub mod logout;
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
use crate::{components::user_details::Group, infra::api::HostService};
 | 
			
		||||
use crate::infra::api::HostService;
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::{
 | 
			
		||||
@ -26,9 +26,8 @@ pub struct RemoveUserFromGroupComponent {
 | 
			
		||||
#[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 group_id: i64,
 | 
			
		||||
    pub on_user_removed_from_group: Callback<(String, i64)>,
 | 
			
		||||
    pub on_error: Callback<Error>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -39,7 +38,7 @@ pub enum Msg {
 | 
			
		||||
 | 
			
		||||
impl RemoveUserFromGroupComponent {
 | 
			
		||||
    fn submit_remove_group(&mut self) -> Result<bool> {
 | 
			
		||||
        let group = self.props.group.id;
 | 
			
		||||
        let group = self.props.group_id;
 | 
			
		||||
        self._task = HostService::graphql_query::<RemoveUserFromGroup>(
 | 
			
		||||
            remove_user_from_group::Variables {
 | 
			
		||||
                user: self.props.username.clone(),
 | 
			
		||||
@ -63,7 +62,7 @@ impl RemoveUserFromGroupComponent {
 | 
			
		||||
                response?;
 | 
			
		||||
                self.props
 | 
			
		||||
                    .on_user_removed_from_group
 | 
			
		||||
                    .emit(self.props.group.clone());
 | 
			
		||||
                    .emit((self.props.username.clone(), self.props.group_id));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
@ -97,21 +96,12 @@ impl Component for RemoveUserFromGroupComponent {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        let group = &self.props.group;
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
            <td>{&group.display_name}</td>
 | 
			
		||||
            { if self.props.is_admin { html! {
 | 
			
		||||
                <td>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            onclick=self.link.callback(|_| Msg::SubmitRemoveGroup)>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Remove user from group" />
 | 
			
		||||
          </button>
 | 
			
		||||
                </td>
 | 
			
		||||
              }} else { html!{} }
 | 
			
		||||
            }
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,8 @@ pub enum AppRoute {
 | 
			
		||||
    UserDetails(String),
 | 
			
		||||
    #[to = "/groups"]
 | 
			
		||||
    ListGroups,
 | 
			
		||||
    #[to = "/group/{group_id}"]
 | 
			
		||||
    GroupDetails(i64),
 | 
			
		||||
    #[to = "/"]
 | 
			
		||||
    Index,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use crate::{
 | 
			
		||||
    components::{
 | 
			
		||||
        add_user_to_group::AddUserToGroupComponent,
 | 
			
		||||
        remove_user_from_group::RemoveUserFromGroupComponent,
 | 
			
		||||
        router::{AppRoute, NavButton},
 | 
			
		||||
        router::{AppRoute, Link, NavButton},
 | 
			
		||||
        user_details_form::UserDetailsForm,
 | 
			
		||||
    },
 | 
			
		||||
    infra::api::HostService,
 | 
			
		||||
@ -45,7 +45,7 @@ pub enum Msg {
 | 
			
		||||
    UserDetailsResponse(Result<get_user_details::ResponseData>),
 | 
			
		||||
    OnError(Error),
 | 
			
		||||
    OnUserAddedToGroup(Group),
 | 
			
		||||
    OnUserRemovedFromGroup(Group),
 | 
			
		||||
    OnUserRemovedFromGroup((String, i64)),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq)]
 | 
			
		||||
@ -83,8 +83,12 @@ impl UserDetails {
 | 
			
		||||
            Msg::OnUserAddedToGroup(group) => {
 | 
			
		||||
                self.user.as_mut().unwrap().groups.push(group);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::OnUserRemovedFromGroup(group) => {
 | 
			
		||||
                self.user.as_mut().unwrap().groups.retain(|g| g != &group);
 | 
			
		||||
            Msg::OnUserRemovedFromGroup((_, group_id)) => {
 | 
			
		||||
                self.user
 | 
			
		||||
                    .as_mut()
 | 
			
		||||
                    .unwrap()
 | 
			
		||||
                    .groups
 | 
			
		||||
                    .retain(|g| g.id != group_id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
@ -107,12 +111,24 @@ impl UserDetails {
 | 
			
		||||
            let display_name = group.display_name.clone();
 | 
			
		||||
            html! {
 | 
			
		||||
              <tr key="groupRow_".to_string() + &display_name>
 | 
			
		||||
                {if self.props.is_admin { html! {
 | 
			
		||||
                  <>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <Link route=AppRoute::GroupDetails(group.id)>
 | 
			
		||||
                        {&group.display_name}
 | 
			
		||||
                      </Link>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <RemoveUserFromGroupComponent
 | 
			
		||||
                        username=u.id.clone()
 | 
			
		||||
                  group=group.clone()
 | 
			
		||||
                  is_admin=self.props.is_admin
 | 
			
		||||
                        group_id=group.id
 | 
			
		||||
                        on_user_removed_from_group=self.link.callback(Msg::OnUserRemovedFromGroup)
 | 
			
		||||
                        on_error=self.link.callback(Msg::OnError)/>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </>
 | 
			
		||||
                } } else { html! {
 | 
			
		||||
                  <td>{&group.display_name}</td>
 | 
			
		||||
                } } }
 | 
			
		||||
              </tr>
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user