Fix accept invitation, fix link send to user. Change state of form, display invitations and users
This commit is contained in:
parent
4316f6888d
commit
ff0d4bb329
30
jirs-client/js/css/users.css
Normal file
30
jirs-client/js/css/users.css
Normal 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);
|
||||
}
|
@ -27,3 +27,4 @@
|
||||
@import "./css/timeTracking.css";
|
||||
@import "./css/login.css";
|
||||
@import "./css/register.css";
|
||||
@import "./css/users.css";
|
||||
|
@ -11,25 +11,19 @@ use crate::validations::is_token;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Project) => {
|
||||
model.page_content = PageContent::Invite(InvitePage::default());
|
||||
if let Msg::ChangePage(Page::Project) = msg {
|
||||
model.page_content = PageContent::Invite(Box::new(InvitePage::default()));
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::Invite(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) => {
|
||||
if let Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) = msg {
|
||||
page.token_touched = true;
|
||||
page.token = text.clone();
|
||||
}
|
||||
_ => (),
|
||||
page.token = text;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,6 +153,7 @@ pub enum Msg {
|
||||
AuthTokenErased,
|
||||
SignInRequest,
|
||||
BindClientRequest,
|
||||
InviteRequest,
|
||||
|
||||
// sign up
|
||||
SignUpRequest,
|
||||
@ -360,7 +361,7 @@ fn authorize_or_redirect() {
|
||||
Err(..) => {
|
||||
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
||||
match pathname.as_str() {
|
||||
"/login" | "/register" => {}
|
||||
"/login" | "/register" | "/invite" => {}
|
||||
_ => {
|
||||
seed::push_route(vec!["login"]);
|
||||
}
|
||||
|
@ -39,9 +39,9 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
|
||||
let payload = jirs_data::CreateIssuePayload {
|
||||
title: modal.title.clone(),
|
||||
issue_type: modal.issue_type.clone(),
|
||||
status: modal.status.clone(),
|
||||
priority: modal.priority.clone(),
|
||||
issue_type: modal.issue_type,
|
||||
status: modal.status,
|
||||
priority: modal.priority,
|
||||
description: modal.description.clone(),
|
||||
description_text: modal.description_text.clone(),
|
||||
estimate: modal.estimate,
|
||||
|
@ -40,7 +40,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
modal.id,
|
||||
IssueFieldId::Type,
|
||||
PayloadVariant::IssueType(modal.payload.issue_type.clone()),
|
||||
PayloadVariant::IssueType(modal.payload.issue_type),
|
||||
));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
@ -51,7 +51,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
modal.id,
|
||||
IssueFieldId::Status,
|
||||
PayloadVariant::IssueStatus(modal.payload.status.clone()),
|
||||
PayloadVariant::IssueStatus(modal.payload.status),
|
||||
));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
@ -143,7 +143,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
modal.id,
|
||||
IssueFieldId::TimeSpend,
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent.clone()),
|
||||
PayloadVariant::OptionI32(modal.payload.time_spent),
|
||||
));
|
||||
}
|
||||
Msg::InputChanged(
|
||||
@ -154,7 +154,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining.clone()),
|
||||
PayloadVariant::OptionI32(modal.payload.time_remaining),
|
||||
));
|
||||
}
|
||||
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(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate.clone()),
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
));
|
||||
}
|
||||
_ 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(
|
||||
modal.id,
|
||||
IssueFieldId::TimeRemaining,
|
||||
PayloadVariant::OptionI32(modal.payload.estimate.clone()),
|
||||
PayloadVariant::OptionI32(modal.payload.estimate),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
|
@ -50,7 +50,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
|
||||
Msg::ChangePage(Page::AddIssue) => {
|
||||
let mut modal = AddIssueModal::default();
|
||||
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,
|
||||
_ => return,
|
||||
};
|
||||
ModalType::EditIssue(issue_id, EditIssueModal::new(issue))
|
||||
ModalType::EditIssue(issue_id, Box::new(EditIssueModal::new(issue)))
|
||||
};
|
||||
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id));
|
||||
model.modals.push(modal);
|
||||
|
@ -11,8 +11,8 @@ use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL};
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum ModalType {
|
||||
AddIssue(AddIssueModal),
|
||||
EditIssue(IssueId, EditIssueModal),
|
||||
AddIssue(Box<AddIssueModal>),
|
||||
EditIssue(IssueId, Box<EditIssueModal>),
|
||||
DeleteIssueConfirm(IssueId),
|
||||
DeleteCommentConfirm(CommentId),
|
||||
TimeTracking(IssueId),
|
||||
@ -49,9 +49,9 @@ impl EditIssueModal {
|
||||
link_copied: false,
|
||||
payload: UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type.clone(),
|
||||
status: issue.status.clone(),
|
||||
priority: issue.priority.clone(),
|
||||
issue_type: issue.issue_type,
|
||||
status: issue.status,
|
||||
priority: issue.priority,
|
||||
list_position: issue.list_position,
|
||||
description: issue.description.clone(),
|
||||
description_text: issue.description_text.clone(),
|
||||
@ -252,6 +252,20 @@ pub struct SignUpPage {
|
||||
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)]
|
||||
pub struct UsersPage {
|
||||
pub name: String,
|
||||
@ -263,6 +277,10 @@ pub struct UsersPage {
|
||||
pub user_role_state: StyledSelectState,
|
||||
pub pending_invitations: Vec<String>,
|
||||
pub error: String,
|
||||
pub form_state: InvitationFormState,
|
||||
|
||||
pub invited_users: Vec<User>,
|
||||
pub invitations: Vec<Invitation>,
|
||||
}
|
||||
|
||||
impl Default for UsersPage {
|
||||
@ -276,18 +294,21 @@ impl Default for UsersPage {
|
||||
user_role_state: StyledSelectState::new(FieldId::Users(UsersFieldId::UserRole)),
|
||||
pending_invitations: vec![],
|
||||
error: "".to_string(),
|
||||
form_state: Default::default(),
|
||||
invited_users: vec![],
|
||||
invitations: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PageContent {
|
||||
SignIn(SignInPage),
|
||||
SignUp(SignUpPage),
|
||||
Project(ProjectPage),
|
||||
ProjectSettings(ProjectSettingsPage),
|
||||
Invite(InvitePage),
|
||||
Users(UsersPage),
|
||||
SignIn(Box<SignInPage>),
|
||||
SignUp(Box<SignUpPage>),
|
||||
Project(Box<ProjectPage>),
|
||||
ProjectSettings(Box<ProjectSettingsPage>),
|
||||
Invite(Box<InvitePage>),
|
||||
Users(Box<UsersPage>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -332,7 +353,7 @@ impl Default for Model {
|
||||
comments_by_project_id: Default::default(),
|
||||
page: Page::Project,
|
||||
host_url,
|
||||
page_content: PageContent::Project(ProjectPage::default()),
|
||||
page_content: PageContent::Project(Box::new(ProjectPage::default())),
|
||||
modals: vec![],
|
||||
project: None,
|
||||
comments: vec![],
|
||||
|
@ -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::AddIssue)
|
||||
| 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
|
||||
.iter()
|
||||
.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_only_my(issue, project_page.only_my_filter, &model.user)
|
||||
&& 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();
|
||||
let label = status.to_label();
|
||||
|
||||
let send_status = status.clone();
|
||||
let send_status = status;
|
||||
let drop_handler = drag_ev(Ev::Drop, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDropZone(send_status)
|
||||
});
|
||||
|
||||
let send_status = status.clone();
|
||||
let send_status = status;
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDragOverStatus(send_status)
|
||||
@ -321,8 +321,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
|
||||
&issue.status == status
|
||||
fn issue_filter_status(issue: &Issue, status: IssueStatus) -> bool {
|
||||
issue.status == status
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -384,7 +384,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
ev.stop_propagation();
|
||||
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 class_list = vec!["issue"];
|
||||
|
@ -76,7 +76,7 @@ fn build_page_content(model: &mut Model) {
|
||||
Some(project) => project,
|
||||
_ => 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> {
|
||||
|
@ -35,6 +35,7 @@ pub struct StyledButtonBuilder {
|
||||
on_click: Option<EventHandler<Msg>>,
|
||||
children: Option<Vec<Node<Msg>>>,
|
||||
class_list: Vec<String>,
|
||||
button_type: Option<String>,
|
||||
}
|
||||
|
||||
impl StyledButtonBuilder {
|
||||
@ -107,6 +108,11 @@ impl StyledButtonBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_type_reset(mut self) -> Self {
|
||||
self.button_type = Some("reset".to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledButton {
|
||||
StyledButton {
|
||||
variant: self.variant.unwrap_or_else(|| Variant::Primary),
|
||||
@ -117,6 +123,7 @@ impl StyledButtonBuilder {
|
||||
on_click: self.on_click,
|
||||
children: self.children.unwrap_or_default(),
|
||||
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>>,
|
||||
children: Vec<Node<Msg>>,
|
||||
class_list: Vec<String>,
|
||||
button_type: String,
|
||||
}
|
||||
|
||||
impl StyledButton {
|
||||
@ -154,6 +162,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
|
||||
on_click,
|
||||
children,
|
||||
mut class_list,
|
||||
button_type,
|
||||
} = values;
|
||||
class_list.push("styledButton".to_string());
|
||||
class_list.push(variant.to_string());
|
||||
@ -185,6 +194,7 @@ pub fn render(values: StyledButton) -> Node<Msg> {
|
||||
seed::button![
|
||||
attrs![
|
||||
At::Class => class_list.join(" "),
|
||||
At::Type => button_type,
|
||||
],
|
||||
handler,
|
||||
if disabled {
|
||||
|
@ -183,7 +183,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
|
||||
let textarea = seed::to_textarea(&target);
|
||||
let value = textarea.value();
|
||||
|
||||
if handler_disable_auto_resize && value.contains("\n") {
|
||||
if handler_disable_auto_resize && value.contains('\n') {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
|
||||
}
|
||||
|
||||
if msg == Msg::ChangePage(Page::SignIn) {
|
||||
model.page_content = PageContent::SignIn(SignInPage::default());
|
||||
model.page_content = PageContent::SignIn(Box::new(SignInPage::default()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
|
||||
}
|
||||
|
||||
if msg == Msg::ChangePage(Page::SignUp) {
|
||||
model.page_content = PageContent::SignUp(SignUpPage::default());
|
||||
model.page_content = PageContent::SignUp(Box::new(SignUpPage::default()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::UserRole;
|
||||
use jirs_data::{ToVec, UsersFieldId};
|
||||
use jirs_data::{ToVec, UserRole, UsersFieldId, WsMsg};
|
||||
|
||||
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_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
@ -15,13 +15,10 @@ use crate::validations::is_email;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Users) => {
|
||||
model.page_content = PageContent::Users(UsersPage::default());
|
||||
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,
|
||||
@ -31,6 +28,16 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
page.user_role_state.update(&msg, orders);
|
||||
|
||||
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(
|
||||
FieldId::Users(UsersFieldId::UserRole),
|
||||
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_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()
|
||||
.add_class("submitUserInvite")
|
||||
.active(true)
|
||||
.active(page.form_state != InvitationFormState::Sent)
|
||||
.primary()
|
||||
.text("Invite user")
|
||||
.build()
|
||||
.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()
|
||||
.heading("Invite new user")
|
||||
@ -116,8 +152,59 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.add_field(email_field)
|
||||
.add_field(user_role_field)
|
||||
.add_field(submit_field)
|
||||
.on_submit(ev(Ev::Submit, |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::InviteRequest
|
||||
}))
|
||||
.build()
|
||||
.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![],
|
||||
)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ pub fn is_email(s: &str) -> bool {
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
return false;
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_token(s: &str) -> bool {
|
||||
|
@ -64,7 +64,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
model.issues.push(c);
|
||||
}
|
||||
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);
|
||||
|
||||
@ -96,7 +96,7 @@ pub fn sync(model: &mut Model) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
issue.id,
|
||||
IssueFieldId::Status,
|
||||
PayloadVariant::IssueStatus(issue.status.clone()),
|
||||
PayloadVariant::IssueStatus(issue.status),
|
||||
));
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||
issue.id,
|
||||
@ -150,7 +150,6 @@ pub fn change_status(status: IssueStatus, model: &mut Model) {
|
||||
|
||||
if issue.status == status {
|
||||
model.issues.push(issue);
|
||||
return;
|
||||
} else {
|
||||
issue.status = status;
|
||||
issue.list_position = pos + 1;
|
||||
|
@ -29,7 +29,7 @@ pub type UsernameString = String;
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
#[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 {
|
||||
Task,
|
||||
Bug,
|
||||
@ -93,7 +93,7 @@ impl std::fmt::Display for IssueType {
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
#[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 {
|
||||
Backlog,
|
||||
Selected,
|
||||
|
@ -57,7 +57,7 @@ impl Handler<CreateInvitation> for DbExecutor {
|
||||
let conn = &self
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
|
||||
|
||||
let form = InvitationForm {
|
||||
name: msg.name,
|
||||
@ -70,7 +70,7 @@ impl Handler<CreateInvitation> for DbExecutor {
|
||||
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
|
||||
query
|
||||
.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());
|
||||
query
|
||||
.execute(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -125,7 +125,7 @@ impl Handler<RevokeInvitation> for DbExecutor {
|
||||
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
|
||||
query
|
||||
.execute(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -154,15 +154,22 @@ impl Handler<AcceptInvitation> for DbExecutor {
|
||||
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
|
||||
let invitation: Invitation = query
|
||||
.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)
|
||||
.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());
|
||||
query
|
||||
.execute(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
|
||||
|
||||
let form = UserForm {
|
||||
name: invitation.name,
|
||||
@ -174,7 +181,7 @@ impl Handler<AcceptInvitation> for DbExecutor {
|
||||
debug!("{}", diesel::debug_query::<Pg, _>(&query).to_string());
|
||||
let user: User = query
|
||||
.get_result(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
.map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use actix::{Handler, Message};
|
||||
use lettre;
|
||||
use lettre_email;
|
||||
// use lettre;
|
||||
// use lettre_email;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::mail::MailExecutor;
|
||||
@ -35,7 +35,7 @@ impl Handler<Invite> for MailExecutor {
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
@ -46,7 +46,7 @@ impl Handler<Invite> for MailExecutor {
|
||||
);
|
||||
|
||||
let email = lettre_email::Email::builder()
|
||||
.from(from.clone())
|
||||
.from(from)
|
||||
.to(msg.email.as_str())
|
||||
.html(html.as_str())
|
||||
.subject("Invitation to JIRS project")
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::fs::*;
|
||||
|
||||
use actix::{Actor, SyncContext};
|
||||
use lettre;
|
||||
// use lettre;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod invite;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use actix::{Handler, Message};
|
||||
use lettre;
|
||||
use lettre_email;
|
||||
// use lettre;
|
||||
// use lettre_email;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::mail::MailExecutor;
|
||||
@ -45,7 +45,7 @@ impl Handler<Welcome> for MailExecutor {
|
||||
);
|
||||
|
||||
let email = lettre_email::Email::builder()
|
||||
.from(from.clone())
|
||||
.from(from)
|
||||
.to(msg.email.as_str())
|
||||
.html(html.as_str())
|
||||
.subject("Welcome to JIRS")
|
||||
|
@ -43,7 +43,7 @@ impl WsHandler<Authenticate> for WebSocketActor {
|
||||
if let Some(bind_token) = token.bind_token.as_ref().cloned() {
|
||||
match block_on(self.mail.send(Welcome {
|
||||
bind_token,
|
||||
email: user.email.clone(),
|
||||
email: user.email,
|
||||
})) {
|
||||
Ok(Ok(_)) => (),
|
||||
Ok(Err(e)) => {
|
||||
@ -69,7 +69,7 @@ impl WsHandler<CheckAuthToken> for WebSocketActor {
|
||||
let user: jirs_data::User = match block_on(self.db.send(AuthorizeUser {
|
||||
access_token: msg.token,
|
||||
})) {
|
||||
Ok(Ok(u)) => u.into(),
|
||||
Ok(Ok(u)) => u,
|
||||
Ok(Err(_)) => {
|
||||
return Ok(Some(WsMsg::AuthorizeLoaded(Err(
|
||||
"Invalid auth token".to_string()
|
||||
|
@ -15,7 +15,7 @@ impl WsHandler<LoadIssueComments> for WebSocketActor {
|
||||
let comments = match block_on(self.db.send(crate::db::comments::LoadIssueComments {
|
||||
issue_id: msg.issue_id,
|
||||
})) {
|
||||
Ok(Ok(comments)) => comments.into_iter().map(|c| c.into()).collect(),
|
||||
Ok(Ok(comments)) => comments,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
|
@ -45,15 +45,29 @@ impl WsHandler<CreateInvitation> for WebSocketActor {
|
||||
name,
|
||||
})) {
|
||||
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 {
|
||||
bind_token: invitation.bind_token.clone(),
|
||||
email: invitation.email.clone(),
|
||||
bind_token: invitation.bind_token,
|
||||
email: invitation.email,
|
||||
inviter_name,
|
||||
})) {
|
||||
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))
|
||||
|
@ -9,9 +9,12 @@ use jirs_data::{ProjectId, UserId, WsMsg};
|
||||
|
||||
use crate::db::DbExecutor;
|
||||
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::issues::UpdateIssueHandler;
|
||||
use crate::ws::issues::*;
|
||||
use crate::ws::projects::*;
|
||||
use crate::ws::users::*;
|
||||
|
||||
pub mod auth;
|
||||
pub mod comments;
|
||||
@ -47,9 +50,8 @@ impl Handler<InnerMsg> for WebSocketActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: InnerMsg, ctx: &mut Self::Context) -> Self::Result {
|
||||
match msg {
|
||||
InnerMsg::Transfer(msg) => ctx.send_msg(&msg),
|
||||
_ => {}
|
||||
if let InnerMsg::Transfer(msg) = msg {
|
||||
ctx.send_msg(&msg)
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -87,11 +89,11 @@ impl WebSocketActor {
|
||||
ctx,
|
||||
)?,
|
||||
WsMsg::IssueCreateRequest(payload) => self.handle_msg(payload, ctx)?,
|
||||
WsMsg::IssueDeleteRequest(id) => self.handle_msg(issues::DeleteIssue { id }, ctx)?,
|
||||
WsMsg::ProjectIssuesRequest => self.handle_msg(issues::LoadIssues, ctx)?,
|
||||
WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
|
||||
WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?,
|
||||
|
||||
// projects
|
||||
WsMsg::ProjectRequest => self.handle_msg(projects::CurrentProject, ctx)?,
|
||||
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
|
||||
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
|
||||
|
||||
// auth
|
||||
@ -107,7 +109,7 @@ impl WebSocketActor {
|
||||
|
||||
// register
|
||||
WsMsg::SignUpRequest(email, username) => self.handle_msg(
|
||||
users::Register {
|
||||
Register {
|
||||
name: username,
|
||||
email,
|
||||
},
|
||||
@ -115,32 +117,26 @@ impl WebSocketActor {
|
||||
)?,
|
||||
|
||||
// users
|
||||
WsMsg::ProjectUsersRequest => self.handle_msg(users::LoadProjectUsers, ctx)?,
|
||||
WsMsg::ProjectUsersRequest => self.handle_msg(LoadProjectUsers, ctx)?,
|
||||
|
||||
// comments
|
||||
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::UpdateComment(payload) => self.handle_msg(payload, ctx)?,
|
||||
WsMsg::CommentDeleteRequest(comment_id) => {
|
||||
self.handle_msg(comments::DeleteComment { comment_id }, ctx)?
|
||||
self.handle_msg(DeleteComment { comment_id }, ctx)?
|
||||
}
|
||||
|
||||
// invitations
|
||||
WsMsg::InvitationSendRequest { name, email } => self.handle_msg(
|
||||
CreateInvitation {
|
||||
name: name.clone(),
|
||||
email: email.clone(),
|
||||
},
|
||||
ctx,
|
||||
)?,
|
||||
WsMsg::InvitationSendRequest { name, email } => {
|
||||
self.handle_msg(CreateInvitation { name, email }, ctx)?
|
||||
}
|
||||
WsMsg::InvitationListRequest => self.handle_msg(ListInvitation, ctx)?,
|
||||
WsMsg::InvitationAcceptRequest(id) => self.handle_msg(AcceptInvitation { id }, ctx)?,
|
||||
|
||||
WsMsg::InvitationRevokeRequest(id) => self.handle_msg(RevokeInvitation { id }, ctx)?,
|
||||
|
||||
WsMsg::InvitedUsersRequest => None,
|
||||
WsMsg::InvitedUsersRequest => self.handle_msg(LoadInvitedUsers, ctx)?,
|
||||
|
||||
// else fail
|
||||
_ => {
|
||||
@ -296,9 +292,7 @@ impl Handler<InnerMsg> for WsServer {
|
||||
|
||||
impl WsServer {
|
||||
pub fn ensure_room(&mut self, room: i32) {
|
||||
if !self.rooms.contains_key(&room) {
|
||||
self.rooms.insert(room, HashSet::new());
|
||||
}
|
||||
self.rooms.entry(room).or_insert_with(HashSet::new);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@ impl WsHandler<CurrentProject> for WebSocketActor {
|
||||
let project_id = self.require_user()?.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)) => {
|
||||
error!("{:?}", e);
|
||||
None
|
||||
@ -39,6 +39,6 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
|
||||
Ok(Ok(project)) => project,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(WsMsg::ProjectLoaded(project.into())))
|
||||
Ok(Some(WsMsg::ProjectLoaded(project)))
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,7 @@ impl WsHandler<LoadProjectUsers> for WebSocketActor {
|
||||
|
||||
let project_id = self.require_user()?.project_id;
|
||||
let m = match block_on(self.db.send(Msg { project_id })) {
|
||||
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(
|
||||
v.into_iter().map(|i| i.into()).collect(),
|
||||
)),
|
||||
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)),
|
||||
_ => None,
|
||||
};
|
||||
Ok(m)
|
||||
|
Loading…
Reference in New Issue
Block a user