Load issue comments and struct cleanup

This commit is contained in:
Adrian Wozniak 2020-04-11 11:18:41 +02:00
parent d72328764d
commit 1663d9a485
15 changed files with 353 additions and 578 deletions

View File

@ -1,7 +1,8 @@
use seed::{prelude::*, *};
use jirs_data::UpdateIssuePayload;
use jirs_data::{UpdateIssuePayload, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_editor::Mode;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
@ -44,6 +45,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
Msg::ChangePage(Page::EditIssue(issue_id)) => {
push_edit_modal(issue_id, model);
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id.clone()));
}
Msg::ChangePage(Page::AddIssue) => {

View File

@ -152,6 +152,7 @@ pub struct Model {
pub user: Option<User>,
pub issues: Vec<Issue>,
pub users: Vec<User>,
pub comments: Vec<Comment>,
}
impl Default for Model {
@ -180,6 +181,7 @@ impl Default for Model {
},
modals: vec![],
project: None,
comments: vec![],
}
}
}

View File

@ -20,20 +20,28 @@ pub fn handle(msg: WsMsg) {
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
model.project = Some(project.clone());
}
// auth
Msg::WsMsg(WsMsg::AuthorizeLoaded(Ok(user))) => {
model.user = Some(user.clone());
}
// project
Msg::WsMsg(WsMsg::ProjectLoaded(project)) => {
model.project = Some(project.clone());
}
// issues
Msg::WsMsg(WsMsg::ProjectIssuesLoaded(v)) => {
let mut v = v.clone();
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
model.issues = v;
}
// users
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
model.users = v.clone();
}
// comments
Msg::WsMsg(WsMsg::IssueCommentsLoaded(comments)) => {
model.comments = comments.clone();
}
_ => (),
};
orders.render();

View File

@ -17,6 +17,12 @@ pub trait ToVec {
fn ordered() -> Vec<Self::Item>;
}
pub type IssueId = i32;
pub type ProjectId = i32;
pub type UserId = i32;
pub type CommentId = i32;
pub type TokenId = i32;
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
@ -260,68 +266,9 @@ pub struct ErrorResponse {
pub errors: Vec<String>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct FullProject {
pub id: i32,
pub name: String,
pub url: String,
pub description: String,
pub category: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub issues: Vec<Issue>,
pub users: Vec<User>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct FullIssue {
pub id: i32,
pub title: String,
pub issue_type: IssueType,
pub status: IssueStatus,
pub priority: IssuePriority,
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: i32,
pub project_id: i32,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_ids: Vec<i32>,
pub comments: Vec<Comment>,
}
impl Into<Issue> for FullIssue {
fn into(self) -> Issue {
Issue {
id: self.id,
title: self.title,
issue_type: self.issue_type,
status: self.status,
priority: self.priority,
list_position: self.list_position,
description: self.description,
description_text: self.description_text,
estimate: self.estimate,
time_spent: self.time_spent,
time_remaining: self.time_remaining,
reporter_id: self.reporter_id,
project_id: self.project_id,
created_at: self.created_at,
updated_at: self.updated_at,
user_ids: self.user_ids,
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Project {
pub id: i32,
pub id: ProjectId,
pub name: String,
pub url: String,
pub description: String,
@ -332,7 +279,7 @@ pub struct Project {
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Issue {
pub id: i32,
pub id: IssueId,
pub title: String,
pub issue_type: IssueType,
pub status: IssueStatus,
@ -343,8 +290,8 @@ pub struct Issue {
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: i32,
pub project_id: i32,
pub reporter_id: UserId,
pub project_id: ProjectId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
@ -353,31 +300,29 @@ pub struct Issue {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Comment {
pub id: i32,
pub id: CommentId,
pub body: String,
pub user_id: i32,
pub issue_id: i32,
pub user_id: UserId,
pub issue_id: IssueId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user: Option<User>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct User {
pub id: i32,
pub id: UserId,
pub name: String,
pub email: String,
pub avatar_url: Option<String>,
pub project_id: i32,
pub project_id: ProjectId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Token {
pub id: i32,
pub user_id: i32,
pub id: TokenId,
pub user_id: UserId,
pub access_token: Uuid,
pub refresh_token: Uuid,
pub created_at: NaiveDateTime,
@ -396,9 +341,9 @@ pub struct UpdateIssuePayload {
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: i32,
pub reporter_id: i32,
pub user_ids: Vec<i32>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub user_ids: Vec<UserId>,
}
impl From<Issue> for UpdateIssuePayload {
@ -423,8 +368,8 @@ impl From<Issue> for UpdateIssuePayload {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateCommentPayload {
pub user_id: Option<i32>,
pub issue_id: i32,
pub user_id: Option<UserId>,
pub issue_id: IssueId,
pub body: String,
}
@ -444,9 +389,9 @@ pub struct CreateIssuePayload {
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: i32,
pub user_ids: Vec<i32>,
pub reporter_id: i32,
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
pub reporter_id: UserId,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
@ -476,10 +421,14 @@ pub enum WsMsg {
ProjectUsersLoaded(Vec<User>),
// issue
IssueUpdateRequest(i32, UpdateIssuePayload),
IssueUpdateRequest(IssueId, UpdateIssuePayload),
IssueUpdated(Issue),
IssueDeleteRequest(i32),
IssueDeleted(i32),
IssueDeleteRequest(IssueId),
IssueDeleted(IssueId),
IssueCreateRequest(CreateIssuePayload),
IssueCreated(Issue),
// comments
IssueCommentsRequest(IssueId),
IssueCommentsLoaded(Vec<Comment>),
}

View File

@ -48,4 +48,55 @@ insert into issues(
2,
1
);
insert into comments (user_id, issue_id, body) values (
1, 1, 'Vestibulum non neque at dui maximus porttitor fermentum consectetur eros.'
),
(
1, 2, 'Fusce varius ligula ut nisl porttitor, in gravida dolor rhoncus.'
),
(
1, 3, 'Cras viverra urna at urna convallis maximus.'
),
(
2, 1, 'Phasellus sollicitudin nisi eget arcu sollicitudin aliquam.'
),
(
2, 2, 'Duis sodales felis in maximus tincidunt.'
),
(
2, 3, 'Aenean sit amet sem sit amet dolor pellentesque rutrum.'
),
(
3, 1, 'Phasellus placerat dui vitae odio mattis convallis.'
),
(
3, 2, 'Suspendisse quis est eu neque vehicula sagittis.'
),
(
3, 3, 'Duis rutrum quam eget maximus laoreet.'
),
(
1, 1, 'Vestibulum eu ipsum a dui fringilla tristique.'
),
(
1, 2, 'Phasellus porttitor dolor vitae urna aliquam porta.'
),
(
2, 1, 'Curabitur volutpat mauris pretium urna laoreet, eget scelerisque neque fringilla.'
),
(
1, 3, 'Curabitur ac arcu eu eros auctor elementum.'
),
(
3, 1, 'Duis facilisis ipsum nec mi porta ultricies.'
),
(
1, 1, 'In elementum orci nec mi porta imperdiet ut ac ante.'
),
(
2, 3, 'Praesent et orci ut metus interdum sollicitudin.'
);
select * from tokens;

View File

@ -34,14 +34,6 @@ async fn main() -> Result<(), String> {
.data(db_addr.clone())
.data(crate::db::build_pool())
.service(crate::ws::index)
.service(
web::scope("/issues")
.service(crate::routes::issues::project_issues)
.service(crate::routes::issues::issue_with_users_and_comments)
.service(crate::routes::issues::create)
.service(crate::routes::issues::update)
.service(crate::routes::issues::delete),
)
.service(
web::scope("/comments")
.service(crate::routes::comments::create)
@ -49,11 +41,6 @@ async fn main() -> Result<(), String> {
.service(crate::routes::comments::delete),
)
.service(web::scope("/currentUser").service(crate::routes::users::current_user))
.service(
web::scope("/project")
.service(crate::routes::projects::project_with_users_and_issues)
.service(crate::routes::projects::update),
)
})
.bind(addr)
.map_err(|e| format!("{}", e))?

View File

@ -26,8 +26,6 @@ impl Into<jirs_data::Comment> for Comment {
issue_id: self.issue_id,
created_at: self.created_at,
updated_at: self.updated_at,
user: None,
}
}
}
@ -86,31 +84,6 @@ impl Into<jirs_data::Issue> for Issue {
}
}
impl Into<jirs_data::FullIssue> for Issue {
fn into(self) -> jirs_data::FullIssue {
jirs_data::FullIssue {
id: self.id,
title: self.title,
issue_type: self.issue_type,
status: self.status,
priority: self.priority,
list_position: self.list_position,
description: self.description,
description_text: self.description_text,
estimate: self.estimate,
time_spent: self.time_spent,
time_remaining: self.time_remaining,
reporter_id: self.reporter_id,
project_id: self.project_id,
created_at: self.created_at,
updated_at: self.updated_at,
user_ids: vec![],
comments: vec![],
}
}
}
#[derive(Debug, Serialize, Deserialize, Insertable)]
#[serde(rename_all = "camelCase")]
#[table_name = "issues"]

View File

@ -1,190 +0,0 @@
use std::collections::HashMap;
use actix::Addr;
use actix_web::web::{Data, Json, Path};
use actix_web::{delete, get, post, put, HttpRequest, HttpResponse};
use crate::db::authorize_user::AuthorizeUser;
use crate::db::comments::LoadIssueComments;
use crate::db::issues::{CreateIssue, DeleteIssue, LoadIssue, UpdateIssue};
use crate::db::users::{LoadIssueAssignees, LoadProjectUsers};
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
use crate::middleware::authorize::token_from_headers;
use crate::routes::user_from_request;
#[get("")]
pub async fn project_issues() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html")
.body("<!DOCTYPE html><html><head><title>Issues</title></head><body>Foo</body></html>")
}
#[get("/{id}")]
pub async fn issue_with_users_and_comments(
req: HttpRequest,
path: Path<i32>,
db: Data<Addr<DbExecutor>>,
) -> HttpResponse {
let issue_id = path.into_inner();
let token = match token_from_headers(req.headers()) {
Ok(uuid) => uuid,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
let _user = match db
.send(AuthorizeUser {
access_token: token,
})
.await
{
Ok(Ok(user)) => user,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
match load_issue(issue_id, db).await {
Ok(full_issue) => HttpResponse::Ok().json(full_issue),
Err(e) => e.into_http_response(),
}
}
#[post("")]
pub async fn create(
req: HttpRequest,
payload: Json<jirs_data::CreateIssuePayload>,
db: Data<Addr<DbExecutor>>,
) -> HttpResponse {
let user = match user_from_request(req, &db).await {
Ok(user) => user,
Err(response) => return response,
};
let msg = CreateIssue {
title: payload.title.clone(),
issue_type: payload.issue_type.clone(),
status: payload.status.clone(),
priority: payload.priority.clone(),
description: payload.description.clone(),
description_text: payload.description_text.clone(),
estimate: payload.estimate.clone(),
time_spent: payload.time_spent.clone(),
time_remaining: payload.time_remaining.clone(),
project_id: payload.project_id,
reporter_id: user.id,
user_ids: payload.user_ids.clone(),
};
match db.send(msg).await {
Ok(Ok(issue)) => HttpResponse::Ok().json(issue),
Ok(Err(e)) => e.into_http_response(),
_ => ServiceErrors::DatabaseConnectionLost.into_http_response(),
}
}
#[put("/{id}")]
pub async fn update(
req: HttpRequest,
payload: Json<jirs_data::UpdateIssuePayload>,
path: Path<i32>,
db: Data<Addr<DbExecutor>>,
) -> HttpResponse {
let issue_id = path.into_inner();
let token = match token_from_headers(req.headers()) {
Ok(uuid) => uuid,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
match db
.send(AuthorizeUser {
access_token: token,
})
.await
{
Ok(Ok(_)) => (),
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
let msg = UpdateIssue {
issue_id,
title: Some(payload.title.clone()),
issue_type: Some(payload.issue_type.clone()),
status: Some(payload.status.clone()),
priority: Some(payload.priority.clone()),
list_position: Some(payload.list_position.clone()),
description: Some(payload.description.clone()),
description_text: Some(payload.description_text.clone()),
estimate: Some(payload.estimate.clone()),
time_spent: Some(payload.time_spent.clone()),
time_remaining: Some(payload.time_remaining.clone()),
project_id: Some(payload.project_id.clone()),
user_ids: Some(payload.user_ids.clone()),
reporter_id: Some(payload.reporter_id),
};
match db.send(msg).await {
Ok(Ok(_)) => (),
Ok(Err(e)) => return e.into_http_response(),
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
};
match load_issue(issue_id, db).await {
Ok(full_issue) => HttpResponse::Ok().json(full_issue),
Err(e) => e.into_http_response(),
}
}
#[delete("/{id}")]
pub async fn delete(req: HttpRequest, path: Path<i32>, db: Data<Addr<DbExecutor>>) -> HttpResponse {
let _user = match user_from_request(req, &db).await {
Ok(user) => user,
Err(response) => return response,
};
let issue_id = path.into_inner();
let msg = DeleteIssue { issue_id };
match db.send(msg).await {
Ok(Ok(_)) => HttpResponse::NoContent().body(""),
Ok(Err(e)) => e.into_http_response(),
_ => ServiceErrors::DatabaseConnectionLost.into_http_response(),
}
}
async fn load_issue(
issue_id: i32,
db: Data<Addr<DbExecutor>>,
) -> Result<jirs_data::FullIssue, ServiceErrors> {
let issue_future = db.send(LoadIssue { issue_id });
let assignees_future = db.send(LoadIssueAssignees { issue_id });
let comments_future = db.send(LoadIssueComments { issue_id });
let issue_result = issue_future.await;
let issue = match issue_result {
Ok(Ok(issue)) => issue,
_ => return Err(ServiceErrors::DatabaseConnectionLost),
};
let users = match db
.send(LoadProjectUsers {
project_id: issue.project_id,
})
.await
{
Ok(Ok(users)) => users,
_ => return Err(ServiceErrors::DatabaseConnectionLost),
};
let mut full_issue: jirs_data::FullIssue = issue.into();
let assignees_result = assignees_future.await;
let assignees = match assignees_result {
Ok(Ok(assignees)) => assignees,
_ => return Err(ServiceErrors::DatabaseConnectionLost),
};
let mut user_map = HashMap::new();
for user in users.into_iter() {
user_map.insert(user.id, user);
}
let comments_result = comments_future.await;
let comments = match comments_result {
Ok(Ok(comments)) => comments,
_ => return Err(ServiceErrors::DatabaseConnectionLost),
};
full_issue.user_ids = assignees.iter().map(|u| u.id).collect();
full_issue.comments = comments
.into_iter()
.map(|c| {
let mut comment: jirs_data::Comment = c.into();
comment.user = user_map.get(&comment.user_id).map(|user| user.into());
comment
})
.collect();
Ok(full_issue)
}

View File

@ -9,8 +9,6 @@ use crate::middleware::authorize::token_from_headers;
use crate::models::User;
pub mod comments;
pub mod issues;
pub mod projects;
pub mod users;
pub async fn user_from_request(

View File

@ -1,100 +0,0 @@
use actix::Addr;
use actix_web::web::{Data, Json, Path};
use actix_web::{get, put, HttpRequest, HttpResponse};
use crate::db::authorize_user::AuthorizeUser;
use crate::db::issues::LoadProjectIssues;
use crate::db::projects::{LoadCurrentProject, UpdateProject};
use crate::db::users::LoadProjectUsers;
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
use crate::middleware::authorize::token_from_headers;
use crate::routes::user_from_request;
#[get("")]
pub async fn project_with_users_and_issues(
req: HttpRequest,
db: Data<Addr<DbExecutor>>,
) -> HttpResponse {
let token = match token_from_headers(req.headers()) {
Ok(uuid) => uuid,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
let user = match db
.send(AuthorizeUser {
access_token: token,
})
.await
{
Ok(Ok(user)) => user,
_ => return crate::errors::ServiceErrors::Unauthorized.into_http_response(),
};
let issues_future = db.send(LoadProjectIssues {
project_id: user.project_id,
});
let project_future = db.send(LoadCurrentProject {
project_id: user.project_id,
});
let users_future = db.send(LoadProjectUsers {
project_id: user.project_id,
});
let issues_result = issues_future.await;
let issues = match issues_result {
Ok(Ok(issues)) => issues,
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
};
let project_result = project_future.await;
let project = match project_result {
Ok(Ok(project)) => project,
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
};
let users_result = users_future.await;
let users = match users_result {
Ok(Ok(users)) => users,
_ => return ServiceErrors::DatabaseConnectionLost.into_http_response(),
};
let res = jirs_data::FullProject {
id: project.id,
name: project.name,
url: project.url,
description: project.description,
category: project.category,
created_at: project.created_at,
updated_at: project.updated_at,
issues: issues
.into_iter()
.map(|i| {
let mut issue: jirs_data::Issue = i.into();
issue.user_ids = users.iter().map(|u| u.id).collect();
issue
})
.collect(),
users: users.into_iter().map(|u| u.into()).collect(),
};
HttpResponse::Ok().json(res)
}
#[put("/{id}")]
pub async fn update(
req: HttpRequest,
payload: Json<jirs_data::UpdateProjectPayload>,
path: Path<i32>,
db: Data<Addr<DbExecutor>>,
) -> HttpResponse {
let _user = match user_from_request(req, &db).await {
Ok(user) => user,
Err(response) => return response,
};
let msg = UpdateProject {
project_id: path.into_inner(),
name: payload.name.clone(),
url: payload.url.clone(),
description: payload.description.clone(),
category: payload.category.clone(),
};
match db.send(msg).await {
Ok(Ok(project)) => HttpResponse::Ok().json(project),
Ok(Err(e)) => e.into_http_response(),
_ => ServiceErrors::DatabaseConnectionLost.into_http_response(),
}
}

View File

@ -0,0 +1,22 @@
use actix::Addr;
use actix_web::web::Data;
use jirs_data::{IssueId, WsMsg};
use crate::db::comments::LoadIssueComments;
use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult};
pub async fn load_issues(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
issue_id: IssueId,
) -> WsResult {
current_user(user)?;
let comments = match db.send(LoadIssueComments { issue_id }).await {
Ok(Ok(comments)) => comments.into_iter().map(|c| c.into()).collect(),
_ => return Ok(None),
};
Ok(Some(WsMsg::IssueCommentsLoaded(comments)))
}

View File

@ -0,0 +1,136 @@
use std::collections::HashMap;
use actix::Addr;
use actix_web::web::Data;
use jirs_data::WsMsg;
use crate::db::issue_assignees::LoadAssignees;
use crate::db::issues::{LoadProjectIssues, UpdateIssue};
use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult};
pub async fn update_issue(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
issue_id: i32,
payload: jirs_data::UpdateIssuePayload,
) -> WsResult {
current_user(user)?;
let mut issue: jirs_data::Issue = match db
.send(UpdateIssue {
issue_id,
title: Some(payload.title),
issue_type: Some(payload.issue_type),
status: Some(payload.status),
priority: Some(payload.priority),
list_position: Some(payload.list_position),
description: Some(payload.description),
description_text: Some(payload.description_text),
estimate: Some(payload.estimate),
time_spent: Some(payload.time_spent),
time_remaining: Some(payload.time_remaining),
project_id: Some(payload.project_id),
user_ids: Some(payload.user_ids),
reporter_id: Some(payload.reporter_id),
})
.await
{
Ok(Ok(issue)) => issue.into(),
_ => return Ok(None),
};
let assignees = match db
.send(LoadAssignees {
issue_id: issue.id.clone(),
})
.await
{
Ok(Ok(v)) => v,
_ => vec![],
};
for assignee in assignees {
issue.user_ids.push(assignee.user_id);
}
Ok(Some(WsMsg::IssueUpdated(issue.into())))
}
pub async fn add_issue(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
payload: jirs_data::CreateIssuePayload,
) -> WsResult {
current_user(user)?;
let msg = crate::db::issues::CreateIssue {
title: payload.title,
issue_type: payload.issue_type,
status: payload.status,
priority: payload.priority,
description: payload.description,
description_text: payload.description_text,
estimate: payload.estimate,
time_spent: payload.time_spent,
time_remaining: payload.time_remaining,
project_id: payload.project_id,
reporter_id: payload.reporter_id,
user_ids: payload.user_ids,
};
let m = match db.send(msg).await {
Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())),
_ => None,
};
Ok(m)
}
pub async fn delete_issue(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
id: i32,
) -> WsResult {
current_user(user)?;
let m = match db
.send(crate::db::issues::DeleteIssue { issue_id: id })
.await
{
Ok(Ok(_)) => Some(WsMsg::IssueDeleted(id)),
_ => None,
};
Ok(m)
}
pub async fn load_issues(db: &Data<Addr<DbExecutor>>, user: &Option<jirs_data::User>) -> WsResult {
let project_id = current_user(user).map(|u| u.project_id)?;
let issues: Vec<jirs_data::Issue> = match db.send(LoadProjectIssues { project_id }).await {
Ok(Ok(v)) => v.into_iter().map(|i| i.into()).collect(),
_ => return Ok(None),
};
let mut issue_map = HashMap::new();
let mut queue = vec![];
for issue in issues.into_iter() {
let f = db.send(LoadAssignees {
issue_id: issue.id.clone(),
});
queue.push(f);
issue_map.insert(issue.id.clone(), issue);
}
for f in queue {
match f.await {
Ok(Ok(assignees)) => {
for assignee in assignees {
if let Some(issue) = issue_map.get_mut(&assignee.issue_id) {
issue.user_ids.push(assignee.user_id);
}
}
}
_ => {}
};
}
let mut issues = vec![];
for (_, issue) in issue_map.into_iter() {
issues.push(issue);
}
Ok(Some(WsMsg::ProjectIssuesLoaded(issues)))
}

View File

@ -1,5 +1,3 @@
use std::collections::HashMap;
use actix::{Actor, Addr, StreamHandler};
use actix_web::web::Data;
use actix_web::{get, web, Error, HttpRequest, HttpResponse};
@ -8,13 +6,21 @@ use actix_web_actors::ws;
use jirs_data::WsMsg;
use crate::db::authorize_user::AuthorizeUser;
use crate::db::issue_assignees::LoadAssignees;
use crate::db::issues::{LoadProjectIssues, UpdateIssue};
use crate::db::projects::LoadCurrentProject;
use crate::db::users::LoadProjectUsers;
use crate::db::DbExecutor;
type WsResult = std::result::Result<Option<WsMsg>, WsMsg>;
pub mod comments;
pub mod issues;
pub mod projects;
pub mod users;
pub type WsResult = std::result::Result<Option<WsMsg>, WsMsg>;
pub fn current_user(current_user: &Option<jirs_data::User>) -> Result<&jirs_data::User, WsMsg> {
current_user
.as_ref()
.map(|u| u)
.ok_or_else(|| WsMsg::AuthorizeExpired)
}
trait WsMessageSender {
fn send_msg(&mut self, msg: jirs_data::WsMsg);
@ -46,13 +52,45 @@ impl WebSocketActor {
let msg = match msg {
WsMsg::Ping => Some(WsMsg::Pong),
WsMsg::Pong => Some(WsMsg::Ping),
WsMsg::IssueUpdateRequest(id, payload) => block_on(self.update_issue(id, payload))?,
WsMsg::IssueCreateRequest(payload) => block_on(self.add_issue(payload))?,
WsMsg::IssueDeleteRequest(id) => block_on(self.delete_issue(id))?,
WsMsg::ProjectRequest => block_on(self.load_project())?,
// Issues
WsMsg::IssueUpdateRequest(id, payload) => block_on(issues::update_issue(
&self.db,
&self.current_user,
id,
payload,
))?,
WsMsg::IssueCreateRequest(payload) => {
block_on(issues::add_issue(&self.db, &self.current_user, payload))?
}
WsMsg::IssueDeleteRequest(id) => {
block_on(issues::delete_issue(&self.db, &self.current_user, id))?
}
WsMsg::ProjectIssuesRequest => {
block_on(issues::load_issues(&self.db, &self.current_user))?
}
// projects
WsMsg::ProjectRequest => {
block_on(projects::current_project(&self.db, &self.current_user))?
}
// auth
WsMsg::AuthorizeRequest(uuid) => block_on(self.authorize(uuid))?,
WsMsg::ProjectIssuesRequest => block_on(self.load_issues())?,
WsMsg::ProjectUsersRequest => block_on(self.load_project_users())?,
// users
WsMsg::ProjectUsersRequest => {
block_on(users::load_project_users(&self.db, &self.current_user))?
}
// comments
WsMsg::IssueCommentsRequest(issue_id) => block_on(comments::load_issues(
&self.db,
&self.current_user,
issue_id,
))?,
// else fail
_ => {
error!("No handle for {:?} specified", msg);
None
@ -81,152 +119,6 @@ impl WebSocketActor {
};
Ok(m)
}
fn current_user(&mut self) -> Result<&jirs_data::User, WsMsg> {
self.current_user
.as_ref()
.map(|u| u)
.ok_or_else(|| WsMsg::AuthorizeExpired)
}
async fn load_project(&mut self) -> WsResult {
let project_id = self.current_user().map(|u| u.project_id)?;
match self.db.send(LoadCurrentProject { project_id }).await {
Ok(Ok(p)) => Ok(Some(WsMsg::ProjectLoaded(p.into()))),
_ => Ok(None),
}
}
async fn load_issues(&mut self) -> WsResult {
let project_id = self.current_user().map(|u| u.project_id)?;
let issues: Vec<jirs_data::Issue> =
match self.db.send(LoadProjectIssues { project_id }).await {
Ok(Ok(v)) => v.into_iter().map(|i| i.into()).collect(),
_ => return Ok(None),
};
let mut issue_map = HashMap::new();
let mut queue = vec![];
for issue in issues.into_iter() {
let f = self.db.send(LoadAssignees {
issue_id: issue.id.clone(),
});
queue.push(f);
issue_map.insert(issue.id.clone(), issue);
}
for f in queue {
match f.await {
Ok(Ok(assignees)) => {
for assignee in assignees {
if let Some(issue) = issue_map.get_mut(&assignee.issue_id) {
issue.user_ids.push(assignee.user_id);
}
}
}
_ => {}
};
}
let mut issues = vec![];
for (_, issue) in issue_map.into_iter() {
issues.push(issue);
}
Ok(Some(WsMsg::ProjectIssuesLoaded(issues)))
}
async fn load_project_users(&mut self) -> WsResult {
let project_id = self.current_user().map(|u| u.project_id)?;
let m = match self.db.send(LoadProjectUsers { project_id }).await {
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(
v.into_iter().map(|i| i.into()).collect(),
)),
_ => None,
};
Ok(m)
}
async fn update_issue(
&mut self,
issue_id: i32,
payload: jirs_data::UpdateIssuePayload,
) -> WsResult {
self.current_user()?;
let mut issue: jirs_data::Issue = match self
.db
.send(UpdateIssue {
issue_id,
title: Some(payload.title),
issue_type: Some(payload.issue_type),
status: Some(payload.status),
priority: Some(payload.priority),
list_position: Some(payload.list_position),
description: Some(payload.description),
description_text: Some(payload.description_text),
estimate: Some(payload.estimate),
time_spent: Some(payload.time_spent),
time_remaining: Some(payload.time_remaining),
project_id: Some(payload.project_id),
user_ids: Some(payload.user_ids),
reporter_id: Some(payload.reporter_id),
})
.await
{
Ok(Ok(issue)) => issue.into(),
_ => return Ok(None),
};
let assignees = match self
.db
.send(LoadAssignees {
issue_id: issue.id.clone(),
})
.await
{
Ok(Ok(v)) => v,
_ => vec![],
};
for assignee in assignees {
issue.user_ids.push(assignee.user_id);
}
Ok(Some(WsMsg::IssueUpdated(issue.into())))
}
async fn add_issue(&mut self, payload: jirs_data::CreateIssuePayload) -> WsResult {
self.current_user()?;
let msg = crate::db::issues::CreateIssue {
title: payload.title,
issue_type: payload.issue_type,
status: payload.status,
priority: payload.priority,
description: payload.description,
description_text: payload.description_text,
estimate: payload.estimate,
time_spent: payload.time_spent,
time_remaining: payload.time_remaining,
project_id: payload.project_id,
reporter_id: payload.reporter_id,
user_ids: payload.user_ids,
};
let m = match self.db.send(msg).await {
Ok(Ok(issue)) => Some(WsMsg::IssueCreated(issue.into())),
_ => None,
};
Ok(m)
}
async fn delete_issue(&mut self, id: i32) -> WsResult {
self.current_user()?;
let m = match self
.db
.send(crate::db::issues::DeleteIssue { issue_id: id })
.await
{
Ok(Ok(_)) => Some(WsMsg::IssueDeleted(id)),
_ => None,
};
Ok(m)
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {

View File

@ -0,0 +1,23 @@
use actix::Addr;
use actix_web::web::Data;
use jirs_data::WsMsg;
use crate::db::users::LoadProjectUsers;
use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult};
pub async fn current_project(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
) -> WsResult {
let project_id = current_user(user).map(|u| u.project_id)?;
let m = match db.send(LoadProjectUsers { project_id }).await {
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(
v.into_iter().map(|i| i.into()).collect(),
)),
_ => None,
};
Ok(m)
}

View File

@ -0,0 +1,22 @@
use actix::Addr;
use actix_web::web::Data;
use jirs_data::WsMsg;
use crate::db::users::LoadProjectUsers;
use crate::db::DbExecutor;
use crate::ws::{current_user, WsResult};
pub async fn load_project_users(
db: &Data<Addr<DbExecutor>>,
user: &Option<jirs_data::User>,
) -> WsResult {
let project_id = current_user(user).map(|u| u.project_id)?;
let m = match db.send(LoadProjectUsers { project_id }).await {
Ok(Ok(v)) => Some(WsMsg::ProjectUsersLoaded(
v.into_iter().map(|i| i.into()).collect(),
)),
_ => None,
};
Ok(m)
}