From 8412a113e787443af8d0641ae460e67a7b942e68 Mon Sep 17 00:00:00 2001 From: eraden Date: Mon, 18 Jan 2021 22:59:12 +0100 Subject: [PATCH] Rewrite modals Refactor query db in ws actor, add edit epic ui and some logic --- .builds/client.yml | 37 --- .builds/db.toml | 2 - .builds/nginx.conf | 29 -- .builds/server.yml | 48 ---- Cargo.lock | 2 + _config.yml | 1 - actors/database-actor/src/models.rs | 2 +- actors/websocket-actor/src/handlers/auth.rs | 83 ++---- .../websocket-actor/src/handlers/comments.rs | 82 ++---- actors/websocket-actor/src/handlers/epics.rs | 11 +- actors/websocket-actor/src/handlers/hi.rs | 23 +- .../src/handlers/invitations.rs | 151 ++++------ .../src/handlers/issue_statuses.rs | 87 ++---- actors/websocket-actor/src/handlers/issues.rs | 88 ++---- .../websocket-actor/src/handlers/messages.rs | 35 +-- .../websocket-actor/src/handlers/projects.rs | 60 ++-- .../src/handlers/user_projects.rs | 42 +-- actors/websocket-actor/src/handlers/users.rs | 99 +++---- actors/websocket-actor/src/prelude.rs | 69 ++++- jirs-client/Cargo.toml | 6 + jirs-client/js/css/styledCheckbox.scss | 11 +- jirs-client/js/css/styledModal.scss | 24 ++ jirs-client/scripts/dev.sh | 10 +- jirs-client/src/components/styled_checkbox.rs | 62 +++-- jirs-client/src/lib.rs | 4 +- jirs-client/src/modals/comments_delete/mod.rs | 4 + .../src/modals/comments_delete/model.rs | 12 + .../src/modals/comments_delete/view.rs | 16 ++ jirs-client/src/modals/epics_delete/model.rs | 2 +- jirs-client/src/modals/epics_delete/update.rs | 14 +- jirs-client/src/modals/epics_edit/model.rs | 25 +- jirs-client/src/modals/epics_edit/update.rs | 46 +++- jirs-client/src/modals/epics_edit/view.rs | 71 ++++- .../modals/issue_statuses_delete/update.rs | 18 +- jirs-client/src/modals/issues_create/model.rs | 34 +-- .../src/modals/issues_create/update.rs | 17 +- jirs-client/src/modals/issues_create/view.rs | 19 +- jirs-client/src/modals/issues_delete/model.rs | 5 + jirs-client/src/modals/issues_delete/view.rs | 22 +- jirs-client/src/modals/issues_edit/model.rs | 4 +- jirs-client/src/modals/issues_edit/update.rs | 19 +- jirs-client/src/modals/issues_edit/view.rs | 21 +- .../src/modals/issues_edit/view/comments.rs | 2 +- jirs-client/src/modals/mod.rs | 1 + jirs-client/src/modals/time_tracking/model.rs | 12 + jirs-client/src/modals/time_tracking/view.rs | 16 +- jirs-client/src/modals/update.rs | 257 ++++++++++++++---- jirs-client/src/modals/view.rs | 103 +++---- jirs-client/src/model.rs | 69 ++++- jirs-client/src/pages/project_page/model.rs | 60 ---- jirs-client/src/pages/project_page/update.rs | 12 +- .../src/pages/project_settings_page/view.rs | 17 +- jirs-client/src/shared/tracking_widget.rs | 2 +- jirs-client/src/ws/mod.rs | 4 +- jirs-server/seed.sql | 45 ++- 55 files changed, 1038 insertions(+), 979 deletions(-) delete mode 100644 .builds/client.yml delete mode 100644 .builds/db.toml delete mode 100644 .builds/nginx.conf delete mode 100644 .builds/server.yml delete mode 100644 _config.yml create mode 100644 jirs-client/src/modals/comments_delete/mod.rs create mode 100644 jirs-client/src/modals/comments_delete/model.rs create mode 100644 jirs-client/src/modals/comments_delete/view.rs diff --git a/.builds/client.yml b/.builds/client.yml deleted file mode 100644 index 3742e418..00000000 --- a/.builds/client.yml +++ /dev/null @@ -1,37 +0,0 @@ -image: archlinux -packages: - - nodejs - - rustup - - yarn -sources: - - https://git.sr.ht/~tsumanu/jirs -environment: - deploy: adrian.wozniak@ita-prog.pl - DEBUG: false - JIRS_CLIENT_PORT: 80 - JIRS_CLIENT_BIND: jirs.ita-prog.pl - JIRS_SERVER_PORT: 80 - JIRS_SERVER_BIND: jirs.ita-prog.pl - CI: true -secrets: - - 46f739e5-4538-45dd-a79f-bf173b7a2ed9 -tasks: - - setup: | - rustup toolchain install nightly - rustup default nightly - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sudo sh - - test: | - cd ~/jirs/jirs-client - export NODE_ENV=development - wasm-pack test --node - - build: | - cd ~/jirs/jirs-client - export NODE_ENV=production - ./scripts/prod.sh - export TAR_NAME=$(date -u +"%Y%m%d%H%M%s") - tar -cJvf ~/${TAR_NAME}.tar.xz ./build - cp ~/${TAR_NAME}.tar.xz ~/latest.tar.xz - scp ~/latest.tar.xz rpi.ita-prog.pl:/www/http/static/jirs-client-{TAR_NAME}.tar.xz - scp ~/latest.tar.xz rpi.ita-prog.pl:/www/http/static/jirs-client-latest.tar.xz -artifacts: - - latest.tar.gz diff --git a/.builds/db.toml b/.builds/db.toml deleted file mode 100644 index 342b2929..00000000 --- a/.builds/db.toml +++ /dev/null @@ -1,2 +0,0 @@ -concurrency = 2 -database_url = "postgres://build@localhost:5432/jirs" diff --git a/.builds/nginx.conf b/.builds/nginx.conf deleted file mode 100644 index 52ebce69..00000000 --- a/.builds/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -server { - listen 80; - server_name jirs.lvh.me; - - charset utf-8; - root /assets; - - location ~ .wasm { - default_type application/wasm; - } - - location *.js { - default_type application/javascript; - } - - location / { - index index.html index.htm; - } - - error_page 404 =200 /index.html; - - location /ws/ { - proxy_pass http://server:5000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Host $host; - } -} diff --git a/.builds/server.yml b/.builds/server.yml deleted file mode 100644 index 16c5fb46..00000000 --- a/.builds/server.yml +++ /dev/null @@ -1,48 +0,0 @@ -image: archlinux -packages: - - postgresql - - rustup -sources: - - https://git.sr.ht/~tsumanu/jirs -environment: - deploy: adrian.wozniak@ita-prog.pl - DATABASE_URL: postgres://build@localhost:5432/jirs - DEBUG: true - NODE_ENV: development - RUST_LOG: debug - JIRS_CLIENT_PORT: 7000 - JIRS_CLIENT_BIND: 0.0.0.0 - JIRS_SERVER_PORT: 5000 - JIRS_SERVER_BIND: 0.0.0.0 -secrets: - - 46f739e5-4538-45dd-a79f-bf173b7a2ed9 -tasks: - - build_config: | - cp ~/jirs/.builds/db.toml ~/jirs/jirs-server/db.toml - cp ~/jirs/.builds/db.toml ~/jirs/jirs-server/db.test.toml - - setup: | - sudo mkdir -p /var/lib/postgres/data - sudo chown build /var/lib/postgres/data - initdb -D /var/lib/postgres/data - sudo mkdir -p /run/postgresql - sudo chown build /run/postgresql - pg_ctl -D /var/lib/postgres/data start - rustup toolchain install nightly - rustup default nightly - cargo install diesel_cli --no-default-features --features postgres - cd jirs/jirs-server - /home/build/.cargo/bin/diesel setup - - test: | - cd jirs/jirs-server - cargo test --bin jirs_server - - build: | - cd jirs - cargo build --all --release - strip -s ./target/release/jirs_server - tar -cJvf ~/server.tar.xz ./target/release/jirs_server - - deploy: | - cp ~/server.tar.xz ~/latest.tar.xz - scp ~/latest.tar.xz rpi.ita-prog.pl:/www/http/static/jirs-server-{TAR_NAME}.tar.xz - scp ~/latest.tar.xz rpi.ita-prog.pl:/www/http/static/jirs-server-latest.tar.xz -artifacts: - - jirs_server diff --git a/Cargo.lock b/Cargo.lock index 5201d447..adfee27b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1954,6 +1954,8 @@ version = "0.1.0" dependencies = [ "bincode", "chrono", + "derive_enum_iter", + "derive_enum_primitive", "futures 0.1.30", "jirs-data", "js-sys", diff --git a/_config.yml b/_config.yml deleted file mode 100644 index 2f7efbea..00000000 --- a/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-minimal \ No newline at end of file diff --git a/actors/database-actor/src/models.rs b/actors/database-actor/src/models.rs index b4eb09c9..5c98e02c 100644 --- a/actors/database-actor/src/models.rs +++ b/actors/database-actor/src/models.rs @@ -9,7 +9,7 @@ use { uuid::Uuid, }; -#[derive(Debug, Serialize, Deserialize, Queryable)] +#[derive(Serialize, Debug, Deserialize, Queryable)] pub struct Issue { pub id: i32, pub title: String, diff --git a/actors/websocket-actor/src/handlers/auth.rs b/actors/websocket-actor/src/handlers/auth.rs index 0e0f23b9..5daf138e 100644 --- a/actors/websocket-actor/src/handlers/auth.rs +++ b/actors/websocket-actor/src/handlers/auth.rs @@ -1,5 +1,7 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{ + db_or_debug_and_return, mail_or_debug_and_return, WebSocketActor, WsHandler, WsResult, + }, actix::AsyncContext, database_actor::{ authorize_user::AuthorizeUser, @@ -20,43 +22,16 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult { let Authenticate { name, email } = msg; // TODO check attempt number, allow only 5 times per day - let user = match block_on(self.db.send(LookupUser { name, email })) { - Ok(Ok(user)) => user, - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); - } - Err(e) => { - log::error!("{:?}", e); - return Ok(None); - } - }; - let token = match block_on(self.db.send(CreateBindToken { user_id: user.id })) { - Ok(Ok(token)) => token, - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); - } - Err(e) => { - log::error!("{:?}", e); - return Ok(None); - } - }; + let user = db_or_debug_and_return!(self, LookupUser { name, email }); + let token = db_or_debug_and_return!(self, CreateBindToken { user_id: user.id }); if let Some(bind_token) = token.bind_token.as_ref().cloned() { - match block_on(self.mail.send(Welcome { - bind_token, - email: user.email, - })) { - Ok(Ok(_)) => (), - Ok(Err(e)) => { - log::error!("{}", e); - return Ok(None); + let _ = mail_or_debug_and_return!( + self, + Welcome { + bind_token, + email: user.email, } - Err(e) => { - log::error!("{}", e); - return Ok(None); - } - } + ); } Ok(Some(WsMsg::AuthenticateSuccess)) } @@ -68,17 +43,16 @@ pub struct CheckAuthToken { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CheckAuthToken, ctx: &mut Self::Context) -> WsResult { - let user: jirs_data::User = match block_on(self.db.send(AuthorizeUser { - access_token: msg.token, - })) { - Ok(Ok(u)) => u, - Ok(Err(_)) => { - return Ok(Some(WsMsg::AuthorizeLoaded(Err( - "Invalid auth token".to_string() - )))); - } - _ => return Ok(Some(WsMsg::AuthorizeExpired)), - }; + let user: jirs_data::User = db_or_debug_and_return!( + self, + AuthorizeUser { + access_token: msg.token, + }, + Ok(Some(WsMsg::AuthorizeLoaded(Err( + "Invalid auth token".to_string() + )))), + Ok(Some(WsMsg::AuthorizeExpired)) + ); self.current_user = Some(user.clone()); self.current_user_project = self.load_user_project().ok(); self.current_project = self.load_project().ok(); @@ -94,13 +68,14 @@ pub struct CheckBindToken { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CheckBindToken, _ctx: &mut Self::Context) -> WsResult { - let token: Token = match block_on(self.db.send(FindBindToken { - token: msg.bind_token, - })) { - Ok(Ok(token)) => token, - Ok(Err(_)) => return Ok(Some(WsMsg::BindTokenBad)), - _ => return Ok(None), - }; + let token: Token = db_or_debug_and_return!( + self, + FindBindToken { + token: msg.bind_token, + }, + Ok(Some(WsMsg::BindTokenBad)), + Ok(None) + ); Ok(Some(WsMsg::BindTokenOk(token.access_token))) } } diff --git a/actors/websocket-actor/src/handlers/comments.rs b/actors/websocket-actor/src/handlers/comments.rs index a86b9f3e..5a6cded5 100644 --- a/actors/websocket-actor/src/handlers/comments.rs +++ b/actors/websocket-actor/src/handlers/comments.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, futures::executor::block_on, jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg}, }; @@ -12,19 +12,12 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; - let comments = match block_on(self.db.send(database_actor::comments::LoadIssueComments { - issue_id: msg.issue_id, - })) { - Ok(Ok(comments)) => comments, - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); + let comments = db_or_debug_and_return!( + self, + database_actor::comments::LoadIssueComments { + issue_id: msg.issue_id, } - Err(e) => { - log::error!("{}", e); - return Ok(None); - } - }; + ); Ok(Some(WsMsg::IssueCommentsLoaded(comments))) } @@ -39,21 +32,14 @@ impl WsHandler for WebSocketActor { msg.user_id = Some(user_id); } let issue_id = msg.issue_id; - match block_on(self.db.send(CreateComment { - user_id, - issue_id, - body: msg.body, - })) { - Ok(Ok(_)) => (), - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); + let _ = db_or_debug_and_return!( + self, + CreateComment { + user_id, + issue_id, + body: msg.body, } - Err(e) => { - log::error!("{}", e); - return Ok(None); - } - }; + ); self.handle_msg(LoadIssueComments { issue_id }, ctx) } } @@ -69,21 +55,14 @@ impl WsHandler for WebSocketActor { body, } = msg; - let comment = match block_on(self.db.send(UpdateComment { - comment_id, - user_id, - body, - })) { - Ok(Ok(comment)) => comment, - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); + let comment = db_or_debug_and_return!( + self, + UpdateComment { + comment_id, + user_id, + body, } - Err(e) => { - log::error!("{}", e); - return Ok(None); - } - }; + ); self.broadcast(&WsMsg::CommentUpdated(comment)); Ok(None) } @@ -99,20 +78,13 @@ impl WsHandler for WebSocketActor { let user_id = self.require_user()?.id; - let m = DeleteComment { - comment_id: msg.comment_id, - user_id, - }; - match block_on(self.db.send(m)) { - Ok(Ok(n)) => Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n))), - Ok(Err(e)) => { - log::error!("{:?}", e); - Ok(None) + let n = db_or_debug_and_return!( + self, + DeleteComment { + comment_id: msg.comment_id, + user_id, } - Err(e) => { - log::error!("{}", e); - Ok(None) - } - } + ); + Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n))) } } diff --git a/actors/websocket-actor/src/handlers/epics.rs b/actors/websocket-actor/src/handlers/epics.rs index 8654d4ca..2300a115 100644 --- a/actors/websocket-actor/src/handlers/epics.rs +++ b/actors/websocket-actor/src/handlers/epics.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, futures::executor::block_on, jirs_data::{EpicId, NameString, UserProject, WsMsg}, }; @@ -9,8 +9,7 @@ pub struct LoadEpics; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult { let project_id = self.require_user_project()?.project_id; - let epics = - crate::query_db_or_print!(self, database_actor::epics::LoadEpics { project_id }); + let epics = db_or_debug_and_return!(self, database_actor::epics::LoadEpics { project_id }); Ok(Some(WsMsg::EpicsLoaded(epics))) } } @@ -27,7 +26,7 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?; - let epic = crate::query_db_or_print!( + let epic = db_or_debug_and_return!( self, database_actor::epics::CreateEpic { user_id: *user_id, @@ -48,7 +47,7 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult { let UpdateEpic { epic_id, name } = msg; let UserProject { project_id, .. } = self.require_user_project()?; - let epic = crate::query_db_or_print!( + let epic = db_or_debug_and_return!( self, database_actor::epics::UpdateEpic { project_id: *project_id, @@ -68,7 +67,7 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult { let DeleteEpic { epic_id } = msg; let UserProject { user_id, .. } = self.require_user_project()?; - let n = crate::query_db_or_print!( + let n = db_or_debug_and_return!( self, database_actor::epics::DeleteEpic { user_id: *user_id, diff --git a/actors/websocket-actor/src/handlers/hi.rs b/actors/websocket-actor/src/handlers/hi.rs index a2a816b9..1141cabe 100644 --- a/actors/websocket-actor/src/handlers/hi.rs +++ b/actors/websocket-actor/src/handlers/hi.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{actor_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, futures::executor::block_on, jirs_data::{Code, Lang, WsMsg}, }; @@ -9,19 +9,14 @@ pub struct HighlightCode(pub Lang, pub Code); impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> WsResult { self.require_user()?.id; - match block_on(self.hi.send(highlight_actor::HighlightCode { - code: msg.1, - lang: msg.0, - })) { - Ok(Ok(res)) => Ok(Some(WsMsg::HighlightedCode(res))), - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) + let res = actor_or_debug_and_return!( + self, + hi, + highlight_actor::HighlightCode { + code: msg.1, + lang: msg.0, } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + ); + Ok(Some(WsMsg::HighlightedCode(res))) } } diff --git a/actors/websocket-actor/src/handlers/invitations.rs b/actors/websocket-actor/src/handlers/invitations.rs index 9c024686..25878dd9 100644 --- a/actors/websocket-actor/src/handlers/invitations.rs +++ b/actors/websocket-actor/src/handlers/invitations.rs @@ -1,5 +1,8 @@ use { - crate::{server::InnerMsg, WebSocketActor, WsHandler, WsMessageSender, WsResult}, + crate::{ + db_or_debug_and_return, mail_or_debug_and_return, server::InnerMsg, WebSocketActor, + WsHandler, WsMessageSender, WsResult, + }, database_actor::{invitations, messages::CreateMessageReceiver}, futures::executor::block_on, jirs_data::{ @@ -15,18 +18,8 @@ impl WsHandler for WebSocketActor { Some(id) => id, _ => return Ok(None), }; - let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) { - Ok(Ok(v)) => Some(WsMsg::InvitationListLoaded(v)), - Ok(Err(e)) => { - log::error!("{:?}", e); - return Ok(None); - } - Err(e) => { - log::error!("{}", e); - return Ok(None); - } - }; - Ok(res) + let v = db_or_debug_and_return!(self, invitations::ListInvitation { user_id }); + Ok(Some(WsMsg::InvitationListLoaded(v))) } } @@ -45,39 +38,28 @@ impl WsHandler for WebSocketActor { let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?; let CreateInvitation { email, name, role } = msg; - let invitation = - match block_on(self.db.send(database_actor::invitations::CreateInvitation { + let invitation = db_or_debug_and_return!( + self, + database_actor::invitations::CreateInvitation { user_id, project_id, email: email.clone(), name: name.clone(), role, - })) { - Ok(Ok(invitation)) => invitation, - 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(mail_actor::invite::Invite { - bind_token: invitation.bind_token, - email: invitation.email, - inviter_name, - })) { - Ok(Ok(_)) => (), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(Some(WsMsg::InvitationSendFailure)); - } - Err(e) => { - error!("{}", e); - return Ok(Some(WsMsg::InvitationSendFailure)); - } - } + }, + Ok(Some(WsMsg::InvitationSendFailure)), + Ok(Some(WsMsg::InvitationSendFailure)) + ); + let _ = mail_or_debug_and_return!( + self, + mail_actor::invite::Invite { + bind_token: invitation.bind_token, + email: invitation.email, + inviter_name, + }, + Ok(Some(WsMsg::InvitationSendFailure)), + Ok(Some(WsMsg::InvitationSendFailure)) + ); // If user exists then send message to him if let Ok(Ok(message)) = block_on(self.db.send(database_actor::messages::CreateMessage { @@ -106,18 +88,8 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; let DeleteInvitation { id } = msg; - let res = match block_on(self.db.send(invitations::DeleteInvitation { id })) { - Ok(Ok(_)) => None, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(res) + let _ = db_or_debug_and_return!(self, invitations::DeleteInvitation { id }); + Ok(None) } } @@ -129,18 +101,8 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; let RevokeInvitation { id } = msg; - let res = match block_on(self.db.send(invitations::RevokeInvitation { id })) { - Ok(Ok(_)) => Some(WsMsg::InvitationRevokeSuccess(id)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(res) + let _ = db_or_debug_and_return!(self, invitations::RevokeInvitation { id }); + Ok(Some(WsMsg::InvitationRevokeSuccess(id))) } } @@ -151,45 +113,34 @@ pub struct AcceptInvitation { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> WsResult { let AcceptInvitation { invitation_token } = msg; - let token = match block_on( - self.db - .send(invitations::AcceptInvitation { invitation_token }), - ) { - Ok(Ok(token)) => token, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))); - } - Err(e) => { - error!("{}", e); - return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))); - } - }; + let token = db_or_debug_and_return!( + self, + invitations::AcceptInvitation { invitation_token }, + Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))), + Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))) + ); - for message in block_on( - self.db - .send(database_actor::messages::LookupMessagesByToken { - token: invitation_token, - user_id: token.user_id, - }), - ) - .unwrap_or_else(|_| Ok(vec![])) - .unwrap_or_default() - { - match block_on(self.db.send(database_actor::messages::MarkMessageSeen { + for message in crate::actor_or_debug_and_fallback!( + self, + db, + database_actor::messages::LookupMessagesByToken { + token: invitation_token, user_id: token.user_id, - message_id: message.id, - })) { - Ok(Ok(n)) => { + }, + vec![], + vec![] + ) { + crate::actor_or_debug_and_ignore!( + self, + db, + database_actor::messages::MarkMessageSeen { + user_id: token.user_id, + message_id: message.id, + }, + |n| { ctx.send_msg(&WsMsg::MessageMarkedSeen(message.id, n)); } - Ok(Err(e)) => { - error!("{:?}", e); - } - Err(e) => { - error!("{}", e); - } - } + ); } Ok(Some(WsMsg::InvitationAcceptSuccess(token.access_token))) diff --git a/actors/websocket-actor/src/handlers/issue_statuses.rs b/actors/websocket-actor/src/handlers/issue_statuses.rs index f81d47d0..c3301ef3 100644 --- a/actors/websocket-actor/src/handlers/issue_statuses.rs +++ b/actors/websocket-actor/src/handlers/issue_statuses.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, database_actor::issue_statuses, futures::executor::block_on, jirs_data::{IssueStatusId, Position, TitleString, WsMsg}, @@ -11,21 +11,8 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult { let project_id = self.require_user_project()?.project_id; - let msg = match block_on( - self.db - .send(issue_statuses::LoadIssueStatuses { project_id }), - ) { - Ok(Ok(v)) => Some(WsMsg::IssueStatusesLoaded(v)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(msg) + let v = db_or_debug_and_return!(self, issue_statuses::LoadIssueStatuses { project_id }); + Ok(Some(WsMsg::IssueStatusesLoaded(v))) } } @@ -39,22 +26,15 @@ impl WsHandler for WebSocketActor { let project_id = self.require_user_project()?.project_id; let CreateIssueStatus { position, name } = msg; - let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus { - project_id, - position, - name, - })) { - Ok(Ok(is)) => Some(WsMsg::IssueStatusCreated(is)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + let issue_status = db_or_debug_and_return!( + self, + issue_statuses::CreateIssueStatus { + project_id, + position, + name, } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(msg) + ); + Ok(Some(WsMsg::IssueStatusCreated(issue_status))) } } @@ -67,21 +47,14 @@ impl WsHandler for WebSocketActor { let project_id = self.require_user_project()?.project_id; let DeleteIssueStatus { issue_status_id } = msg; - let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus { - issue_status_id, - project_id, - })) { - Ok(Ok(n)) => Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + let n = db_or_debug_and_return!( + self, + issue_statuses::DeleteIssueStatus { + issue_status_id, + project_id, } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(msg) + ); + Ok(Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n))) } } @@ -100,22 +73,16 @@ impl WsHandler for WebSocketActor { position, name, } = msg; - let msg = match block_on(self.db.send(issue_statuses::UpdateIssueStatus { - issue_status_id, - position, - name, - project_id, - })) { - Ok(Ok(is)) => Some(WsMsg::IssueStatusUpdated(is)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + let issue_status = db_or_debug_and_return!( + self, + issue_statuses::UpdateIssueStatus { + issue_status_id, + position, + name, + project_id, } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; + ); + let msg = Some(WsMsg::IssueStatusUpdated(issue_status)); if let Some(ws_msg) = msg.as_ref() { self.broadcast(ws_msg) } diff --git a/actors/websocket-actor/src/handlers/issues.rs b/actors/websocket-actor/src/handlers/issues.rs index cd06183c..c316f2f0 100644 --- a/actors/websocket-actor/src/handlers/issues.rs +++ b/actors/websocket-actor/src/handlers/issues.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, database_actor::{ issue_assignees::LoadAssignees, issues::{LoadProjectIssues, UpdateIssue}, @@ -125,23 +125,11 @@ impl WsHandler for WebSocketActor { _ => (), }; - let mut issue: jirs_data::Issue = match block_on(self.db.send(msg)) { - Ok(Ok(issue)) => issue.into(), - _ => return Ok(None), - }; + let issue = db_or_debug_and_return!(self, msg); + let mut issue: jirs_data::Issue = issue.into(); let assignees: Vec = - match block_on(self.db.send(LoadAssignees { issue_id: issue.id })) { - Ok(Ok(v)) => v, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; + db_or_debug_and_return!(self, LoadAssignees { issue_id: issue.id }); for assignee in assignees { issue.user_ids.push(assignee.user_id); @@ -170,18 +158,8 @@ impl WsHandler for WebSocketActor { user_ids: msg.user_ids, epic_id: msg.epic_id, }; - let m = match block_on(self.db.send(msg)) { - Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; - Ok(m) + let issue = db_or_debug_and_return!(self, msg); + Ok(Some(WsMsg::IssueCreated(issue.into()))) } } @@ -192,21 +170,11 @@ pub struct DeleteIssue { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: DeleteIssue, _ctx: &mut Self::Context) -> WsResult { self.require_user()?; - let m = match block_on( - self.db - .send(database_actor::issues::DeleteIssue { issue_id: msg.id }), - ) { - Ok(Ok(n)) => Some(WsMsg::IssueDeleted(msg.id, n)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; - Ok(m) + let n = db_or_debug_and_return!( + self, + database_actor::issues::DeleteIssue { issue_id: msg.id } + ); + Ok(Some(WsMsg::IssueDeleted(msg.id, n))) } } @@ -216,14 +184,11 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult { let project_id = self.require_user_project()?.project_id; - let issues: Vec = - match block_on(self.db.send(LoadProjectIssues { project_id })) { - Ok(Ok(v)) => v.into_iter().map(|i| i.into()).collect(), - _ => return Ok(None), - }; + let v = db_or_debug_and_return!(self, LoadProjectIssues { project_id }); + let issues: Vec = v.into_iter().map(|i| i.into()).collect(); let mut issue_map = HashMap::new(); let mut queue = vec![]; - for issue in issues.into_iter() { + for issue in issues { let f = self.db.send(LoadAssignees { issue_id: issue.id }); queue.push(f); issue_map.insert(issue.id, issue); @@ -238,9 +203,10 @@ impl WsHandler for WebSocketActor { }; } let mut issues = vec![]; - for (_, issue) in issue_map.into_iter() { + for (_, issue) in issue_map { issues.push(issue); } + issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); Ok(Some(WsMsg::ProjectIssuesLoaded(issues))) } @@ -252,16 +218,18 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: SyncIssueListPosition, ctx: &mut Self::Context) -> WsResult { let _project_id = self.require_user_project()?.project_id; for (issue_id, list_position, status_id, epic_id) in msg.0 { - match block_on(self.db.send(database_actor::issues::UpdateIssue { - issue_id, - list_position: Some(list_position), - issue_status_id: Some(status_id), - epic_id: Some(epic_id), - ..Default::default() - })) { - Ok(Ok(_)) => (), - _ => return Ok(None), - }; + crate::actor_or_debug_and_ignore!( + self, + db, + database_actor::issues::UpdateIssue { + issue_id, + list_position: Some(list_position), + issue_status_id: Some(status_id), + epic_id: Some(epic_id), + ..Default::default() + }, + |_| {} + ); } self.handle_msg(LoadIssues, ctx) diff --git a/actors/websocket-actor/src/handlers/messages.rs b/actors/websocket-actor/src/handlers/messages.rs index abb69b6f..88b60c40 100644 --- a/actors/websocket-actor/src/handlers/messages.rs +++ b/actors/websocket-actor/src/handlers/messages.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, database_actor::messages, futures::executor::block_on, jirs_data::{MessageId, WsMsg}, @@ -10,17 +10,8 @@ pub struct LoadMessages; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadMessages, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - match block_on(self.db.send(messages::LoadMessages { user_id })) { - Ok(Ok(v)) => Ok(Some(WsMsg::MessagesLoaded(v))), - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) - } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + let v = db_or_debug_and_return!(self, messages::LoadMessages { user_id }); + Ok(Some(WsMsg::MessagesLoaded(v))) } } @@ -31,19 +22,13 @@ pub struct MarkMessageSeen { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - match block_on(self.db.send(messages::MarkMessageSeen { - message_id: msg.id, - user_id, - })) { - Ok(Ok(count)) => Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count))), - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) + let count = db_or_debug_and_return!( + self, + messages::MarkMessageSeen { + message_id: msg.id, + user_id, } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + ); + Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count))) } } diff --git a/actors/websocket-actor/src/handlers/projects.rs b/actors/websocket-actor/src/handlers/projects.rs index 5248d4bb..cec503e4 100644 --- a/actors/websocket-actor/src/handlers/projects.rs +++ b/actors/websocket-actor/src/handlers/projects.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, database_actor as db, futures::executor::block_on, jirs_data::{UpdateProjectPayload, UserProject, WsMsg}, @@ -12,38 +12,21 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?; - match block_on(self.db.send(database_actor::projects::UpdateProject { - project_id: *project_id, - name: msg.name, - url: msg.url, - description: msg.description, - category: msg.category, - time_tracking: msg.time_tracking, - })) { - Ok(Ok(_)) => (), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + let _ = db_or_debug_and_return!( + self, + database_actor::projects::UpdateProject { + project_id: *project_id, + name: msg.name, + url: msg.url, + description: msg.description, + category: msg.category, + time_tracking: msg.time_tracking, } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; - let projects = match block_on( - self.db - .send(database_actor::projects::LoadProjects { user_id: *user_id }), - ) { - Ok(Ok(projects)) => projects, - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{:?}", e); - return Ok(None); - } - }; + ); + let projects = db_or_debug_and_return!( + self, + database_actor::projects::LoadProjects { user_id: *user_id } + ); Ok(Some(WsMsg::ProjectsLoaded(projects))) } } @@ -53,16 +36,7 @@ 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) - } - } + let v = db_or_debug_and_return!(self, db::projects::LoadProjects { user_id }); + Ok(Some(WsMsg::ProjectsLoaded(v))) } } diff --git a/actors/websocket-actor/src/handlers/user_projects.rs b/actors/websocket-actor/src/handlers/user_projects.rs index 9917d9bc..ce149f79 100644 --- a/actors/websocket-actor/src/handlers/user_projects.rs +++ b/actors/websocket-actor/src/handlers/user_projects.rs @@ -1,5 +1,5 @@ use { - crate::{WebSocketActor, WsHandler, WsResult}, + crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, database_actor as db, futures::executor::block_on, jirs_data::{UserProjectId, WsMsg}, @@ -10,20 +10,8 @@ pub struct LoadUserProjects; impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadUserProjects, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - match block_on( - self.db - .send(db::user_projects::LoadUserProjects { user_id }), - ) { - Ok(Ok(v)) => Ok(Some(WsMsg::UserProjectsLoaded(v))), - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) - } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + let v = db_or_debug_and_return!(self, db::user_projects::LoadUserProjects { user_id }); + Ok(Some(WsMsg::UserProjectsLoaded(v))) } } @@ -34,22 +22,14 @@ pub struct SetCurrentUserProject { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: SetCurrentUserProject, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - match block_on(self.db.send(db::user_projects::ChangeCurrentUserProject { - user_id, - id: msg.id, - })) { - Ok(Ok(user_project)) => { - self.current_user_project = Some(user_project.clone()); - Ok(Some(WsMsg::UserProjectCurrentChanged(user_project))) + let user_project = db_or_debug_and_return!( + self, + db::user_projects::ChangeCurrentUserProject { + user_id, + id: msg.id, } - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) - } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + ); + self.current_user_project = Some(user_project.clone()); + Ok(Some(WsMsg::UserProjectCurrentChanged(user_project))) } } diff --git a/actors/websocket-actor/src/handlers/users.rs b/actors/websocket-actor/src/handlers/users.rs index c818b4fa..0ad7947d 100644 --- a/actors/websocket-actor/src/handlers/users.rs +++ b/actors/websocket-actor/src/handlers/users.rs @@ -1,5 +1,7 @@ use { - crate::{handlers::auth::Authenticate, WebSocketActor, WsHandler, WsResult}, + crate::{ + db_or_debug_and_return, handlers::auth::Authenticate, WebSocketActor, WsHandler, WsResult, + }, database_actor::{self, users::Register as DbRegister}, futures::executor::block_on, jirs_data::{UserId, UserProject, UserRole, WsMsg}, @@ -12,18 +14,8 @@ impl WsHandler for WebSocketActor { use database_actor::users::LoadProjectUsers as Msg; let project_id = self.require_user_project()?.project_id; - let m = match block_on(self.db.send(Msg { project_id })) { - Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); - } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; - Ok(m) + let v = db_or_debug_and_return!(self, Msg { project_id }); + Ok(Some(WsMsg::ProjectUsersLoaded(v))) } } @@ -35,26 +27,24 @@ pub struct Register { impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: Register, ctx: &mut Self::Context) -> WsResult { let Register { name, email } = msg; - let msg = match block_on(self.db.send(DbRegister { - name: name.clone(), - email: email.clone(), - project_id: None, - role: UserRole::Owner, - })) { - Ok(Ok(_)) => Some(WsMsg::SignUpSuccess), - Ok(Err(_)) => Some(WsMsg::SignUpPairTaken), - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; + let _ = db_or_debug_and_return!( + self, + DbRegister { + name: name.clone(), + email: email.clone(), + project_id: None, + role: UserRole::Owner, + }, + Ok(Some(WsMsg::SignUpPairTaken)), + Ok(None) + ); match self.handle_msg(Authenticate { name, email }, ctx) { Ok(_) => (), Err(e) => return Ok(Some(e)), }; - Ok(msg) + Ok(Some(WsMsg::SignUpSuccess)) } } @@ -64,13 +54,8 @@ impl WsHandler for WebSocketActor { fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult { let user_id = self.require_user()?.id; - let users = match block_on( - self.db - .send(database_actor::users::LoadInvitedUsers { user_id }), - ) { - Ok(Ok(users)) => users, - _ => return Ok(None), - }; + let users = + db_or_debug_and_return!(self, database_actor::users::LoadInvitedUsers { user_id }); Ok(Some(WsMsg::InvitedUsersLoaded(users))) } @@ -86,21 +71,14 @@ impl WsHandler for WebSocketActor { let user_id = self.require_user()?.id; let ProfileUpdate { name, email } = msg; - match block_on(self.db.send(database_actor::users::ProfileUpdate { - user_id, - name, - email, - })) { - Ok(Ok(_users)) => (), - Ok(Err(e)) => { - error!("{:?}", e); - return Ok(None); + let _ = db_or_debug_and_return!( + self, + database_actor::users::ProfileUpdate { + user_id, + name, + email, } - Err(e) => { - error!("{}", e); - return Ok(None); - } - }; + ); Ok(Some(WsMsg::ProfileUpdated)) } @@ -120,23 +98,14 @@ impl WsHandler for WebSocketActor { project_id, .. } = self.require_user_project()?.clone(); - match block_on( - self.db - .send(database_actor::user_projects::RemoveInvitedUser { - invited_id, - inviter_id, - project_id, - }), - ) { - Ok(Ok(_users)) => Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id))), - Ok(Err(e)) => { - error!("{:?}", e); - Ok(None) + let _ = db_or_debug_and_return!( + self, + database_actor::user_projects::RemoveInvitedUser { + invited_id, + inviter_id, + project_id, } - Err(e) => { - error!("{}", e); - Ok(None) - } - } + ); + Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id))) } } diff --git a/actors/websocket-actor/src/prelude.rs b/actors/websocket-actor/src/prelude.rs index 21671326..1418face 100644 --- a/actors/websocket-actor/src/prelude.rs +++ b/actors/websocket-actor/src/prelude.rs @@ -1,15 +1,72 @@ #[macro_export] -macro_rules! query_db_or_print { - ($s:expr,$msg:expr) => { - match block_on($s.db.send($msg)) { +macro_rules! db_or_debug_and_return { + ($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => { + $crate::actor_or_debug_and_return!($s, db, $msg, $actor_err, $mailbox_err) + }; + ($s: ident, $msg: expr) => { + $crate::actor_or_debug_and_return!($s, db, $msg) + }; +} + +#[macro_export] +macro_rules! mail_or_debug_and_return { + ($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => { + $crate::actor_or_debug_and_return!($s, mail, $msg, $actor_err, $mailbox_err) + }; + ($s: ident, $msg: expr) => { + $crate::actor_or_debug_and_return!($s, mail, $msg) + }; +} + +#[macro_export] +macro_rules! actor_or_debug_and_return { + ($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => { + match block_on($s.$actor.send($msg)) { Ok(Ok(r)) => r, Ok(Err(e)) => { log::error!("{:?}", e); - return Ok(None); + return $actor_err; } Err(e) => { - log::error!("{}", e); - return Ok(None); + log::error!("{:?}", e); + return $mailbox_err; + } + } + }; + ($s: ident, $actor: ident, $msg: expr) => { + crate::actor_or_debug_and_return!($s, $actor, $msg, Ok(None), Ok(None)) + }; +} + +#[macro_export] +macro_rules! actor_or_debug_and_ignore { + ($s: ident, $actor: ident, $msg: expr, $on_success: expr) => { + match block_on($s.$actor.send($msg)) { + Ok(Ok(r)) => { + $on_success(r); + } + Ok(Err(e)) => { + log::error!("{:?}", e); + } + Err(e) => { + log::error!("{:?}", e); + } + } + }; +} + +#[macro_export] +macro_rules! actor_or_debug_and_fallback { + ($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => { + match block_on($s.$actor.send($msg)) { + Ok(Ok(r)) => r, + Ok(Err(e)) => { + log::error!("{:?}", e); + $actor_err + } + Err(e) => { + log::error!("{:?}", e); + $mailbox_err } } }; diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index 355f6900..759237a1 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -76,5 +76,11 @@ features = [ "DragEvent", ] +[dependencies.derive_enum_primitive] +path = "../derive/derive_enum_primitive" + +[dependencies.derive_enum_iter] +path = "../derive/derive_enum_iter" + [dev-dependencies] wasm-bindgen-test = { version = "*" } diff --git a/jirs-client/js/css/styledCheckbox.scss b/jirs-client/js/css/styledCheckbox.scss index 51a2cb45..1067cb75 100644 --- a/jirs-client/js/css/styledCheckbox.scss +++ b/jirs-client/js/css/styledCheckbox.scss @@ -10,7 +10,6 @@ white-space: nowrap; cursor: pointer; font-size: 14.5px; - padding: 0 12px; &:focus { border-color: var(--borderInputFocus); @@ -19,6 +18,16 @@ > input[type=radio] { display: none; } + + > label { + display: block; + padding: 0 12px; + cursor: pointer; + font-family: var(--font-medium); + line-height: 2; + white-space: nowrap; + font-size: 14.5px; + } } > .styledCheckboxChild.selected { diff --git a/jirs-client/js/css/styledModal.scss b/jirs-client/js/css/styledModal.scss index dc2b60d1..a5d63ba7 100644 --- a/jirs-client/js/css/styledModal.scss +++ b/jirs-client/js/css/styledModal.scss @@ -109,6 +109,30 @@ padding-left: 15px; } + &.editEpic { + padding: 35px 40px 40px; + + .header { + display: flex; + justify-content: space-between; + margin-bottom: 15px; + } + + .transform { + display: flex; + justify-content: space-between; + margin-top: 15px; + + &.unavailable { + color: var(--textLight); + } + + &.available { + color: var(--textDark); + } + } + } + &.deleteEpic { > section { > .header { diff --git a/jirs-client/scripts/dev.sh b/jirs-client/scripts/dev.sh index 03757ca8..793757f2 100755 --- a/jirs-client/scripts/dev.sh +++ b/jirs-client/scripts/dev.sh @@ -1,11 +1,17 @@ #!/usr/bin/env bash -which rsass -if [[ "$status" != "0" ]]; +RSASS_PATH=$(command -v rsass) +if [[ "${RSASS_PATH}" == "" ]]; then cargo install rsass --features=commandline fi +WASM_PACK_PATH=$(command -v wasm-pack) +if [[ "${WASM_PACK_PATH}" == "" ]]; +then + cargo install wasm-pack +fi + export PROJECT_ROOT=$(git rev-parse --show-toplevel) export CLIENT_ROOT=${PROJECT_ROOT}/jirs-client export HI_ROOT=${PROJECT_ROOT}/highlight/jirs-highlight diff --git a/jirs-client/src/components/styled_checkbox.rs b/jirs-client/src/components/styled_checkbox.rs index 41dd6e34..801c7f8e 100644 --- a/jirs-client/src/components/styled_checkbox.rs +++ b/jirs-client/src/components/styled_checkbox.rs @@ -1,6 +1,6 @@ use { crate::{ - shared::{ToChild, ToNode}, + shared::{IntoChild, ToNode}, FieldId, Msg, }, jirs_data::TimeTracking, @@ -122,36 +122,51 @@ impl<'l> ToNode for ChildBuilder<'l> { } #[derive(Debug)] -pub struct StyledCheckbox<'l> { +pub struct StyledCheckbox<'l, Options> +where + Options: Iterator>, +{ id: FieldId, - options: Vec>, + options: Option, selected: u32, class_list: Vec<&'l str>, } -impl<'l> ToNode for StyledCheckbox<'l> { +impl<'l, Options> ToNode for StyledCheckbox<'l, Options> +where + Options: Iterator>, +{ fn into_node(self) -> Node { render(self) } } -impl<'l> StyledCheckbox<'l> { - pub fn build() -> StyledCheckboxBuilder<'l> { +impl<'l, Options> StyledCheckbox<'l, Options> +where + Options: Iterator>, +{ + pub fn build() -> StyledCheckboxBuilder<'l, Options> { StyledCheckboxBuilder { - options: vec![], + options: None, selected: 0, class_list: vec![], } } } -pub struct StyledCheckboxBuilder<'l> { - options: Vec>, +pub struct StyledCheckboxBuilder<'l, Options> +where + Options: Iterator>, +{ + options: Option, selected: u32, class_list: Vec<&'l str>, } -impl<'l> StyledCheckboxBuilder<'l> { +impl<'l, Options> StyledCheckboxBuilder<'l, Options> +where + Options: Iterator>, +{ pub fn state(mut self, state: &StyledCheckboxState) -> Self { self.selected = state.value; self @@ -162,12 +177,12 @@ impl<'l> StyledCheckboxBuilder<'l> { self } - pub fn options(mut self, options: Vec>) -> Self { - self.options = options; + pub fn options(mut self, options: Options) -> Self { + self.options = Some(options); self } - pub fn build(self, field_id: FieldId) -> StyledCheckbox<'l> { + pub fn build(self, field_id: FieldId) -> StyledCheckbox<'l, Options> { StyledCheckbox { id: field_id, options: self.options, @@ -177,7 +192,10 @@ impl<'l> StyledCheckboxBuilder<'l> { } } -fn render(values: StyledCheckbox) -> Node { +fn render<'l, Options>(values: StyledCheckbox<'l, Options>) -> Node +where + Options: Iterator>, +{ let StyledCheckbox { id, options, @@ -185,10 +203,12 @@ fn render(values: StyledCheckbox) -> Node { class_list, } = values; - let opt: Vec> = options - .into_iter() - .map(|child| child.with_id(id.clone()).try_select(selected).into_node()) - .collect(); + let opt: Vec> = match options { + Some(options) => options + .map(|child| child.with_id(id.clone()).try_select(selected).into_node()) + .collect(), + _ => vec![Node::Empty], + }; div![ C!["styledCheckbox"], @@ -197,10 +217,10 @@ fn render(values: StyledCheckbox) -> Node { ] } -impl<'l> ToChild<'l> for TimeTracking { +impl<'l> IntoChild<'l> for TimeTracking { type Builder = ChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + fn into_child(self) -> Self::Builder { Self::Builder::default() .label(match self { TimeTracking::Untracked => "No tracking", @@ -212,11 +232,11 @@ impl<'l> ToChild<'l> for TimeTracking { TimeTracking::Fibonacci => "fibonacci", TimeTracking::Hourly => "hourly", }) - .value((*self).into()) .add_class(match self { TimeTracking::Untracked => "untracked", TimeTracking::Fibonacci => "fibonacci", TimeTracking::Hourly => "hourly", }) + .value((self).into()) } } diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 7ee05076..67d13362 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -123,7 +123,7 @@ pub enum Msg { AvatarUpdateFetched(String), // modals - ModalOpened(Box), + ModalOpened(ModalType), ModalDropped, ModalChanged(FieldChange), @@ -280,11 +280,11 @@ fn resolve_page(url: Url) -> Option { let page = match url.path()[0].as_ref() { "board" => Page::Project, + "profile" => Page::Profile, "issues" => match url.path().get(1).as_ref().map(|s| s.parse::()) { Some(Ok(id)) => Page::EditIssue(id), _ => return None, }, - "profile" => Page::Profile, "add-issue" => Page::AddIssue, "project-settings" => Page::ProjectSettings, "login" => Page::SignIn, diff --git a/jirs-client/src/modals/comments_delete/mod.rs b/jirs-client/src/modals/comments_delete/mod.rs new file mode 100644 index 00000000..6bd17a77 --- /dev/null +++ b/jirs-client/src/modals/comments_delete/mod.rs @@ -0,0 +1,4 @@ +pub use {view::*, model::*}; + +mod view; +mod model; diff --git a/jirs-client/src/modals/comments_delete/model.rs b/jirs-client/src/modals/comments_delete/model.rs new file mode 100644 index 00000000..63044268 --- /dev/null +++ b/jirs-client/src/modals/comments_delete/model.rs @@ -0,0 +1,12 @@ +use jirs_data::CommentId; + +#[derive(Debug, Default)] +pub struct Model { + pub comment_id: CommentId, +} + +impl Model { + pub fn new(comment_id: CommentId) -> Self { + Self { comment_id } + } +} diff --git a/jirs-client/src/modals/comments_delete/view.rs b/jirs-client/src/modals/comments_delete/view.rs new file mode 100644 index 00000000..defa3345 --- /dev/null +++ b/jirs-client/src/modals/comments_delete/view.rs @@ -0,0 +1,16 @@ +use { + crate::{model, shared::ToNode, styled_confirm_modal::StyledConfirmModal, Msg}, + jirs_data::CommentId, + seed::prelude::*, +}; + +pub fn view(_model: &model::Model, modal: &super::Model) -> Node { + let comment_id: CommentId = modal.comment_id; + StyledConfirmModal::build() + .title("Are you sure you want to delete this comment?") + .message("Once you delete, it's gone for good.") + .confirm_text("Delete comment") + .on_confirm(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))) + .build() + .into_node() +} diff --git a/jirs-client/src/modals/epics_delete/model.rs b/jirs-client/src/modals/epics_delete/model.rs index 399d68c0..5cd19e36 100644 --- a/jirs-client/src/modals/epics_delete/model.rs +++ b/jirs-client/src/modals/epics_delete/model.rs @@ -10,7 +10,7 @@ pub struct Model { } impl Model { - pub fn new(epic_id: i32, model: &mut model::Model) -> Self { + pub fn new(epic_id: i32, model: &model::Model) -> Self { let related_issues = model.epic_issue_ids(epic_id); Self { epic_id, diff --git a/jirs-client/src/modals/epics_delete/update.rs b/jirs-client/src/modals/epics_delete/update.rs index 3334707b..ff15cd05 100644 --- a/jirs-client/src/modals/epics_delete/update.rs +++ b/jirs-client/src/modals/epics_delete/update.rs @@ -1,30 +1,20 @@ use { - crate::{shared::go_to_board, ws::send_ws_msg, ModalType, Msg, OperationKind, ResourceKind}, + crate::{ws::send_ws_msg, Msg, OperationKind, ResourceKind}, jirs_data::WsMsg, seed::prelude::*, }; pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { - let modal = match model.modals.iter_mut().find_map(|modal| { - if let ModalType::DeleteEpic(modal) = modal { - Some(modal) - } else { - None - } - }) { + let modal = match &mut model.modals_mut().delete_epic { Some(modal) => modal, _ => return, }; match msg { - Msg::ModalDropped => { - go_to_board(orders); - } Msg::DeleteEpic => { send_ws_msg(WsMsg::EpicDelete(modal.epic_id), model.ws.as_ref(), orders); } Msg::ResourceChanged(ResourceKind::Epic, OperationKind::SingleRemoved, Some(_)) => { - go_to_board(orders); orders.skip().send_msg(Msg::ModalDropped); } _ => {} diff --git a/jirs-client/src/modals/epics_edit/model.rs b/jirs-client/src/modals/epics_edit/model.rs index b7065031..e235181d 100644 --- a/jirs-client/src/modals/epics_edit/model.rs +++ b/jirs-client/src/modals/epics_edit/model.rs @@ -1,19 +1,18 @@ -use crate::FieldId; -use jirs_data::EpicFieldId; use { crate::{ - components::{styled_input::*, styled_select::StyledSelectState}, - model, + components::{styled_checkbox::StyledCheckboxState, styled_input::*}, + model, FieldId, Msg, }, - jirs_data::{EpicId, IssueId}, + jirs_data::*, + seed::prelude::Orders, }; -#[derive(Clone, Debug, PartialOrd, PartialEq)] +#[derive(Debug)] pub struct Model { pub epic_id: EpicId, pub related_issues: Vec, pub name: StyledInputState, - pub transform_into: StyledSelectState, + pub transform_into: StyledCheckboxState, } impl Model { @@ -23,6 +22,7 @@ impl Model { .get(&epic_id) .map(|epic| epic.name.as_str()) .unwrap_or_default(); + let related_issues = model .issues() .iter() @@ -38,10 +38,15 @@ impl Model { epic_id, related_issues, name: StyledInputState::new(FieldId::EditEpic(EpicFieldId::Name), name), - transform_into: StyledSelectState::new( - FieldId::EditEpic(EpicFieldId::StartsAt), - vec![], + transform_into: StyledCheckboxState::new( + FieldId::EditEpic(EpicFieldId::TransformInto), + 0, ), } } + + pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders) { + self.name.update(msg); + self.transform_into.update(msg); + } } diff --git a/jirs-client/src/modals/epics_edit/update.rs b/jirs-client/src/modals/epics_edit/update.rs index d18a2862..7ff261fc 100644 --- a/jirs-client/src/modals/epics_edit/update.rs +++ b/jirs-client/src/modals/epics_edit/update.rs @@ -1,5 +1,45 @@ -use {crate::Msg, seed::prelude::*}; +use { + crate::{send_ws_msg, FieldId, Msg, OperationKind, ResourceKind}, + jirs_data::{EpicFieldId, WsMsg}, + seed::prelude::*, +}; -pub fn update(_msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders) { - let _modal = crate::match_modal_mut!(model, DeleteEpic); +pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { + let modal = match &mut model.modals.edit_epic { + Some(modal) => modal, + _ => return, + }; + modal.update(msg, orders); + match msg { + Msg::ResourceChanged( + ResourceKind::Epic, + OperationKind::SingleLoaded | OperationKind::SingleModified, + Some(id), + ) => { + let name = model + .epics_by_id + .get(id) + .map(|epic| epic.name.as_str()) + .unwrap_or_default(); + modal.name.value = name.to_string(); + } + Msg::ResourceChanged(ResourceKind::Epic, OperationKind::ListLoaded, None) => { + let epic_id = modal.epic_id; + let name = model + .epics_by_id + .get(&epic_id) + .map(|epic| epic.name.as_str()) + .unwrap_or_default(); + modal.name.value = name.to_string(); + } + Msg::StrInputChanged(FieldId::EditEpic(EpicFieldId::Name), s) => { + let epic_id = modal.epic_id; + send_ws_msg( + WsMsg::EpicUpdate(epic_id, s.to_string()), + model.ws.as_ref(), + orders, + ); + } + _ => (), + }; } diff --git a/jirs-client/src/modals/epics_edit/view.rs b/jirs-client/src/modals/epics_edit/view.rs index 5004f0a1..1bc4afbb 100644 --- a/jirs-client/src/modals/epics_edit/view.rs +++ b/jirs-client/src/modals/epics_edit/view.rs @@ -1,26 +1,56 @@ use { crate::{ - components::{styled_input::*, styled_modal::*}, + components::{ + styled_button::*, styled_checkbox::*, styled_icon::Icon, styled_input::*, + styled_modal::*, + }, modals::epics_edit::Model, model, - shared::ToNode, + shared::{IntoChild, ToNode}, FieldId, Msg, }, - jirs_data::EpicFieldId, + jirs_data::{EpicFieldId, IssueType}, seed::{prelude::*, *}, }; +pub struct IssueTypeWrapper(IssueType); + +impl<'l> IntoChild<'l> for IssueTypeWrapper { + type Builder = ChildBuilder<'l>; + + fn into_child(self) -> Self::Builder { + Self::Builder::default() + .label(self.0.to_label()) + .name(self.0.to_str()) + .value(self.0.into()) + .add_class(self.0.to_str()) + } +} + pub fn view(_model: &model::Model, modal: &Model) -> Node { let transform = if modal.related_issues.is_empty() { - Node::Empty + transform_into_available(modal) } else { - div![] + transform_into_unavailable(modal) }; + let close = StyledButton::build() + .on_click(mouse_ev("click", |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::ModalDropped + })) + .empty() + .icon(Icon::Close) + .build() + .into_node(); StyledModal::build() .center() - .child(h1!["Edit epic"]) + .width(600) + .add_class("editEpic") + .child(div![C!["header"], h1!["Edit epic"], close]) .child( StyledInput::build() + .state(&modal.name) .build(FieldId::EditEpic(EpicFieldId::Name)) .into_node(), ) @@ -28,3 +58,32 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node { .build() .into_node() } + +fn transform_into_available(modal: &super::Model) -> Node { + let types = StyledCheckbox::build() + .options( + IssueType::default() + .into_iter() + .map(|ty| IssueTypeWrapper(ty).into_child()), + ) + .state(&modal.transform_into) + .build(FieldId::EditEpic(EpicFieldId::TransformInto)) + .into_node(); + let execute = StyledButton::build().text("Transform").build().into_node(); + div![C!["transform available"], div![types], div![execute]] +} + +fn transform_into_unavailable(modal: &super::Model) -> Node { + let (n, s) = match modal.related_issues.len() { + 1 => (1.to_string(), "issue"), + n => (n.to_string(), "issues"), + }; + div![ + C!["transform unavailable"], + span![ + C!["info"], + "This epic have related issues so you can't change it type." + ], + span![C!["count"], format!("Epic have {} {}", n, s)] + ] +} diff --git a/jirs-client/src/modals/issue_statuses_delete/update.rs b/jirs-client/src/modals/issue_statuses_delete/update.rs index ff5347c3..83f4aa88 100644 --- a/jirs-client/src/modals/issue_statuses_delete/update.rs +++ b/jirs-client/src/modals/issue_statuses_delete/update.rs @@ -1,22 +1,14 @@ use { - crate::{ - modals::issue_statuses_delete::Model as DeleteIssueStatusModal, - model::{ModalType, Model}, - Msg, OperationKind, ResourceKind, - }, + crate::{model::Model, Msg, OperationKind, ResourceKind}, jirs_data::WsMsg, seed::prelude::*, }; pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { - let _modal: &mut Box = - match model.modals.iter_mut().find_map(|modal| match modal { - ModalType::DeleteIssueStatusModal(modal) => Some(modal), - _ => None, - }) { - Some(m) => m, - _ => return, - }; + let _modal = match &mut model.modals_mut().delete_issue_status_modal { + Some(m) => m, + _ => return, + }; match msg { Msg::DeleteIssueStatus(issue_status_id) => { diff --git a/jirs-client/src/modals/issues_create/model.rs b/jirs-client/src/modals/issues_create/model.rs index 1c14f6ba..50c8fff6 100644 --- a/jirs-client/src/modals/issues_create/model.rs +++ b/jirs-client/src/modals/issues_create/model.rs @@ -4,14 +4,16 @@ use { styled_date_time_input::*, styled_input::*, styled_select::*, styled_select_child::*, }, model::IssueModal, - shared::{ToChild, ToNode}, + shared::{IntoChild, ToNode}, FieldId, Msg, }, + derive_enum_iter::EnumIter, + derive_enum_primitive::EnumPrimitive, jirs_data::{IssueFieldId, IssuePriority}, seed::prelude::*, }; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, EnumPrimitive, EnumIter)] pub enum Type { Task, Bug, @@ -19,24 +21,13 @@ pub enum Type { Epic, } -impl From for Type { - fn from(n: u32) -> Self { - match n { - 0 => Type::Task, - 1 => Type::Bug, - 2 => Type::Story, - 3 => Type::Epic, - _ => Type::Task, - } +impl Default for Type { + fn default() -> Self { + Self::Task } } impl Type { - pub(crate) fn ordered<'l>() -> &'l [Type] { - use Type::*; - &[Task, Bug, Story, Epic] - } - pub(crate) fn submit_label(&self) -> &str { use Type::*; match self { @@ -62,22 +53,17 @@ impl Type { } } -impl<'l> ToChild<'l> for Type { +impl<'l> IntoChild<'l> for Type { type Builder = StyledSelectChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + fn into_child(self) -> Self::Builder { let name = match self { Type::Task => "Task", Type::Bug => "Bug", Type::Story => "Story", Type::Epic => "Epic", }; - let value = match self { - Type::Task => 0, - Type::Bug => 1, - Type::Story => 2, - Type::Epic => 3, - }; + let value: u32 = self.into(); let type_icon = { use crate::components::styled_icon::*; diff --git a/jirs-client/src/modals/issues_create/update.rs b/jirs-client/src/modals/issues_create/update.rs index 929c1d1d..79b56f88 100644 --- a/jirs-client/src/modals/issues_create/update.rs +++ b/jirs-client/src/modals/issues_create/update.rs @@ -1,8 +1,6 @@ use { crate::{ - components::styled_select::StyledSelectChanged, - model::{IssueModal, ModalType}, - ws::send_ws_msg, + components::styled_select::StyledSelectChanged, model::IssueModal, ws::send_ws_msg, FieldId, Msg, OperationKind, ResourceKind, }, jirs_data::{IssueFieldId, UserId, WsMsg}, @@ -10,12 +8,11 @@ use { }; pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { - let modal = model - .modals - .iter_mut() - .find(|modal| matches!(modal, ModalType::AddIssue(..))); - let modal = match modal { - Some(ModalType::AddIssue(modal)) => modal, + let user_id = model.user_id().unwrap_or_default(); + let project_id = model.project_id().unwrap_or_default(); + + let modal = match &mut model.modals_mut().add_issue { + Some(modal) => modal, _ => return, }; @@ -30,8 +27,6 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde ); } Msg::AddIssue => { - let user_id = model.user.as_ref().map(|u| u.id).unwrap_or_default(); - let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default(); let type_value = modal.type_state.values.get(0).cloned().unwrap_or_default(); match type_value { 0 | 1 | 2 => { diff --git a/jirs-client/src/modals/issues_create/view.rs b/jirs-client/src/modals/issues_create/view.rs index ba13f333..54cb0973 100644 --- a/jirs-client/src/modals/issues_create/view.rs +++ b/jirs-client/src/modals/issues_create/view.rs @@ -24,7 +24,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { .values .get(0) .cloned() - .map(Type::from) + .map(Into::into) .unwrap_or_else(|| Type::Task); let issue_type_field = issue_type_field(modal); @@ -127,11 +127,18 @@ fn issue_type_field(modal: &AddIssueModal) -> Node { .text_filter(modal.type_state.text_filter.as_str()) .opened(modal.type_state.opened) .valid(true) - .options(Type::ordered().iter().map(|t| t.to_child().name("type"))) - .selected(vec![Type::from( - modal.type_state.values.get(0).cloned().unwrap_or_default(), - ) - .to_child() + .options(Type::Task.into_iter().map(|t| t.into_child().name("type"))) + .selected(vec![{ + let v: Type = modal + .type_state + .values + .get(0) + .cloned() + .unwrap_or_default() + .into(); + v + } + .into_child() .name("type")]) .build(FieldId::AddIssueModal(IssueFieldId::Type)) .into_node(); diff --git a/jirs-client/src/modals/issues_delete/model.rs b/jirs-client/src/modals/issues_delete/model.rs index 8b137891..feb39300 100644 --- a/jirs-client/src/modals/issues_delete/model.rs +++ b/jirs-client/src/modals/issues_delete/model.rs @@ -1 +1,6 @@ +use jirs_data::IssueId; +#[derive(Debug, Default)] +pub struct Model { + pub issue_id: IssueId, +} diff --git a/jirs-client/src/modals/issues_delete/view.rs b/jirs-client/src/modals/issues_delete/view.rs index f1ff6143..067e7439 100644 --- a/jirs-client/src/modals/issues_delete/view.rs +++ b/jirs-client/src/modals/issues_delete/view.rs @@ -1,24 +1,12 @@ use { - crate::{ - components::styled_confirm_modal::StyledConfirmModal, model, model::ModalType, - shared::ToNode, Msg, - }, - seed::{prelude::*, *}, + crate::{components::styled_confirm_modal::StyledConfirmModal, model, shared::ToNode, Msg}, + seed::prelude::*, }; pub fn view(model: &model::Model) -> Node { - let opt_id = model - .modals - .iter() - .filter_map(|modal| match modal { - ModalType::EditIssue(issue_id, _) => Some(issue_id), - _ => None, - }) - .find(|id| id.eq(id)); - - let issue_id = match opt_id { - Some(id) => *id, - _ => return empty![], + let issue_id = match &model.modals().delete_issue_confirm { + Some(modal) => modal.issue_id, + _ => return Node::Empty, }; let handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id)); diff --git a/jirs-client/src/modals/issues_edit/model.rs b/jirs-client/src/modals/issues_edit/model.rs index 53ecb42c..25789a30 100644 --- a/jirs-client/src/modals/issues_edit/model.rs +++ b/jirs-client/src/modals/issues_edit/model.rs @@ -9,13 +9,13 @@ use { model::{CommentForm, IssueModal}, EditIssueModalSection, FieldId, Msg, }, - jirs_data::{EpicId, Issue, IssueFieldId, TimeTracking, UpdateIssuePayload}, + jirs_data::{Issue, IssueFieldId, IssueId, TimeTracking, UpdateIssuePayload}, seed::prelude::*, }; #[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct Model { - pub id: EpicId, + pub id: IssueId, pub link_copied: bool, pub payload: UpdateIssuePayload, pub top_type_state: StyledSelectState, diff --git a/jirs-client/src/modals/issues_edit/update.rs b/jirs-client/src/modals/issues_edit/update.rs index acefc545..4c36246c 100644 --- a/jirs-client/src/modals/issues_edit/update.rs +++ b/jirs-client/src/modals/issues_edit/update.rs @@ -1,8 +1,7 @@ use { crate::{ components::styled_select::StyledSelectChanged, - modals::issues_edit::Model as EditIssueModal, - model::{IssueModal, ModalType, Model}, + model::{IssueModal, Model}, ws::send_ws_msg, EditIssueModalSection, FieldChange, FieldId, Msg, OperationKind, ResourceKind, }, @@ -11,18 +10,22 @@ use { }; pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { - let modal: &mut EditIssueModal = match model.modals.get_mut(0) { - Some(ModalType::EditIssue(_issue_id, modal)) => modal, + let modal = match &mut model.modals.edit_issue { + Some(modal) => modal, _ => return, }; modal.update_states(msg, orders); match msg { Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => { - if let Some(issue) = model.issues_by_id.get(id) { - modal.payload = issue.clone().into(); - modal.description_state.initial_text = - issue.description_text.clone().unwrap_or_default(); + let m = model.issues_by_id.get(id).cloned(); + if let Some(issue) = m { + modal.description_state.initial_text = issue + .description_text + .as_deref() + .unwrap_or_default() + .to_string(); + modal.payload = issue.into(); } } diff --git a/jirs-client/src/modals/issues_edit/view.rs b/jirs-client/src/modals/issues_edit/view.rs index 70604b4c..77c8c98d 100644 --- a/jirs-client/src/modals/issues_edit/view.rs +++ b/jirs-client/src/modals/issues_edit/view.rs @@ -3,7 +3,7 @@ use { components::{ styled_avatar::StyledAvatar, styled_button::StyledButton, styled_editor::StyledEditor, styled_field::StyledField, styled_icon::Icon, styled_input::StyledInput, - styled_select::StyledSelect, + styled_modal::*, styled_select::StyledSelect, }, modals::{ epic_field, issues_edit::Model as EditIssueModal, time_tracking::time_tracking_field, @@ -20,6 +20,21 @@ use { mod comments; pub fn view(model: &Model, modal: &EditIssueModal) -> Node { + let issue_id = modal.id; + if let Some(_issue) = model.issues_by_id.get(&issue_id) { + let details = details(model, modal); + StyledModal::build() + .variant(crate::components::styled_modal::Variant::Center) + .width(1040) + .child(details) + .build() + .into_node() + } else { + Node::Empty + } +} + +pub fn details(model: &Model, modal: &EditIssueModal) -> Node { div![ C!["issueDetails"], modal_header(model, modal), @@ -72,12 +87,10 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node { let close_handler = mouse_ev(Ev::Click, |ev| { ev.prevent_default(); ev.stop_propagation(); - seed::Url::new().add_path_part("board").go_and_push(); - Msg::ModalDropped }); let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| { - Msg::ModalOpened(Box::new(ModalType::DeleteIssueConfirm(issue_id))) + Msg::ModalOpened(ModalType::DeleteIssueConfirm(Some(issue_id))) }); let copy_button = StyledButton::build() diff --git a/jirs-client/src/modals/issues_edit/view/comments.rs b/jirs-client/src/modals/issues_edit/view/comments.rs index 4be53170..07bbf1e7 100644 --- a/jirs-client/src/modals/issues_edit/view/comments.rs +++ b/jirs-client/src/modals/issues_edit/view/comments.rs @@ -66,7 +66,7 @@ pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Opti let comment_id = comment.id; let delete_comment_handler = mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); - Msg::ModalOpened(Box::new(ModalType::DeleteCommentConfirm(comment_id))) + Msg::ModalOpened(ModalType::DeleteCommentConfirm(Some(comment_id))) }); let edit_button = StyledButton::build() .add_class("editButton") diff --git a/jirs-client/src/modals/mod.rs b/jirs-client/src/modals/mod.rs index 7566db44..87392fd5 100644 --- a/jirs-client/src/modals/mod.rs +++ b/jirs-client/src/modals/mod.rs @@ -1,5 +1,6 @@ pub use {epic_field::*, update::*, view::*}; +pub mod comments_delete; #[cfg(debug_assertions)] pub mod debug; pub mod epics_delete; diff --git a/jirs-client/src/modals/time_tracking/model.rs b/jirs-client/src/modals/time_tracking/model.rs index e69de29b..c0777c46 100644 --- a/jirs-client/src/modals/time_tracking/model.rs +++ b/jirs-client/src/modals/time_tracking/model.rs @@ -0,0 +1,12 @@ +use jirs_data::IssueId; + +#[derive(Debug, Default)] +pub struct Model { + pub issue_id: IssueId, +} + +impl Model { + pub fn new(issue_id: IssueId) -> Self { + Self { issue_id } + } +} diff --git a/jirs-client/src/modals/time_tracking/view.rs b/jirs-client/src/modals/time_tracking/view.rs index 7401a55e..e855fbe3 100644 --- a/jirs-client/src/modals/time_tracking/view.rs +++ b/jirs-client/src/modals/time_tracking/view.rs @@ -7,14 +7,14 @@ use { styled_modal::StyledModal, styled_select::{StyledSelect, StyledSelectState}, }, - model::{ModalType, Model}, + model::Model, shared::{ tracking_widget::{fibonacci_values, tracking_widget}, ToChild, ToNode, }, EditIssueModalSection, FieldId, Msg, }, - jirs_data::{EpicId, IssueFieldId, TimeTracking}, + jirs_data::{IssueFieldId, IssueId, TimeTracking}, seed::{prelude::*, *}, }; @@ -27,20 +27,22 @@ pub fn value_for_time_tracking(v: &Option, time_tracking_type: &TimeTrackin } } -pub fn view(model: &Model, issue_id: EpicId) -> Node { +pub fn view(model: &Model, modal: &super::Model) -> Node { + let issue_id: IssueId = modal.issue_id; if model.issues_by_id.get(&issue_id).is_none() { return Node::Empty; } - let edit_issue_modal = match model.modals.get(0) { - Some(ModalType::EditIssue(_, modal)) => modal, - _ => return empty![], + let edit_issue_modal = match &model.modals().edit_issue { + Some(modal) => modal, + _ => return Node::Empty, }; + let time_tracking_type = model .project .as_ref() .map(|p| p.time_tracking) - .unwrap_or_else(|| TimeTracking::Untracked); + .unwrap_or(TimeTracking::Untracked); let modal_title = div![C!["modalTitle"], "Time tracking"]; diff --git a/jirs-client/src/modals/update.rs b/jirs-client/src/modals/update.rs index d5ed8136..d20a0118 100644 --- a/jirs-client/src/modals/update.rs +++ b/jirs-client/src/modals/update.rs @@ -1,3 +1,4 @@ +use jirs_data::{CommentId, IssueStatusId}; use { crate::{ model::{ModalType, Model, Page}, @@ -11,47 +12,37 @@ use { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { match msg { - Msg::ModalDropped => match model.modals.pop() { - Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => { - go_to_board(orders); - } - _ => (), - }, + Msg::ModalDropped if !model.modal_stack().is_empty() => { + drop_modal(model, orders); + } Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) => { - for modal in model.modals.iter_mut() { - if let ModalType::EditIssue(_, edit) = modal { - edit.link_copied = true; - } + if let Some(edit) = &mut model.modals_mut().edit_issue { + edit.link_copied = true; } } Msg::ModalOpened(modal_type) => { - model.modals.push(modal_type.as_ref().clone()); + push_modal(modal_type, model, orders); } Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => { match model.page { - Page::EditIssue(issue_id) if model.modals.is_empty() => { - push_edit_modal(issue_id, model, orders) - } - Page::AddIssue if model.modals.is_empty() => push_add_modal(model, orders), + Page::EditIssue(issue_id) => push_edit_issue_modal(issue_id, model, orders), + Page::AddIssue => push_add_issue_modal(model, orders), + Page::DeleteEpic(id) => push_delete_epic_modal(id, model, orders), + Page::EditEpic(id) => push_edit_epic_modal(id, model, orders), _ => (), } } - Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders), - - Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders), - Msg::ChangePage(Page::DeleteEpic(issue_id)) => { - push_delete_epic_modal(*issue_id, model, orders) - } - Msg::ChangePage(Page::EditEpic(issue_id)) => push_edit_epic_modal(*issue_id, model, orders), + Msg::ChangePage(Page::EditIssue(id)) => push_edit_issue_modal(*id, model, orders), + Msg::ChangePage(Page::AddIssue) => push_add_issue_modal(model, orders), + Msg::ChangePage(Page::DeleteEpic(id)) => push_delete_epic_modal(*id, model, orders), + Msg::ChangePage(Page::EditEpic(id)) => push_edit_epic_modal(*id, model, orders), #[cfg(debug_assertions)] - Msg::GlobalKeyDown { key, .. } if key.eq("#") => { - model.modals.push(ModalType::DebugModal); - } + Msg::GlobalKeyDown { key, .. } if key.eq("#") => push_debug_modal(model), #[cfg(debug_assertions)] Msg::GlobalKeyDown { key, .. } if key.eq(">") => { @@ -75,49 +66,223 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } } -fn push_add_modal(model: &mut Model, _orders: &mut impl Orders) { - use crate::modals::issues_create::Model; - model.modals.push(ModalType::AddIssue(Box::new(Model { - project_id: model.project.as_ref().map(|p| p.id), - ..Model::default() - }))); +// MODALS + +fn push_modal(modal_type: &ModalType, model: &mut Model, orders: &mut impl Orders) { + match modal_type { + ModalType::AddIssue(_) => push_add_issue_modal(model, orders), + ModalType::EditIssue(id) => { + if let Some(id) = id { + push_edit_issue_modal(*id, model, orders); + } + } + ModalType::DeleteIssueConfirm(id) => { + if let Some(id) = id { + push_delete_issue_modal(*id, model, orders); + } + } + ModalType::DeleteEpic(id) => { + if let Some(id) = id { + push_delete_epic_modal(*id, model, orders); + } + } + ModalType::EditEpic(id) => { + if let Some(id) = id { + push_edit_epic_modal(*id, model, orders); + } + } + ModalType::DeleteCommentConfirm(id) => { + if let Some(id) = id { + push_delete_comment_modal(*id, model, orders); + } + } + ModalType::TimeTracking(id) => { + if let Some(id) = id { + push_time_track_modal(*id, model, orders); + } + } + ModalType::DeleteIssueStatusModal(id) => { + if let Some(id) = id { + push_delete_issue_status_modal(*id, model, orders); + } + } + #[cfg(debug_assertions)] + ModalType::DebugModal(_) => push_debug_modal(model), + } } -fn push_edit_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl Orders) { +fn drop_modal(model: &mut Model, orders: &mut impl Orders) { + let modal = model.modal_stack_mut().pop().unwrap(); + let modals = model.modals_mut(); + match modal { + ModalType::AddIssue(_) => { + modals.add_issue = None; + } + ModalType::EditIssue(_) => { + modals.edit_issue = None; + } + ModalType::DeleteIssueConfirm(_) => { + modals.delete_issue_confirm = None; + } + ModalType::DeleteEpic(_) => { + modals.delete_epic = None; + } + ModalType::EditEpic(_) => { + modals.edit_epic = None; + } + ModalType::DeleteCommentConfirm(_) => { + modals.delete_comment_confirm = None; + } + ModalType::TimeTracking(_) => { + modals.time_tracking = None; + } + ModalType::DeleteIssueStatusModal(_) => { + modals.delete_issue_status_modal = None; + } + + #[cfg(debug_assertions)] + ModalType::DebugModal(_) => { + modals.debug_modal = None; + } + }; + match modal { + ModalType::EditIssue(_) + | ModalType::AddIssue(_) + | ModalType::DeleteEpic(_) + | ModalType::EditEpic(_) => { + go_to_board(orders); + } + _ => (), + } +} + +// ISSUE + +fn push_add_issue_modal(model: &mut Model, _orders: &mut impl Orders) { + use crate::modals::issues_create::Model; + if model.modals().add_issue.is_some() { + return; + } + model.modal_stack_mut().push(ModalType::AddIssue(None)); + model.modals_mut().add_issue = Some(Model { + project_id: model.project.as_ref().map(|p| p.id), + ..Model::default() + }); +} + +fn push_edit_issue_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl Orders) { + if model.modals().edit_issue.is_some() { + return; + } let time_tracking_type = model .project .as_ref() .map(|p| p.time_tracking) .unwrap_or(TimeTracking::Untracked); + let modal = { let issue = match model.issues_by_id.get(&issue_id) { Some(issue) => issue, _ => return, }; - ModalType::EditIssue( - issue_id, - Box::new(crate::modals::issues_edit::Model::new( - issue, - time_tracking_type, - )), - ) + crate::modals::issues_edit::Model::new(issue, time_tracking_type) }; send_ws_msg( WsMsg::IssueCommentsLoad(issue_id), model.ws.as_ref(), orders, ); - model.modals.push(modal); + model + .modal_stack_mut() + .push(ModalType::EditIssue(Some(issue_id))); + model.modals_mut().edit_issue = Some(modal); } -fn push_edit_epic_modal(epic_id: EpicId, model: &mut Model, _orders: &mut impl Orders) { +fn push_delete_issue_modal(id: IssueId, model: &mut Model, _orders: &mut impl Orders) { + if model.modals().delete_issue_confirm.is_some() { + return; + } + model + .modal_stack_mut() + .push(ModalType::DeleteIssueConfirm(Some(id))); + model.modals_mut().delete_issue_confirm = + Some(crate::modals::issues_delete::Model { issue_id: id }); +} + +// ISSUE STATUS + +fn push_delete_issue_status_modal( + id: IssueStatusId, + model: &mut Model, + _orders: &mut impl Orders, +) { + use crate::modals::issue_statuses_delete::Model; + if model.modals().delete_issue_status_modal.is_some() { + return; + } + let modal = Model::new(id); + model + .modal_stack_mut() + .push(ModalType::DeleteIssueStatusModal(Some(id))); + model.modals_mut().delete_issue_status_modal = Some(modal); +} + +// EPIC + +fn push_edit_epic_modal(id: EpicId, model: &mut Model, _orders: &mut impl Orders) { use crate::modals::epics_edit::Model; - let modal = Model::new(epic_id, model); - model.modals.push(ModalType::EditEpic(Box::new(modal))); + if model.modals().edit_epic.is_some() { + return; + } + let modal = Model::new(id, model); + model.modal_stack_mut().push(ModalType::EditEpic(Some(id))); + model.modals_mut().edit_epic = Some(modal); } -fn push_delete_epic_modal(issue_id: IssueId, model: &mut Model, _orders: &mut impl Orders) { +fn push_delete_epic_modal(id: EpicId, model: &mut Model, _orders: &mut impl Orders) { use crate::modals::epics_delete::Model; - let modal = Model::new(issue_id, model); - model.modals.push(ModalType::DeleteEpic(Box::new(modal))); + if model.modals_mut().delete_epic.is_some() { + return; + } + model + .modal_stack_mut() + .push(ModalType::DeleteEpic(Some(id))); + model.modals_mut().delete_epic = Some(Model::new(id, model)); +} + +// COMMENTS + +fn push_delete_comment_modal(id: CommentId, model: &mut Model, _orders: &mut impl Orders) { + use crate::modals::comments_delete::Model; + if model.modals_mut().delete_comment_confirm.is_some() { + return; + } + model + .modal_stack_mut() + .push(ModalType::DeleteCommentConfirm(Some(id))); + model.modals_mut().delete_comment_confirm = Some(Model::new(id)); +} + +// TIME TRACK + +fn push_time_track_modal(id: IssueId, model: &mut Model, _orders: &mut impl Orders) { + use crate::modals::time_tracking::Model; + if model.modals_mut().time_tracking.is_some() { + return; + } + model + .modal_stack_mut() + .push(ModalType::TimeTracking(Some(id))); + model.modals_mut().time_tracking = Some(Model::new(id)); +} + +// DEBUG + +#[cfg(debug_assertions)] +fn push_debug_modal(model: &mut Model) { + if model.modals().debug_modal.is_some() { + return; + } + model.modal_stack_mut().push(ModalType::DebugModal(None)); + model.modals_mut().debug_modal = Some(true); } diff --git a/jirs-client/src/modals/view.rs b/jirs-client/src/modals/view.rs index 7c3fd1a7..7fe2aad8 100644 --- a/jirs-client/src/modals/view.rs +++ b/jirs-client/src/modals/view.rs @@ -1,58 +1,69 @@ use { - crate::{ - components::{styled_confirm_modal::StyledConfirmModal, styled_modal::StyledModal}, - model::*, - shared::ToNode, - Msg, - }, + crate::{model::*, Msg}, seed::{prelude::*, *}, }; pub fn view(model: &Model) -> Node { - use crate::modals::{issue_statuses_delete, issues_create, issues_edit}; - let modals: Vec> = model - .modals - .iter() - .map(|modal| match modal { - // epic - ModalType::DeleteEpic(modal) => crate::modals::epics_delete::view(model, modal), - ModalType::EditEpic(modal) => crate::modals::epics_edit::view(model, modal), - // issue - ModalType::EditIssue(issue_id, modal) => { - if let Some(_issue) = model.issues_by_id.get(issue_id) { - let details = issues_edit::view(model, modal.as_ref()); - StyledModal::build() - .variant(crate::components::styled_modal::Variant::Center) - .width(1040) - .child(details) - .build() - .into_node() - } else { - empty![] + let mut nodes = Vec::with_capacity(model.modal_stack().len()); + + for modal_type in model.modal_stack() { + match modal_type { + ModalType::AddIssue(_) => { + if let Some(modal) = &model.modals().add_issue { + let node = crate::modals::issues_create::view(model, modal); + nodes.push(node); } } - ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model), - ModalType::AddIssue(modal) => issues_create::view(model, modal), - // comment - ModalType::DeleteCommentConfirm(comment_id) => { - let comment_id = *comment_id; - StyledConfirmModal::build() - .title("Are you sure you want to delete this comment?") - .message("Once you delete, it's gone for good.") - .confirm_text("Delete comment") - .on_confirm(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))) - .build() - .into_node() + ModalType::EditIssue(_) => { + if let Some(modal) = &model.modals().edit_issue { + let node = crate::modals::issues_edit::view(model, modal); + nodes.push(node); + } } - ModalType::TimeTracking(issue_id) => { - crate::modals::time_tracking::view(model, *issue_id) + ModalType::DeleteEpic(_) => { + if let Some(modal) = &model.modals().delete_epic { + let node = crate::modals::epics_delete::view(model, modal); + nodes.push(node); + } } - ModalType::DeleteIssueStatusModal(delete_issue_modal) => { - issue_statuses_delete::view(model, delete_issue_modal.delete_id) + ModalType::EditEpic(_) => { + if let Some(modal) = &model.modals().edit_epic { + let node = crate::modals::epics_edit::view(model, modal); + nodes.push(node); + } + } + ModalType::DeleteIssueConfirm(_) => { + if let Some(_issue_id) = &model.modals().delete_issue_confirm { + let node = crate::modals::issues_delete::view(model); + nodes.push(node); + } + } + ModalType::DeleteCommentConfirm(_) => { + if let Some(modal) = &model.modals().delete_comment_confirm { + let node = crate::modals::comments_delete::view(model, modal); + nodes.push(node); + } + } + ModalType::TimeTracking(_) => { + if let Some(modal) = &model.modals().time_tracking { + let node = crate::modals::time_tracking::view(model, modal); + nodes.push(node); + } + } + ModalType::DeleteIssueStatusModal(_) => { + if let Some(modal) = &model.modals().delete_issue_status_modal { + let node = crate::modals::issue_statuses_delete::view(model, modal.delete_id); + nodes.push(node); + } } #[cfg(debug_assertions)] - ModalType::DebugModal => crate::modals::debug::view(model), - }) - .collect(); - section![id!["modals"], modals] + ModalType::DebugModal(_) => { + if let Some(true) = &model.modals().debug_modal { + let node = crate::modals::debug::view(model); + nodes.push(node) + } + } + }; + } + section![id!["modals"], nodes] } diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index ffc0cea0..da4a18b5 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -24,21 +24,38 @@ pub trait IssueModal { fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders); } -#[derive(Clone, Debug, PartialOrd, PartialEq)] +#[derive(Debug, Default)] +pub struct Modals { + // issue + pub add_issue: Option, + pub edit_issue: Option, + // epic + pub delete_epic: Option, + pub edit_epic: Option, + + pub delete_issue_confirm: Option, + pub delete_comment_confirm: Option, + pub time_tracking: Option, + pub delete_issue_status_modal: Option, + #[cfg(debug_assertions)] + pub debug_modal: Option, +} + +#[derive(Clone, Debug, PartialEq)] pub enum ModalType { // issue - AddIssue(Box), - EditIssue(EpicId, Box), + AddIssue(Option), + EditIssue(Option), + DeleteIssueConfirm(Option), // epic - DeleteEpic(Box), - EditEpic(Box), + DeleteEpic(Option), + EditEpic(Option), - DeleteIssueConfirm(EpicId), - DeleteCommentConfirm(CommentId), - TimeTracking(EpicId), - DeleteIssueStatusModal(Box), + DeleteCommentConfirm(Option), + TimeTracking(Option), + DeleteIssueStatusModal(Option), #[cfg(debug_assertions)] - DebugModal, + DebugModal(Option), } #[derive(Clone, Debug, PartialOrd, PartialEq)] @@ -168,7 +185,8 @@ pub struct Model { pub comment_form: Option, // modals - pub modals: Vec, + modals_stack: Vec, + pub modals: Modals, // pages pub page: Page, @@ -226,7 +244,6 @@ impl Model { host_url, ws_url, page_content: PageContent::Project(Box::new(ProjectPage::default())), - modals: vec![], project: None, current_user_project: None, about_tooltip_visible: false, @@ -246,6 +263,8 @@ impl Model { issues_by_id: Default::default(), show_extras: false, epics_by_id: Default::default(), + modals_stack: vec![], + modals: Default::default(), } } @@ -274,6 +293,16 @@ impl Model { &self.user } + #[inline(always)] + pub fn user_id(&self) -> Option { + self.user.as_ref().map(|u| u.id) + } + + #[inline(always)] + pub fn project_id(&self) -> Option { + self.project.as_ref().map(|p| p.id) + } + pub fn current_user_role(&self) -> UserRole { self.current_user_project .as_ref() @@ -293,4 +322,20 @@ impl Model { }) .collect() } + + pub fn modals(&self) -> &Modals { + &self.modals + } + + pub fn modals_mut(&mut self) -> &mut Modals { + &mut self.modals + } + + pub fn modal_stack(&self) -> &[ModalType] { + &self.modals_stack + } + + pub fn modal_stack_mut(&mut self) -> &mut Vec { + &mut self.modals_stack + } } diff --git a/jirs-client/src/pages/project_page/model.rs b/jirs-client/src/pages/project_page/model.rs index 2a6fa6c3..ea039903 100644 --- a/jirs-client/src/pages/project_page/model.rs +++ b/jirs-client/src/pages/project_page/model.rs @@ -24,66 +24,6 @@ pub struct ProjectPage { } impl ProjectPage { - pub fn rebuild_visible( - &mut self, - epics: &[Epic], - statuses: &[IssueStatus], - issues: &[Issue], - user: &Option, - ) { - let mut map = vec![]; - let epics = vec![None] - .into_iter() - .chain(epics.iter().map(|s| Some((s.id, s.name.as_str())))); - - let statuses = statuses.iter().map(|s| (s.id, s.name.as_str())); - - let mut issues: Vec<&Issue> = issues.iter().collect(); - if self.recently_updated_filter { - let mut m = HashMap::new(); - let mut sorted = vec![]; - for issue in issues.into_iter() { - sorted.push((issue.id, issue.updated_at)); - m.insert(issue.id, issue); - } - sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time)); - issues = sorted - .into_iter() - .take(10) - .flat_map(|(id, _)| m.remove(&id)) - .collect(); - issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); - } - - for epic in epics { - let mut per_epic_map = EpicIssuePerStatus { - epic_ref: epic.map(|(id, name)| (id, name.to_string())), - ..Default::default() - }; - - for (current_status_id, issue_status_name) in statuses.to_owned() { - let mut per_status_map = StatusIssueIds { - status_id: current_status_id, - status_name: issue_status_name.to_string(), - ..Default::default() - }; - for issue in issues.iter() { - if issue.epic_id == epic.map(|(id, _)| id) - && issue_filter_status(issue, current_status_id) - && issue_filter_with_avatars(issue, &self.active_avatar_filters) - && issue_filter_with_text(issue, self.text_filter.as_str()) - && issue_filter_with_only_my(issue, self.only_my_filter, user) - { - per_status_map.issue_ids.push(issue.id); - } - } - per_epic_map.per_status_issues.push(per_status_map); - } - map.push(per_epic_map); - } - self.visible_issues = map; - } - pub fn visible_issues( page: &ProjectPage, epics: &[Epic], diff --git a/jirs-client/src/pages/project_page/update.rs b/jirs-client/src/pages/project_page/update.rs index cc25b5c6..60dd584b 100644 --- a/jirs-client/src/pages/project_page/update.rs +++ b/jirs-client/src/pages/project_page/update.rs @@ -1,7 +1,7 @@ use { crate::{ components::styled_select::StyledSelectChanged, - model::{ModalType, Model, Page, PageContent}, + model::{Model, Page, PageContent}, pages::project_page::model::ProjectPage, ws::{board_load, send_ws_msg}, BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, @@ -54,15 +54,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), StyledSelectChanged::Text(text), ) => { - let modal = model - .modals - .iter_mut() - .filter_map(|modal| match modal { - ModalType::EditIssue(_, modal) => Some(modal), - _ => None, - }) - .last(); - if let Some(m) = modal { + if let Some(m) = &mut model.modals_mut().edit_issue { m.top_type_state.text_filter = text; } } diff --git a/jirs-client/src/pages/project_settings_page/view.rs b/jirs-client/src/pages/project_settings_page/view.rs index d87be91a..36157503 100644 --- a/jirs-client/src/pages/project_settings_page/view.rs +++ b/jirs-client/src/pages/project_settings_page/view.rs @@ -11,10 +11,9 @@ use { styled_select::StyledSelect, styled_textarea::StyledTextarea, }, - modals::issue_statuses_delete::Model as DeleteIssueStatusModal, model::{self, ModalType, Model, PageContent}, pages::project_settings_page::ProjectSettingsPage, - shared::{inner_layout, IntoChild, ToChild, ToNode}, + shared::{inner_layout, IntoChild, ToNode}, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, }, jirs_data::{IssueStatus, ProjectCategory, TimeTracking}, @@ -51,11 +50,11 @@ pub fn view(model: &model::Model) -> Node { let category_field = category_field(page); let time_tracking = StyledCheckbox::build() - .options(vec![ - TimeTracking::Untracked.to_child(), - TimeTracking::Fibonacci.to_child(), - TimeTracking::Hourly.to_child(), - ]) + .options( + TimeTracking::default() + .into_iter() + .map(|tt| tt.into_child()), + ) .state(&page.time_tracking) .add_class("timeTracking") .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) @@ -336,9 +335,7 @@ fn show_column_preview( let on_delete = mouse_ev(Ev::Click, move |ev| { ev.prevent_default(); ev.stop_propagation(); - Msg::ModalOpened(Box::new(ModalType::DeleteIssueStatusModal(Box::new( - DeleteIssueStatusModal::new(id), - )))) + Msg::ModalOpened(ModalType::DeleteIssueStatusModal(Some(id))) }); let delete = StyledButton::build() .primary() diff --git a/jirs-client/src/shared/tracking_widget.rs b/jirs-client/src/shared/tracking_widget.rs index e47be88a..c430bcbb 100644 --- a/jirs-client/src/shared/tracking_widget.rs +++ b/jirs-client/src/shared/tracking_widget.rs @@ -21,7 +21,7 @@ pub fn tracking_link(model: &Model, modal: &crate::modals::issues_edit::Model) - let issue_id = *id; let handler = mouse_ev(Ev::Click, move |_| { - Msg::ModalOpened(Box::new(ModalType::TimeTracking(issue_id))) + Msg::ModalOpened(ModalType::TimeTracking(Some(issue_id))) }); div![C!["trackingLink"], handler, tracking_widget(model, modal),] diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 6034b663..a0b1ac13 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -276,8 +276,8 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { } // comments WsMsg::IssueCommentsLoaded(mut comments) => { - let issue_id = match model.modals.get(0) { - Some(ModalType::EditIssue(issue_id, _)) => *issue_id, + let issue_id = match &model.modals().edit_issue { + Some(modal) => modal.id, _ => return, }; if comments.iter().any(|c| c.issue_id != issue_id) { diff --git a/jirs-server/seed.sql b/jirs-server/seed.sql index 13884595..f8952494 100644 --- a/jirs-server/seed.sql +++ b/jirs-server/seed.sql @@ -77,6 +77,17 @@ insert into invitations (email, name, state, project_id, invited_by_id) values ( 2 ); insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); +insert into epics (name, project_id, user_id) VALUES ( + 'Foo', 1, 1 +), ( + 'Bar', 1, 2 +), ( + 'Foz', 2 ,1 +), ( + 'Baz', 1, 2 +), ( + 'Hello World', 2, 2 +); insert into issues( title, issue_type, @@ -86,7 +97,8 @@ insert into issues( description_text, reporter_id, project_id, - issue_status_id + issue_status_id, + epic_id ) values ( 'Foo', 'task', @@ -96,7 +108,8 @@ insert into issues( 'foz baz', 1, 1, - 1 + 1, + NULL ), ( 'Foo2', 'bug', @@ -106,7 +119,8 @@ insert into issues( 'foz baz 2', 1, 1, - 2 + 2, + NULL ), ( 'Foo3', 'story', @@ -116,7 +130,30 @@ insert into issues( 'foz baz 3', 2, 1, - 3 + 3, + NULL +), ( + 'Story 1 in Epic 1', + 'story', + 'low', + 3, + 'hello world 3', + 'foz baz 3', + 2, + 1, + 3, + 1 +), ( + 'Story 2 in Epic 1', + 'story', + 'low', + 3, + 'hello world 3', + 'foz baz 3', + 2, + 1, + 3, + 1 ); insert into comments (user_id, issue_id, body) values ( 1, 1, 'Vestibulum non neque at dui maximus porttitor fermentum consectetur eros.'