Fix roles

This commit is contained in:
Adrian Wozniak 2020-05-21 21:38:46 +02:00
parent 372b9d9b8d
commit e618a4f23c
18 changed files with 259 additions and 146 deletions

View File

@ -103,3 +103,7 @@
.modal > .clickableOverlay > .styledModal.confirmModal > .actions > .styledButton { .modal > .clickableOverlay > .styledModal.confirmModal > .actions > .styledButton {
margin-right: 10px; margin-right: 10px;
} }
.modal > .clickableOverlay > .styledModal.debugModal {
padding-left: 15px;
}

View File

@ -174,8 +174,8 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
}, },
_ => (), _ => (),
} }
crate::modal::update(&msg, model, orders);
crate::shared::aside::update(&msg, model, orders); crate::shared::aside::update(&msg, model, orders);
crate::modal::update(&msg, model, orders);
match model.page { match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders), Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
Page::ProjectSettings => project_settings::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders),

View File

@ -0,0 +1,18 @@
use seed::{prelude::*, *};
use crate::model::Model;
use crate::shared::styled_modal::StyledModal;
use crate::shared::ToNode;
use crate::Msg;
pub fn view(model: &Model) -> Node<Msg> {
let text = format!("{:#?}", model);
let code = pre![text];
StyledModal::build()
.width(1200)
.add_class("debugModal")
.center()
.children(vec![code])
.build()
.into_node()
}

View File

@ -11,6 +11,8 @@ use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged};
mod add_issue; mod add_issue;
mod confirm_delete_issue; mod confirm_delete_issue;
#[cfg(debug_assertions)]
mod debug_modal;
mod delete_issue_status; mod delete_issue_status;
mod issue_details; mod issue_details;
pub mod time_tracking; pub mod time_tracking;
@ -55,6 +57,11 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
model.modals.push(ModalType::AddIssue(Box::new(modal))); model.modals.push(ModalType::AddIssue(Box::new(modal)));
} }
#[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
model.modals.push(ModalType::DebugModal);
}
_ => (), _ => (),
} }
add_issue::update(msg, model, orders); add_issue::update(msg, model, orders);
@ -96,6 +103,8 @@ pub fn view(model: &model::Model) -> Node<Msg> {
ModalType::DeleteIssueStatusModal(delete_issue_modal) => { ModalType::DeleteIssueStatusModal(delete_issue_modal) => {
delete_issue_status::view(model, delete_issue_modal.delete_id) delete_issue_status::view(model, delete_issue_modal.delete_id)
} }
#[cfg(debug_assertions)]
ModalType::DebugModal => debug_modal::view(model),
}) })
.collect(); .collect();
section![id!["modals"], modals] section![id!["modals"], modals]

View File

@ -23,6 +23,8 @@ pub enum ModalType {
DeleteCommentConfirm(CommentId), DeleteCommentConfirm(CommentId),
TimeTracking(IssueId), TimeTracking(IssueId),
DeleteIssueStatusModal(Box<DeleteIssueStatusModal>), DeleteIssueStatusModal(Box<DeleteIssueStatusModal>),
#[cfg(debug_assertions)]
DebugModal,
} }
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]

View File

@ -63,9 +63,9 @@ impl StyledModalBuilder {
self self
} }
// pub fn center(mut self) -> Self { pub fn center(self) -> Self {
// self.variant(Variant::Center) self.variant(Variant::Center)
// } }
pub fn width(mut self, width: usize) -> Self { pub fn width(mut self, width: usize) -> Self {
self.width = Some(width); self.width = Some(width);

View File

@ -0,0 +1,5 @@
pub use update::*;
pub use view::*;
mod update;
mod view;

View File

@ -0,0 +1,134 @@
use seed::prelude::Orders;
use jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg};
use crate::model::{InvitationFormState, Model, Page, PageContent, UsersPage};
use crate::shared::styled_select::StyledSelectChange;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Msg::ChangePage(Page::Users) = msg {
build_page_content(model);
// return;
}
let page = match &mut model.page_content {
PageContent::Users(page) => page,
_ => return,
};
page.user_role_state.update(&msg, orders);
match msg {
Msg::ChangePage(Page::Users) if model.user.is_some() => {
init_load(model, orders);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) if model.user.is_some() => {
init_load(model, orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
}
WebSocketChanged::WsMsg(WsMsg::InvitationListLoaded(invitations)) => {
page.invitations = invitations;
}
WebSocketChanged::WsMsg(WsMsg::InvitationRevokeSuccess(id)) => {
let mut old = vec![];
std::mem::swap(&mut page.invitations, &mut old);
for mut invitation in old {
if id == invitation.id {
invitation.state = InvitationState::Revoked;
}
page.invitations.push(invitation);
}
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUserRemoveSuccess(email)) => {
let mut old = vec![];
std::mem::swap(&mut page.invited_users, &mut old);
for user in old {
if user.email != email {
page.invited_users.push(user);
}
}
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
page.form_state = InvitationFormState::Succeed;
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendFailure) => {
page.form_state = InvitationFormState::Failed;
}
_ => (),
},
Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm)) => {
page.name.clear();
page.name_touched = false;
page.email.clear();
page.email_touched = false;
page.user_role = UserRole::User;
page.user_role_state.reset();
page.form_state = InvitationFormState::Initial;
}
Msg::StyledSelectChanged(
FieldId::Users(UsersFieldId::UserRole),
StyledSelectChange::Changed(role),
) => {
page.user_role = role.into();
}
Msg::StrInputChanged(FieldId::Users(UsersFieldId::Username), name) => {
page.name = name;
page.name_touched = true;
}
Msg::StrInputChanged(FieldId::Users(UsersFieldId::Email), email) => {
page.email = email;
page.email_touched = true;
}
Msg::InviteRequest => {
let role: UserRole = match page.user_role_state.values.first() {
Some(i) => (*i).into(),
_ => return,
};
page.form_state = InvitationFormState::Sent;
send_ws_msg(
WsMsg::InvitationSendRequest {
name: page.name.clone(),
email: page.email.clone(),
role,
},
model.ws.as_ref(),
orders,
);
}
Msg::InviteRevokeRequest(invitation_id) => {
send_ws_msg(
WsMsg::InvitationRevokeRequest(invitation_id),
model.ws.as_ref(),
orders,
);
}
Msg::InvitedUserRemove(email) => {
send_ws_msg(
WsMsg::InvitedUserRemoveRequest(email),
model.ws.as_ref(),
orders,
);
}
_ => (),
}
}
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Users(Box::new(UsersPage::default()));
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}

View File

@ -1,133 +1,16 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId, WsMsg}; use jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId};
use crate::model::*; use crate::model::{InvitationFormState, Model, PageContent};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::*; use crate::shared::styled_select::StyledSelect;
use crate::shared::{inner_layout, ToChild, ToNode}; use crate::shared::{inner_layout, ToChild, ToNode};
use crate::validations::is_email; use crate::validations::is_email;
use crate::ws::{enqueue_ws_msg, send_ws_msg}; use crate::{FieldId, Msg, PageChanged, UsersPageChange};
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Msg::ChangePage(Page::Users) = msg {
model.page_content = PageContent::Users(Box::new(UsersPage::default()));
// return;
}
let page = match &mut model.page_content {
PageContent::Users(page) => page,
_ => return,
};
page.user_role_state.update(&msg, orders);
match msg {
Msg::ChangePage(Page::Users) if model.user.is_some() => {
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) if model.user.is_some() => {
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
}
WebSocketChanged::WsMsg(WsMsg::InvitationListLoaded(invitations)) => {
page.invitations = invitations;
}
WebSocketChanged::WsMsg(WsMsg::InvitationRevokeSuccess(id)) => {
let mut old = vec![];
std::mem::swap(&mut page.invitations, &mut old);
for mut invitation in old {
if id == invitation.id {
invitation.state = InvitationState::Revoked;
}
page.invitations.push(invitation);
}
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUserRemoveSuccess(email)) => {
let mut old = vec![];
std::mem::swap(&mut page.invited_users, &mut old);
for user in old {
if user.email != email {
page.invited_users.push(user);
}
}
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
page.form_state = InvitationFormState::Succeed;
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendFailure) => {
page.form_state = InvitationFormState::Failed;
}
_ => (),
},
Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm)) => {
page.name.clear();
page.name_touched = false;
page.email.clear();
page.email_touched = false;
page.user_role = UserRole::User;
page.user_role_state.reset();
page.form_state = InvitationFormState::Initial;
}
Msg::StyledSelectChanged(
FieldId::Users(UsersFieldId::UserRole),
StyledSelectChange::Changed(role),
) => {
page.user_role = role.into();
}
Msg::StrInputChanged(FieldId::Users(UsersFieldId::Username), name) => {
page.name = name;
page.name_touched = true;
}
Msg::StrInputChanged(FieldId::Users(UsersFieldId::Email), email) => {
page.email = email;
page.email_touched = true;
}
Msg::InviteRequest => {
page.form_state = InvitationFormState::Sent;
send_ws_msg(
WsMsg::InvitationSendRequest {
name: page.name.clone(),
email: page.email.clone(),
},
model.ws.as_ref(),
orders,
);
}
Msg::InviteRevokeRequest(invitation_id) => {
send_ws_msg(
WsMsg::InvitationRevokeRequest(invitation_id),
model.ws.as_ref(),
orders,
);
}
Msg::InvitedUserRemove(email) => {
send_ws_msg(
WsMsg::InvitedUserRemoveRequest(email),
model.ws.as_ref(),
orders,
);
}
_ => (),
}
}
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
if model.user.is_none() { if model.user.is_none() {
@ -231,12 +114,18 @@ pub fn view(model: &Model) -> Node<Msg> {
.on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email))) .on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email)))
.build() .build()
.into_node(); .into_node();
let role = page
.invitations
.iter()
.find(|iv| iv.email.eq(user.email.as_str()) && iv.name.eq(user.name.as_str()))
.map(|iv| iv.role)
.unwrap_or_default();
// span![format!("{}", user.user_role)],
li![ li![
class!["user"], class!["user"],
span![user.name.as_str()], span![user.name.as_str()],
span![user.email.as_str()], span![user.email.as_str()],
span![format!("{}", role)],
remove, remove,
] ]
}) })

View File

@ -79,8 +79,10 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// auth // auth
WsMsg::AuthorizeLoaded(Ok(user)) => { WsMsg::AuthorizeLoaded(Ok(user)) => {
model.user = Some(user.clone()); model.user = Some(user.clone());
if is_non_logged_area() {
go_to_board(orders); go_to_board(orders);
} }
}
WsMsg::AuthorizeExpired => { WsMsg::AuthorizeExpired => {
if let Ok(msg) = write_auth_token(None) { if let Ok(msg) = write_auth_token(None) {
orders.skip().send_msg(msg); orders.skip().send_msg(msg);
@ -217,3 +219,11 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}; };
orders.render(); orders.render();
} }
fn is_non_logged_area() -> bool {
let pathname = seed::document().location().unwrap().pathname().unwrap();
match pathname.as_str() {
"/login" | "/register" | "/invite" => true,
_ => false,
}
}

View File

@ -449,6 +449,7 @@ pub struct Invitation {
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
pub bind_token: Uuid, pub bind_token: Uuid,
pub role: UserRole,
} }
#[cfg_attr(feature = "backend", derive(Queryable))] #[cfg_attr(feature = "backend", derive(Queryable))]
@ -696,6 +697,7 @@ pub enum WsMsg {
InvitationSendRequest { InvitationSendRequest {
name: UsernameString, name: UsernameString,
email: EmailString, email: EmailString,
role: UserRole,
}, },
InvitationSendSuccess, InvitationSendSuccess,
InvitationSendFailure, InvitationSendFailure,

View File

@ -0,0 +1 @@
ALTER TABLE invitations DROP COLUMN role;

View File

@ -0,0 +1 @@
ALTER TABLE invitations ADD COLUMN role "UserRoleType" NOT NULL DEFAULT 'user';

View File

@ -3,12 +3,12 @@ use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::{ use jirs_data::{
EmailString, Invitation, InvitationId, InvitationState, ProjectId, User, UserId, UsernameString, EmailString, Invitation, InvitationId, InvitationState, ProjectId, User, UserId, UserRole,
UsernameString,
}; };
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
use crate::models::InvitationForm;
pub struct ListInvitation { pub struct ListInvitation {
pub user_id: UserId, pub user_id: UserId,
@ -46,6 +46,7 @@ pub struct CreateInvitation {
pub project_id: ProjectId, pub project_id: ProjectId,
pub email: EmailString, pub email: EmailString,
pub name: UsernameString, pub name: UsernameString,
pub role: UserRole,
} }
impl Message for CreateInvitation { impl Message for CreateInvitation {
@ -63,14 +64,14 @@ impl Handler<CreateInvitation> for DbExecutor {
.get() .get()
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let form = InvitationForm { let query = diesel::insert_into(invitations).values((
name: msg.name, name.eq(msg.name),
email: msg.email, email.eq(msg.email),
state: InvitationState::Sent, state.eq(InvitationState::Sent),
project_id: msg.project_id, project_id.eq(msg.project_id),
invited_by_id: msg.user_id, invited_by_id.eq(msg.user_id),
}; role.eq(msg.role),
let query = diesel::insert_into(invitations).values(form); ));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query query
.get_result(conn) .get_result(conn)
@ -183,18 +184,22 @@ impl Handler<AcceptInvitation> for DbExecutor {
let user: User = { let user: User = {
use crate::schema::users::dsl::*; use crate::schema::users::dsl::*;
let query = diesel::insert_into(users) let query = users
.values((name.eq(invitation.name), email.eq(invitation.email))); .filter(name.eq(invitation.name).and(email.eq(invitation.email)))
.limit(1);
debug!("{}", diesel::debug_query::<Pg, _>(&query)); debug!("{}", diesel::debug_query::<Pg, _>(&query));
query query
.get_result(conn) .first(conn)
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))? .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?
}; };
{ {
use crate::schema::user_projects::dsl::*; use crate::schema::user_projects::dsl::*;
let query = diesel::insert_into(user_projects) let query = diesel::insert_into(user_projects).values((
.values((user_id.eq(user.id), project_id.eq(invitation.project_id))); user_id.eq(user.id),
project_id.eq(invitation.project_id),
role.eq(invitation.role),
));
debug!("{}", diesel::debug_query::<Pg, _>(&query)); debug!("{}", diesel::debug_query::<Pg, _>(&query));
query query
.execute(conn) .execute(conn)

View File

@ -109,6 +109,12 @@ table! {
/// ///
/// (Automatically generated by Diesel.) /// (Automatically generated by Diesel.)
bind_token -> Uuid, bind_token -> Uuid,
/// The `role` column of the `invitations` table.
///
/// Its SQL type is `UserRoleType`.
///
/// (Automatically generated by Diesel.)
role -> UserRoleType,
} }
} }

View File

@ -1,6 +1,6 @@
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::{EmailString, InvitationId, UsernameString, WsMsg}; use jirs_data::{EmailString, InvitationId, UserRole, UsernameString, WsMsg};
use crate::db::invitations; use crate::db::invitations;
use crate::ws::{WebSocketActor, WsHandler, WsResult}; use crate::ws::{WebSocketActor, WsHandler, WsResult};
@ -31,6 +31,7 @@ impl WsHandler<ListInvitation> for WebSocketActor {
pub struct CreateInvitation { pub struct CreateInvitation {
pub email: EmailString, pub email: EmailString,
pub name: UsernameString, pub name: UsernameString,
pub role: UserRole,
} }
impl WsHandler<CreateInvitation> for WebSocketActor { impl WsHandler<CreateInvitation> for WebSocketActor {
@ -45,12 +46,13 @@ impl WsHandler<CreateInvitation> for WebSocketActor {
_ => return Ok(None), _ => return Ok(None),
}; };
let CreateInvitation { email, name } = msg; let CreateInvitation { email, name, role } = msg;
let invitation = match block_on(self.db.send(invitations::CreateInvitation { let invitation = match block_on(self.db.send(invitations::CreateInvitation {
user_id, user_id,
project_id, project_id,
email, email,
name, name,
role,
})) { })) {
Ok(Ok(invitation)) => invitation, Ok(Ok(invitation)) => invitation,
Ok(Err(e)) => { Ok(Err(e)) => {

View File

@ -20,6 +20,7 @@ use crate::ws::invitations::*;
use crate::ws::issue_statuses::*; use crate::ws::issue_statuses::*;
use crate::ws::issues::*; use crate::ws::issues::*;
use crate::ws::projects::*; use crate::ws::projects::*;
use crate::ws::user_projects::LoadUserProjects;
use crate::ws::users::*; use crate::ws::users::*;
pub mod auth; pub mod auth;
@ -121,8 +122,12 @@ impl WebSocketActor {
// projects // projects
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?, WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
WsMsg::ProjectsLoad => self.handle_msg(LoadProjects, ctx)?,
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?, WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
// user projects
WsMsg::UserProjectLoad => self.handle_msg(LoadUserProjects, ctx)?,
// auth // auth
WsMsg::AuthorizeRequest(uuid) => { WsMsg::AuthorizeRequest(uuid) => {
self.handle_msg(CheckAuthToken { token: uuid }, ctx)? self.handle_msg(CheckAuthToken { token: uuid }, ctx)?
@ -157,8 +162,8 @@ impl WebSocketActor {
} }
// invitations // invitations
WsMsg::InvitationSendRequest { name, email } => { WsMsg::InvitationSendRequest { name, email, role } => {
self.handle_msg(CreateInvitation { name, email }, ctx)? self.handle_msg(CreateInvitation { name, email, role }, ctx)?
} }
WsMsg::InvitationListRequest => self.handle_msg(ListInvitation, ctx)?, WsMsg::InvitationListRequest => self.handle_msg(ListInvitation, ctx)?,
WsMsg::InvitationAcceptRequest(id) => self.handle_msg(AcceptInvitation { id }, ctx)?, WsMsg::InvitationAcceptRequest(id) => self.handle_msg(AcceptInvitation { id }, ctx)?,

View File

@ -2,6 +2,7 @@ use futures::executor::block_on;
use jirs_data::{UpdateProjectPayload, WsMsg}; use jirs_data::{UpdateProjectPayload, WsMsg};
use crate::db;
use crate::db::projects::LoadCurrentProject; use crate::db::projects::LoadCurrentProject;
use crate::ws::{WebSocketActor, WsHandler, WsResult}; use crate::ws::{WebSocketActor, WsHandler, WsResult};
@ -50,3 +51,22 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
Ok(Some(WsMsg::ProjectLoaded(project))) Ok(Some(WsMsg::ProjectLoaded(project)))
} }
} }
pub struct LoadProjects;
impl WsHandler<LoadProjects> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadProjects, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id;
match block_on(self.db.send(db::projects::LoadProjects { user_id })) {
Ok(Ok(v)) => Ok(Some(WsMsg::ProjectsLoaded(v))),
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)
}
Err(e) => {
error!("{:?}", e);
Ok(None)
}
}
}
}