Fix accept invitation, fix link send to user. Change state of form, display invitations and users

This commit is contained in:
Adrian Woźniak 2020-04-22 14:12:12 +02:00
parent 4316f6888d
commit ff0d4bb329
28 changed files with 274 additions and 118 deletions

View File

@ -0,0 +1,30 @@
#users > .usersSection,
#users > .invitationsSection {
padding: 25px 40px 35px;
}
#users > .usersSection > .usersList,
#users > .invitationsSection > .invitationsList {
list-style: none;
}
#users > .usersSection > .usersList > .user,
#users > .invitationsSection > .invitationsList > .invitation {
list-style: none;
display: flex;
justify-content: space-between;
margin-top: 20px;
}
#users > .invitationsSection > .invitationsList > .invitation > * {
width: 25%;
}
#users .invitationActions {
display: flex;
justify-content: space-between;
}
#users .invitationActions > .error {
color: var(--danger);
}

View File

@ -27,3 +27,4 @@
@import "./css/timeTracking.css"; @import "./css/timeTracking.css";
@import "./css/login.css"; @import "./css/login.css";
@import "./css/register.css"; @import "./css/register.css";
@import "./css/users.css";

View File

@ -11,12 +11,9 @@ use crate::validations::is_token;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
match msg { if let Msg::ChangePage(Page::Project) = msg {
Msg::ChangePage(Page::Project) => { model.page_content = PageContent::Invite(Box::new(InvitePage::default()));
model.page_content = PageContent::Invite(InvitePage::default()); return;
return;
}
_ => (),
} }
let page = match &mut model.page_content { let page = match &mut model.page_content {
@ -24,12 +21,9 @@ pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
_ => return, _ => return,
}; };
match msg { if let Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) = msg {
Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) => { page.token_touched = true;
page.token_touched = true; page.token = text;
page.token = text.clone();
}
_ => (),
} }
} }

View File

@ -153,6 +153,7 @@ pub enum Msg {
AuthTokenErased, AuthTokenErased,
SignInRequest, SignInRequest,
BindClientRequest, BindClientRequest,
InviteRequest,
// sign up // sign up
SignUpRequest, SignUpRequest,
@ -360,7 +361,7 @@ fn authorize_or_redirect() {
Err(..) => { Err(..) => {
let pathname = seed::document().location().unwrap().pathname().unwrap(); let pathname = seed::document().location().unwrap().pathname().unwrap();
match pathname.as_str() { match pathname.as_str() {
"/login" | "/register" => {} "/login" | "/register" | "/invite" => {}
_ => { _ => {
seed::push_route(vec!["login"]); seed::push_route(vec!["login"]);
} }

View File

@ -39,9 +39,9 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
let payload = jirs_data::CreateIssuePayload { let payload = jirs_data::CreateIssuePayload {
title: modal.title.clone(), title: modal.title.clone(),
issue_type: modal.issue_type.clone(), issue_type: modal.issue_type,
status: modal.status.clone(), status: modal.status,
priority: modal.priority.clone(), priority: modal.priority,
description: modal.description.clone(), description: modal.description.clone(),
description_text: modal.description_text.clone(), description_text: modal.description_text.clone(),
estimate: modal.estimate, estimate: modal.estimate,

View File

@ -40,7 +40,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::Type, IssueFieldId::Type,
PayloadVariant::IssueType(modal.payload.issue_type.clone()), PayloadVariant::IssueType(modal.payload.issue_type),
)); ));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
@ -51,7 +51,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::Status, IssueFieldId::Status,
PayloadVariant::IssueStatus(modal.payload.status.clone()), PayloadVariant::IssueStatus(modal.payload.status),
)); ));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
@ -143,7 +143,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::TimeSpend, IssueFieldId::TimeSpend,
PayloadVariant::OptionI32(modal.payload.time_spent.clone()), PayloadVariant::OptionI32(modal.payload.time_spent),
)); ));
} }
Msg::InputChanged( Msg::InputChanged(
@ -154,7 +154,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::TimeRemaining, IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining.clone()), PayloadVariant::OptionI32(modal.payload.time_remaining),
)); ));
} }
Msg::ModalChanged(FieldChange::TabChanged( Msg::ModalChanged(FieldChange::TabChanged(
@ -189,7 +189,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::TimeRemaining, IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.estimate.clone()), PayloadVariant::OptionI32(modal.payload.estimate),
)); ));
} }
_ if value.is_empty() => { _ if value.is_empty() => {
@ -197,7 +197,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id, modal.id,
IssueFieldId::TimeRemaining, IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.estimate.clone()), PayloadVariant::OptionI32(modal.payload.estimate),
)); ));
} }
_ => {} _ => {}

View File

@ -50,7 +50,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
Msg::ChangePage(Page::AddIssue) => { Msg::ChangePage(Page::AddIssue) => {
let mut modal = AddIssueModal::default(); let mut modal = AddIssueModal::default();
modal.project_id = model.project.as_ref().map(|p| p.id); modal.project_id = model.project.as_ref().map(|p| p.id);
model.modals.push(ModalType::AddIssue(modal)); model.modals.push(ModalType::AddIssue(Box::new(modal)));
} }
_ => (), _ => (),
@ -101,7 +101,7 @@ fn push_edit_modal(issue_id: i32, model: &mut Model) {
Some(issue) => issue, Some(issue) => issue,
_ => return, _ => return,
}; };
ModalType::EditIssue(issue_id, EditIssueModal::new(issue)) ModalType::EditIssue(issue_id, Box::new(EditIssueModal::new(issue)))
}; };
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id)); send_ws_msg(WsMsg::IssueCommentsRequest(issue_id));
model.modals.push(modal); model.modals.push(modal);

View File

@ -11,8 +11,8 @@ use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL};
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum ModalType { pub enum ModalType {
AddIssue(AddIssueModal), AddIssue(Box<AddIssueModal>),
EditIssue(IssueId, EditIssueModal), EditIssue(IssueId, Box<EditIssueModal>),
DeleteIssueConfirm(IssueId), DeleteIssueConfirm(IssueId),
DeleteCommentConfirm(CommentId), DeleteCommentConfirm(CommentId),
TimeTracking(IssueId), TimeTracking(IssueId),
@ -49,9 +49,9 @@ impl EditIssueModal {
link_copied: false, link_copied: false,
payload: UpdateIssuePayload { payload: UpdateIssuePayload {
title: issue.title.clone(), title: issue.title.clone(),
issue_type: issue.issue_type.clone(), issue_type: issue.issue_type,
status: issue.status.clone(), status: issue.status,
priority: issue.priority.clone(), priority: issue.priority,
list_position: issue.list_position, list_position: issue.list_position,
description: issue.description.clone(), description: issue.description.clone(),
description_text: issue.description_text.clone(), description_text: issue.description_text.clone(),
@ -252,6 +252,20 @@ pub struct SignUpPage {
pub email_touched: bool, pub email_touched: bool,
} }
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)]
pub enum InvitationFormState {
Initial = 1,
Sent = 2,
Succeed = 3,
Failed = 4,
}
impl Default for InvitationFormState {
fn default() -> Self {
InvitationFormState::Initial
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct UsersPage { pub struct UsersPage {
pub name: String, pub name: String,
@ -263,6 +277,10 @@ pub struct UsersPage {
pub user_role_state: StyledSelectState, pub user_role_state: StyledSelectState,
pub pending_invitations: Vec<String>, pub pending_invitations: Vec<String>,
pub error: String, pub error: String,
pub form_state: InvitationFormState,
pub invited_users: Vec<User>,
pub invitations: Vec<Invitation>,
} }
impl Default for UsersPage { impl Default for UsersPage {
@ -276,18 +294,21 @@ impl Default for UsersPage {
user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole)), user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole)),
pending_invitations: vec![], pending_invitations: vec![],
error: "".to_string(), error: "".to_string(),
form_state: Default::default(),
invited_users: vec![],
invitations: vec![],
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum PageContent { pub enum PageContent {
SignIn(SignInPage), SignIn(Box<SignInPage>),
SignUp(SignUpPage), SignUp(Box<SignUpPage>),
Project(ProjectPage), Project(Box<ProjectPage>),
ProjectSettings(ProjectSettingsPage), ProjectSettings(Box<ProjectSettingsPage>),
Invite(InvitePage), Invite(Box<InvitePage>),
Users(UsersPage), Users(Box<UsersPage>),
} }
#[derive(Debug)] #[derive(Debug)]
@ -332,7 +353,7 @@ impl Default for Model {
comments_by_project_id: Default::default(), comments_by_project_id: Default::default(),
page: Page::Project, page: Page::Project,
host_url, host_url,
page_content: PageContent::Project(ProjectPage::default()), page_content: PageContent::Project(Box::new(ProjectPage::default())),
modals: vec![], modals: vec![],
project: None, project: None,
comments: vec![], comments: vec![],

View File

@ -22,7 +22,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
Msg::ChangePage(Page::Project) Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => { | Msg::ChangePage(Page::EditIssue(..)) => {
model.page_content = PageContent::Project(ProjectPage::default()); model.page_content = PageContent::Project(Box::new(ProjectPage::default()));
} }
_ => (), _ => (),
} }
@ -283,7 +283,7 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
.issues .issues
.iter() .iter()
.filter(|issue| { .filter(|issue| {
issue_filter_status(issue, &status) issue_filter_status(issue, status)
&& issue_filter_with_text(issue, project_page.text_filter.as_str()) && issue_filter_with_text(issue, project_page.text_filter.as_str())
&& issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user) && issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
&& issue_filter_with_only_recent(issue, ids.as_slice()) && issue_filter_with_only_recent(issue, ids.as_slice())
@ -292,13 +292,13 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
.collect(); .collect();
let label = status.to_label(); let label = status.to_label();
let send_status = status.clone(); let send_status = status;
let drop_handler = drag_ev(Ev::Drop, move |ev| { let drop_handler = drag_ev(Ev::Drop, move |ev| {
ev.prevent_default(); ev.prevent_default();
Msg::IssueDropZone(send_status) Msg::IssueDropZone(send_status)
}); });
let send_status = status.clone(); let send_status = status;
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| { let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default(); ev.prevent_default();
Msg::IssueDragOverStatus(send_status) Msg::IssueDragOverStatus(send_status)
@ -321,8 +321,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
} }
#[inline] #[inline]
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool { fn issue_filter_status(issue: &Issue, status: IssueStatus) -> bool {
&issue.status == status issue.status == status
} }
#[inline] #[inline]
@ -384,7 +384,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
ev.stop_propagation(); ev.stop_propagation();
Msg::ExchangePosition(issue_id) Msg::ExchangePosition(issue_id)
}); });
let issue_id = issue.id.clone(); let issue_id = issue.id;
let drag_out = drag_ev(Ev::DragLeave, move |_| Msg::DragLeave(issue_id)); let drag_out = drag_ev(Ev::DragLeave, move |_| Msg::DragLeave(issue_id));
let class_list = vec!["issue"]; let class_list = vec!["issue"];

View File

@ -76,7 +76,7 @@ fn build_page_content(model: &mut Model) {
Some(project) => project, Some(project) => project,
_ => return, _ => return,
}; };
model.page_content = PageContent::ProjectSettings(ProjectSettingsPage::new(project)); model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
} }
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {

View File

@ -35,6 +35,7 @@ pub struct StyledButtonBuilder {
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
children: Option<Vec<Node<Msg>>>, children: Option<Vec<Node<Msg>>>,
class_list: Vec<String>, class_list: Vec<String>,
button_type: Option<String>,
} }
impl StyledButtonBuilder { impl StyledButtonBuilder {
@ -107,6 +108,11 @@ impl StyledButtonBuilder {
self self
} }
pub fn set_type_reset(mut self) -> Self {
self.button_type = Some("reset".to_string());
self
}
pub fn build(self) -> StyledButton { pub fn build(self) -> StyledButton {
StyledButton { StyledButton {
variant: self.variant.unwrap_or_else(|| Variant::Primary), variant: self.variant.unwrap_or_else(|| Variant::Primary),
@ -117,6 +123,7 @@ impl StyledButtonBuilder {
on_click: self.on_click, on_click: self.on_click,
children: self.children.unwrap_or_default(), children: self.children.unwrap_or_default(),
class_list: self.class_list, class_list: self.class_list,
button_type: self.button_type.unwrap_or_else(|| "submit".to_string()),
} }
} }
} }
@ -130,6 +137,7 @@ pub struct StyledButton {
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<String>,
button_type: String,
} }
impl StyledButton { impl StyledButton {
@ -154,6 +162,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
on_click, on_click,
children, children,
mut class_list, mut class_list,
button_type,
} = values; } = values;
class_list.push("styledButton".to_string()); class_list.push("styledButton".to_string());
class_list.push(variant.to_string()); class_list.push(variant.to_string());
@ -185,6 +194,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
seed::button![ seed::button![
attrs![ attrs![
At::Class => class_list.join(" "), At::Class => class_list.join(" "),
At::Type => button_type,
], ],
handler, handler,
if disabled { if disabled {

View File

@ -183,7 +183,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
let textarea = seed::to_textarea(&target); let textarea = seed::to_textarea(&target);
let value = textarea.value(); let value = textarea.value();
if handler_disable_auto_resize && value.contains("\n") { if handler_disable_auto_resize && value.contains('\n') {
event.prevent_default(); event.prevent_default();
} }

View File

@ -23,7 +23,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
} }
if msg == Msg::ChangePage(Page::SignIn) { if msg == Msg::ChangePage(Page::SignIn) {
model.page_content = PageContent::SignIn(SignInPage::default()); model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
return; return;
} }

View File

@ -20,7 +20,7 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
} }
if msg == Msg::ChangePage(Page::SignUp) { if msg == Msg::ChangePage(Page::SignUp) {
model.page_content = PageContent::SignUp(SignUpPage::default()); model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
return; return;
} }

View File

@ -1,9 +1,9 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::UserRole; use jirs_data::{ToVec, UserRole, UsersFieldId, WsMsg};
use jirs_data::{ToVec, UsersFieldId};
use crate::model::{Model, Page, PageContent, UsersPage}; use crate::api::send_ws_msg;
use crate::model::{InvitationFormState, Model, Page, PageContent, UsersPage};
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;
@ -15,12 +15,9 @@ use crate::validations::is_email;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { if let Msg::ChangePage(Page::Users) = msg {
Msg::ChangePage(Page::Users) => { model.page_content = PageContent::Users(Box::new(UsersPage::default()));
model.page_content = PageContent::Users(UsersPage::default()); return;
return;
}
_ => (),
} }
let page = match &mut model.page_content { let page = match &mut model.page_content {
@ -31,6 +28,16 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
page.user_role_state.update(&msg, orders); page.user_role_state.update(&msg, orders);
match msg { match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) | Msg::ChangePage(Page::Users) => {
send_ws_msg(WsMsg::InvitationListRequest);
send_ws_msg(WsMsg::InvitedUsersRequest);
}
Msg::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
}
Msg::WsMsg(WsMsg::InvitationListLoaded(invitations)) => {
page.invitations = invitations;
}
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::Users(UsersFieldId::UserRole), FieldId::Users(UsersFieldId::UserRole),
StyledSelectChange::Changed(role), StyledSelectChange::Changed(role),
@ -45,6 +52,20 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
page.email = email; page.email = email;
page.email_touched = true; page.email_touched = true;
} }
Msg::InviteRequest => {
page.form_state = InvitationFormState::Sent;
send_ws_msg(WsMsg::InvitationSendRequest {
name: page.name.clone(),
email: page.email.clone(),
})
}
Msg::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest);
page.form_state = InvitationFormState::Succeed;
}
Msg::WsMsg(WsMsg::InvitationSendFailure) => {
page.form_state = InvitationFormState::Failed;
}
_ => (), _ => (),
} }
} }
@ -103,12 +124,27 @@ pub fn view(model: &Model) -> Node<Msg> {
let submit = StyledButton::build() let submit = StyledButton::build()
.add_class("submitUserInvite") .add_class("submitUserInvite")
.active(true) .active(page.form_state != InvitationFormState::Sent)
.primary() .primary()
.text("Invite user") .text("Invite user")
.build() .build()
.into_node(); .into_node();
let submit_field = StyledField::build().input(submit).build().into_node(); let submit_supplement = match page.form_state {
InvitationFormState::Succeed => StyledButton::build()
.add_class("resetUserInvite")
.active(true)
.empty()
.set_type_reset()
.text("Reset")
.build()
.into_node(),
InvitationFormState::Failed => div![class!["error"], "There was an error"],
_ => empty![],
};
let submit_field = StyledField::build()
.input(div![class!["invitationActions"], submit, submit_supplement])
.build()
.into_node();
let form = StyledForm::build() let form = StyledForm::build()
.heading("Invite new user") .heading("Invite new user")
@ -116,8 +152,59 @@ pub fn view(model: &Model) -> Node<Msg> {
.add_field(email_field) .add_field(email_field)
.add_field(user_role_field) .add_field(user_role_field)
.add_field(submit_field) .add_field(submit_field)
.on_submit(ev(Ev::Submit, |ev| {
ev.prevent_default();
Msg::InviteRequest
}))
.build() .build()
.into_node(); .into_node();
inner_layout(model, "users", vec![form], empty![]) let users: Vec<Node<Msg>> = page
.invited_users
.iter()
.map(|user| {
let remove = StyledButton::build().text("Remove").build().into_node();
li![
class!["user"],
span![user.name],
span![user.email],
span![format!("{}", user.user_role)],
remove,
]
})
.collect();
let users_section = section![
class!["usersSection"],
h1![class!["heading"], "Users"],
ul![class!["usersList"], users],
];
let invitations: Vec<Node<Msg>> = page
.invitations
.iter()
.map(|invitation| {
let revoke = StyledButton::build().text("Revoke").build().into_node();
li![
class!["invitation"],
span![invitation.name],
span![invitation.email],
span![format!("{}", invitation.state)],
revoke,
]
})
.collect();
let invitations_section = section![
class!["invitationsSection"],
h1![class!["heading"], "Invitations"],
ul![class!["invitationsList"], invitations],
];
inner_layout(
model,
"users",
vec![form, users_section, invitations_section],
empty![],
)
} }

View File

@ -18,7 +18,7 @@ pub fn is_email(s: &str) -> bool {
_ => (), _ => (),
} }
} }
return false; false
} }
pub fn is_token(s: &str) -> bool { pub fn is_token(s: &str) -> bool {

View File

@ -64,7 +64,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
model.issues.push(c); model.issues.push(c);
} }
dragged.list_position = below.list_position + 1; dragged.list_position = below.list_position + 1;
dragged.status = below.status.clone(); dragged.status = below.status;
} }
std::mem::swap(&mut dragged.list_position, &mut below.list_position); std::mem::swap(&mut dragged.list_position, &mut below.list_position);
@ -96,7 +96,7 @@ pub fn sync(model: &mut Model) {
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
issue.id, issue.id,
IssueFieldId::Status, IssueFieldId::Status,
PayloadVariant::IssueStatus(issue.status.clone()), PayloadVariant::IssueStatus(issue.status),
)); ));
send_ws_msg(WsMsg::IssueUpdateRequest( send_ws_msg(WsMsg::IssueUpdateRequest(
issue.id, issue.id,
@ -150,7 +150,6 @@ pub fn change_status(status: IssueStatus, model: &mut Model) {
if issue.status == status { if issue.status == status {
model.issues.push(issue); model.issues.push(issue);
return;
} else { } else {
issue.status = status; issue.status = status;
issue.list_position = pos + 1; issue.list_position = pos + 1;

View File

@ -29,7 +29,7 @@ pub type UsernameString = String;
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
pub enum IssueType { pub enum IssueType {
Task, Task,
Bug, Bug,
@ -93,7 +93,7 @@ impl std::fmt::Display for IssueType {
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueStatusType")] #[cfg_attr(feature = "backend", sql_type = "IssueStatusType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
pub enum IssueStatus { pub enum IssueStatus {
Backlog, Backlog,
Selected, Selected,

View File

@ -57,7 +57,7 @@ impl Handler<CreateInvitation> for DbExecutor {
let conn = &self let conn = &self
.pool .pool
.get() .get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let form = InvitationForm { let form = InvitationForm {
name: msg.name, name: msg.name,
@ -70,7 +70,7 @@ impl Handler<CreateInvitation> for DbExecutor {
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query query
.get_result(conn) .get_result(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost) .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))
} }
} }
@ -96,7 +96,7 @@ impl Handler<DeleteInvitation> for DbExecutor {
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query query
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(()) Ok(())
} }
} }
@ -125,7 +125,7 @@ impl Handler<RevokeInvitation> for DbExecutor {
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query query
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(()) Ok(())
} }
} }
@ -154,15 +154,22 @@ impl Handler<AcceptInvitation> for DbExecutor {
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let invitation: Invitation = query let invitation: Invitation = query
.first(conn) .first(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
if invitation.state == InvitationState::Revoked {
return Err(ServiceErrors::DatabaseQueryFailed(
"This invitation is no longer valid".to_string(),
));
}
let query = diesel::update(invitations) let query = diesel::update(invitations)
.set(state.eq(InvitationState::Accepted)) .set(state.eq(InvitationState::Accepted))
.filter(id.eq(invitation.id)); .filter(id.eq(invitation.id))
.filter(state.eq(InvitationState::Sent));
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
query query
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
let form = UserForm { let form = UserForm {
name: invitation.name, name: invitation.name,
@ -174,7 +181,7 @@ impl Handler<AcceptInvitation> for DbExecutor {
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string()); debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
let user: User = query let user: User = query
.get_result(conn) .get_result(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
Ok(user) Ok(user)
} }

View File

@ -1,6 +1,6 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use lettre; // use lettre;
use lettre_email; // use lettre_email;
use uuid::Uuid; use uuid::Uuid;
use crate::mail::MailExecutor; use crate::mail::MailExecutor;
@ -35,7 +35,7 @@ impl Handler<Invite> for MailExecutor {
<p> <p>
</p> </p>
<p> <p>
Please click this link: <a href="{addr}/invite?token={bind_token}"></a> Please click this link: <a href="{addr}/invite?token={bind_token}">{addr}/invite?token={bind_token}</a>
</p> </p>
</body> </body>
</html> </html>
@ -46,7 +46,7 @@ impl Handler<Invite> for MailExecutor {
); );
let email = lettre_email::Email::builder() let email = lettre_email::Email::builder()
.from(from.clone()) .from(from)
.to(msg.email.as_str()) .to(msg.email.as_str())
.html(html.as_str()) .html(html.as_str())
.subject("Invitation to JIRS project") .subject("Invitation to JIRS project")

View File

@ -1,7 +1,7 @@
use std::fs::*; use std::fs::*;
use actix::{Actor, SyncContext}; use actix::{Actor, SyncContext};
use lettre; // use lettre;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
pub mod invite; pub mod invite;

View File

@ -1,6 +1,6 @@
use actix::{Handler, Message}; use actix::{Handler, Message};
use lettre; // use lettre;
use lettre_email; // use lettre_email;
use uuid::Uuid; use uuid::Uuid;
use crate::mail::MailExecutor; use crate::mail::MailExecutor;
@ -45,7 +45,7 @@ impl Handler<Welcome> for MailExecutor {
); );
let email = lettre_email::Email::builder() let email = lettre_email::Email::builder()
.from(from.clone()) .from(from)
.to(msg.email.as_str()) .to(msg.email.as_str())
.html(html.as_str()) .html(html.as_str())
.subject("Welcome to JIRS") .subject("Welcome to JIRS")

View File

@ -43,7 +43,7 @@ impl WsHandler<Authenticate> for WebSocketActor {
if let Some(bind_token) = token.bind_token.as_ref().cloned() { if let Some(bind_token) = token.bind_token.as_ref().cloned() {
match block_on(self.mail.send(Welcome { match block_on(self.mail.send(Welcome {
bind_token, bind_token,
email: user.email.clone(), email: user.email,
})) { })) {
Ok(Ok(_)) => (), Ok(Ok(_)) => (),
Ok(Err(e)) => { Ok(Err(e)) => {
@ -69,7 +69,7 @@ impl WsHandler<CheckAuthToken> for WebSocketActor {
let user: jirs_data::User = match block_on(self.db.send(AuthorizeUser { let user: jirs_data::User = match block_on(self.db.send(AuthorizeUser {
access_token: msg.token, access_token: msg.token,
})) { })) {
Ok(Ok(u)) => u.into(), Ok(Ok(u)) => u,
Ok(Err(_)) => { Ok(Err(_)) => {
return Ok(Some(WsMsg::AuthorizeLoaded(Err( return Ok(Some(WsMsg::AuthorizeLoaded(Err(
"Invalid auth token".to_string() "Invalid auth token".to_string()

View File

@ -15,7 +15,7 @@ impl WsHandler<LoadIssueComments> for WebSocketActor {
let comments = match block_on(self.db.send(crate::db::comments::LoadIssueComments { let comments = match block_on(self.db.send(crate::db::comments::LoadIssueComments {
issue_id: msg.issue_id, issue_id: msg.issue_id,
})) { })) {
Ok(Ok(comments)) => comments.into_iter().map(|c| c.into()).collect(), Ok(Ok(comments)) => comments,
_ => return Ok(None), _ => return Ok(None),
}; };

View File

@ -45,15 +45,29 @@ impl WsHandler<CreateInvitation> for WebSocketActor {
name, name,
})) { })) {
Ok(Ok(invitation)) => invitation, Ok(Ok(invitation)) => invitation,
_ => return Ok(Some(WsMsg::InvitationSendFailure)), Ok(Err(e)) => {
error!("{:?}", e);
return Ok(Some(WsMsg::InvitationSendFailure));
}
Err(e) => {
error!("{}", e);
return Ok(Some(WsMsg::InvitationSendFailure));
}
}; };
match block_on(self.mail.send(crate::mail::invite::Invite { match block_on(self.mail.send(crate::mail::invite::Invite {
bind_token: invitation.bind_token.clone(), bind_token: invitation.bind_token,
email: invitation.email.clone(), email: invitation.email,
inviter_name, inviter_name,
})) { })) {
Ok(Ok(_)) => (), Ok(Ok(_)) => (),
_ => return Ok(Some(WsMsg::InvitationSendFailure)), Ok(Err(e)) => {
error!("{:?}", e);
return Ok(Some(WsMsg::InvitationSendFailure));
}
Err(e) => {
error!("{}", e);
return Ok(Some(WsMsg::InvitationSendFailure));
}
} }
Ok(Some(WsMsg::InvitationSendSuccess)) Ok(Some(WsMsg::InvitationSendSuccess))

View File

@ -9,9 +9,12 @@ use jirs_data::{ProjectId, UserId, WsMsg};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::mail::MailExecutor; use crate::mail::MailExecutor;
use crate::ws::auth::{Authenticate, CheckAuthToken, CheckBindToken}; use crate::ws::auth::*;
use crate::ws::comments::*;
use crate::ws::invitations::*; use crate::ws::invitations::*;
use crate::ws::issues::UpdateIssueHandler; use crate::ws::issues::*;
use crate::ws::projects::*;
use crate::ws::users::*;
pub mod auth; pub mod auth;
pub mod comments; pub mod comments;
@ -47,9 +50,8 @@ impl Handler<InnerMsg> for WebSocketActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: InnerMsg, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: InnerMsg, ctx: &mut Self::Context) -> Self::Result {
match msg { if let InnerMsg::Transfer(msg) = msg {
InnerMsg::Transfer(msg) => ctx.send_msg(&msg), ctx.send_msg(&msg)
_ => {}
}; };
} }
} }
@ -87,11 +89,11 @@ impl WebSocketActor {
ctx, ctx,
)?, )?,
WsMsg::IssueCreateRequest(payload) => self.handle_msg(payload, ctx)?, WsMsg::IssueCreateRequest(payload) => self.handle_msg(payload, ctx)?,
WsMsg::IssueDeleteRequest(id) => self.handle_msg(issues::DeleteIssue { id }, ctx)?, WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
WsMsg::ProjectIssuesRequest => self.handle_msg(issues::LoadIssues, ctx)?, WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?,
// projects // projects
WsMsg::ProjectRequest => self.handle_msg(projects::CurrentProject, ctx)?, WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?, WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
// auth // auth
@ -107,7 +109,7 @@ impl WebSocketActor {
// register // register
WsMsg::SignUpRequest(email, username) => self.handle_msg( WsMsg::SignUpRequest(email, username) => self.handle_msg(
users::Register { Register {
name: username, name: username,
email, email,
}, },
@ -115,32 +117,26 @@ impl WebSocketActor {
)?, )?,
// users // users
WsMsg::ProjectUsersRequest => self.handle_msg(users::LoadProjectUsers, ctx)?, WsMsg::ProjectUsersRequest => self.handle_msg(LoadProjectUsers, ctx)?,
// comments // comments
WsMsg::IssueCommentsRequest(issue_id) => { WsMsg::IssueCommentsRequest(issue_id) => {
self.handle_msg(comments::LoadIssueComments { issue_id }, ctx)? self.handle_msg(LoadIssueComments { issue_id }, ctx)?
} }
WsMsg::CreateComment(payload) => self.handle_msg(payload, ctx)?, WsMsg::CreateComment(payload) => self.handle_msg(payload, ctx)?,
WsMsg::UpdateComment(payload) => self.handle_msg(payload, ctx)?, WsMsg::UpdateComment(payload) => self.handle_msg(payload, ctx)?,
WsMsg::CommentDeleteRequest(comment_id) => { WsMsg::CommentDeleteRequest(comment_id) => {
self.handle_msg(comments::DeleteComment { comment_id }, ctx)? self.handle_msg(DeleteComment { comment_id }, ctx)?
} }
// invitations // invitations
WsMsg::InvitationSendRequest { name, email } => self.handle_msg( WsMsg::InvitationSendRequest { name, email } => {
CreateInvitation { self.handle_msg(CreateInvitation { name, email }, ctx)?
name: name.clone(), }
email: email.clone(),
},
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)?,
WsMsg::InvitationRevokeRequest(id) => self.handle_msg(RevokeInvitation { id }, ctx)?, WsMsg::InvitationRevokeRequest(id) => self.handle_msg(RevokeInvitation { id }, ctx)?,
WsMsg::InvitedUsersRequest => self.handle_msg(LoadInvitedUsers, ctx)?,
WsMsg::InvitedUsersRequest => None,
// else fail // else fail
_ => { _ => {
@ -296,9 +292,7 @@ impl Handler<InnerMsg> for WsServer {
impl WsServer { impl WsServer {
pub fn ensure_room(&mut self, room: i32) { pub fn ensure_room(&mut self, room: i32) {
if !self.rooms.contains_key(&room) { self.rooms.entry(room).or_insert_with(HashSet::new);
self.rooms.insert(room, HashSet::new());
}
} }
} }

View File

@ -12,7 +12,7 @@ impl WsHandler<CurrentProject> for WebSocketActor {
let project_id = self.require_user()?.project_id; let project_id = self.require_user()?.project_id;
let m = match block_on(self.db.send(LoadCurrentProject { project_id })) { let m = match block_on(self.db.send(LoadCurrentProject { project_id })) {
Ok(Ok(project)) => Some(WsMsg::ProjectLoaded(project.into())), Ok(Ok(project)) => Some(WsMsg::ProjectLoaded(project)),
Ok(Err(e)) => { Ok(Err(e)) => {
error!("{:?}", e); error!("{:?}", e);
None None
@ -39,6 +39,6 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
Ok(Ok(project)) => project, Ok(Ok(project)) => project,
_ => return Ok(None), _ => return Ok(None),
}; };
Ok(Some(WsMsg::ProjectLoaded(project.into()))) Ok(Some(WsMsg::ProjectLoaded(project)))
} }
} }

View File

@ -14,9 +14,7 @@ impl WsHandler<LoadProjectUsers> for WebSocketActor {
let project_id = self.require_user()?.project_id; let project_id = self.require_user()?.project_id;
let m = match block_on(self.db.send(Msg { project_id })) { let m = match block_on(self.db.send(Msg { project_id })) {
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded( Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)),
v.into_iter().map(|i| i.into()).collect(),
)),
_ => None, _ => None,
}; };
Ok(m) Ok(m)