583 lines
19 KiB
Rust
583 lines
19 KiB
Rust
//! List all users in case an admin views it, list the "self" user otherwise.
|
|
|
|
use enum_map::EnumMap;
|
|
use pslink_locales::I18n;
|
|
use pslink_shared::{
|
|
apirequests::general::{Operation, Ordering},
|
|
apirequests::{
|
|
general::{EditMode, Status},
|
|
users::{Role, UserDelta, UserOverviewColumns, UserRequestForm},
|
|
},
|
|
datatypes::{Lang, User},
|
|
};
|
|
use seed::{a, attrs, div, h1, input, log, p, prelude::*, section, table, td, th, tr, Url, C, IF};
|
|
/*
|
|
* init
|
|
*/
|
|
use crate::unwrap_or_return;
|
|
#[must_use]
|
|
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
let user_edit = match url.next_path_part() {
|
|
Some("create_user") => Some(UserDelta::default()),
|
|
None | Some(_) => None,
|
|
};
|
|
Model {
|
|
users: Vec::new(),
|
|
i18n,
|
|
formconfig: UserRequestForm::default(),
|
|
inputs: EnumMap::default(),
|
|
user_edit,
|
|
last_message: None,
|
|
}
|
|
}
|
|
#[derive(Debug)]
|
|
pub struct Model {
|
|
users: Vec<User>,
|
|
i18n: I18n,
|
|
formconfig: UserRequestForm,
|
|
inputs: EnumMap<UserOverviewColumns, FilterInput>,
|
|
user_edit: Option<UserDelta>,
|
|
last_message: Option<Status>,
|
|
}
|
|
|
|
impl Model {
|
|
/// set the language of this page (part)
|
|
pub fn set_lang(&mut self, l: Lang) {
|
|
self.i18n.set_lang(l);
|
|
}
|
|
}
|
|
|
|
impl Model {
|
|
/// removing all open dialogs (often to open another afterwards).
|
|
fn clean_dialogs(&mut self) {
|
|
self.last_message = None;
|
|
self.user_edit = None;
|
|
}
|
|
}
|
|
|
|
/// A type containing one input field for later use.
|
|
#[derive(Default, Debug, Clone)]
|
|
struct FilterInput {
|
|
filter_input: ElRef<web_sys::HtmlInputElement>,
|
|
}
|
|
|
|
/// The message splits the contained message into messages related to querrying and messages related to editing.
|
|
#[derive(Clone)]
|
|
pub enum Msg {
|
|
Query(UserQueryMsg),
|
|
Edit(UserEditMsg),
|
|
ClearAll,
|
|
}
|
|
|
|
/// All the messages on user Querying
|
|
#[derive(Clone)]
|
|
pub enum UserQueryMsg {
|
|
Fetch,
|
|
FailedToFetchUsers,
|
|
OrderBy(UserOverviewColumns),
|
|
Received(Vec<User>),
|
|
IdFilterChanged(String),
|
|
EmailFilterChanged(String),
|
|
UsernameFilterChanged(String),
|
|
}
|
|
/// All the messages on user editing
|
|
#[derive(Clone)]
|
|
pub enum UserEditMsg {
|
|
EditUserSelected(UserDelta),
|
|
CreateNewUser,
|
|
UserCreated(Status),
|
|
EditUsernameChanged(String),
|
|
EditEmailChanged(String),
|
|
EditPasswordChanged(String),
|
|
MakeAdmin(UserDelta),
|
|
MakeRegular(UserDelta),
|
|
SaveUser,
|
|
FailedToCreateUser,
|
|
}
|
|
/*
|
|
* update
|
|
*/
|
|
|
|
/// Split the update to Query updates and Edit updates
|
|
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|
match msg {
|
|
Msg::Query(msg) => process_query_messages(msg, model, orders),
|
|
Msg::Edit(msg) => process_user_edit_messages(msg, model, orders),
|
|
Msg::ClearAll => {
|
|
model.clean_dialogs();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process all updates related to getting data from the server.
|
|
pub fn process_query_messages(msg: UserQueryMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|
match msg {
|
|
UserQueryMsg::Fetch => {
|
|
orders.skip(); // No need to rerender only after the data is fetched the page has to be rerendered.
|
|
load_users(model.formconfig.clone(), orders);
|
|
}
|
|
UserQueryMsg::OrderBy(column) => {
|
|
model.formconfig.order = model.formconfig.order.as_ref().map_or_else(
|
|
|| {
|
|
Some(Operation {
|
|
column: column.clone(),
|
|
value: Ordering::Ascending,
|
|
})
|
|
},
|
|
|order| {
|
|
Some(Operation {
|
|
column: column.clone(),
|
|
value: if order.column == column && order.value == Ordering::Ascending {
|
|
Ordering::Descending
|
|
} else {
|
|
Ordering::Ascending
|
|
},
|
|
})
|
|
},
|
|
);
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
|
|
model.users.sort_by(match column {
|
|
UserOverviewColumns::Id => |o: &User, t: &User| o.id.cmp(&t.id),
|
|
UserOverviewColumns::Username => |o: &User, t: &User| o.username.cmp(&t.username),
|
|
UserOverviewColumns::Email => |o: &User, t: &User| o.email.cmp(&t.email),
|
|
});
|
|
}
|
|
UserQueryMsg::Received(response) => {
|
|
model.users = response;
|
|
}
|
|
UserQueryMsg::IdFilterChanged(s) => {
|
|
log!("Filter is: ", &s);
|
|
let sanit = s.chars().filter(|x| x.is_numeric()).collect();
|
|
model.formconfig.filter[UserOverviewColumns::Id].sieve = sanit;
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
}
|
|
UserQueryMsg::UsernameFilterChanged(s) => {
|
|
log!("Filter is: ", &s);
|
|
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
|
model.formconfig.filter[UserOverviewColumns::Username].sieve = sanit;
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
}
|
|
UserQueryMsg::EmailFilterChanged(s) => {
|
|
log!("Filter is: ", &s);
|
|
// FIXME: Sanitazion does not work for @
|
|
let sanit = s.chars().filter(|x| x.is_alphanumeric()).collect();
|
|
model.formconfig.filter[UserOverviewColumns::Email].sieve = sanit;
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
}
|
|
|
|
UserQueryMsg::FailedToFetchUsers => {
|
|
log!("Failed to fetch users");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Load the list of users from the server.
|
|
fn load_users(data: UserRequestForm, orders: &mut impl Orders<Msg>) {
|
|
orders.perform_cmd(async {
|
|
let data = data;
|
|
// create the request
|
|
let request = unwrap_or_return!(
|
|
Request::new("/admin/json/list_users/")
|
|
.method(Method::Post)
|
|
.json(&data),
|
|
Msg::Query(UserQueryMsg::FailedToFetchUsers)
|
|
);
|
|
// request and get response
|
|
let response = unwrap_or_return!(
|
|
fetch(request).await,
|
|
Msg::Query(UserQueryMsg::FailedToFetchUsers)
|
|
);
|
|
// check the response status
|
|
let response = unwrap_or_return!(
|
|
response.check_status(),
|
|
Msg::Query(UserQueryMsg::FailedToFetchUsers)
|
|
);
|
|
// deserialize the users list
|
|
let users: Vec<User> = unwrap_or_return!(
|
|
response.json().await,
|
|
Msg::Query(UserQueryMsg::FailedToFetchUsers)
|
|
);
|
|
|
|
Msg::Query(UserQueryMsg::Received(users))
|
|
});
|
|
}
|
|
|
|
/// Process all the messages related to editing users.
|
|
pub fn process_user_edit_messages(
|
|
msg: UserEditMsg,
|
|
model: &mut Model,
|
|
orders: &mut impl Orders<Msg>,
|
|
) {
|
|
match msg {
|
|
UserEditMsg::EditUserSelected(user) => {
|
|
model.clean_dialogs();
|
|
model.user_edit = Some(user);
|
|
}
|
|
UserEditMsg::CreateNewUser => {
|
|
model.clean_dialogs();
|
|
model.user_edit = Some(UserDelta::default());
|
|
}
|
|
UserEditMsg::EditUsernameChanged(s) => {
|
|
if let Some(ref mut ue) = model.user_edit {
|
|
ue.username = s;
|
|
};
|
|
}
|
|
UserEditMsg::EditEmailChanged(s) => {
|
|
if let Some(ref mut ue) = model.user_edit {
|
|
ue.email = s;
|
|
};
|
|
}
|
|
UserEditMsg::EditPasswordChanged(s) => {
|
|
if let Some(ref mut ue) = model.user_edit {
|
|
ue.password = Some(s);
|
|
};
|
|
}
|
|
UserEditMsg::SaveUser => {
|
|
let data = model
|
|
.user_edit
|
|
.take()
|
|
.expect("A user should allways be there on save");
|
|
log!("Saving User: ", &data.username);
|
|
save_user(data, orders);
|
|
}
|
|
UserEditMsg::FailedToCreateUser => {
|
|
log!("Failed to create user");
|
|
}
|
|
UserEditMsg::UserCreated(u) => {
|
|
log!(u, "created user");
|
|
model.last_message = Some(u);
|
|
model.user_edit = None;
|
|
orders.send_msg(Msg::Query(UserQueryMsg::Fetch));
|
|
}
|
|
UserEditMsg::MakeAdmin(user) | UserEditMsg::MakeRegular(user) => {
|
|
update_privileges(user, orders);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Update the role of a user - this toggles between admin and regular.
|
|
fn update_privileges(user: UserDelta, orders: &mut impl Orders<Msg>) {
|
|
orders.perform_cmd(async {
|
|
let data = user;
|
|
// create the request
|
|
let request = unwrap_or_return!(
|
|
Request::new("/admin/json/update_privileges/")
|
|
.method(Method::Post)
|
|
.json(&data),
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// perform the request and get the response
|
|
let response = unwrap_or_return!(
|
|
fetch(request).await,
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// check for the status
|
|
let response = unwrap_or_return!(
|
|
response.check_status(),
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// deserialize the response
|
|
let message: Status = unwrap_or_return!(
|
|
response.json().await,
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
|
|
Msg::Edit(UserEditMsg::UserCreated(message))
|
|
});
|
|
}
|
|
|
|
/// Save a new user or edit an existing user
|
|
fn save_user(user: UserDelta, orders: &mut impl Orders<Msg>) {
|
|
orders.perform_cmd(async {
|
|
let data = user;
|
|
// create the request
|
|
let request = unwrap_or_return!(
|
|
Request::new(match data.edit {
|
|
EditMode::Create => "/admin/json/create_user/",
|
|
EditMode::Edit => "/admin/json/update_user/",
|
|
})
|
|
.method(Method::Post)
|
|
.json(&data),
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// perform the request and get the response
|
|
let response = unwrap_or_return!(
|
|
fetch(request).await,
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// check for the status
|
|
let response = unwrap_or_return!(
|
|
response.check_status(),
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
// deserialize the response
|
|
let message: Status = unwrap_or_return!(
|
|
response.json().await,
|
|
Msg::Edit(UserEditMsg::FailedToCreateUser)
|
|
);
|
|
|
|
Msg::Edit(UserEditMsg::UserCreated(message))
|
|
});
|
|
}
|
|
|
|
#[must_use]
|
|
/// View the users page.
|
|
pub fn view(model: &Model, logged_in_user: &User) -> Node<Msg> {
|
|
let lang = model.i18n.clone();
|
|
// shortcut for easier translations
|
|
let t = move |key: &str| lang.translate(key, None);
|
|
section![
|
|
// Clear all dialogs on press of the ESC button.
|
|
keyboard_ev(Ev::KeyDown, |keyboard_event| {
|
|
IF!(keyboard_event.key() == "Escape" => Msg::ClearAll)
|
|
}),
|
|
// display the messages to the user
|
|
if let Some(message) = &model.last_message {
|
|
div![
|
|
C!["message", "center"],
|
|
close_button(),
|
|
match message {
|
|
Status::Success(m) | Status::Error(m) => {
|
|
&m.message
|
|
}
|
|
}
|
|
]
|
|
} else {
|
|
section![]
|
|
},
|
|
// display the table with users
|
|
table![
|
|
// Column Headlines
|
|
view_user_table_head(&t),
|
|
// Add filter fields right below the headlines
|
|
view_user_table_filter_input(model, &t),
|
|
// Add all the users one line for each
|
|
model
|
|
.users
|
|
.iter()
|
|
.map(|u| { view_user(u, logged_in_user, &t) })
|
|
],
|
|
// Display the user edit dialog if available
|
|
if let Some(l) = &model.user_edit {
|
|
edit_or_create_user(l.clone(), t)
|
|
} else {
|
|
section!()
|
|
},
|
|
]
|
|
}
|
|
|
|
/// View the headlines of the table
|
|
fn view_user_table_head<F: Fn(&str) -> String>(t: F) -> Node<Msg> {
|
|
tr![
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Id
|
|
))),
|
|
t("userid")
|
|
],
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Email
|
|
))),
|
|
t("email")
|
|
],
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Username
|
|
))),
|
|
t("username")
|
|
],
|
|
th![t("role")],
|
|
]
|
|
}
|
|
|
|
/// Display the filterboxes below the headlines
|
|
fn view_user_table_filter_input<F: Fn(&str) -> String>(model: &Model, t: F) -> Node<Msg> {
|
|
tr![
|
|
C!["filters"],
|
|
td![input![
|
|
attrs! {
|
|
At::Value => &model.formconfig.filter[UserOverviewColumns::Id].sieve,
|
|
At::Type => "search",
|
|
At::Placeholder => t("search-placeholder")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Query(UserQueryMsg::IdFilterChanged(s))
|
|
}),
|
|
el_ref(&model.inputs[UserOverviewColumns::Id].filter_input),
|
|
]],
|
|
td![input![
|
|
attrs! {At::Value =>
|
|
&model
|
|
.formconfig.filter[UserOverviewColumns::Email].sieve,
|
|
At::Type => "search",
|
|
At::Placeholder => t("search-placeholder")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Query(UserQueryMsg::EmailFilterChanged(s))
|
|
}),
|
|
el_ref(&model.inputs[UserOverviewColumns::Email].filter_input),
|
|
]],
|
|
td![input![
|
|
attrs! {At::Value =>
|
|
&model
|
|
.formconfig.filter[UserOverviewColumns::Username].sieve,
|
|
At::Type => "search",
|
|
At::Placeholder => t("search-placeholder")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Query(UserQueryMsg::UsernameFilterChanged(s))
|
|
}),
|
|
el_ref(&model.inputs[UserOverviewColumns::Username].filter_input),
|
|
]],
|
|
td![],
|
|
]
|
|
}
|
|
|
|
/// Display one user-line of the table
|
|
fn view_user<F: Fn(&str) -> String>(l: &User, logged_in_user: &User, t: F) -> Node<Msg> {
|
|
let user = UserDelta::from(l.clone());
|
|
tr![
|
|
{
|
|
let user = user.clone();
|
|
ev(Ev::Click, |_| {
|
|
Msg::Edit(UserEditMsg::EditUserSelected(user))
|
|
})
|
|
},
|
|
match l.role {
|
|
Role::NotAuthenticated | Role::Disabled => C!("inactive"),
|
|
Role::Regular => C!("regular"),
|
|
Role::Admin => C!("admin"),
|
|
},
|
|
td![&l.id],
|
|
td![&l.email],
|
|
td![&l.username],
|
|
match logged_in_user.role {
|
|
Role::Admin => {
|
|
match l.role {
|
|
Role::NotAuthenticated | Role::Disabled | Role::Regular => td![
|
|
ev(Ev::Click, |event| {
|
|
event.stop_propagation();
|
|
Msg::Edit(UserEditMsg::MakeAdmin(user))
|
|
}),
|
|
t("make-user-admin")
|
|
],
|
|
Role::Admin => td![
|
|
ev(Ev::Click, |event| {
|
|
event.stop_propagation();
|
|
Msg::Edit(UserEditMsg::MakeRegular(user))
|
|
}),
|
|
t("make-user-regular"),
|
|
],
|
|
}
|
|
}
|
|
Role::Regular => match l.role {
|
|
Role::NotAuthenticated | Role::Disabled | Role::Regular => td![t("user")],
|
|
Role::Admin => td![t("admin")],
|
|
},
|
|
Role::NotAuthenticated | Role::Disabled => td![],
|
|
}
|
|
]
|
|
}
|
|
|
|
/// display the edit and create dialog
|
|
fn edit_or_create_user<F: Fn(&str) -> String>(l: UserDelta, t: F) -> Node<Msg> {
|
|
let user = l;
|
|
let headline: Node<Msg> = match &user.role {
|
|
Role::NotAuthenticated | Role::Disabled | Role::Regular => {
|
|
h1![match &user.edit {
|
|
EditMode::Edit => t("edit-user"),
|
|
EditMode::Create => t("new-user"),
|
|
}]
|
|
}
|
|
Role::Admin => {
|
|
h1![match &user.edit {
|
|
EditMode::Edit => t("edit-admin"),
|
|
EditMode::Create => t("new-admin"),
|
|
}]
|
|
}
|
|
};
|
|
div![
|
|
C!["editdialog", "center"],
|
|
close_button(),
|
|
headline,
|
|
table![
|
|
tr![
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Username
|
|
))),
|
|
t("username")
|
|
],
|
|
td![input![
|
|
attrs! {
|
|
At::Value => &user.username,
|
|
At::Type => "text",
|
|
At::Placeholder => t("username")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Edit(UserEditMsg::EditUsernameChanged(s))
|
|
}),
|
|
]]
|
|
],
|
|
tr![
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Email
|
|
))),
|
|
t("email")
|
|
],
|
|
td![input![
|
|
attrs! {
|
|
At::Value => &user.email,
|
|
At::Type => "email",
|
|
At::Placeholder => t("email")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Edit(UserEditMsg::EditEmailChanged(s))
|
|
}),
|
|
]]
|
|
],
|
|
tr![
|
|
th![
|
|
ev(Ev::Click, |_| Msg::Query(UserQueryMsg::OrderBy(
|
|
UserOverviewColumns::Email
|
|
))),
|
|
t("password")
|
|
],
|
|
td![
|
|
input![
|
|
attrs! {
|
|
At::Type => "password",
|
|
At::Placeholder => t("password")
|
|
},
|
|
input_ev(Ev::Input, |s| {
|
|
Msg::Edit(UserEditMsg::EditPasswordChanged(s))
|
|
}),
|
|
],
|
|
IF!(user.edit == EditMode::Edit => p![t("leave-password-empty-hint")])
|
|
]
|
|
]
|
|
],
|
|
a![
|
|
match &user.edit {
|
|
EditMode::Edit => t("edit-user"),
|
|
EditMode::Create => t("create-user"),
|
|
},
|
|
C!["button"],
|
|
ev(Ev::Click, |_| Msg::Edit(UserEditMsg::SaveUser))
|
|
]
|
|
]
|
|
}
|
|
|
|
/// a close button for dialogs
|
|
fn close_button() -> Node<Msg> {
|
|
div![
|
|
C!["closebutton"],
|
|
a!["\u{d7}"],
|
|
ev(Ev::Click, |_| Msg::ClearAll)
|
|
]
|
|
}
|