Rewrite modals

Refactor query db in ws actor, add edit epic ui and some logic
This commit is contained in:
eraden 2021-01-18 22:59:12 +01:00 committed by Adrian Woźniak
parent 82eb025359
commit 8412a113e7
55 changed files with 1038 additions and 979 deletions

View File

@ -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

View File

@ -1,2 +0,0 @@
concurrency = 2
database_url = "postgres://build@localhost:5432/jirs"

View File

@ -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;
}
}

View File

@ -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

2
Cargo.lock generated
View File

@ -1954,6 +1954,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"chrono", "chrono",
"derive_enum_iter",
"derive_enum_primitive",
"futures 0.1.30", "futures 0.1.30",
"jirs-data", "jirs-data",
"js-sys", "js-sys",

View File

@ -1 +0,0 @@
theme: jekyll-theme-minimal

View File

@ -9,7 +9,7 @@ use {
uuid::Uuid, uuid::Uuid,
}; };
#[derive(Debug, Serialize, Deserialize, Queryable)] #[derive(Serialize, Debug, Deserialize, Queryable)]
pub struct Issue { pub struct Issue {
pub id: i32, pub id: i32,
pub title: String, pub title: String,

View File

@ -1,5 +1,7 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{
db_or_debug_and_return, mail_or_debug_and_return, WebSocketActor, WsHandler, WsResult,
},
actix::AsyncContext, actix::AsyncContext,
database_actor::{ database_actor::{
authorize_user::AuthorizeUser, authorize_user::AuthorizeUser,
@ -20,43 +22,16 @@ impl WsHandler<Authenticate> for WebSocketActor {
fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: Authenticate, _ctx: &mut Self::Context) -> WsResult {
let Authenticate { name, email } = msg; let Authenticate { name, email } = msg;
// TODO check attempt number, allow only 5 times per day // TODO check attempt number, allow only 5 times per day
let user = match block_on(self.db.send(LookupUser { name, email })) { let user = db_or_debug_and_return!(self, LookupUser { name, email });
Ok(Ok(user)) => user, let token = db_or_debug_and_return!(self, CreateBindToken { user_id: user.id });
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);
}
};
if let Some(bind_token) = token.bind_token.as_ref().cloned() { if let Some(bind_token) = token.bind_token.as_ref().cloned() {
match block_on(self.mail.send(Welcome { let _ = mail_or_debug_and_return!(
bind_token, self,
email: user.email, Welcome {
})) { bind_token,
Ok(Ok(_)) => (), email: user.email,
Ok(Err(e)) => {
log::error!("{}", e);
return Ok(None);
} }
Err(e) => { );
log::error!("{}", e);
return Ok(None);
}
}
} }
Ok(Some(WsMsg::AuthenticateSuccess)) Ok(Some(WsMsg::AuthenticateSuccess))
} }
@ -68,17 +43,16 @@ pub struct CheckAuthToken {
impl WsHandler<CheckAuthToken> for WebSocketActor { impl WsHandler<CheckAuthToken> for WebSocketActor {
fn handle_msg(&mut self, msg: CheckAuthToken, ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: CheckAuthToken, ctx: &mut Self::Context) -> WsResult {
let user: jirs_data::User = match block_on(self.db.send(AuthorizeUser { let user: jirs_data::User = db_or_debug_and_return!(
access_token: msg.token, self,
})) { AuthorizeUser {
Ok(Ok(u)) => u, access_token: msg.token,
Ok(Err(_)) => { },
return Ok(Some(WsMsg::AuthorizeLoaded(Err( Ok(Some(WsMsg::AuthorizeLoaded(Err(
"Invalid auth token".to_string() "Invalid auth token".to_string()
)))); )))),
} Ok(Some(WsMsg::AuthorizeExpired))
_ => return Ok(Some(WsMsg::AuthorizeExpired)), );
};
self.current_user = Some(user.clone()); self.current_user = Some(user.clone());
self.current_user_project = self.load_user_project().ok(); self.current_user_project = self.load_user_project().ok();
self.current_project = self.load_project().ok(); self.current_project = self.load_project().ok();
@ -94,13 +68,14 @@ pub struct CheckBindToken {
impl WsHandler<CheckBindToken> for WebSocketActor { impl WsHandler<CheckBindToken> for WebSocketActor {
fn handle_msg(&mut self, msg: CheckBindToken, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: CheckBindToken, _ctx: &mut Self::Context) -> WsResult {
let token: Token = match block_on(self.db.send(FindBindToken { let token: Token = db_or_debug_and_return!(
token: msg.bind_token, self,
})) { FindBindToken {
Ok(Ok(token)) => token, token: msg.bind_token,
Ok(Err(_)) => return Ok(Some(WsMsg::BindTokenBad)), },
_ => return Ok(None), Ok(Some(WsMsg::BindTokenBad)),
}; Ok(None)
);
Ok(Some(WsMsg::BindTokenOk(token.access_token))) Ok(Some(WsMsg::BindTokenOk(token.access_token)))
} }
} }

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
futures::executor::block_on, futures::executor::block_on,
jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg}, jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg},
}; };
@ -12,19 +12,12 @@ impl WsHandler<LoadIssueComments> for WebSocketActor {
fn handle_msg(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: LoadIssueComments, _ctx: &mut Self::Context) -> WsResult {
self.require_user()?; self.require_user()?;
let comments = match block_on(self.db.send(database_actor::comments::LoadIssueComments { let comments = db_or_debug_and_return!(
issue_id: msg.issue_id, self,
})) { database_actor::comments::LoadIssueComments {
Ok(Ok(comments)) => comments, issue_id: msg.issue_id,
Ok(Err(e)) => {
log::error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
log::error!("{}", e);
return Ok(None);
}
};
Ok(Some(WsMsg::IssueCommentsLoaded(comments))) Ok(Some(WsMsg::IssueCommentsLoaded(comments)))
} }
@ -39,21 +32,14 @@ impl WsHandler<CreateCommentPayload> for WebSocketActor {
msg.user_id = Some(user_id); msg.user_id = Some(user_id);
} }
let issue_id = msg.issue_id; let issue_id = msg.issue_id;
match block_on(self.db.send(CreateComment { let _ = db_or_debug_and_return!(
user_id, self,
issue_id, CreateComment {
body: msg.body, user_id,
})) { issue_id,
Ok(Ok(_)) => (), body: msg.body,
Ok(Err(e)) => {
log::error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
log::error!("{}", e);
return Ok(None);
}
};
self.handle_msg(LoadIssueComments { issue_id }, ctx) self.handle_msg(LoadIssueComments { issue_id }, ctx)
} }
} }
@ -69,21 +55,14 @@ impl WsHandler<UpdateCommentPayload> for WebSocketActor {
body, body,
} = msg; } = msg;
let comment = match block_on(self.db.send(UpdateComment { let comment = db_or_debug_and_return!(
comment_id, self,
user_id, UpdateComment {
body, comment_id,
})) { user_id,
Ok(Ok(comment)) => comment, body,
Ok(Err(e)) => {
log::error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
log::error!("{}", e);
return Ok(None);
}
};
self.broadcast(&WsMsg::CommentUpdated(comment)); self.broadcast(&WsMsg::CommentUpdated(comment));
Ok(None) Ok(None)
} }
@ -99,20 +78,13 @@ impl WsHandler<DeleteComment> for WebSocketActor {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
let m = DeleteComment { let n = db_or_debug_and_return!(
comment_id: msg.comment_id, self,
user_id, DeleteComment {
}; comment_id: msg.comment_id,
match block_on(self.db.send(m)) { user_id,
Ok(Ok(n)) => Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n))),
Ok(Err(e)) => {
log::error!("{:?}", e);
Ok(None)
} }
Err(e) => { );
log::error!("{}", e); Ok(Some(WsMsg::CommentDeleted(msg.comment_id, n)))
Ok(None)
}
}
} }
} }

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
futures::executor::block_on, futures::executor::block_on,
jirs_data::{EpicId, NameString, UserProject, WsMsg}, jirs_data::{EpicId, NameString, UserProject, WsMsg},
}; };
@ -9,8 +9,7 @@ pub struct LoadEpics;
impl WsHandler<LoadEpics> for WebSocketActor { impl WsHandler<LoadEpics> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let epics = let epics = db_or_debug_and_return!(self, database_actor::epics::LoadEpics { project_id });
crate::query_db_or_print!(self, database_actor::epics::LoadEpics { project_id });
Ok(Some(WsMsg::EpicsLoaded(epics))) Ok(Some(WsMsg::EpicsLoaded(epics)))
} }
} }
@ -27,7 +26,7 @@ impl WsHandler<CreateEpic> for WebSocketActor {
project_id, project_id,
.. ..
} = self.require_user_project()?; } = self.require_user_project()?;
let epic = crate::query_db_or_print!( let epic = db_or_debug_and_return!(
self, self,
database_actor::epics::CreateEpic { database_actor::epics::CreateEpic {
user_id: *user_id, user_id: *user_id,
@ -48,7 +47,7 @@ impl WsHandler<UpdateEpic> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult {
let UpdateEpic { epic_id, name } = msg; let UpdateEpic { epic_id, name } = msg;
let UserProject { project_id, .. } = self.require_user_project()?; let UserProject { project_id, .. } = self.require_user_project()?;
let epic = crate::query_db_or_print!( let epic = db_or_debug_and_return!(
self, self,
database_actor::epics::UpdateEpic { database_actor::epics::UpdateEpic {
project_id: *project_id, project_id: *project_id,
@ -68,7 +67,7 @@ impl WsHandler<DeleteEpic> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult {
let DeleteEpic { epic_id } = msg; let DeleteEpic { epic_id } = msg;
let UserProject { user_id, .. } = self.require_user_project()?; let UserProject { user_id, .. } = self.require_user_project()?;
let n = crate::query_db_or_print!( let n = db_or_debug_and_return!(
self, self,
database_actor::epics::DeleteEpic { database_actor::epics::DeleteEpic {
user_id: *user_id, user_id: *user_id,

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{actor_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
futures::executor::block_on, futures::executor::block_on,
jirs_data::{Code, Lang, WsMsg}, jirs_data::{Code, Lang, WsMsg},
}; };
@ -9,19 +9,14 @@ pub struct HighlightCode(pub Lang, pub Code);
impl WsHandler<HighlightCode> for WebSocketActor { impl WsHandler<HighlightCode> for WebSocketActor {
fn handle_msg(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> WsResult {
self.require_user()?.id; self.require_user()?.id;
match block_on(self.hi.send(highlight_actor::HighlightCode { let res = actor_or_debug_and_return!(
code: msg.1, self,
lang: msg.0, hi,
})) { highlight_actor::HighlightCode {
Ok(Ok(res)) => Ok(Some(WsMsg::HighlightedCode(res))), code: msg.1,
Ok(Err(e)) => { lang: msg.0,
error!("{:?}", e);
Ok(None)
} }
Err(e) => { );
error!("{}", e); Ok(Some(WsMsg::HighlightedCode(res)))
Ok(None)
}
}
} }
} }

View File

@ -1,5 +1,8 @@
use { 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}, database_actor::{invitations, messages::CreateMessageReceiver},
futures::executor::block_on, futures::executor::block_on,
jirs_data::{ jirs_data::{
@ -15,18 +18,8 @@ impl WsHandler<ListInvitation> for WebSocketActor {
Some(id) => id, Some(id) => id,
_ => return Ok(None), _ => return Ok(None),
}; };
let res = match block_on(self.db.send(invitations::ListInvitation { user_id })) { let v = db_or_debug_and_return!(self, invitations::ListInvitation { user_id });
Ok(Ok(v)) => Some(WsMsg::InvitationListLoaded(v)), Ok(Some(WsMsg::InvitationListLoaded(v)))
Ok(Err(e)) => {
log::error!("{:?}", e);
return Ok(None);
}
Err(e) => {
log::error!("{}", e);
return Ok(None);
}
};
Ok(res)
} }
} }
@ -45,39 +38,28 @@ impl WsHandler<CreateInvitation> for WebSocketActor {
let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?; let (user_id, inviter_name) = self.require_user().map(|u| (u.id, u.name.clone()))?;
let CreateInvitation { email, name, role } = msg; let CreateInvitation { email, name, role } = msg;
let invitation = let invitation = db_or_debug_and_return!(
match block_on(self.db.send(database_actor::invitations::CreateInvitation { self,
database_actor::invitations::CreateInvitation {
user_id, user_id,
project_id, project_id,
email: email.clone(), email: email.clone(),
name: name.clone(), name: name.clone(),
role, role,
})) { },
Ok(Ok(invitation)) => invitation, Ok(Some(WsMsg::InvitationSendFailure)),
Ok(Err(e)) => { Ok(Some(WsMsg::InvitationSendFailure))
error!("{:?}", e); );
return Ok(Some(WsMsg::InvitationSendFailure)); let _ = mail_or_debug_and_return!(
} self,
Err(e) => { mail_actor::invite::Invite {
error!("{}", e); bind_token: invitation.bind_token,
return Ok(Some(WsMsg::InvitationSendFailure)); email: invitation.email,
} inviter_name,
}; },
match block_on(self.mail.send(mail_actor::invite::Invite { Ok(Some(WsMsg::InvitationSendFailure)),
bind_token: invitation.bind_token, Ok(Some(WsMsg::InvitationSendFailure))
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));
}
}
// If user exists then send message to him // If user exists then send message to him
if let Ok(Ok(message)) = block_on(self.db.send(database_actor::messages::CreateMessage { if let Ok(Ok(message)) = block_on(self.db.send(database_actor::messages::CreateMessage {
@ -106,18 +88,8 @@ impl WsHandler<DeleteInvitation> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: DeleteInvitation, _ctx: &mut Self::Context) -> WsResult {
self.require_user()?; self.require_user()?;
let DeleteInvitation { id } = msg; let DeleteInvitation { id } = msg;
let res = match block_on(self.db.send(invitations::DeleteInvitation { id })) { let _ = db_or_debug_and_return!(self, invitations::DeleteInvitation { id });
Ok(Ok(_)) => None, Ok(None)
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{}", e);
return Ok(None);
}
};
Ok(res)
} }
} }
@ -129,18 +101,8 @@ impl WsHandler<RevokeInvitation> for WebSocketActor {
fn handle_msg(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: RevokeInvitation, _ctx: &mut Self::Context) -> WsResult {
self.require_user()?; self.require_user()?;
let RevokeInvitation { id } = msg; let RevokeInvitation { id } = msg;
let res = match block_on(self.db.send(invitations::RevokeInvitation { id })) { let _ = db_or_debug_and_return!(self, invitations::RevokeInvitation { id });
Ok(Ok(_)) => Some(WsMsg::InvitationRevokeSuccess(id)), Ok(Some(WsMsg::InvitationRevokeSuccess(id)))
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{}", e);
return Ok(None);
}
};
Ok(res)
} }
} }
@ -151,45 +113,34 @@ pub struct AcceptInvitation {
impl WsHandler<AcceptInvitation> for WebSocketActor { impl WsHandler<AcceptInvitation> for WebSocketActor {
fn handle_msg(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: AcceptInvitation, ctx: &mut Self::Context) -> WsResult {
let AcceptInvitation { invitation_token } = msg; let AcceptInvitation { invitation_token } = msg;
let token = match block_on( let token = db_or_debug_and_return!(
self.db self,
.send(invitations::AcceptInvitation { invitation_token }), invitations::AcceptInvitation { invitation_token },
) { Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token))),
Ok(Ok(token)) => token, Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token)))
Ok(Err(e)) => { );
error!("{:?}", e);
return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token)));
}
Err(e) => {
error!("{}", e);
return Ok(Some(WsMsg::InvitationAcceptFailure(invitation_token)));
}
};
for message in block_on( for message in crate::actor_or_debug_and_fallback!(
self.db self,
.send(database_actor::messages::LookupMessagesByToken { db,
token: invitation_token, database_actor::messages::LookupMessagesByToken {
user_id: token.user_id, token: invitation_token,
}),
)
.unwrap_or_else(|_| Ok(vec![]))
.unwrap_or_default()
{
match block_on(self.db.send(database_actor::messages::MarkMessageSeen {
user_id: token.user_id, user_id: token.user_id,
message_id: message.id, },
})) { vec![],
Ok(Ok(n)) => { 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)); ctx.send_msg(&WsMsg::MessageMarkedSeen(message.id, n));
} }
Ok(Err(e)) => { );
error!("{:?}", e);
}
Err(e) => {
error!("{}", e);
}
}
} }
Ok(Some(WsMsg::InvitationAcceptSuccess(token.access_token))) Ok(Some(WsMsg::InvitationAcceptSuccess(token.access_token)))

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
database_actor::issue_statuses, database_actor::issue_statuses,
futures::executor::block_on, futures::executor::block_on,
jirs_data::{IssueStatusId, Position, TitleString, WsMsg}, jirs_data::{IssueStatusId, Position, TitleString, WsMsg},
@ -11,21 +11,8 @@ impl WsHandler<LoadIssueStatuses> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let msg = match block_on( let v = db_or_debug_and_return!(self, issue_statuses::LoadIssueStatuses { project_id });
self.db Ok(Some(WsMsg::IssueStatusesLoaded(v)))
.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)
} }
} }
@ -39,22 +26,15 @@ impl WsHandler<CreateIssueStatus> for WebSocketActor {
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let CreateIssueStatus { position, name } = msg; let CreateIssueStatus { position, name } = msg;
let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus { let issue_status = db_or_debug_and_return!(
project_id, self,
position, issue_statuses::CreateIssueStatus {
name, project_id,
})) { position,
Ok(Ok(is)) => Some(WsMsg::IssueStatusCreated(is)), name,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
error!("{}", e); Ok(Some(WsMsg::IssueStatusCreated(issue_status)))
return Ok(None);
}
};
Ok(msg)
} }
} }
@ -67,21 +47,14 @@ impl WsHandler<DeleteIssueStatus> for WebSocketActor {
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let DeleteIssueStatus { issue_status_id } = msg; let DeleteIssueStatus { issue_status_id } = msg;
let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus { let n = db_or_debug_and_return!(
issue_status_id, self,
project_id, issue_statuses::DeleteIssueStatus {
})) { issue_status_id,
Ok(Ok(n)) => Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n)), project_id,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
error!("{}", e); Ok(Some(WsMsg::IssueStatusDeleted(msg.issue_status_id, n)))
return Ok(None);
}
};
Ok(msg)
} }
} }
@ -100,22 +73,16 @@ impl WsHandler<UpdateIssueStatus> for WebSocketActor {
position, position,
name, name,
} = msg; } = msg;
let msg = match block_on(self.db.send(issue_statuses::UpdateIssueStatus { let issue_status = db_or_debug_and_return!(
issue_status_id, self,
position, issue_statuses::UpdateIssueStatus {
name, issue_status_id,
project_id, position,
})) { name,
Ok(Ok(is)) => Some(WsMsg::IssueStatusUpdated(is)), project_id,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
error!("{}", e); let msg = Some(WsMsg::IssueStatusUpdated(issue_status));
return Ok(None);
}
};
if let Some(ws_msg) = msg.as_ref() { if let Some(ws_msg) = msg.as_ref() {
self.broadcast(ws_msg) self.broadcast(ws_msg)
} }

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
database_actor::{ database_actor::{
issue_assignees::LoadAssignees, issue_assignees::LoadAssignees,
issues::{LoadProjectIssues, UpdateIssue}, issues::{LoadProjectIssues, UpdateIssue},
@ -125,23 +125,11 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
_ => (), _ => (),
}; };
let mut issue: jirs_data::Issue = match block_on(self.db.send(msg)) { let issue = db_or_debug_and_return!(self, msg);
Ok(Ok(issue)) => issue.into(), let mut issue: jirs_data::Issue = issue.into();
_ => return Ok(None),
};
let assignees: Vec<IssueAssignee> = let assignees: Vec<IssueAssignee> =
match block_on(self.db.send(LoadAssignees { issue_id: issue.id })) { db_or_debug_and_return!(self, LoadAssignees { issue_id: issue.id });
Ok(Ok(v)) => v,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{:?}", e);
return Ok(None);
}
};
for assignee in assignees { for assignee in assignees {
issue.user_ids.push(assignee.user_id); issue.user_ids.push(assignee.user_id);
@ -170,18 +158,8 @@ impl WsHandler<CreateIssuePayload> for WebSocketActor {
user_ids: msg.user_ids, user_ids: msg.user_ids,
epic_id: msg.epic_id, epic_id: msg.epic_id,
}; };
let m = match block_on(self.db.send(msg)) { let issue = db_or_debug_and_return!(self, msg);
Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())), Ok(Some(WsMsg::IssueCreated(issue.into())))
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{:?}", e);
return Ok(None);
}
};
Ok(m)
} }
} }
@ -192,21 +170,11 @@ pub struct DeleteIssue {
impl WsHandler<DeleteIssue> for WebSocketActor { impl WsHandler<DeleteIssue> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteIssue, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: DeleteIssue, _ctx: &mut Self::Context) -> WsResult {
self.require_user()?; self.require_user()?;
let m = match block_on( let n = db_or_debug_and_return!(
self.db self,
.send(database_actor::issues::DeleteIssue { issue_id: msg.id }), database_actor::issues::DeleteIssue { issue_id: msg.id }
) { );
Ok(Ok(n)) => Some(WsMsg::IssueDeleted(msg.id, n)), Ok(Some(WsMsg::IssueDeleted(msg.id, n)))
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{:?}", e);
return Ok(None);
}
};
Ok(m)
} }
} }
@ -216,14 +184,11 @@ impl WsHandler<LoadIssues> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadIssues, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let issues: Vec<jirs_data::Issue> = let v = db_or_debug_and_return!(self, LoadProjectIssues { project_id });
match block_on(self.db.send(LoadProjectIssues { project_id })) { let issues: Vec<jirs_data::Issue> = v.into_iter().map(|i| i.into()).collect();
Ok(Ok(v)) => v.into_iter().map(|i| i.into()).collect(),
_ => return Ok(None),
};
let mut issue_map = HashMap::new(); let mut issue_map = HashMap::new();
let mut queue = vec![]; let mut queue = vec![];
for issue in issues.into_iter() { for issue in issues {
let f = self.db.send(LoadAssignees { issue_id: issue.id }); let f = self.db.send(LoadAssignees { issue_id: issue.id });
queue.push(f); queue.push(f);
issue_map.insert(issue.id, issue); issue_map.insert(issue.id, issue);
@ -238,9 +203,10 @@ impl WsHandler<LoadIssues> for WebSocketActor {
}; };
} }
let mut issues = vec![]; let mut issues = vec![];
for (_, issue) in issue_map.into_iter() { for (_, issue) in issue_map {
issues.push(issue); issues.push(issue);
} }
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
Ok(Some(WsMsg::ProjectIssuesLoaded(issues))) Ok(Some(WsMsg::ProjectIssuesLoaded(issues)))
} }
@ -252,16 +218,18 @@ impl WsHandler<SyncIssueListPosition> for WebSocketActor {
fn handle_msg(&mut self, msg: SyncIssueListPosition, ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: SyncIssueListPosition, ctx: &mut Self::Context) -> WsResult {
let _project_id = self.require_user_project()?.project_id; let _project_id = self.require_user_project()?.project_id;
for (issue_id, list_position, status_id, epic_id) in msg.0 { for (issue_id, list_position, status_id, epic_id) in msg.0 {
match block_on(self.db.send(database_actor::issues::UpdateIssue { crate::actor_or_debug_and_ignore!(
issue_id, self,
list_position: Some(list_position), db,
issue_status_id: Some(status_id), database_actor::issues::UpdateIssue {
epic_id: Some(epic_id), issue_id,
..Default::default() list_position: Some(list_position),
})) { issue_status_id: Some(status_id),
Ok(Ok(_)) => (), epic_id: Some(epic_id),
_ => return Ok(None), ..Default::default()
}; },
|_| {}
);
} }
self.handle_msg(LoadIssues, ctx) self.handle_msg(LoadIssues, ctx)

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
database_actor::messages, database_actor::messages,
futures::executor::block_on, futures::executor::block_on,
jirs_data::{MessageId, WsMsg}, jirs_data::{MessageId, WsMsg},
@ -10,17 +10,8 @@ pub struct LoadMessages;
impl WsHandler<LoadMessages> for WebSocketActor { impl WsHandler<LoadMessages> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadMessages, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadMessages, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on(self.db.send(messages::LoadMessages { user_id })) { let v = db_or_debug_and_return!(self, messages::LoadMessages { user_id });
Ok(Ok(v)) => Ok(Some(WsMsg::MessagesLoaded(v))), Ok(Some(WsMsg::MessagesLoaded(v)))
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)
}
Err(e) => {
error!("{}", e);
Ok(None)
}
}
} }
} }
@ -31,19 +22,13 @@ pub struct MarkMessageSeen {
impl WsHandler<MarkMessageSeen> for WebSocketActor { impl WsHandler<MarkMessageSeen> for WebSocketActor {
fn handle_msg(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: MarkMessageSeen, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on(self.db.send(messages::MarkMessageSeen { let count = db_or_debug_and_return!(
message_id: msg.id, self,
user_id, messages::MarkMessageSeen {
})) { message_id: msg.id,
Ok(Ok(count)) => Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count))), user_id,
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)
} }
Err(e) => { );
error!("{}", e); Ok(Some(WsMsg::MessageMarkedSeen(msg.id, count)))
Ok(None)
}
}
} }
} }

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
database_actor as db, database_actor as db,
futures::executor::block_on, futures::executor::block_on,
jirs_data::{UpdateProjectPayload, UserProject, WsMsg}, jirs_data::{UpdateProjectPayload, UserProject, WsMsg},
@ -12,38 +12,21 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
project_id, project_id,
.. ..
} = self.require_user_project()?; } = self.require_user_project()?;
match block_on(self.db.send(database_actor::projects::UpdateProject { let _ = db_or_debug_and_return!(
project_id: *project_id, self,
name: msg.name, database_actor::projects::UpdateProject {
url: msg.url, project_id: *project_id,
description: msg.description, name: msg.name,
category: msg.category, url: msg.url,
time_tracking: msg.time_tracking, description: msg.description,
})) { category: msg.category,
Ok(Ok(_)) => (), time_tracking: msg.time_tracking,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
error!("{:?}", e); let projects = db_or_debug_and_return!(
return Ok(None); self,
} database_actor::projects::LoadProjects { user_id: *user_id }
}; );
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);
}
};
Ok(Some(WsMsg::ProjectsLoaded(projects))) Ok(Some(WsMsg::ProjectsLoaded(projects)))
} }
} }
@ -53,16 +36,7 @@ pub struct LoadProjects;
impl WsHandler<LoadProjects> for WebSocketActor { impl WsHandler<LoadProjects> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadProjects, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadProjects, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on(self.db.send(db::projects::LoadProjects { user_id })) { let v = db_or_debug_and_return!(self, db::projects::LoadProjects { user_id });
Ok(Ok(v)) => Ok(Some(WsMsg::ProjectsLoaded(v))), Ok(Some(WsMsg::ProjectsLoaded(v)))
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)
}
Err(e) => {
error!("{:?}", e);
Ok(None)
}
}
} }
} }

View File

@ -1,5 +1,5 @@
use { use {
crate::{WebSocketActor, WsHandler, WsResult}, crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult},
database_actor as db, database_actor as db,
futures::executor::block_on, futures::executor::block_on,
jirs_data::{UserProjectId, WsMsg}, jirs_data::{UserProjectId, WsMsg},
@ -10,20 +10,8 @@ pub struct LoadUserProjects;
impl WsHandler<LoadUserProjects> for WebSocketActor { impl WsHandler<LoadUserProjects> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadUserProjects, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadUserProjects, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on( let v = db_or_debug_and_return!(self, db::user_projects::LoadUserProjects { user_id });
self.db Ok(Some(WsMsg::UserProjectsLoaded(v)))
.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)
}
}
} }
} }
@ -34,22 +22,14 @@ pub struct SetCurrentUserProject {
impl WsHandler<SetCurrentUserProject> for WebSocketActor { impl WsHandler<SetCurrentUserProject> for WebSocketActor {
fn handle_msg(&mut self, msg: SetCurrentUserProject, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: SetCurrentUserProject, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
match block_on(self.db.send(db::user_projects::ChangeCurrentUserProject { let user_project = db_or_debug_and_return!(
user_id, self,
id: msg.id, db::user_projects::ChangeCurrentUserProject {
})) { user_id,
Ok(Ok(user_project)) => { id: msg.id,
self.current_user_project = Some(user_project.clone());
Ok(Some(WsMsg::UserProjectCurrentChanged(user_project)))
} }
Ok(Err(e)) => { );
error!("{:?}", e); self.current_user_project = Some(user_project.clone());
Ok(None) Ok(Some(WsMsg::UserProjectCurrentChanged(user_project)))
}
Err(e) => {
error!("{}", e);
Ok(None)
}
}
} }
} }

View File

@ -1,5 +1,7 @@
use { 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}, database_actor::{self, users::Register as DbRegister},
futures::executor::block_on, futures::executor::block_on,
jirs_data::{UserId, UserProject, UserRole, WsMsg}, jirs_data::{UserId, UserProject, UserRole, WsMsg},
@ -12,18 +14,8 @@ impl WsHandler<LoadProjectUsers> for WebSocketActor {
use database_actor::users::LoadProjectUsers as Msg; use database_actor::users::LoadProjectUsers as Msg;
let project_id = self.require_user_project()?.project_id; let project_id = self.require_user_project()?.project_id;
let m = match block_on(self.db.send(Msg { project_id })) { let v = db_or_debug_and_return!(self, Msg { project_id });
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(v)), Ok(Some(WsMsg::ProjectUsersLoaded(v)))
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{}", e);
return Ok(None);
}
};
Ok(m)
} }
} }
@ -35,26 +27,24 @@ pub struct Register {
impl WsHandler<Register> for WebSocketActor { impl WsHandler<Register> for WebSocketActor {
fn handle_msg(&mut self, msg: Register, ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, msg: Register, ctx: &mut Self::Context) -> WsResult {
let Register { name, email } = msg; let Register { name, email } = msg;
let msg = match block_on(self.db.send(DbRegister { let _ = db_or_debug_and_return!(
name: name.clone(), self,
email: email.clone(), DbRegister {
project_id: None, name: name.clone(),
role: UserRole::Owner, email: email.clone(),
})) { project_id: None,
Ok(Ok(_)) => Some(WsMsg::SignUpSuccess), role: UserRole::Owner,
Ok(Err(_)) => Some(WsMsg::SignUpPairTaken), },
Err(e) => { Ok(Some(WsMsg::SignUpPairTaken)),
error!("{}", e); Ok(None)
return Ok(None); );
}
};
match self.handle_msg(Authenticate { name, email }, ctx) { match self.handle_msg(Authenticate { name, email }, ctx) {
Ok(_) => (), Ok(_) => (),
Err(e) => return Ok(Some(e)), Err(e) => return Ok(Some(e)),
}; };
Ok(msg) Ok(Some(WsMsg::SignUpSuccess))
} }
} }
@ -64,13 +54,8 @@ impl WsHandler<LoadInvitedUsers> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult { fn handle_msg(&mut self, _msg: LoadInvitedUsers, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
let users = match block_on( let users =
self.db db_or_debug_and_return!(self, database_actor::users::LoadInvitedUsers { user_id });
.send(database_actor::users::LoadInvitedUsers { user_id }),
) {
Ok(Ok(users)) => users,
_ => return Ok(None),
};
Ok(Some(WsMsg::InvitedUsersLoaded(users))) Ok(Some(WsMsg::InvitedUsersLoaded(users)))
} }
@ -86,21 +71,14 @@ impl WsHandler<ProfileUpdate> for WebSocketActor {
let user_id = self.require_user()?.id; let user_id = self.require_user()?.id;
let ProfileUpdate { name, email } = msg; let ProfileUpdate { name, email } = msg;
match block_on(self.db.send(database_actor::users::ProfileUpdate { let _ = db_or_debug_and_return!(
user_id, self,
name, database_actor::users::ProfileUpdate {
email, user_id,
})) { name,
Ok(Ok(_users)) => (), email,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
} }
Err(e) => { );
error!("{}", e);
return Ok(None);
}
};
Ok(Some(WsMsg::ProfileUpdated)) Ok(Some(WsMsg::ProfileUpdated))
} }
@ -120,23 +98,14 @@ impl WsHandler<RemoveInvitedUser> for WebSocketActor {
project_id, project_id,
.. ..
} = self.require_user_project()?.clone(); } = self.require_user_project()?.clone();
match block_on( let _ = db_or_debug_and_return!(
self.db self,
.send(database_actor::user_projects::RemoveInvitedUser { database_actor::user_projects::RemoveInvitedUser {
invited_id, invited_id,
inviter_id, inviter_id,
project_id, project_id,
}),
) {
Ok(Ok(_users)) => Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id))),
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)
} }
Err(e) => { );
error!("{}", e); Ok(Some(WsMsg::InvitedUserRemoveSuccess(invited_id)))
Ok(None)
}
}
} }
} }

View File

@ -1,15 +1,72 @@
#[macro_export] #[macro_export]
macro_rules! query_db_or_print { macro_rules! db_or_debug_and_return {
($s:expr,$msg:expr) => { ($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
match block_on($s.db.send($msg)) { $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(Ok(r)) => r,
Ok(Err(e)) => { Ok(Err(e)) => {
log::error!("{:?}", e); log::error!("{:?}", e);
return Ok(None); return $actor_err;
} }
Err(e) => { Err(e) => {
log::error!("{}", e); log::error!("{:?}", e);
return Ok(None); 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
} }
} }
}; };

View File

@ -76,5 +76,11 @@ features = [
"DragEvent", "DragEvent",
] ]
[dependencies.derive_enum_primitive]
path = "../derive/derive_enum_primitive"
[dependencies.derive_enum_iter]
path = "../derive/derive_enum_iter"
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = { version = "*" } wasm-bindgen-test = { version = "*" }

View File

@ -10,7 +10,6 @@
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
font-size: 14.5px; font-size: 14.5px;
padding: 0 12px;
&:focus { &:focus {
border-color: var(--borderInputFocus); border-color: var(--borderInputFocus);
@ -19,6 +18,16 @@
> input[type=radio] { > input[type=radio] {
display: none; 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 { > .styledCheckboxChild.selected {

View File

@ -109,6 +109,30 @@
padding-left: 15px; 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 { &.deleteEpic {
> section { > section {
> .header { > .header {

View File

@ -1,11 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
which rsass RSASS_PATH=$(command -v rsass)
if [[ "$status" != "0" ]]; if [[ "${RSASS_PATH}" == "" ]];
then then
cargo install rsass --features=commandline cargo install rsass --features=commandline
fi 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 PROJECT_ROOT=$(git rev-parse --show-toplevel)
export CLIENT_ROOT=${PROJECT_ROOT}/jirs-client export CLIENT_ROOT=${PROJECT_ROOT}/jirs-client
export HI_ROOT=${PROJECT_ROOT}/highlight/jirs-highlight export HI_ROOT=${PROJECT_ROOT}/highlight/jirs-highlight

View File

@ -1,6 +1,6 @@
use { use {
crate::{ crate::{
shared::{ToChild, ToNode}, shared::{IntoChild, ToNode},
FieldId, Msg, FieldId, Msg,
}, },
jirs_data::TimeTracking, jirs_data::TimeTracking,
@ -122,36 +122,51 @@ impl<'l> ToNode for ChildBuilder<'l> {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledCheckbox<'l> { pub struct StyledCheckbox<'l, Options>
where
Options: Iterator<Item = ChildBuilder<'l>>,
{
id: FieldId, id: FieldId,
options: Vec<ChildBuilder<'l>>, options: Option<Options>,
selected: u32, selected: u32,
class_list: Vec<&'l str>, class_list: Vec<&'l str>,
} }
impl<'l> ToNode for StyledCheckbox<'l> { impl<'l, Options> ToNode for StyledCheckbox<'l, Options>
where
Options: Iterator<Item = ChildBuilder<'l>>,
{
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl<'l> StyledCheckbox<'l> { impl<'l, Options> StyledCheckbox<'l, Options>
pub fn build() -> StyledCheckboxBuilder<'l> { where
Options: Iterator<Item = ChildBuilder<'l>>,
{
pub fn build() -> StyledCheckboxBuilder<'l, Options> {
StyledCheckboxBuilder { StyledCheckboxBuilder {
options: vec![], options: None,
selected: 0, selected: 0,
class_list: vec![], class_list: vec![],
} }
} }
} }
pub struct StyledCheckboxBuilder<'l> { pub struct StyledCheckboxBuilder<'l, Options>
options: Vec<ChildBuilder<'l>>, where
Options: Iterator<Item = ChildBuilder<'l>>,
{
options: Option<Options>,
selected: u32, selected: u32,
class_list: Vec<&'l str>, class_list: Vec<&'l str>,
} }
impl<'l> StyledCheckboxBuilder<'l> { impl<'l, Options> StyledCheckboxBuilder<'l, Options>
where
Options: Iterator<Item = ChildBuilder<'l>>,
{
pub fn state(mut self, state: &StyledCheckboxState) -> Self { pub fn state(mut self, state: &StyledCheckboxState) -> Self {
self.selected = state.value; self.selected = state.value;
self self
@ -162,12 +177,12 @@ impl<'l> StyledCheckboxBuilder<'l> {
self self
} }
pub fn options(mut self, options: Vec<ChildBuilder<'l>>) -> Self { pub fn options(mut self, options: Options) -> Self {
self.options = options; self.options = Some(options);
self self
} }
pub fn build(self, field_id: FieldId) -> StyledCheckbox<'l> { pub fn build(self, field_id: FieldId) -> StyledCheckbox<'l, Options> {
StyledCheckbox { StyledCheckbox {
id: field_id, id: field_id,
options: self.options, options: self.options,
@ -177,7 +192,10 @@ impl<'l> StyledCheckboxBuilder<'l> {
} }
} }
fn render(values: StyledCheckbox) -> Node<Msg> { fn render<'l, Options>(values: StyledCheckbox<'l, Options>) -> Node<Msg>
where
Options: Iterator<Item = ChildBuilder<'l>>,
{
let StyledCheckbox { let StyledCheckbox {
id, id,
options, options,
@ -185,10 +203,12 @@ fn render(values: StyledCheckbox) -> Node<Msg> {
class_list, class_list,
} = values; } = values;
let opt: Vec<Node<Msg>> = options let opt: Vec<Node<Msg>> = match options {
.into_iter() Some(options) => options
.map(|child| child.with_id(id.clone()).try_select(selected).into_node()) .map(|child| child.with_id(id.clone()).try_select(selected).into_node())
.collect(); .collect(),
_ => vec![Node::Empty],
};
div![ div![
C!["styledCheckbox"], C!["styledCheckbox"],
@ -197,10 +217,10 @@ fn render(values: StyledCheckbox) -> Node<Msg> {
] ]
} }
impl<'l> ToChild<'l> for TimeTracking { impl<'l> IntoChild<'l> for TimeTracking {
type Builder = ChildBuilder<'l>; type Builder = ChildBuilder<'l>;
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn into_child(self) -> Self::Builder {
Self::Builder::default() Self::Builder::default()
.label(match self { .label(match self {
TimeTracking::Untracked => "No tracking", TimeTracking::Untracked => "No tracking",
@ -212,11 +232,11 @@ impl<'l> ToChild<'l> for TimeTracking {
TimeTracking::Fibonacci => "fibonacci", TimeTracking::Fibonacci => "fibonacci",
TimeTracking::Hourly => "hourly", TimeTracking::Hourly => "hourly",
}) })
.value((*self).into())
.add_class(match self { .add_class(match self {
TimeTracking::Untracked => "untracked", TimeTracking::Untracked => "untracked",
TimeTracking::Fibonacci => "fibonacci", TimeTracking::Fibonacci => "fibonacci",
TimeTracking::Hourly => "hourly", TimeTracking::Hourly => "hourly",
}) })
.value((self).into())
} }
} }

View File

@ -123,7 +123,7 @@ pub enum Msg {
AvatarUpdateFetched(String), AvatarUpdateFetched(String),
// modals // modals
ModalOpened(Box<ModalType>), ModalOpened(ModalType),
ModalDropped, ModalDropped,
ModalChanged(FieldChange), ModalChanged(FieldChange),
@ -280,11 +280,11 @@ fn resolve_page(url: Url) -> Option<Page> {
let page = match url.path()[0].as_ref() { let page = match url.path()[0].as_ref() {
"board" => Page::Project, "board" => Page::Project,
"profile" => Page::Profile,
"issues" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) { "issues" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Page::EditIssue(id), Some(Ok(id)) => Page::EditIssue(id),
_ => return None, _ => return None,
}, },
"profile" => Page::Profile,
"add-issue" => Page::AddIssue, "add-issue" => Page::AddIssue,
"project-settings" => Page::ProjectSettings, "project-settings" => Page::ProjectSettings,
"login" => Page::SignIn, "login" => Page::SignIn,

View File

@ -0,0 +1,4 @@
pub use {view::*, model::*};
mod view;
mod model;

View File

@ -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 }
}
}

View File

@ -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<Msg> {
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()
}

View File

@ -10,7 +10,7 @@ pub struct Model {
} }
impl 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); let related_issues = model.epic_issue_ids(epic_id);
Self { Self {
epic_id, epic_id,

View File

@ -1,30 +1,20 @@
use { 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, jirs_data::WsMsg,
seed::prelude::*, seed::prelude::*,
}; };
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let modal = match model.modals.iter_mut().find_map(|modal| { let modal = match &mut model.modals_mut().delete_epic {
if let ModalType::DeleteEpic(modal) = modal {
Some(modal)
} else {
None
}
}) {
Some(modal) => modal, Some(modal) => modal,
_ => return, _ => return,
}; };
match msg { match msg {
Msg::ModalDropped => {
go_to_board(orders);
}
Msg::DeleteEpic => { Msg::DeleteEpic => {
send_ws_msg(WsMsg::EpicDelete(modal.epic_id), model.ws.as_ref(), orders); send_ws_msg(WsMsg::EpicDelete(modal.epic_id), model.ws.as_ref(), orders);
} }
Msg::ResourceChanged(ResourceKind::Epic, OperationKind::SingleRemoved, Some(_)) => { Msg::ResourceChanged(ResourceKind::Epic, OperationKind::SingleRemoved, Some(_)) => {
go_to_board(orders);
orders.skip().send_msg(Msg::ModalDropped); orders.skip().send_msg(Msg::ModalDropped);
} }
_ => {} _ => {}

View File

@ -1,19 +1,18 @@
use crate::FieldId;
use jirs_data::EpicFieldId;
use { use {
crate::{ crate::{
components::{styled_input::*, styled_select::StyledSelectState}, components::{styled_checkbox::StyledCheckboxState, styled_input::*},
model, model, FieldId, Msg,
}, },
jirs_data::{EpicId, IssueId}, jirs_data::*,
seed::prelude::Orders,
}; };
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Debug)]
pub struct Model { pub struct Model {
pub epic_id: EpicId, pub epic_id: EpicId,
pub related_issues: Vec<IssueId>, pub related_issues: Vec<IssueId>,
pub name: StyledInputState, pub name: StyledInputState,
pub transform_into: StyledSelectState, pub transform_into: StyledCheckboxState,
} }
impl Model { impl Model {
@ -23,6 +22,7 @@ impl Model {
.get(&epic_id) .get(&epic_id)
.map(|epic| epic.name.as_str()) .map(|epic| epic.name.as_str())
.unwrap_or_default(); .unwrap_or_default();
let related_issues = model let related_issues = model
.issues() .issues()
.iter() .iter()
@ -38,10 +38,15 @@ impl Model {
epic_id, epic_id,
related_issues, related_issues,
name: StyledInputState::new(FieldId::EditEpic(EpicFieldId::Name), name), name: StyledInputState::new(FieldId::EditEpic(EpicFieldId::Name), name),
transform_into: StyledSelectState::new( transform_into: StyledCheckboxState::new(
FieldId::EditEpic(EpicFieldId::StartsAt), FieldId::EditEpic(EpicFieldId::TransformInto),
vec![], 0,
), ),
} }
} }
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) {
self.name.update(msg);
self.transform_into.update(msg);
}
} }

View File

@ -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<Msg>) { pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let _modal = crate::match_modal_mut!(model, DeleteEpic); 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,
);
}
_ => (),
};
} }

View File

@ -1,26 +1,56 @@
use { use {
crate::{ crate::{
components::{styled_input::*, styled_modal::*}, components::{
styled_button::*, styled_checkbox::*, styled_icon::Icon, styled_input::*,
styled_modal::*,
},
modals::epics_edit::Model, modals::epics_edit::Model,
model, model,
shared::ToNode, shared::{IntoChild, ToNode},
FieldId, Msg, FieldId, Msg,
}, },
jirs_data::EpicFieldId, jirs_data::{EpicFieldId, IssueType},
seed::{prelude::*, *}, 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<Msg> { pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
let transform = if modal.related_issues.is_empty() { let transform = if modal.related_issues.is_empty() {
Node::Empty transform_into_available(modal)
} else { } 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() StyledModal::build()
.center() .center()
.child(h1!["Edit epic"]) .width(600)
.add_class("editEpic")
.child(div![C!["header"], h1!["Edit epic"], close])
.child( .child(
StyledInput::build() StyledInput::build()
.state(&modal.name)
.build(FieldId::EditEpic(EpicFieldId::Name)) .build(FieldId::EditEpic(EpicFieldId::Name))
.into_node(), .into_node(),
) )
@ -28,3 +58,32 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
.build() .build()
.into_node() .into_node()
} }
fn transform_into_available(modal: &super::Model) -> Node<Msg> {
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<Msg> {
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)]
]
}

View File

@ -1,22 +1,14 @@
use { use {
crate::{ crate::{model::Model, Msg, OperationKind, ResourceKind},
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
model::{ModalType, Model},
Msg, OperationKind, ResourceKind,
},
jirs_data::WsMsg, jirs_data::WsMsg,
seed::prelude::*, seed::prelude::*,
}; };
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let _modal: &mut Box<DeleteIssueStatusModal> = let _modal = match &mut model.modals_mut().delete_issue_status_modal {
match model.modals.iter_mut().find_map(|modal| match modal { Some(m) => m,
ModalType::DeleteIssueStatusModal(modal) => Some(modal), _ => return,
_ => None, };
}) {
Some(m) => m,
_ => return,
};
match msg { match msg {
Msg::DeleteIssueStatus(issue_status_id) => { Msg::DeleteIssueStatus(issue_status_id) => {

View File

@ -4,14 +4,16 @@ use {
styled_date_time_input::*, styled_input::*, styled_select::*, styled_select_child::*, styled_date_time_input::*, styled_input::*, styled_select::*, styled_select_child::*,
}, },
model::IssueModal, model::IssueModal,
shared::{ToChild, ToNode}, shared::{IntoChild, ToNode},
FieldId, Msg, FieldId, Msg,
}, },
derive_enum_iter::EnumIter,
derive_enum_primitive::EnumPrimitive,
jirs_data::{IssueFieldId, IssuePriority}, jirs_data::{IssueFieldId, IssuePriority},
seed::prelude::*, seed::prelude::*,
}; };
#[derive(Copy, Clone)] #[derive(Copy, Clone, EnumPrimitive, EnumIter)]
pub enum Type { pub enum Type {
Task, Task,
Bug, Bug,
@ -19,24 +21,13 @@ pub enum Type {
Epic, Epic,
} }
impl From<u32> for Type { impl Default for Type {
fn from(n: u32) -> Self { fn default() -> Self {
match n { Self::Task
0 => Type::Task,
1 => Type::Bug,
2 => Type::Story,
3 => Type::Epic,
_ => Type::Task,
}
} }
} }
impl Type { impl Type {
pub(crate) fn ordered<'l>() -> &'l [Type] {
use Type::*;
&[Task, Bug, Story, Epic]
}
pub(crate) fn submit_label(&self) -> &str { pub(crate) fn submit_label(&self) -> &str {
use Type::*; use Type::*;
match self { match self {
@ -62,22 +53,17 @@ impl Type {
} }
} }
impl<'l> ToChild<'l> for Type { impl<'l> IntoChild<'l> for Type {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn into_child(self) -> Self::Builder {
let name = match self { let name = match self {
Type::Task => "Task", Type::Task => "Task",
Type::Bug => "Bug", Type::Bug => "Bug",
Type::Story => "Story", Type::Story => "Story",
Type::Epic => "Epic", Type::Epic => "Epic",
}; };
let value = match self { let value: u32 = self.into();
Type::Task => 0,
Type::Bug => 1,
Type::Story => 2,
Type::Epic => 3,
};
let type_icon = { let type_icon = {
use crate::components::styled_icon::*; use crate::components::styled_icon::*;

View File

@ -1,8 +1,6 @@
use { use {
crate::{ crate::{
components::styled_select::StyledSelectChanged, components::styled_select::StyledSelectChanged, model::IssueModal, ws::send_ws_msg,
model::{IssueModal, ModalType},
ws::send_ws_msg,
FieldId, Msg, OperationKind, ResourceKind, FieldId, Msg, OperationKind, ResourceKind,
}, },
jirs_data::{IssueFieldId, UserId, WsMsg}, jirs_data::{IssueFieldId, UserId, WsMsg},
@ -10,12 +8,11 @@ use {
}; };
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let modal = model let user_id = model.user_id().unwrap_or_default();
.modals let project_id = model.project_id().unwrap_or_default();
.iter_mut()
.find(|modal| matches!(modal, ModalType::AddIssue(..))); let modal = match &mut model.modals_mut().add_issue {
let modal = match modal { Some(modal) => modal,
Some(ModalType::AddIssue(modal)) => modal,
_ => return, _ => return,
}; };
@ -30,8 +27,6 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
); );
} }
Msg::AddIssue => { 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(); let type_value = modal.type_state.values.get(0).cloned().unwrap_or_default();
match type_value { match type_value {
0 | 1 | 2 => { 0 | 1 | 2 => {

View File

@ -24,7 +24,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.values .values
.get(0) .get(0)
.cloned() .cloned()
.map(Type::from) .map(Into::into)
.unwrap_or_else(|| Type::Task); .unwrap_or_else(|| Type::Task);
let issue_type_field = issue_type_field(modal); let issue_type_field = issue_type_field(modal);
@ -127,11 +127,18 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
.text_filter(modal.type_state.text_filter.as_str()) .text_filter(modal.type_state.text_filter.as_str())
.opened(modal.type_state.opened) .opened(modal.type_state.opened)
.valid(true) .valid(true)
.options(Type::ordered().iter().map(|t| t.to_child().name("type"))) .options(Type::Task.into_iter().map(|t| t.into_child().name("type")))
.selected(vec![Type::from( .selected(vec![{
modal.type_state.values.get(0).cloned().unwrap_or_default(), let v: Type = modal
) .type_state
.to_child() .values
.get(0)
.cloned()
.unwrap_or_default()
.into();
v
}
.into_child()
.name("type")]) .name("type")])
.build(FieldId::AddIssueModal(IssueFieldId::Type)) .build(FieldId::AddIssueModal(IssueFieldId::Type))
.into_node(); .into_node();

View File

@ -1 +1,6 @@
use jirs_data::IssueId;
#[derive(Debug, Default)]
pub struct Model {
pub issue_id: IssueId,
}

View File

@ -1,24 +1,12 @@
use { use {
crate::{ crate::{components::styled_confirm_modal::StyledConfirmModal, model, shared::ToNode, Msg},
components::styled_confirm_modal::StyledConfirmModal, model, model::ModalType, seed::prelude::*,
shared::ToNode, Msg,
},
seed::{prelude::*, *},
}; };
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let opt_id = model let issue_id = match &model.modals().delete_issue_confirm {
.modals Some(modal) => modal.issue_id,
.iter() _ => return Node::Empty,
.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 handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id)); let handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id));

View File

@ -9,13 +9,13 @@ use {
model::{CommentForm, IssueModal}, model::{CommentForm, IssueModal},
EditIssueModalSection, FieldId, Msg, EditIssueModalSection, FieldId, Msg,
}, },
jirs_data::{EpicId, Issue, IssueFieldId, TimeTracking, UpdateIssuePayload}, jirs_data::{Issue, IssueFieldId, IssueId, TimeTracking, UpdateIssuePayload},
seed::prelude::*, seed::prelude::*,
}; };
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct Model { pub struct Model {
pub id: EpicId, pub id: IssueId,
pub link_copied: bool, pub link_copied: bool,
pub payload: UpdateIssuePayload, pub payload: UpdateIssuePayload,
pub top_type_state: StyledSelectState, pub top_type_state: StyledSelectState,

View File

@ -1,8 +1,7 @@
use { use {
crate::{ crate::{
components::styled_select::StyledSelectChanged, components::styled_select::StyledSelectChanged,
modals::issues_edit::Model as EditIssueModal, model::{IssueModal, Model},
model::{IssueModal, ModalType, Model},
ws::send_ws_msg, ws::send_ws_msg,
EditIssueModalSection, FieldChange, FieldId, Msg, OperationKind, ResourceKind, EditIssueModalSection, FieldChange, FieldId, Msg, OperationKind, ResourceKind,
}, },
@ -11,18 +10,22 @@ use {
}; };
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal: &mut EditIssueModal = match model.modals.get_mut(0) { let modal = match &mut model.modals.edit_issue {
Some(ModalType::EditIssue(_issue_id, modal)) => modal, Some(modal) => modal,
_ => return, _ => return,
}; };
modal.update_states(msg, orders); modal.update_states(msg, orders);
match msg { match msg {
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => { Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => {
if let Some(issue) = model.issues_by_id.get(id) { let m = model.issues_by_id.get(id).cloned();
modal.payload = issue.clone().into(); if let Some(issue) = m {
modal.description_state.initial_text = modal.description_state.initial_text = issue
issue.description_text.clone().unwrap_or_default(); .description_text
.as_deref()
.unwrap_or_default()
.to_string();
modal.payload = issue.into();
} }
} }

View File

@ -3,7 +3,7 @@ use {
components::{ components::{
styled_avatar::StyledAvatar, styled_button::StyledButton, styled_editor::StyledEditor, styled_avatar::StyledAvatar, styled_button::StyledButton, styled_editor::StyledEditor,
styled_field::StyledField, styled_icon::Icon, styled_input::StyledInput, styled_field::StyledField, styled_icon::Icon, styled_input::StyledInput,
styled_select::StyledSelect, styled_modal::*, styled_select::StyledSelect,
}, },
modals::{ modals::{
epic_field, issues_edit::Model as EditIssueModal, time_tracking::time_tracking_field, epic_field, issues_edit::Model as EditIssueModal, time_tracking::time_tracking_field,
@ -20,6 +20,21 @@ use {
mod comments; mod comments;
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
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<Msg> {
div![ div![
C!["issueDetails"], C!["issueDetails"],
modal_header(model, modal), modal_header(model, modal),
@ -72,12 +87,10 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let close_handler = mouse_ev(Ev::Click, |ev| { let close_handler = mouse_ev(Ev::Click, |ev| {
ev.prevent_default(); ev.prevent_default();
ev.stop_propagation(); ev.stop_propagation();
seed::Url::new().add_path_part("board").go_and_push();
Msg::ModalDropped Msg::ModalDropped
}); });
let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| { 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() let copy_button = StyledButton::build()

View File

@ -66,7 +66,7 @@ pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Opti
let comment_id = comment.id; let comment_id = comment.id;
let delete_comment_handler = mouse_ev(Ev::Click, move |ev| { let delete_comment_handler = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalOpened(Box::new(ModalType::DeleteCommentConfirm(comment_id))) Msg::ModalOpened(ModalType::DeleteCommentConfirm(Some(comment_id)))
}); });
let edit_button = StyledButton::build() let edit_button = StyledButton::build()
.add_class("editButton") .add_class("editButton")

View File

@ -1,5 +1,6 @@
pub use {epic_field::*, update::*, view::*}; pub use {epic_field::*, update::*, view::*};
pub mod comments_delete;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub mod debug; pub mod debug;
pub mod epics_delete; pub mod epics_delete;

View File

@ -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 }
}
}

View File

@ -7,14 +7,14 @@ use {
styled_modal::StyledModal, styled_modal::StyledModal,
styled_select::{StyledSelect, StyledSelectState}, styled_select::{StyledSelect, StyledSelectState},
}, },
model::{ModalType, Model}, model::Model,
shared::{ shared::{
tracking_widget::{fibonacci_values, tracking_widget}, tracking_widget::{fibonacci_values, tracking_widget},
ToChild, ToNode, ToChild, ToNode,
}, },
EditIssueModalSection, FieldId, Msg, EditIssueModalSection, FieldId, Msg,
}, },
jirs_data::{EpicId, IssueFieldId, TimeTracking}, jirs_data::{IssueFieldId, IssueId, TimeTracking},
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
@ -27,20 +27,22 @@ pub fn value_for_time_tracking(v: &Option<i32>, time_tracking_type: &TimeTrackin
} }
} }
pub fn view(model: &Model, issue_id: EpicId) -> Node<Msg> { pub fn view(model: &Model, modal: &super::Model) -> Node<Msg> {
let issue_id: IssueId = modal.issue_id;
if model.issues_by_id.get(&issue_id).is_none() { if model.issues_by_id.get(&issue_id).is_none() {
return Node::Empty; return Node::Empty;
} }
let edit_issue_modal = match model.modals.get(0) { let edit_issue_modal = match &model.modals().edit_issue {
Some(ModalType::EditIssue(_, modal)) => modal, Some(modal) => modal,
_ => return empty![], _ => return Node::Empty,
}; };
let time_tracking_type = model let time_tracking_type = model
.project .project
.as_ref() .as_ref()
.map(|p| p.time_tracking) .map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked); .unwrap_or(TimeTracking::Untracked);
let modal_title = div![C!["modalTitle"], "Time tracking"]; let modal_title = div![C!["modalTitle"], "Time tracking"];

View File

@ -1,3 +1,4 @@
use jirs_data::{CommentId, IssueStatusId};
use { use {
crate::{ crate::{
model::{ModalType, Model, Page}, model::{ModalType, Model, Page},
@ -11,47 +12,37 @@ use {
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::ModalDropped => match model.modals.pop() { Msg::ModalDropped if !model.modal_stack().is_empty() => {
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => { drop_modal(model, orders);
go_to_board(orders); }
}
_ => (),
},
Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) => { Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) => {
for modal in model.modals.iter_mut() { if let Some(edit) = &mut model.modals_mut().edit_issue {
if let ModalType::EditIssue(_, edit) = modal { edit.link_copied = true;
edit.link_copied = true;
}
} }
} }
Msg::ModalOpened(modal_type) => { Msg::ModalOpened(modal_type) => {
model.modals.push(modal_type.as_ref().clone()); push_modal(modal_type, model, orders);
} }
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => { Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => {
match model.page { match model.page {
Page::EditIssue(issue_id) if model.modals.is_empty() => { Page::EditIssue(issue_id) => push_edit_issue_modal(issue_id, model, orders),
push_edit_modal(issue_id, model, orders) Page::AddIssue => push_add_issue_modal(model, orders),
} Page::DeleteEpic(id) => push_delete_epic_modal(id, model, orders),
Page::AddIssue if model.modals.is_empty() => push_add_modal(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::EditIssue(id)) => push_edit_issue_modal(*id, model, orders),
Msg::ChangePage(Page::AddIssue) => push_add_issue_modal(model, orders),
Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders), Msg::ChangePage(Page::DeleteEpic(id)) => push_delete_epic_modal(*id, model, orders),
Msg::ChangePage(Page::DeleteEpic(issue_id)) => { Msg::ChangePage(Page::EditEpic(id)) => push_edit_epic_modal(*id, model, orders),
push_delete_epic_modal(*issue_id, model, orders)
}
Msg::ChangePage(Page::EditEpic(issue_id)) => push_edit_epic_modal(*issue_id, model, orders),
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => { Msg::GlobalKeyDown { key, .. } if key.eq("#") => push_debug_modal(model),
model.modals.push(ModalType::DebugModal);
}
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq(">") => { Msg::GlobalKeyDown { key, .. } if key.eq(">") => {
@ -75,49 +66,223 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
} }
fn push_add_modal(model: &mut Model, _orders: &mut impl Orders<Msg>) { // MODALS
use crate::modals::issues_create::Model;
model.modals.push(ModalType::AddIssue(Box::new(Model { fn push_modal(modal_type: &ModalType, model: &mut Model, orders: &mut impl Orders<Msg>) {
project_id: model.project.as_ref().map(|p| p.id), match modal_type {
..Model::default() 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<Msg>) { fn drop_modal(model: &mut Model, orders: &mut impl Orders<Msg>) {
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<Msg>) {
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<Msg>) {
if model.modals().edit_issue.is_some() {
return;
}
let time_tracking_type = model let time_tracking_type = model
.project .project
.as_ref() .as_ref()
.map(|p| p.time_tracking) .map(|p| p.time_tracking)
.unwrap_or(TimeTracking::Untracked); .unwrap_or(TimeTracking::Untracked);
let modal = { let modal = {
let issue = match model.issues_by_id.get(&issue_id) { let issue = match model.issues_by_id.get(&issue_id) {
Some(issue) => issue, Some(issue) => issue,
_ => return, _ => return,
}; };
ModalType::EditIssue( crate::modals::issues_edit::Model::new(issue, time_tracking_type)
issue_id,
Box::new(crate::modals::issues_edit::Model::new(
issue,
time_tracking_type,
)),
)
}; };
send_ws_msg( send_ws_msg(
WsMsg::IssueCommentsLoad(issue_id), WsMsg::IssueCommentsLoad(issue_id),
model.ws.as_ref(), model.ws.as_ref(),
orders, 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<Msg>) { fn push_delete_issue_modal(id: IssueId, model: &mut Model, _orders: &mut impl Orders<Msg>) {
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<Msg>,
) {
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<Msg>) {
use crate::modals::epics_edit::Model; use crate::modals::epics_edit::Model;
let modal = Model::new(epic_id, model); if model.modals().edit_epic.is_some() {
model.modals.push(ModalType::EditEpic(Box::new(modal))); 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<Msg>) { fn push_delete_epic_modal(id: EpicId, model: &mut Model, _orders: &mut impl Orders<Msg>) {
use crate::modals::epics_delete::Model; use crate::modals::epics_delete::Model;
let modal = Model::new(issue_id, model); if model.modals_mut().delete_epic.is_some() {
model.modals.push(ModalType::DeleteEpic(Box::new(modal))); 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<Msg>) {
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<Msg>) {
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);
} }

View File

@ -1,58 +1,69 @@
use { use {
crate::{ crate::{model::*, Msg},
components::{styled_confirm_modal::StyledConfirmModal, styled_modal::StyledModal},
model::*,
shared::ToNode,
Msg,
},
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
use crate::modals::{issue_statuses_delete, issues_create, issues_edit}; let mut nodes = Vec::with_capacity(model.modal_stack().len());
let modals: Vec<Node<Msg>> = model
.modals for modal_type in model.modal_stack() {
.iter() match modal_type {
.map(|modal| match modal { ModalType::AddIssue(_) => {
// epic if let Some(modal) = &model.modals().add_issue {
ModalType::DeleteEpic(modal) => crate::modals::epics_delete::view(model, modal), let node = crate::modals::issues_create::view(model, modal);
ModalType::EditEpic(modal) => crate::modals::epics_edit::view(model, modal), nodes.push(node);
// 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![]
} }
} }
ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model), ModalType::EditIssue(_) => {
ModalType::AddIssue(modal) => issues_create::view(model, modal), if let Some(modal) = &model.modals().edit_issue {
// comment let node = crate::modals::issues_edit::view(model, modal);
ModalType::DeleteCommentConfirm(comment_id) => { nodes.push(node);
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::TimeTracking(issue_id) => { ModalType::DeleteEpic(_) => {
crate::modals::time_tracking::view(model, *issue_id) 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) => { ModalType::EditEpic(_) => {
issue_statuses_delete::view(model, delete_issue_modal.delete_id) 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)] #[cfg(debug_assertions)]
ModalType::DebugModal => crate::modals::debug::view(model), ModalType::DebugModal(_) => {
}) if let Some(true) = &model.modals().debug_modal {
.collect(); let node = crate::modals::debug::view(model);
section![id!["modals"], modals] nodes.push(node)
}
}
};
}
section![id!["modals"], nodes]
} }

View File

@ -24,21 +24,38 @@ pub trait IssueModal {
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>); fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>);
} }
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Debug, Default)]
pub struct Modals {
// issue
pub add_issue: Option<crate::modals::issues_create::Model>,
pub edit_issue: Option<crate::modals::issues_edit::Model>,
// epic
pub delete_epic: Option<crate::modals::epics_delete::Model>,
pub edit_epic: Option<crate::modals::epics_edit::Model>,
pub delete_issue_confirm: Option<crate::modals::issues_delete::Model>,
pub delete_comment_confirm: Option<crate::modals::comments_delete::Model>,
pub time_tracking: Option<crate::modals::time_tracking::Model>,
pub delete_issue_status_modal: Option<crate::modals::issue_statuses_delete::Model>,
#[cfg(debug_assertions)]
pub debug_modal: Option<bool>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ModalType { pub enum ModalType {
// issue // issue
AddIssue(Box<crate::modals::issues_create::Model>), AddIssue(Option<i32>),
EditIssue(EpicId, Box<crate::modals::issues_edit::Model>), EditIssue(Option<i32>),
DeleteIssueConfirm(Option<i32>),
// epic // epic
DeleteEpic(Box<crate::modals::epics_delete::Model>), DeleteEpic(Option<i32>),
EditEpic(Box<crate::modals::epics_edit::Model>), EditEpic(Option<i32>),
DeleteIssueConfirm(EpicId), DeleteCommentConfirm(Option<i32>),
DeleteCommentConfirm(CommentId), TimeTracking(Option<i32>),
TimeTracking(EpicId), DeleteIssueStatusModal(Option<i32>),
DeleteIssueStatusModal(Box<crate::modals::issue_statuses_delete::Model>),
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
DebugModal, DebugModal(Option<i32>),
} }
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
@ -168,7 +185,8 @@ pub struct Model {
pub comment_form: Option<CreateCommentForm>, pub comment_form: Option<CreateCommentForm>,
// modals // modals
pub modals: Vec<ModalType>, modals_stack: Vec<ModalType>,
pub modals: Modals,
// pages // pages
pub page: Page, pub page: Page,
@ -226,7 +244,6 @@ impl Model {
host_url, host_url,
ws_url, ws_url,
page_content: PageContent::Project(Box::new(ProjectPage::default())), page_content: PageContent::Project(Box::new(ProjectPage::default())),
modals: vec![],
project: None, project: None,
current_user_project: None, current_user_project: None,
about_tooltip_visible: false, about_tooltip_visible: false,
@ -246,6 +263,8 @@ impl Model {
issues_by_id: Default::default(), issues_by_id: Default::default(),
show_extras: false, show_extras: false,
epics_by_id: Default::default(), epics_by_id: Default::default(),
modals_stack: vec![],
modals: Default::default(),
} }
} }
@ -274,6 +293,16 @@ impl Model {
&self.user &self.user
} }
#[inline(always)]
pub fn user_id(&self) -> Option<UserId> {
self.user.as_ref().map(|u| u.id)
}
#[inline(always)]
pub fn project_id(&self) -> Option<ProjectId> {
self.project.as_ref().map(|p| p.id)
}
pub fn current_user_role(&self) -> UserRole { pub fn current_user_role(&self) -> UserRole {
self.current_user_project self.current_user_project
.as_ref() .as_ref()
@ -293,4 +322,20 @@ impl Model {
}) })
.collect() .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<ModalType> {
&mut self.modals_stack
}
} }

View File

@ -24,66 +24,6 @@ pub struct ProjectPage {
} }
impl ProjectPage { impl ProjectPage {
pub fn rebuild_visible(
&mut self,
epics: &[Epic],
statuses: &[IssueStatus],
issues: &[Issue],
user: &Option<User>,
) {
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( pub fn visible_issues(
page: &ProjectPage, page: &ProjectPage,
epics: &[Epic], epics: &[Epic],

View File

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
components::styled_select::StyledSelectChanged, components::styled_select::StyledSelectChanged,
model::{ModalType, Model, Page, PageContent}, model::{Model, Page, PageContent},
pages::project_page::model::ProjectPage, pages::project_page::model::ProjectPage,
ws::{board_load, send_ws_msg}, ws::{board_load, send_ws_msg},
BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, 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)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
StyledSelectChanged::Text(text), StyledSelectChanged::Text(text),
) => { ) => {
let modal = model if let Some(m) = &mut model.modals_mut().edit_issue {
.modals
.iter_mut()
.filter_map(|modal| match modal {
ModalType::EditIssue(_, modal) => Some(modal),
_ => None,
})
.last();
if let Some(m) = modal {
m.top_type_state.text_filter = text; m.top_type_state.text_filter = text;
} }
} }

View File

@ -11,10 +11,9 @@ use {
styled_select::StyledSelect, styled_select::StyledSelect,
styled_textarea::StyledTextarea, styled_textarea::StyledTextarea,
}, },
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
model::{self, ModalType, Model, PageContent}, model::{self, ModalType, Model, PageContent},
pages::project_settings_page::ProjectSettingsPage, pages::project_settings_page::ProjectSettingsPage,
shared::{inner_layout, IntoChild, ToChild, ToNode}, shared::{inner_layout, IntoChild, ToNode},
FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange,
}, },
jirs_data::{IssueStatus, ProjectCategory, TimeTracking}, jirs_data::{IssueStatus, ProjectCategory, TimeTracking},
@ -51,11 +50,11 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let category_field = category_field(page); let category_field = category_field(page);
let time_tracking = StyledCheckbox::build() let time_tracking = StyledCheckbox::build()
.options(vec![ .options(
TimeTracking::Untracked.to_child(), TimeTracking::default()
TimeTracking::Fibonacci.to_child(), .into_iter()
TimeTracking::Hourly.to_child(), .map(|tt| tt.into_child()),
]) )
.state(&page.time_tracking) .state(&page.time_tracking)
.add_class("timeTracking") .add_class("timeTracking")
.build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking))
@ -336,9 +335,7 @@ fn show_column_preview(
let on_delete = mouse_ev(Ev::Click, move |ev| { let on_delete = mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalOpened(Box::new(ModalType::DeleteIssueStatusModal(Box::new( Msg::ModalOpened(ModalType::DeleteIssueStatusModal(Some(id)))
DeleteIssueStatusModal::new(id),
))))
}); });
let delete = StyledButton::build() let delete = StyledButton::build()
.primary() .primary()

View File

@ -21,7 +21,7 @@ pub fn tracking_link(model: &Model, modal: &crate::modals::issues_edit::Model) -
let issue_id = *id; let issue_id = *id;
let handler = mouse_ev(Ev::Click, move |_| { 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),] div![C!["trackingLink"], handler, tracking_widget(model, modal),]

View File

@ -276,8 +276,8 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
// comments // comments
WsMsg::IssueCommentsLoaded(mut comments) => { WsMsg::IssueCommentsLoaded(mut comments) => {
let issue_id = match model.modals.get(0) { let issue_id = match &model.modals().edit_issue {
Some(ModalType::EditIssue(issue_id, _)) => *issue_id, Some(modal) => modal.id,
_ => return, _ => return,
}; };
if comments.iter().any(|c| c.issue_id != issue_id) { if comments.iter().any(|c| c.issue_id != issue_id) {

View File

@ -77,6 +77,17 @@ insert into invitations (email, name, state, project_id, invited_by_id) values (
2 2
); );
insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); 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( insert into issues(
title, title,
issue_type, issue_type,
@ -86,7 +97,8 @@ insert into issues(
description_text, description_text,
reporter_id, reporter_id,
project_id, project_id,
issue_status_id issue_status_id,
epic_id
) values ( ) values (
'Foo', 'Foo',
'task', 'task',
@ -96,7 +108,8 @@ insert into issues(
'foz baz', 'foz baz',
1, 1,
1, 1,
1 1,
NULL
), ( ), (
'Foo2', 'Foo2',
'bug', 'bug',
@ -106,7 +119,8 @@ insert into issues(
'foz baz 2', 'foz baz 2',
1, 1,
1, 1,
2 2,
NULL
), ( ), (
'Foo3', 'Foo3',
'story', 'story',
@ -116,7 +130,30 @@ insert into issues(
'foz baz 3', 'foz baz 3',
2, 2,
1, 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 ( insert into comments (user_id, issue_id, body) values (
1, 1, 'Vestibulum non neque at dui maximus porttitor fermentum consectetur eros.' 1, 1, 'Vestibulum non neque at dui maximus porttitor fermentum consectetur eros.'