diff --git a/jirs-client/js/css/styledModal.css b/jirs-client/js/css/styledModal.css index 27e0f060..f40df27e 100644 --- a/jirs-client/js/css/styledModal.css +++ b/jirs-client/js/css/styledModal.css @@ -103,3 +103,7 @@ .modal > .clickableOverlay > .styledModal.confirmModal > .actions > .styledButton { margin-right: 10px; } + +.modal > .clickableOverlay > .styledModal.debugModal { + padding-left: 15px; +} diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index cd32cb54..af8ccffc 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -174,8 +174,8 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { }, _ => (), } - crate::modal::update(&msg, model, orders); crate::shared::aside::update(&msg, model, orders); + crate::modal::update(&msg, model, orders); match model.page { Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders), diff --git a/jirs-client/src/modal/debug_modal.rs b/jirs-client/src/modal/debug_modal.rs new file mode 100644 index 00000000..187a191f --- /dev/null +++ b/jirs-client/src/modal/debug_modal.rs @@ -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 { + let text = format!("{:#?}", model); + let code = pre![text]; + StyledModal::build() + .width(1200) + .add_class("debugModal") + .center() + .children(vec![code]) + .build() + .into_node() +} diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs index 11dc8012..82677c86 100644 --- a/jirs-client/src/modal/mod.rs +++ b/jirs-client/src/modal/mod.rs @@ -11,6 +11,8 @@ use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged}; mod add_issue; mod confirm_delete_issue; +#[cfg(debug_assertions)] +mod debug_modal; mod delete_issue_status; mod issue_details; pub mod time_tracking; @@ -55,6 +57,11 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders 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); @@ -96,6 +103,8 @@ pub fn view(model: &model::Model) -> Node { ModalType::DeleteIssueStatusModal(delete_issue_modal) => { delete_issue_status::view(model, delete_issue_modal.delete_id) } + #[cfg(debug_assertions)] + ModalType::DebugModal => debug_modal::view(model), }) .collect(); section![id!["modals"], modals] diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index fdae99c6..6385139e 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -23,6 +23,8 @@ pub enum ModalType { DeleteCommentConfirm(CommentId), TimeTracking(IssueId), DeleteIssueStatusModal(Box), + #[cfg(debug_assertions)] + DebugModal, } #[derive(Clone, Debug, PartialOrd, PartialEq)] diff --git a/jirs-client/src/shared/styled_modal.rs b/jirs-client/src/shared/styled_modal.rs index 68c0ddc4..5b547b87 100644 --- a/jirs-client/src/shared/styled_modal.rs +++ b/jirs-client/src/shared/styled_modal.rs @@ -63,9 +63,9 @@ impl StyledModalBuilder { self } - // pub fn center(mut self) -> Self { - // self.variant(Variant::Center) - // } + pub fn center(self) -> Self { + self.variant(Variant::Center) + } pub fn width(mut self, width: usize) -> Self { self.width = Some(width); diff --git a/jirs-client/src/users/mod.rs b/jirs-client/src/users/mod.rs new file mode 100644 index 00000000..d42c7ecd --- /dev/null +++ b/jirs-client/src/users/mod.rs @@ -0,0 +1,5 @@ +pub use update::*; +pub use view::*; + +mod update; +mod view; diff --git a/jirs-client/src/users/update.rs b/jirs-client/src/users/update.rs new file mode 100644 index 00000000..95058448 --- /dev/null +++ b/jirs-client/src/users/update.rs @@ -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) { + 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) { + enqueue_ws_msg( + vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest], + model.ws.as_ref(), + orders, + ); +} diff --git a/jirs-client/src/users.rs b/jirs-client/src/users/view.rs similarity index 51% rename from jirs-client/src/users.rs rename to jirs-client/src/users/view.rs index 7e6cdf63..0c953ec4 100644 --- a/jirs-client/src/users.rs +++ b/jirs-client/src/users/view.rs @@ -1,133 +1,16 @@ 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_field::StyledField; use crate::shared::styled_form::StyledForm; 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::validations::is_email; -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) { - 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, - ); - } - _ => (), - } -} +use crate::{FieldId, Msg, PageChanged, UsersPageChange}; pub fn view(model: &Model) -> Node { if model.user.is_none() { @@ -231,12 +114,18 @@ pub fn view(model: &Model) -> Node { .on_click(mouse_ev(Ev::Click, move |_| Msg::InvitedUserRemove(email))) .build() .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![ class!["user"], span![user.name.as_str()], span![user.email.as_str()], + span![format!("{}", role)], remove, ] }) diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 13b70710..f9090ee3 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -79,7 +79,9 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { // auth WsMsg::AuthorizeLoaded(Ok(user)) => { model.user = Some(user.clone()); - go_to_board(orders); + if is_non_logged_area() { + go_to_board(orders); + } } WsMsg::AuthorizeExpired => { if let Ok(msg) = write_auth_token(None) { @@ -217,3 +219,11 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders) { }; 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, + } +} diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 9da11c1a..7364fd10 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -449,6 +449,7 @@ pub struct Invitation { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub bind_token: Uuid, + pub role: UserRole, } #[cfg_attr(feature = "backend", derive(Queryable))] @@ -696,6 +697,7 @@ pub enum WsMsg { InvitationSendRequest { name: UsernameString, email: EmailString, + role: UserRole, }, InvitationSendSuccess, InvitationSendFailure, diff --git a/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/down.sql b/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/down.sql new file mode 100644 index 00000000..fc01cbf6 --- /dev/null +++ b/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/down.sql @@ -0,0 +1 @@ +ALTER TABLE invitations DROP COLUMN role; diff --git a/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/up.sql b/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/up.sql new file mode 100644 index 00000000..43501c63 --- /dev/null +++ b/jirs-server/migrations/2020-05-21-160206_add_role_to_invitation/up.sql @@ -0,0 +1 @@ +ALTER TABLE invitations ADD COLUMN role "UserRoleType" NOT NULL DEFAULT 'user'; diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs index 5bd6240f..516f41b2 100644 --- a/jirs-server/src/db/invitations.rs +++ b/jirs-server/src/db/invitations.rs @@ -3,12 +3,12 @@ use diesel::pg::Pg; use diesel::prelude::*; 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::errors::ServiceErrors; -use crate::models::InvitationForm; pub struct ListInvitation { pub user_id: UserId, @@ -46,6 +46,7 @@ pub struct CreateInvitation { pub project_id: ProjectId, pub email: EmailString, pub name: UsernameString, + pub role: UserRole, } impl Message for CreateInvitation { @@ -63,14 +64,14 @@ impl Handler for DbExecutor { .get() .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; - let form = InvitationForm { - name: msg.name, - email: msg.email, - state: InvitationState::Sent, - project_id: msg.project_id, - invited_by_id: msg.user_id, - }; - let query = diesel::insert_into(invitations).values(form); + let query = diesel::insert_into(invitations).values(( + name.eq(msg.name), + email.eq(msg.email), + state.eq(InvitationState::Sent), + project_id.eq(msg.project_id), + invited_by_id.eq(msg.user_id), + role.eq(msg.role), + )); debug!("{}", diesel::debug_query::(&query).to_string()); query .get_result(conn) @@ -183,18 +184,22 @@ impl Handler for DbExecutor { let user: User = { use crate::schema::users::dsl::*; - let query = diesel::insert_into(users) - .values((name.eq(invitation.name), email.eq(invitation.email))); + let query = users + .filter(name.eq(invitation.name).and(email.eq(invitation.email))) + .limit(1); debug!("{}", diesel::debug_query::(&query)); query - .get_result(conn) + .first(conn) .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))? }; { use crate::schema::user_projects::dsl::*; - let query = diesel::insert_into(user_projects) - .values((user_id.eq(user.id), project_id.eq(invitation.project_id))); + let query = diesel::insert_into(user_projects).values(( + user_id.eq(user.id), + project_id.eq(invitation.project_id), + role.eq(invitation.role), + )); debug!("{}", diesel::debug_query::(&query)); query .execute(conn) diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index df84c115..fb82789c 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -109,6 +109,12 @@ table! { /// /// (Automatically generated by Diesel.) bind_token -> Uuid, + /// The `role` column of the `invitations` table. + /// + /// Its SQL type is `UserRoleType`. + /// + /// (Automatically generated by Diesel.) + role -> UserRoleType, } } diff --git a/jirs-server/src/ws/invitations.rs b/jirs-server/src/ws/invitations.rs index 1f1b939f..0a2a6f2a 100644 --- a/jirs-server/src/ws/invitations.rs +++ b/jirs-server/src/ws/invitations.rs @@ -1,6 +1,6 @@ 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::ws::{WebSocketActor, WsHandler, WsResult}; @@ -31,6 +31,7 @@ impl WsHandler for WebSocketActor { pub struct CreateInvitation { pub email: EmailString, pub name: UsernameString, + pub role: UserRole, } impl WsHandler for WebSocketActor { @@ -45,12 +46,13 @@ impl WsHandler for WebSocketActor { _ => return Ok(None), }; - let CreateInvitation { email, name } = msg; + let CreateInvitation { email, name, role } = msg; let invitation = match block_on(self.db.send(invitations::CreateInvitation { user_id, project_id, email, name, + role, })) { Ok(Ok(invitation)) => invitation, Ok(Err(e)) => { diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 07285104..b4dc97bd 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -20,6 +20,7 @@ use crate::ws::invitations::*; use crate::ws::issue_statuses::*; use crate::ws::issues::*; use crate::ws::projects::*; +use crate::ws::user_projects::LoadUserProjects; use crate::ws::users::*; pub mod auth; @@ -121,8 +122,12 @@ impl WebSocketActor { // projects WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?, + WsMsg::ProjectsLoad => self.handle_msg(LoadProjects, ctx)?, WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?, + // user projects + WsMsg::UserProjectLoad => self.handle_msg(LoadUserProjects, ctx)?, + // auth WsMsg::AuthorizeRequest(uuid) => { self.handle_msg(CheckAuthToken { token: uuid }, ctx)? @@ -157,8 +162,8 @@ impl WebSocketActor { } // invitations - WsMsg::InvitationSendRequest { name, email } => { - self.handle_msg(CreateInvitation { name, email }, ctx)? + WsMsg::InvitationSendRequest { name, email, role } => { + self.handle_msg(CreateInvitation { name, email, role }, ctx)? } WsMsg::InvitationListRequest => self.handle_msg(ListInvitation, ctx)?, WsMsg::InvitationAcceptRequest(id) => self.handle_msg(AcceptInvitation { id }, ctx)?, diff --git a/jirs-server/src/ws/projects.rs b/jirs-server/src/ws/projects.rs index 25139552..4d722c87 100644 --- a/jirs-server/src/ws/projects.rs +++ b/jirs-server/src/ws/projects.rs @@ -2,6 +2,7 @@ use futures::executor::block_on; use jirs_data::{UpdateProjectPayload, WsMsg}; +use crate::db; use crate::db::projects::LoadCurrentProject; use crate::ws::{WebSocketActor, WsHandler, WsResult}; @@ -50,3 +51,22 @@ impl WsHandler for WebSocketActor { Ok(Some(WsMsg::ProjectLoaded(project))) } } + +pub struct LoadProjects; + +impl WsHandler 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) + } + } + } +}