use crate::Role;
use crate::users::{FieldUpdate, UserRecord, UserStore};
use std::collections::HashMap;
use strum::IntoEnumIterator;
use iced::Task;
pub(crate) fn user_store() -> Result<&'static UserStore, String> {
crate::users::USER_STORE
.get()
.ok_or_else(|| "User store not initialized".to_string())
}
async fn update_user_field(sender: String, value: String, is_role: bool) -> Result<(), String> {
let store = user_store()?;
let val = if value.is_empty() {
FieldUpdate::Clear
} else {
FieldUpdate::Set(&value)
};
let (role, ws) = if is_role {
(val, FieldUpdate::Unchanged)
} else {
(FieldUpdate::Unchanged, val)
};
store
.update_user(&sender, role, ws, FieldUpdate::Unchanged)
.await
.map_err(|e| e.to_string())
}
#[derive(Debug, Clone)]
pub enum UsersMessage {
Refreshed(Vec<UserRecord>, Vec<super::widgets::PickOption>),
RefreshError(String),
UpdateRole(String, String),
UpdateWorkspace(String, String),
UpdateResult(Result<(), String>),
RoleUpdateResult(Result<(), String>),
DeleteUser(String),
ConfirmDelete(String),
CancelDelete,
DeleteResult(Result<(), String>, String),
SwitchUser(String),
OpenBindInput(String),
CloseBindInput,
BindInputChanged(String),
SubmitBind(String),
UnbindChannel(String, String),
BindResult(Result<(), String>, String),
OpenPoolEdit(String),
ClosePoolEdit,
TogglePoolRole(usize),
SubmitPoolEdit(String),
PoolEditResult(Result<(), String>),
Escape,
Toast(super::ToastMessage),
}
pub struct UsersState {
pub(crate) users: Vec<UserRecord>,
pub(crate) load_state: super::common::AsyncLoadState,
pub(crate) workspace_options: Vec<super::widgets::PickOption>,
pub(crate) role_options: Vec<super::widgets::PickOption>,
pub(crate) active_role_options: HashMap<String, Vec<super::widgets::PickOption>>,
pub(crate) delete_target: Option<String>,
pub(crate) deleting: bool,
pub(crate) bind_target: Option<String>,
pub(crate) bind_input: String,
pub(crate) bind_error: Option<String>,
pub(crate) binding: bool,
pub(crate) pool_edit_target: Option<String>,
pub(crate) pool_edit_checked: Vec<bool>,
}
impl UsersState {
#[must_use]
pub fn new() -> Self {
Self {
users: Vec::new(),
load_state: super::common::AsyncLoadState::new(),
workspace_options: Vec::new(),
role_options: Vec::new(),
active_role_options: HashMap::new(),
delete_target: None,
deleting: false,
bind_target: None,
bind_input: String::new(),
bind_error: None,
binding: false,
pool_edit_target: None,
pool_edit_checked: Vec::new(),
}
}
#[allow(clippy::unused_self)]
pub fn refresh(&self) -> Task<UsersMessage> {
Task::perform(
async {
let store = user_store()?;
let users = store.list_users().await.map_err(|e| e.to_string())?;
let mut ws_options = Vec::new();
ws_options.push(super::widgets::PickOption {
value: String::new(),
label: "Personal".to_string(),
});
if let Ok(ws_list) = crate::workspace::store().list().await {
for ws in ws_list {
let display = ws.display_name();
ws_options.push(super::widgets::PickOption {
value: ws.name,
label: display,
});
}
}
Ok::<_, String>((users, ws_options))
},
|res| match res {
Ok((users, ws_options)) => UsersMessage::Refreshed(users, ws_options),
Err(e) => UsersMessage::RefreshError(e),
},
)
}
#[expect(clippy::too_many_lines)]
pub fn update(&mut self, msg: UsersMessage) -> Task<UsersMessage> {
match msg {
UsersMessage::Refreshed(users, ws_options) => {
self.users = users;
self.load_state.finish_loading();
self.workspace_options = ws_options;
self.role_options = Role::iter()
.map(|r| {
let name = r.to_string();
super::widgets::PickOption {
value: name.clone(),
label: name,
}
})
.collect();
self.active_role_options = self
.users
.iter()
.map(|u| {
let options = u
.roles
.iter()
.filter_map(|name| {
self.role_options.iter().find(|o| o.value == *name).cloned()
})
.collect();
(u.name.clone(), options)
})
.collect();
Task::none()
}
UsersMessage::RefreshError(e) => {
self.load_state.fail(e);
Task::none()
}
UsersMessage::UpdateRole(sender, role) => Task::perform(
async move { update_user_field(sender, role, true).await },
UsersMessage::RoleUpdateResult,
),
UsersMessage::UpdateWorkspace(sender, ws) => Task::perform(
async move { update_user_field(sender, ws, false).await },
UsersMessage::UpdateResult,
),
UsersMessage::UpdateResult(Ok(())) | UsersMessage::RoleUpdateResult(Ok(())) => {
self.refresh()
}
UsersMessage::UpdateResult(Err(e)) | UsersMessage::RoleUpdateResult(Err(e)) => {
self.load_state.fail(e.clone());
Task::done(UsersMessage::Toast(super::ToastMessage::Error(e)))
}
UsersMessage::DeleteUser(sender) => {
self.delete_target = Some(sender);
Task::none()
}
UsersMessage::ConfirmDelete(sender) => {
self.delete_target = None;
self.deleting = true;
let s = sender;
let s_clone = s.clone();
Task::perform(
async move {
let store = user_store()?;
store.delete_user(&s).await.map_err(|e| e.to_string())
},
move |res| UsersMessage::DeleteResult(res, s_clone),
)
}
UsersMessage::CancelDelete | UsersMessage::Escape => {
self.delete_target = None;
self.bind_target = None;
self.bind_input.clear();
self.bind_error = None;
self.pool_edit_target = None;
self.pool_edit_checked.clear();
Task::none()
}
UsersMessage::DeleteResult(Ok(()), _deleted_user) => {
self.deleting = false;
self.load_state.clear_error();
self.refresh()
}
UsersMessage::DeleteResult(Err(e), _deleted_user) => {
self.deleting = false;
self.load_state.fail(e.clone());
Task::done(UsersMessage::Toast(super::ToastMessage::Error(e)))
}
UsersMessage::Toast(_) => Task::none(),
UsersMessage::SwitchUser(_) => {
Task::none()
}
UsersMessage::OpenBindInput(user_name) => {
self.bind_target = Some(user_name);
self.bind_input.clear();
self.bind_error = None;
self.delete_target = None;
Task::none()
}
UsersMessage::CloseBindInput => {
self.bind_target = None;
self.bind_input.clear();
self.bind_error = None;
Task::none()
}
UsersMessage::BindInputChanged(v) => {
self.bind_input = v;
self.bind_error = None;
Task::none()
}
UsersMessage::SubmitBind(user_name) => {
if self.bind_input.trim().is_empty() {
self.bind_error = Some("Telegram username required".into());
return Task::none();
}
self.binding = true;
self.bind_error = None;
let mut identifier = self.bind_input.trim().to_string();
if let Some(stripped) = identifier.strip_prefix('@') {
identifier = stripped.to_string();
}
let user_clone = user_name.clone();
Task::perform(
async move {
let store = user_store()?;
store
.bind_channel(&user_clone, "telegram", &identifier)
.await
.map_err(|e| e.to_string())
},
move |res| UsersMessage::BindResult(res, user_name),
)
}
UsersMessage::UnbindChannel(user_name, identifier) => {
self.binding = true;
self.bind_error = None;
let user_clone = user_name.clone();
Task::perform(
async move {
let store = user_store()?;
store
.unbind_channel(&user_clone, "telegram", &identifier)
.await
.map_err(|e| e.to_string())
},
move |res| UsersMessage::BindResult(res, user_name),
)
}
UsersMessage::BindResult(Ok(()), _user_name) => {
self.binding = false;
self.bind_target = None;
self.bind_input.clear();
self.bind_error = None;
self.refresh()
}
UsersMessage::BindResult(Err(e), user_name) => {
self.binding = false;
if self.bind_target.as_deref() == Some(&user_name) {
self.bind_error = Some(format!("Failed to bind: {e}"));
} else {
self.load_state.fail(format!("Failed to unbind: {e}"));
}
Task::done(UsersMessage::Toast(super::ToastMessage::Error(e)))
}
UsersMessage::OpenPoolEdit(user_name) => {
let checked = self.users.iter().find(|u| u.name == user_name).map_or_else(
|| Role::iter().map(|_| false).collect(),
|u| {
Role::iter()
.map(|r| u.roles.iter().any(|name| name == r.as_str()))
.collect()
},
);
self.pool_edit_target = Some(user_name);
self.pool_edit_checked = checked;
self.delete_target = None;
self.bind_target = None;
Task::none()
}
UsersMessage::ClosePoolEdit => {
self.pool_edit_target = None;
self.pool_edit_checked.clear();
Task::none()
}
UsersMessage::TogglePoolRole(idx) => {
if let Some(checked) = self.pool_edit_checked.get_mut(idx) {
*checked = !*checked;
}
Task::none()
}
UsersMessage::SubmitPoolEdit(user_name) => {
let roles: Vec<Role> = Role::iter()
.zip(self.pool_edit_checked.iter())
.filter(|(_, checked)| **checked)
.map(|(r, _)| r)
.collect();
let Some(store) = crate::users::USER_STORE.get() else {
return Task::none();
};
let name = user_name.clone();
Task::perform(
async move {
store
.set_user_roles(&name, &roles)
.await
.map_err(|e| e.to_string())
},
UsersMessage::PoolEditResult,
)
}
UsersMessage::PoolEditResult(Ok(())) => {
self.pool_edit_target = None;
self.pool_edit_checked.clear();
self.refresh()
}
UsersMessage::PoolEditResult(Err(e)) => {
Task::done(UsersMessage::Toast(super::ToastMessage::Error(e)))
}
}
}
}