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 = [
"bincode",
"chrono",
"derive_enum_iter",
"derive_enum_primitive",
"futures 0.1.30",
"jirs-data",
"js-sys",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,72 @@
#[macro_export]
macro_rules! query_db_or_print {
($s:expr,$msg:expr) => {
match block_on($s.db.send($msg)) {
macro_rules! db_or_debug_and_return {
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
$crate::actor_or_debug_and_return!($s, db, $msg, $actor_err, $mailbox_err)
};
($s: ident, $msg: expr) => {
$crate::actor_or_debug_and_return!($s, db, $msg)
};
}
#[macro_export]
macro_rules! mail_or_debug_and_return {
($s: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
$crate::actor_or_debug_and_return!($s, mail, $msg, $actor_err, $mailbox_err)
};
($s: ident, $msg: expr) => {
$crate::actor_or_debug_and_return!($s, mail, $msg)
};
}
#[macro_export]
macro_rules! actor_or_debug_and_return {
($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
match block_on($s.$actor.send($msg)) {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{:?}", e);
return Ok(None);
return $actor_err;
}
Err(e) => {
log::error!("{}", e);
return Ok(None);
log::error!("{:?}", e);
return $mailbox_err;
}
}
};
($s: ident, $actor: ident, $msg: expr) => {
crate::actor_or_debug_and_return!($s, $actor, $msg, Ok(None), Ok(None))
};
}
#[macro_export]
macro_rules! actor_or_debug_and_ignore {
($s: ident, $actor: ident, $msg: expr, $on_success: expr) => {
match block_on($s.$actor.send($msg)) {
Ok(Ok(r)) => {
$on_success(r);
}
Ok(Err(e)) => {
log::error!("{:?}", e);
}
Err(e) => {
log::error!("{:?}", e);
}
}
};
}
#[macro_export]
macro_rules! actor_or_debug_and_fallback {
($s: ident, $actor: ident, $msg: expr, $actor_err: expr, $mailbox_err: expr) => {
match block_on($s.$actor.send($msg)) {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
log::error!("{:?}", e);
$actor_err
}
Err(e) => {
log::error!("{:?}", e);
$mailbox_err
}
}
};

View File

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

View File

@ -10,7 +10,6 @@
white-space: nowrap;
cursor: pointer;
font-size: 14.5px;
padding: 0 12px;
&:focus {
border-color: var(--borderInputFocus);
@ -19,6 +18,16 @@
> input[type=radio] {
display: none;
}
> label {
display: block;
padding: 0 12px;
cursor: pointer;
font-family: var(--font-medium);
line-height: 2;
white-space: nowrap;
font-size: 14.5px;
}
}
> .styledCheckboxChild.selected {

View File

@ -109,6 +109,30 @@
padding-left: 15px;
}
&.editEpic {
padding: 35px 40px 40px;
.header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.transform {
display: flex;
justify-content: space-between;
margin-top: 15px;
&.unavailable {
color: var(--textLight);
}
&.available {
color: var(--textDark);
}
}
}
&.deleteEpic {
> section {
> .header {

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,18 @@
use crate::FieldId;
use jirs_data::EpicFieldId;
use {
crate::{
components::{styled_input::*, styled_select::StyledSelectState},
model,
components::{styled_checkbox::StyledCheckboxState, styled_input::*},
model, FieldId, Msg,
},
jirs_data::{EpicId, IssueId},
jirs_data::*,
seed::prelude::Orders,
};
#[derive(Clone, Debug, PartialOrd, PartialEq)]
#[derive(Debug)]
pub struct Model {
pub epic_id: EpicId,
pub related_issues: Vec<IssueId>,
pub name: StyledInputState,
pub transform_into: StyledSelectState,
pub transform_into: StyledCheckboxState,
}
impl Model {
@ -23,6 +22,7 @@ impl Model {
.get(&epic_id)
.map(|epic| epic.name.as_str())
.unwrap_or_default();
let related_issues = model
.issues()
.iter()
@ -38,10 +38,15 @@ impl Model {
epic_id,
related_issues,
name: StyledInputState::new(FieldId::EditEpic(EpicFieldId::Name), name),
transform_into: StyledSelectState::new(
FieldId::EditEpic(EpicFieldId::StartsAt),
vec![],
transform_into: StyledCheckboxState::new(
FieldId::EditEpic(EpicFieldId::TransformInto),
0,
),
}
}
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<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>) {
let _modal = crate::match_modal_mut!(model, DeleteEpic);
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
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 {
crate::{
components::{styled_input::*, styled_modal::*},
components::{
styled_button::*, styled_checkbox::*, styled_icon::Icon, styled_input::*,
styled_modal::*,
},
modals::epics_edit::Model,
model,
shared::ToNode,
shared::{IntoChild, ToNode},
FieldId, Msg,
},
jirs_data::EpicFieldId,
jirs_data::{EpicFieldId, IssueType},
seed::{prelude::*, *},
};
pub struct IssueTypeWrapper(IssueType);
impl<'l> IntoChild<'l> for IssueTypeWrapper {
type Builder = ChildBuilder<'l>;
fn into_child(self) -> Self::Builder {
Self::Builder::default()
.label(self.0.to_label())
.name(self.0.to_str())
.value(self.0.into())
.add_class(self.0.to_str())
}
}
pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
let transform = if modal.related_issues.is_empty() {
Node::Empty
transform_into_available(modal)
} else {
div![]
transform_into_unavailable(modal)
};
let close = StyledButton::build()
.on_click(mouse_ev("click", |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::ModalDropped
}))
.empty()
.icon(Icon::Close)
.build()
.into_node();
StyledModal::build()
.center()
.child(h1!["Edit epic"])
.width(600)
.add_class("editEpic")
.child(div![C!["header"], h1!["Edit epic"], close])
.child(
StyledInput::build()
.state(&modal.name)
.build(FieldId::EditEpic(EpicFieldId::Name))
.into_node(),
)
@ -28,3 +58,32 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
.build()
.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 {
crate::{
modals::issue_statuses_delete::Model as DeleteIssueStatusModal,
model::{ModalType, Model},
Msg, OperationKind, ResourceKind,
},
crate::{model::Model, Msg, OperationKind, ResourceKind},
jirs_data::WsMsg,
seed::prelude::*,
};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let _modal: &mut Box<DeleteIssueStatusModal> =
match model.modals.iter_mut().find_map(|modal| match modal {
ModalType::DeleteIssueStatusModal(modal) => Some(modal),
_ => None,
}) {
Some(m) => m,
_ => return,
};
let _modal = match &mut model.modals_mut().delete_issue_status_modal {
Some(m) => m,
_ => return,
};
match msg {
Msg::DeleteIssueStatus(issue_status_id) => {

View File

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

View File

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

View File

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

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 {
crate::{
components::styled_confirm_modal::StyledConfirmModal, model, model::ModalType,
shared::ToNode, Msg,
},
seed::{prelude::*, *},
crate::{components::styled_confirm_modal::StyledConfirmModal, model, shared::ToNode, Msg},
seed::prelude::*,
};
pub fn view(model: &model::Model) -> Node<Msg> {
let opt_id = model
.modals
.iter()
.filter_map(|modal| match modal {
ModalType::EditIssue(issue_id, _) => Some(issue_id),
_ => None,
})
.find(|id| id.eq(id));
let issue_id = match opt_id {
Some(id) => *id,
_ => return empty![],
let issue_id = match &model.modals().delete_issue_confirm {
Some(modal) => modal.issue_id,
_ => return Node::Empty,
};
let handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id));

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
pub use {epic_field::*, update::*, view::*};
pub mod comments_delete;
#[cfg(debug_assertions)]
pub mod debug;
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_select::{StyledSelect, StyledSelectState},
},
model::{ModalType, Model},
model::Model,
shared::{
tracking_widget::{fibonacci_values, tracking_widget},
ToChild, ToNode,
},
EditIssueModalSection, FieldId, Msg,
},
jirs_data::{EpicId, IssueFieldId, TimeTracking},
jirs_data::{IssueFieldId, IssueId, TimeTracking},
seed::{prelude::*, *},
};
@ -27,20 +27,22 @@ pub fn value_for_time_tracking(v: &Option<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() {
return Node::Empty;
}
let edit_issue_modal = match model.modals.get(0) {
Some(ModalType::EditIssue(_, modal)) => modal,
_ => return empty![],
let edit_issue_modal = match &model.modals().edit_issue {
Some(modal) => modal,
_ => return Node::Empty,
};
let time_tracking_type = model
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
.unwrap_or(TimeTracking::Untracked);
let modal_title = div![C!["modalTitle"], "Time tracking"];

View File

@ -1,3 +1,4 @@
use jirs_data::{CommentId, IssueStatusId};
use {
crate::{
model::{ModalType, Model, Page},
@ -11,47 +12,37 @@ use {
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
go_to_board(orders);
}
_ => (),
},
Msg::ModalDropped if !model.modal_stack().is_empty() => {
drop_modal(model, orders);
}
Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) => {
for modal in model.modals.iter_mut() {
if let ModalType::EditIssue(_, edit) = modal {
edit.link_copied = true;
}
if let Some(edit) = &mut model.modals_mut().edit_issue {
edit.link_copied = true;
}
}
Msg::ModalOpened(modal_type) => {
model.modals.push(modal_type.as_ref().clone());
push_modal(modal_type, model, orders);
}
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => {
match model.page {
Page::EditIssue(issue_id) if model.modals.is_empty() => {
push_edit_modal(issue_id, model, orders)
}
Page::AddIssue if model.modals.is_empty() => push_add_modal(model, orders),
Page::EditIssue(issue_id) => push_edit_issue_modal(issue_id, model, orders),
Page::AddIssue => push_add_issue_modal(model, orders),
Page::DeleteEpic(id) => push_delete_epic_modal(id, model, orders),
Page::EditEpic(id) => push_edit_epic_modal(id, model, orders),
_ => (),
}
}
Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders),
Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders),
Msg::ChangePage(Page::DeleteEpic(issue_id)) => {
push_delete_epic_modal(*issue_id, model, orders)
}
Msg::ChangePage(Page::EditEpic(issue_id)) => push_edit_epic_modal(*issue_id, model, orders),
Msg::ChangePage(Page::EditIssue(id)) => push_edit_issue_modal(*id, model, orders),
Msg::ChangePage(Page::AddIssue) => push_add_issue_modal(model, orders),
Msg::ChangePage(Page::DeleteEpic(id)) => push_delete_epic_modal(*id, model, orders),
Msg::ChangePage(Page::EditEpic(id)) => push_edit_epic_modal(*id, model, orders),
#[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
model.modals.push(ModalType::DebugModal);
}
Msg::GlobalKeyDown { key, .. } if key.eq("#") => push_debug_modal(model),
#[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq(">") => {
@ -75,49 +66,223 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
fn push_add_modal(model: &mut Model, _orders: &mut impl Orders<Msg>) {
use crate::modals::issues_create::Model;
model.modals.push(ModalType::AddIssue(Box::new(Model {
project_id: model.project.as_ref().map(|p| p.id),
..Model::default()
})));
// MODALS
fn push_modal(modal_type: &ModalType, model: &mut Model, orders: &mut impl Orders<Msg>) {
match modal_type {
ModalType::AddIssue(_) => push_add_issue_modal(model, orders),
ModalType::EditIssue(id) => {
if let Some(id) = id {
push_edit_issue_modal(*id, model, orders);
}
}
ModalType::DeleteIssueConfirm(id) => {
if let Some(id) = id {
push_delete_issue_modal(*id, model, orders);
}
}
ModalType::DeleteEpic(id) => {
if let Some(id) = id {
push_delete_epic_modal(*id, model, orders);
}
}
ModalType::EditEpic(id) => {
if let Some(id) = id {
push_edit_epic_modal(*id, model, orders);
}
}
ModalType::DeleteCommentConfirm(id) => {
if let Some(id) = id {
push_delete_comment_modal(*id, model, orders);
}
}
ModalType::TimeTracking(id) => {
if let Some(id) = id {
push_time_track_modal(*id, model, orders);
}
}
ModalType::DeleteIssueStatusModal(id) => {
if let Some(id) = id {
push_delete_issue_status_modal(*id, model, orders);
}
}
#[cfg(debug_assertions)]
ModalType::DebugModal(_) => push_debug_modal(model),
}
}
fn push_edit_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl Orders<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
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or(TimeTracking::Untracked);
let modal = {
let issue = match model.issues_by_id.get(&issue_id) {
Some(issue) => issue,
_ => return,
};
ModalType::EditIssue(
issue_id,
Box::new(crate::modals::issues_edit::Model::new(
issue,
time_tracking_type,
)),
)
crate::modals::issues_edit::Model::new(issue, time_tracking_type)
};
send_ws_msg(
WsMsg::IssueCommentsLoad(issue_id),
model.ws.as_ref(),
orders,
);
model.modals.push(modal);
model
.modal_stack_mut()
.push(ModalType::EditIssue(Some(issue_id)));
model.modals_mut().edit_issue = Some(modal);
}
fn push_edit_epic_modal(epic_id: EpicId, model: &mut Model, _orders: &mut impl Orders<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;
let modal = Model::new(epic_id, model);
model.modals.push(ModalType::EditEpic(Box::new(modal)));
if model.modals().edit_epic.is_some() {
return;
}
let modal = Model::new(id, model);
model.modal_stack_mut().push(ModalType::EditEpic(Some(id)));
model.modals_mut().edit_epic = Some(modal);
}
fn push_delete_epic_modal(issue_id: IssueId, model: &mut Model, _orders: &mut impl Orders<Msg>) {
fn push_delete_epic_modal(id: EpicId, model: &mut Model, _orders: &mut impl Orders<Msg>) {
use crate::modals::epics_delete::Model;
let modal = Model::new(issue_id, model);
model.modals.push(ModalType::DeleteEpic(Box::new(modal)));
if model.modals_mut().delete_epic.is_some() {
return;
}
model
.modal_stack_mut()
.push(ModalType::DeleteEpic(Some(id)));
model.modals_mut().delete_epic = Some(Model::new(id, model));
}
// COMMENTS
fn push_delete_comment_modal(id: CommentId, model: &mut Model, _orders: &mut impl Orders<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 {
crate::{
components::{styled_confirm_modal::StyledConfirmModal, styled_modal::StyledModal},
model::*,
shared::ToNode,
Msg,
},
crate::{model::*, Msg},
seed::{prelude::*, *},
};
pub fn view(model: &Model) -> Node<Msg> {
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
let modals: Vec<Node<Msg>> = model
.modals
.iter()
.map(|modal| match modal {
// epic
ModalType::DeleteEpic(modal) => crate::modals::epics_delete::view(model, modal),
ModalType::EditEpic(modal) => crate::modals::epics_edit::view(model, modal),
// issue
ModalType::EditIssue(issue_id, modal) => {
if let Some(_issue) = model.issues_by_id.get(issue_id) {
let details = issues_edit::view(model, modal.as_ref());
StyledModal::build()
.variant(crate::components::styled_modal::Variant::Center)
.width(1040)
.child(details)
.build()
.into_node()
} else {
empty![]
let mut nodes = Vec::with_capacity(model.modal_stack().len());
for modal_type in model.modal_stack() {
match modal_type {
ModalType::AddIssue(_) => {
if let Some(modal) = &model.modals().add_issue {
let node = crate::modals::issues_create::view(model, modal);
nodes.push(node);
}
}
ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model),
ModalType::AddIssue(modal) => issues_create::view(model, modal),
// comment
ModalType::DeleteCommentConfirm(comment_id) => {
let comment_id = *comment_id;
StyledConfirmModal::build()
.title("Are you sure you want to delete this comment?")
.message("Once you delete, it's gone for good.")
.confirm_text("Delete comment")
.on_confirm(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id)))
.build()
.into_node()
ModalType::EditIssue(_) => {
if let Some(modal) = &model.modals().edit_issue {
let node = crate::modals::issues_edit::view(model, modal);
nodes.push(node);
}
}
ModalType::TimeTracking(issue_id) => {
crate::modals::time_tracking::view(model, *issue_id)
ModalType::DeleteEpic(_) => {
if let Some(modal) = &model.modals().delete_epic {
let node = crate::modals::epics_delete::view(model, modal);
nodes.push(node);
}
}
ModalType::DeleteIssueStatusModal(delete_issue_modal) => {
issue_statuses_delete::view(model, delete_issue_modal.delete_id)
ModalType::EditEpic(_) => {
if let Some(modal) = &model.modals().edit_epic {
let node = crate::modals::epics_edit::view(model, modal);
nodes.push(node);
}
}
ModalType::DeleteIssueConfirm(_) => {
if let Some(_issue_id) = &model.modals().delete_issue_confirm {
let node = crate::modals::issues_delete::view(model);
nodes.push(node);
}
}
ModalType::DeleteCommentConfirm(_) => {
if let Some(modal) = &model.modals().delete_comment_confirm {
let node = crate::modals::comments_delete::view(model, modal);
nodes.push(node);
}
}
ModalType::TimeTracking(_) => {
if let Some(modal) = &model.modals().time_tracking {
let node = crate::modals::time_tracking::view(model, modal);
nodes.push(node);
}
}
ModalType::DeleteIssueStatusModal(_) => {
if let Some(modal) = &model.modals().delete_issue_status_modal {
let node = crate::modals::issue_statuses_delete::view(model, modal.delete_id);
nodes.push(node);
}
}
#[cfg(debug_assertions)]
ModalType::DebugModal => crate::modals::debug::view(model),
})
.collect();
section![id!["modals"], modals]
ModalType::DebugModal(_) => {
if let Some(true) = &model.modals().debug_modal {
let node = crate::modals::debug::view(model);
nodes.push(node)
}
}
};
}
section![id!["modals"], nodes]
}

View File

@ -24,21 +24,38 @@ pub trait IssueModal {
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 {
// issue
AddIssue(Box<crate::modals::issues_create::Model>),
EditIssue(EpicId, Box<crate::modals::issues_edit::Model>),
AddIssue(Option<i32>),
EditIssue(Option<i32>),
DeleteIssueConfirm(Option<i32>),
// epic
DeleteEpic(Box<crate::modals::epics_delete::Model>),
EditEpic(Box<crate::modals::epics_edit::Model>),
DeleteEpic(Option<i32>),
EditEpic(Option<i32>),
DeleteIssueConfirm(EpicId),
DeleteCommentConfirm(CommentId),
TimeTracking(EpicId),
DeleteIssueStatusModal(Box<crate::modals::issue_statuses_delete::Model>),
DeleteCommentConfirm(Option<i32>),
TimeTracking(Option<i32>),
DeleteIssueStatusModal(Option<i32>),
#[cfg(debug_assertions)]
DebugModal,
DebugModal(Option<i32>),
}
#[derive(Clone, Debug, PartialOrd, PartialEq)]
@ -168,7 +185,8 @@ pub struct Model {
pub comment_form: Option<CreateCommentForm>,
// modals
pub modals: Vec<ModalType>,
modals_stack: Vec<ModalType>,
pub modals: Modals,
// pages
pub page: Page,
@ -226,7 +244,6 @@ impl Model {
host_url,
ws_url,
page_content: PageContent::Project(Box::new(ProjectPage::default())),
modals: vec![],
project: None,
current_user_project: None,
about_tooltip_visible: false,
@ -246,6 +263,8 @@ impl Model {
issues_by_id: Default::default(),
show_extras: false,
epics_by_id: Default::default(),
modals_stack: vec![],
modals: Default::default(),
}
}
@ -274,6 +293,16 @@ impl Model {
&self.user
}
#[inline(always)]
pub fn user_id(&self) -> Option<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 {
self.current_user_project
.as_ref()
@ -293,4 +322,20 @@ impl Model {
})
.collect()
}
pub fn modals(&self) -> &Modals {
&self.modals
}
pub fn modals_mut(&mut self) -> &mut Modals {
&mut self.modals
}
pub fn modal_stack(&self) -> &[ModalType] {
&self.modals_stack
}
pub fn modal_stack_mut(&mut self) -> &mut Vec<ModalType> {
&mut self.modals_stack
}
}

View File

@ -24,66 +24,6 @@ pub struct 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(
page: &ProjectPage,
epics: &[Epic],

View File

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

View File

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

View File

@ -21,7 +21,7 @@ pub fn tracking_link(model: &Model, modal: &crate::modals::issues_edit::Model) -
let issue_id = *id;
let handler = mouse_ev(Ev::Click, move |_| {
Msg::ModalOpened(Box::new(ModalType::TimeTracking(issue_id)))
Msg::ModalOpened(ModalType::TimeTracking(Some(issue_id)))
});
div![C!["trackingLink"], handler, tracking_widget(model, modal),]

View File

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

View File

@ -77,6 +77,17 @@ insert into invitations (email, name, state, project_id, invited_by_id) values (
2
);
insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() );
insert into epics (name, project_id, user_id) VALUES (
'Foo', 1, 1
), (
'Bar', 1, 2
), (
'Foz', 2 ,1
), (
'Baz', 1, 2
), (
'Hello World', 2, 2
);
insert into issues(
title,
issue_type,
@ -86,7 +97,8 @@ insert into issues(
description_text,
reporter_id,
project_id,
issue_status_id
issue_status_id,
epic_id
) values (
'Foo',
'task',
@ -96,7 +108,8 @@ insert into issues(
'foz baz',
1,
1,
1
1,
NULL
), (
'Foo2',
'bug',
@ -106,7 +119,8 @@ insert into issues(
'foz baz 2',
1,
1,
2
2,
NULL
), (
'Foo3',
'story',
@ -116,7 +130,30 @@ insert into issues(
'foz baz 3',
2,
1,
3
3,
NULL
), (
'Story 1 in Epic 1',
'story',
'low',
3,
'hello world 3',
'foz baz 3',
2,
1,
3,
1
), (
'Story 2 in Epic 1',
'story',
'low',
3,
'hello world 3',
'foz baz 3',
2,
1,
3,
1
);
insert into comments (user_id, issue_id, body) values (
1, 1, 'Vestibulum non neque at dui maximus porttitor fermentum consectetur eros.'