diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 69d20469..322561f0 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -63,7 +63,7 @@ impl std::fmt::Display for FieldId { EditIssueModalSection::Issue(IssueFieldId::Description) => { f.write_str("descriptionIssueEditModal") } - EditIssueModalSection::Issue(IssueFieldId::Status) => { + EditIssueModalSection::Issue(IssueFieldId::IssueStatusId) => { f.write_str("statusIssueEditModal") } EditIssueModalSection::Issue(IssueFieldId::Assignees) => { @@ -98,7 +98,7 @@ impl std::fmt::Display for FieldId { IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"), IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"), IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"), - IssueFieldId::Status => f.write_str("addIssueModal-status"), + IssueFieldId::IssueStatusId => f.write_str("addIssueModal-status"), IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"), IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"), IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"), @@ -214,8 +214,8 @@ pub enum Msg { IssueDragStopped(IssueId), DragLeave(IssueId), ExchangePosition(IssueId), - IssueDragOverStatus(IssueStatus), - IssueDropZone(IssueStatus), + IssueDragOverStatus(IssueStatusId), + IssueDropZone(IssueStatusId), UnlockDragOver, // inputs diff --git a/jirs-client/src/modal/add_issue.rs b/jirs-client/src/modal/add_issue.rs index 00e6a82a..f65f19c2 100644 --- a/jirs-client/src/modal/add_issue.rs +++ b/jirs-client/src/modal/add_issue.rs @@ -39,7 +39,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde let payload = jirs_data::CreateIssuePayload { title: modal.title.clone(), issue_type: modal.issue_type, - status: modal.status, + issue_status_id: modal.issue_status_id, priority: modal.priority, description: modal.description.clone(), description_text: modal.description_text.clone(), diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issue_details.rs index d9ead4bc..959d67c1 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issue_details.rs @@ -50,14 +50,14 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { )); } Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Status)), + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), StyledSelectChange::Changed(value), ) => { - modal.payload.status = (*value).into(); + modal.payload.issue_status_id = *value as IssueStatusId; send_ws_msg(WsMsg::IssueUpdateRequest( modal.id, - IssueFieldId::Status, - PayloadVariant::IssueStatus(modal.payload.status), + IssueFieldId::IssueStatusId, + PayloadVariant::I32(modal.payload.issue_status_id), )); } Msg::StyledSelectChanged( @@ -599,19 +599,27 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { } = modal; let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Status, + IssueFieldId::IssueStatusId, ))) .name("status") .opened(status_state.opened) .normal() .text_filter(status_state.text_filter.as_str()) .options( - IssueStatus::ordered() - .into_iter() + model + .issue_statuses + .iter() .map(|opt| opt.to_child().name("status")) .collect(), ) - .selected(vec![payload.status.to_child().name("status")]) + .selected( + model + .issue_statuses + .iter() + .filter(|is| is.id == payload.issue_status_id) + .map(|is| is.to_child().name("status")) + .collect(), + ) .valid(true) .build() .into_node(); diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 53a0a029..540f925b 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -61,7 +61,7 @@ impl EditIssueModal { payload: UpdateIssuePayload { title: issue.title.clone(), issue_type: issue.issue_type, - status: issue.status, + issue_status_id: issue.issue_status_id, priority: issue.priority, list_position: issue.list_position, description: issue.description.clone(), @@ -78,8 +78,8 @@ impl EditIssueModal { issue.estimate.map(|v| vec![v as u32]).unwrap_or_default(), ), status_state: StyledSelectState::new( - FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Status)), - vec![issue.status.into()], + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), + vec![issue.issue_status_id as u32], ), reporter_state: StyledSelectState::new( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), @@ -134,7 +134,6 @@ impl EditIssueModal { pub struct AddIssueModal { pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub description: Option, pub description_text: Option, @@ -144,6 +143,7 @@ pub struct AddIssueModal { pub project_id: Option, pub user_ids: Vec, pub reporter_id: Option, + pub issue_status_id: i32, // modal fields pub type_state: StyledSelectState, @@ -157,7 +157,6 @@ impl Default for AddIssueModal { Self { title: Default::default(), issue_type: Default::default(), - status: Default::default(), priority: Default::default(), description: Default::default(), description_text: Default::default(), @@ -167,6 +166,7 @@ impl Default for AddIssueModal { project_id: Default::default(), user_ids: Default::default(), reporter_id: Default::default(), + issue_status_id: Default::default(), type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]), reporter_state: StyledSelectState::new( FieldId::AddIssueModal(IssueFieldId::Reporter), @@ -424,6 +424,7 @@ pub struct Model { pub issues: Vec, pub users: Vec, pub comments: Vec, + pub issue_statuses: Vec, } impl Default for Model { @@ -445,6 +446,7 @@ impl Default for Model { project: None, comments: vec![], about_tooltip_visible: false, + issue_statuses: vec![], } } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index fd7dc22a..804660ee 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -40,6 +40,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order send_ws_msg(jirs_data::WsMsg::ProjectRequest); send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest); send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest); + send_ws_msg(jirs_data::WsMsg::IssueStatusesRequest); } Msg::WsMsg(WsMsg::IssueUpdated(issue)) => { let mut old: Vec = vec![]; @@ -254,16 +255,15 @@ fn avatars_filters(model: &Model) -> Node { } fn project_board_lists(model: &Model) -> Node { - div![ - id!["projectBoardLists"], - project_issue_list(model, IssueStatus::Backlog), - project_issue_list(model, IssueStatus::Selected), - project_issue_list(model, IssueStatus::InProgress), - project_issue_list(model, IssueStatus::Done), - ] + let columns: Vec> = model + .issue_statuses + .iter() + .map(|is| project_issue_list(model, is)) + .collect(); + div![id!["projectBoardLists"], columns] } -fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node { +fn project_issue_list(model: &Model, status: &jirs_data::IssueStatus) -> Node { let project_page = match &model.page_content { PageContent::Project(project_page) => project_page, _ => return empty![], @@ -293,15 +293,15 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node Node bool { - issue.status == status +fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool { + issue.issue_status_id == status.id } #[inline] diff --git a/jirs-client/src/shared/styled_select_child.rs b/jirs-client/src/shared/styled_select_child.rs index 41d8d225..5d3895a6 100644 --- a/jirs-client/src/shared/styled_select_child.rs +++ b/jirs-client/src/shared/styled_select_child.rs @@ -214,12 +214,12 @@ impl ToChild for jirs_data::IssueStatus { type Builder = StyledSelectChildBuilder; fn to_child(&self) -> StyledSelectChildBuilder { - let text = self.to_label(); + let text = &self.name; StyledSelectChild::build() - .value(self.clone().into()) + .value(self.id as u32) .add_class(text) - .text(text) + .text(text.as_str()) } } diff --git a/jirs-client/src/ws/issue.rs b/jirs-client/src/ws/issue.rs index 63e54654..836684ba 100644 --- a/jirs-client/src/ws/issue.rs +++ b/jirs-client/src/ws/issue.rs @@ -53,18 +53,18 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) { return; } }; - if dragged.status != below.status { + if dragged.issue_status_id != below.issue_status_id { let mut issues = vec![]; std::mem::swap(&mut issues, &mut model.issues); for mut c in issues.into_iter() { - if c.status == below.status && c.list_position > below.list_position { + if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position { c.list_position += 1; mark_dirty(c.id, project_page); } model.issues.push(c); } dragged.list_position = below.list_position + 1; - dragged.status = below.status; + dragged.issue_status_id = below.issue_status_id; } std::mem::swap(&mut dragged.list_position, &mut below.list_position); @@ -95,8 +95,8 @@ pub fn sync(model: &mut Model) { send_ws_msg(WsMsg::IssueUpdateRequest( issue.id, - IssueFieldId::Status, - PayloadVariant::IssueStatus(issue.status), + IssueFieldId::IssueStatusId, + PayloadVariant::I32(issue.issue_status_id), )); send_ws_msg(WsMsg::IssueUpdateRequest( issue.id, @@ -109,7 +109,7 @@ pub fn sync(model: &mut Model) { project_page.dirty_issues.clear(); } -pub fn change_status(status: IssueStatus, model: &mut Model) { +pub fn change_status(status_id: IssueStatusId, model: &mut Model) { let project_page = match &mut model.page_content { PageContent::Project(project_page) => project_page, _ => return, @@ -127,7 +127,7 @@ pub fn change_status(status: IssueStatus, model: &mut Model) { old.sort_by(|a, b| a.list_position.cmp(&b.list_position)); for mut issue in old.into_iter() { - if issue.status == status { + if issue.issue_status_id == status_id { if issue.list_position != pos { issue.list_position = pos; mark_dirty(issue.id, project_page); @@ -148,10 +148,10 @@ pub fn change_status(status: IssueStatus, model: &mut Model) { } }; - if issue.status == status { + if issue.issue_status_id == status_id { model.issues.push(issue); } else { - issue.status = status; + issue.issue_status_id = status_id; issue.list_position = pos + 1; model.issues.push(issue); mark_dirty(issue_id, project_page); diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 3301256b..913b216e 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -41,6 +41,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); model.issues = v; } + // issue statuses + Msg::WsMsg(WsMsg::IssueStatusesResponse(v)) => { + model.issue_statuses = v.clone(); + model + .issue_statuses + .sort_by(|a, b| a.position.cmp(&b.position)); + } // users Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => { model.users = v.clone(); diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 6ed371d1..a08ce55a 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -23,6 +23,7 @@ pub type ProjectId = i32; pub type UserId = i32; pub type CommentId = i32; pub type TokenId = i32; +pub type IssueStatusId = i32; pub type InvitationId = i32; pub type EmailString = String; pub type UsernameString = String; @@ -91,83 +92,6 @@ impl std::fmt::Display for IssueType { } } -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] -#[cfg_attr(feature = "backend", sql_type = "IssueStatusType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] -pub enum IssueStatus { - Backlog, - Selected, - InProgress, - Done, -} - -impl Default for IssueStatus { - fn default() -> Self { - IssueStatus::Backlog - } -} - -impl Into for IssueStatus { - fn into(self) -> u32 { - match self { - IssueStatus::Backlog => 0, - IssueStatus::Selected => 1, - IssueStatus::InProgress => 2, - IssueStatus::Done => 3, - } - } -} - -impl Into for u32 { - fn into(self) -> IssueStatus { - match self { - 0 => IssueStatus::Backlog, - 1 => IssueStatus::Selected, - 2 => IssueStatus::InProgress, - 3 => IssueStatus::Done, - _ => IssueStatus::Backlog, - } - } -} - -impl FromStr for IssueStatus { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "backlog" => Ok(IssueStatus::Backlog), - "selected" => Ok(IssueStatus::Selected), - "in_progress" => Ok(IssueStatus::InProgress), - "done" => Ok(IssueStatus::Done), - _ => Err(format!("Invalid status {:?}", s)), - } - } -} - -impl ToVec for IssueStatus { - type Item = IssueStatus; - - fn ordered() -> Vec { - vec![ - IssueStatus::Backlog, - IssueStatus::Selected, - IssueStatus::InProgress, - IssueStatus::Done, - ] - } -} - -impl IssueStatus { - pub fn to_label(&self) -> &str { - match self { - IssueStatus::Backlog => "Backlog", - IssueStatus::Selected => "Selected for development", - IssueStatus::InProgress => "In Progress", - IssueStatus::Done => "Done", - } - } -} - #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")] #[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] @@ -482,7 +406,6 @@ pub struct Issue { pub id: IssueId, pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub list_position: i32, pub description: Option, @@ -494,10 +417,22 @@ pub struct Issue { pub project_id: ProjectId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub issue_status_id: IssueStatusId, pub user_ids: Vec, } +#[cfg_attr(feature = "backend", derive(Queryable))] +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +pub struct IssueStatus { + pub id: IssueStatusId, + pub name: String, + pub position: ProjectId, + pub project_id: ProjectId, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + #[cfg_attr(feature = "backend", derive(Queryable))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Invitation { @@ -552,7 +487,6 @@ pub struct Token { pub struct UpdateIssuePayload { pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub list_position: i32, pub description: Option, @@ -562,6 +496,7 @@ pub struct UpdateIssuePayload { pub time_remaining: Option, pub project_id: ProjectId, pub reporter_id: UserId, + pub issue_status_id: IssueStatusId, pub user_ids: Vec, } @@ -580,7 +515,6 @@ impl From for UpdateIssuePayload { Self { title: issue.title, issue_type: issue.issue_type, - status: issue.status, priority: issue.priority, list_position: issue.list_position, description: issue.description, @@ -591,6 +525,7 @@ impl From for UpdateIssuePayload { project_id: issue.project_id, reporter_id: issue.reporter_id, user_ids: issue.user_ids, + issue_status_id: issue.issue_status_id, } } } @@ -612,7 +547,6 @@ pub struct UpdateCommentPayload { pub struct CreateIssuePayload { pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub description: Option, pub description_text: Option, @@ -622,6 +556,7 @@ pub struct CreateIssuePayload { pub project_id: ProjectId, pub user_ids: Vec, pub reporter_id: UserId, + pub issue_status_id: IssueStatusId, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -641,7 +576,6 @@ pub enum PayloadVariant { I32(i32), String(String), IssueType(IssueType), - IssueStatus(IssueStatus), IssuePriority(IssuePriority), ProjectCategory(ProjectCategory), } @@ -691,7 +625,6 @@ pub enum IssueFieldId { Type, Title, Description, - Status, ListPosition, Assignees, Reporter, @@ -699,6 +632,7 @@ pub enum IssueFieldId { Estimate, TimeSpent, TimeRemaining, + IssueStatusId, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -760,6 +694,10 @@ pub enum WsMsg { IssueCreateRequest(CreateIssuePayload), IssueCreated(Issue), + // issue status + IssueStatusesRequest, + IssueStatusesResponse(Vec), + // comments IssueCommentsRequest(IssueId), IssueCommentsLoaded(Vec), diff --git a/jirs-data/src/sql.rs b/jirs-data/src/sql.rs index 088f04f0..ed91833b 100644 --- a/jirs-data/src/sql.rs +++ b/jirs-data/src/sql.rs @@ -2,9 +2,7 @@ use std::io::Write; use diesel::{deserialize::*, pg::*, serialize::*, *}; -use crate::{ - InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking, UserRole, -}; +use crate::{InvitationState, IssuePriority, IssueType, ProjectCategory, TimeTracking, UserRole}; #[derive(SqlType)] #[postgres(type_name = "IssuePriorityType")] @@ -82,48 +80,6 @@ impl ToSql for IssueType { } } -#[derive(SqlType)] -#[postgres(type_name = "IssueStatusType")] -pub struct IssueStatusType; - -impl diesel::query_builder::QueryId for IssueStatusType { - type QueryId = IssueStatus; -} - -fn issue_status_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"backlog" => Ok(IssueStatus::Backlog), - b"selected" => Ok(IssueStatus::Selected), - b"in_progress" | b"inprogress" => Ok(IssueStatus::InProgress), - b"done" => Ok(IssueStatus::Done), - _ => Ok(IssueStatus::Backlog), - } -} - -impl FromSql for IssueStatus { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_status_from_sql(bytes) - } -} - -impl FromSql for IssueStatus { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_status_from_sql(bytes) - } -} - -impl ToSql for IssueStatus { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - IssueStatus::Backlog => out.write_all(b"backlog")?, - IssueStatus::Selected => out.write_all(b"selected")?, - IssueStatus::InProgress => out.write_all(b"in_progress")?, - IssueStatus::Done => out.write_all(b"done")?, - } - Ok(IsNull::No) - } -} - #[derive(SqlType)] #[postgres(type_name = "ProjectCategoryType")] pub struct ProjectCategoryType; diff --git a/jirs-server/migrations/2020-05-06-130610_add_custom_columns/down.sql b/jirs-server/migrations/2020-05-06-130610_add_custom_columns/down.sql new file mode 100644 index 00000000..21dbe042 --- /dev/null +++ b/jirs-server/migrations/2020-05-06-130610_add_custom_columns/down.sql @@ -0,0 +1,16 @@ +DROP TYPE IF EXISTS "IssueStatusType" CASCADE; +CREATE TYPE "IssueStatusType" AS ENUM ( + 'backlog', + 'selected', + 'in_progress', + 'done' +); +ALTER TABLE issues ADD COLUMN status "IssueStatusType"; +UPDATE issues +SET status = issue_statuses.name :: "IssueStatusType" +FROM issue_statuses +WHERE issue_statuses.id = issues.issue_status_id; + +ALTER TABLE issues DROP COLUMN issue_status_id; +ALTER TABLE issues ALTER COLUMN status SET NOT NULL; +DROP TABLE issue_statuses; diff --git a/jirs-server/migrations/2020-05-06-130610_add_custom_columns/up.sql b/jirs-server/migrations/2020-05-06-130610_add_custom_columns/up.sql new file mode 100644 index 00000000..5bbf5bb0 --- /dev/null +++ b/jirs-server/migrations/2020-05-06-130610_add_custom_columns/up.sql @@ -0,0 +1,37 @@ +CREATE TABLE issue_statuses ( + id serial PRIMARY KEY NOT NULL, + name VARCHAR NOT NULL, + position int NOT NULL, + project_id int NOT NULL REFERENCES projects (id), + created_at timestamp NOT NULL DEFAULT now(), + updated_at timestamp NOT NULL DEFAULT now() +); + +INSERT INTO issue_statuses (name, project_id, position) +SELECT 'backlog', id, 1 +FROM projects; + +INSERT INTO issue_statuses (name, project_id, position) +SELECT 'selected', id, 2 +FROM projects; + +INSERT INTO issue_statuses (name, project_id, position) +SELECT 'in_progress', id, 3 +FROM projects; + +INSERT INTO issue_statuses (name, project_id, position) +SELECT 'done', id, 4 +FROM projects; + +ALTER TABLE issues +ADD COLUMN issue_status_id INT REFERENCES issue_statuses ( id ); + +UPDATE issues +SET issue_status_id = issue_statuses.id +FROM issue_statuses +WHERE issue_statuses.name = issues.status :: text; + +ALTER TABLE issues DROP COLUMN status; +ALTER TABLE issues ALTER COLUMN issue_status_id SET NOT NULL; + +DROP TYPE IF EXISTS "IssueStatusType"; diff --git a/jirs-server/src/db/issue_statuses.rs b/jirs-server/src/db/issue_statuses.rs new file mode 100644 index 00000000..15f48646 --- /dev/null +++ b/jirs-server/src/db/issue_statuses.rs @@ -0,0 +1,98 @@ +use actix::{Handler, Message}; +use diesel::pg::Pg; +use diesel::prelude::*; + +use jirs_data::{IssueStatus, IssueStatusId, ProjectId}; + +use crate::db::DbExecutor; +use crate::errors::ServiceErrors; + +pub struct LoadIssueStatuses { + pub project_id: ProjectId, +} + +impl Message for LoadIssueStatuses { + type Result = Result, ServiceErrors>; +} + +impl Handler for DbExecutor { + type Result = Result, ServiceErrors>; + + fn handle(&mut self, msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id}; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let issue_assignees_query = issue_statuses + .distinct_on(id) + .filter(project_id.eq(msg.project_id)); + debug!("{}", diesel::debug_query::(&issue_assignees_query)); + issue_assignees_query + .load::(conn) + .map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) + } +} + +pub struct CreateIssueStatus { + pub project_id: ProjectId, + pub position: i32, + pub name: String, +} + +impl Message for CreateIssueStatus { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::issue_statuses::dsl::{issue_statuses, name, position, project_id}; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let issue_assignees_query = diesel::insert_into(issue_statuses).values(( + project_id.eq(msg.project_id), + name.eq(msg.name), + position.eq(msg.position), + )); + debug!("{}", diesel::debug_query::(&issue_assignees_query)); + issue_assignees_query + .get_result::(conn) + .map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) + } +} + +pub struct DeleteIssueStatus { + pub issue_status_id: IssueStatusId, +} + +impl Message for DeleteIssueStatus { + type Result = Result; +} + +impl Handler for DbExecutor { + type Result = Result; + + fn handle(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> Self::Result { + use crate::schema::issue_statuses::dsl::{id, issue_statuses}; + + let conn = &self + .pool + .get() + .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + + let issue_assignees_query = + diesel::delete(issue_statuses).filter(id.eq(msg.issue_status_id)); + debug!("{}", diesel::debug_query::(&issue_assignees_query)); + issue_assignees_query + .execute(conn) + .map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) + } +} diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs index 7f48d9a9..63656a53 100644 --- a/jirs-server/src/db/issues.rs +++ b/jirs-server/src/db/issues.rs @@ -4,7 +4,7 @@ use diesel::expression::sql_literal::sql; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use jirs_data::{IssuePriority, IssueStatus, IssueType}; +use jirs_data::{IssuePriority, IssueStatusId, IssueType}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; @@ -74,7 +74,6 @@ pub struct UpdateIssue { pub issue_id: i32, pub title: Option, pub issue_type: Option, - pub status: Option, pub priority: Option, pub list_position: Option, pub description: Option, @@ -85,6 +84,7 @@ pub struct UpdateIssue { pub project_id: Option, pub user_ids: Option>, pub reporter_id: Option, + pub issue_status_id: Option, } impl Message for UpdateIssue { @@ -107,7 +107,7 @@ impl Handler for DbExecutor { msg.title.map(|title| dsl::title.eq(title)), msg.issue_type .map(|issue_type| dsl::issue_type.eq(issue_type)), - msg.status.map(|status| dsl::status.eq(status)), + msg.issue_status_id.map(|id| dsl::issue_status_id.eq(id)), msg.priority.map(|priority| dsl::priority.eq(priority)), msg.list_position .map(|list_position| dsl::list_position.eq(list_position)), @@ -204,7 +204,7 @@ impl Handler for DbExecutor { pub struct CreateIssue { pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, + pub issue_status_id: IssueStatusId, pub priority: IssuePriority, pub description: Option, pub description_text: Option, @@ -225,7 +225,7 @@ impl Handler for DbExecutor { fn handle(&mut self, msg: CreateIssue, _ctx: &mut Self::Context) -> Self::Result { use crate::schema::issue_assignees::dsl; - use crate::schema::issues::dsl::{issues, status}; + use crate::schema::issues::dsl::issues; let conn = &self .pool @@ -233,7 +233,7 @@ impl Handler for DbExecutor { .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; let list_position = issues - .filter(status.eq(IssueStatus::Backlog)) + // .filter(issue_status_id.eq(IssueStatus::Backlog)) .select(sql("max(list_position) + 1")) .get_result::(conn) .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; @@ -241,7 +241,7 @@ impl Handler for DbExecutor { let form = crate::models::CreateIssueForm { title: msg.title, issue_type: msg.issue_type, - status: msg.status, + issue_status_id: msg.issue_status_id, priority: msg.priority, list_position, description: msg.description, diff --git a/jirs-server/src/db/mod.rs b/jirs-server/src/db/mod.rs index f891c86a..88fd9293 100644 --- a/jirs-server/src/db/mod.rs +++ b/jirs-server/src/db/mod.rs @@ -13,6 +13,7 @@ pub mod authorize_user; pub mod comments; pub mod invitations; pub mod issue_assignees; +pub mod issue_statuses; pub mod issues; pub mod projects; pub mod tokens; diff --git a/jirs-server/src/models.rs b/jirs-server/src/models.rs index 7ec986dc..97e5ab7d 100644 --- a/jirs-server/src/models.rs +++ b/jirs-server/src/models.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use jirs_data::{ - InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking, + InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId, + TimeTracking, UserId, }; use crate::schema::*; @@ -21,7 +22,6 @@ pub struct Issue { pub id: i32, pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub list_position: i32, pub description: Option, @@ -33,6 +33,7 @@ pub struct Issue { pub project_id: i32, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub issue_status_id: IssueStatusId, } impl Into for Issue { @@ -41,7 +42,6 @@ impl Into for 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, @@ -53,6 +53,7 @@ impl Into for Issue { project_id: self.project_id, created_at: self.created_at, updated_at: self.updated_at, + issue_status_id: self.issue_status_id, user_ids: vec![], } @@ -64,7 +65,6 @@ impl Into for Issue { pub struct CreateIssueForm { pub title: String, pub issue_type: IssueType, - pub status: IssueStatus, pub priority: IssuePriority, pub list_position: i32, pub description: Option, @@ -72,8 +72,9 @@ pub struct CreateIssueForm { pub estimate: Option, pub time_spent: Option, pub time_remaining: Option, - pub reporter_id: i32, - pub project_id: i32, + pub reporter_id: UserId, + pub project_id: ProjectId, + pub issue_status_id: IssueStatusId, } #[derive(Debug, Serialize, Deserialize, Insertable)] diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index 46031d10..6a16806e 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -179,12 +179,6 @@ table! { /// /// (Automatically generated by Diesel.) issue_type -> IssueTypeType, - /// The `status` column of the `issues` table. - /// - /// Its SQL type is `IssueStatusType`. - /// - /// (Automatically generated by Diesel.) - status -> IssueStatusType, /// The `priority` column of the `issues` table. /// /// Its SQL type is `IssuePriorityType`. @@ -251,6 +245,59 @@ table! { /// /// (Automatically generated by Diesel.) updated_at -> Timestamp, + /// The `issue_status_id` column of the `issues` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + issue_status_id -> Int4, + } +} + +table! { + use diesel::sql_types::*; + use jirs_data::sql::*; + + /// Representation of the `issue_statuses` table. + /// + /// (Automatically generated by Diesel.) + issue_statuses (id) { + /// The `id` column of the `issue_statuses` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + id -> Int4, + /// The `name` column of the `issue_statuses` table. + /// + /// Its SQL type is `Varchar`. + /// + /// (Automatically generated by Diesel.) + name -> Varchar, + /// The `position` column of the `issue_statuses` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + position -> Int4, + /// The `project_id` column of the `issue_statuses` table. + /// + /// Its SQL type is `Int4`. + /// + /// (Automatically generated by Diesel.) + project_id -> Int4, + /// The `created_at` column of the `issue_statuses` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + created_at -> Timestamp, + /// The `updated_at` column of the `issue_statuses` table. + /// + /// Its SQL type is `Timestamp`. + /// + /// (Automatically generated by Diesel.) + updated_at -> Timestamp, } } @@ -431,6 +478,8 @@ joinable!(invitations -> projects (project_id)); joinable!(invitations -> users (invited_by_id)); joinable!(issue_assignees -> issues (issue_id)); joinable!(issue_assignees -> users (user_id)); +joinable!(issue_statuses -> projects (project_id)); +joinable!(issues -> issue_statuses (issue_status_id)); joinable!(issues -> projects (project_id)); joinable!(issues -> users (reporter_id)); joinable!(tokens -> users (user_id)); @@ -441,6 +490,7 @@ allow_tables_to_appear_in_same_query!( invitations, issue_assignees, issues, + issue_statuses, projects, tokens, users, diff --git a/jirs-server/src/ws/issue_statuses.rs b/jirs-server/src/ws/issue_statuses.rs new file mode 100644 index 00000000..24b5eb2f --- /dev/null +++ b/jirs-server/src/ws/issue_statuses.rs @@ -0,0 +1,23 @@ +use futures::executor::block_on; + +use jirs_data::WsMsg; + +use crate::db::issue_statuses; +use crate::ws::{WebSocketActor, WsHandler, WsResult}; + +pub struct LoadIssueStatuses; + +impl WsHandler for WebSocketActor { + fn handle_msg(&mut self, _msg: LoadIssueStatuses, _ctx: &mut Self::Context) -> WsResult { + let project_id = self.require_user()?.project_id; + + let msg = match block_on( + self.db + .send(issue_statuses::LoadIssueStatuses { project_id }), + ) { + Ok(Ok(v)) => Some(WsMsg::IssueStatusesResponse(v)), + _ => None, + }; + Ok(msg) + } +} diff --git a/jirs-server/src/ws/issues.rs b/jirs-server/src/ws/issues.rs index 35baa527..2da76ea9 100644 --- a/jirs-server/src/ws/issues.rs +++ b/jirs-server/src/ws/issues.rs @@ -36,8 +36,8 @@ impl WsHandler for WebSocketActor { (IssueFieldId::Description, PayloadVariant::String(s)) => { msg.description = Some(s); } - (IssueFieldId::Status, PayloadVariant::IssueStatus(s)) => { - msg.status = Some(s); + (IssueFieldId::IssueStatusId, PayloadVariant::I32(s)) => { + msg.issue_status_id = Some(s); } (IssueFieldId::ListPosition, PayloadVariant::I32(i)) => { msg.list_position = Some(i); @@ -89,7 +89,7 @@ impl WsHandler for WebSocketActor { let msg = crate::db::issues::CreateIssue { title: msg.title, issue_type: msg.issue_type, - status: msg.status, + issue_status_id: msg.issue_status_id, priority: msg.priority, description: msg.description, description_text: msg.description_text, diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 9c5b9fcf..6018bc68 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -14,6 +14,7 @@ use crate::mail::MailExecutor; use crate::ws::auth::*; use crate::ws::comments::*; use crate::ws::invitations::*; +use crate::ws::issue_statuses::*; use crate::ws::issues::*; use crate::ws::projects::*; use crate::ws::users::*; @@ -21,6 +22,7 @@ use crate::ws::users::*; pub mod auth; pub mod comments; pub mod invitations; +pub mod issue_statuses; pub mod issues; pub mod projects; pub mod users; @@ -81,7 +83,7 @@ impl WebSocketActor { WsMsg::Ping => Some(WsMsg::Pong), WsMsg::Pong => Some(WsMsg::Ping), - // Issues + // issues WsMsg::IssueUpdateRequest(id, field_id, payload) => self.handle_msg( UpdateIssueHandler { id, @@ -94,6 +96,9 @@ impl WebSocketActor { WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?, WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?, + // issue statuses + WsMsg::IssueStatusesRequest => self.handle_msg(LoadIssueStatuses, ctx)?, + // projects WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?, WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,