Add custom issue statuses

This commit is contained in:
Adrian Wozniak 2020-05-06 22:24:58 +02:00
parent 8ee6566e3b
commit 4f1148dd1a
20 changed files with 336 additions and 194 deletions

View File

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

View File

@ -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(),

View File

@ -50,14 +50,14 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
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<Msg> {
} = 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();

View File

@ -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<String>,
pub description_text: Option<String>,
@ -144,6 +143,7 @@ pub struct AddIssueModal {
pub project_id: Option<i32>,
pub user_ids: Vec<i32>,
pub reporter_id: Option<i32>,
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<Issue>,
pub users: Vec<User>,
pub comments: Vec<Comment>,
pub issue_statuses: Vec<IssueStatus>,
}
impl Default for Model {
@ -445,6 +446,7 @@ impl Default for Model {
project: None,
comments: vec![],
about_tooltip_visible: false,
issue_statuses: vec![],
}
}
}

View File

@ -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<Issue> = vec![];
@ -254,16 +255,15 @@ fn avatars_filters(model: &Model) -> Node<Msg> {
}
fn project_board_lists(model: &Model) -> Node<Msg> {
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<Node<Msg>> = 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<Msg> {
fn project_issue_list(model: &Model, status: &jirs_data::IssueStatus) -> Node<Msg> {
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<Msg
})
.map(|issue| project_issue(model, issue))
.collect();
let label = status.to_label();
let label = status.name.clone();
let send_status = status;
let send_status = status.id;
let drop_handler = drag_ev(Ev::Drop, move |ev| {
ev.prevent_default();
Msg::IssueDropZone(send_status)
});
let send_status = status;
let send_status = status.id;
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
Msg::IssueDragOverStatus(send_status)
@ -324,8 +324,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
}
#[inline]
fn issue_filter_status(issue: &Issue, status: IssueStatus) -> bool {
issue.status == status
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
issue.issue_status_id == status.id
}
#[inline]

View File

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

View File

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

View File

@ -41,6 +41,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
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();

View File

@ -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<u32> for IssueStatus {
fn into(self) -> u32 {
match self {
IssueStatus::Backlog => 0,
IssueStatus::Selected => 1,
IssueStatus::InProgress => 2,
IssueStatus::Done => 3,
}
}
}
impl Into<IssueStatus> 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<Self, Self::Err> {
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<Self> {
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<String>,
@ -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<i32>,
}
#[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<String>,
@ -562,6 +496,7 @@ pub struct UpdateIssuePayload {
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub issue_status_id: IssueStatusId,
pub user_ids: Vec<UserId>,
}
@ -580,7 +515,6 @@ impl From<Issue> 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<Issue> 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<String>,
pub description_text: Option<String>,
@ -622,6 +556,7 @@ pub struct CreateIssuePayload {
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
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<IssueStatus>),
// comments
IssueCommentsRequest(IssueId),
IssueCommentsLoaded(Vec<Comment>),

View File

@ -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<IssueTypeType, Pg> 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<IssueStatus> {
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<IssueStatusType, Pg> for IssueStatus {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_status_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for IssueStatus {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_status_from_sql(bytes)
}
}
impl ToSql<IssueStatusType, Pg> for IssueStatus {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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;

View File

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

View File

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

View File

@ -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<Vec<IssueStatus>, ServiceErrors>;
}
impl Handler<LoadIssueStatuses> for DbExecutor {
type Result = Result<Vec<IssueStatus>, 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::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.load::<IssueStatus>(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<IssueStatus, ServiceErrors>;
}
impl Handler<CreateIssueStatus> for DbExecutor {
type Result = Result<IssueStatus, ServiceErrors>;
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::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.get_result::<IssueStatus>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
}
}
pub struct DeleteIssueStatus {
pub issue_status_id: IssueStatusId,
}
impl Message for DeleteIssueStatus {
type Result = Result<usize, ServiceErrors>;
}
impl Handler<DeleteIssueStatus> for DbExecutor {
type Result = Result<usize, ServiceErrors>;
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::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.execute(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
}
}

View File

@ -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<String>,
pub issue_type: Option<IssueType>,
pub status: Option<IssueStatus>,
pub priority: Option<IssuePriority>,
pub list_position: Option<i32>,
pub description: Option<String>,
@ -85,6 +84,7 @@ pub struct UpdateIssue {
pub project_id: Option<i32>,
pub user_ids: Option<Vec<i32>>,
pub reporter_id: Option<i32>,
pub issue_status_id: Option<i32>,
}
impl Message for UpdateIssue {
@ -107,7 +107,7 @@ impl Handler<UpdateIssue> 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<DeleteIssue> 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<String>,
pub description_text: Option<String>,
@ -225,7 +225,7 @@ impl Handler<CreateIssue> 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<CreateIssue> 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::<i32>(conn)
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
@ -241,7 +241,7 @@ impl Handler<CreateIssue> 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,

View File

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

View File

@ -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<String>,
@ -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<jirs_data::Issue> for Issue {
@ -41,7 +42,6 @@ impl Into<jirs_data::Issue> 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<jirs_data::Issue> 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<jirs_data::Issue> 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<String>,
@ -72,8 +72,9 @@ pub struct CreateIssueForm {
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 issue_status_id: IssueStatusId,
}
#[derive(Debug, Serialize, Deserialize, Insertable)]

View File

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

View File

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

View File

@ -36,8 +36,8 @@ impl WsHandler<UpdateIssueHandler> 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<CreateIssuePayload> 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,

View File

@ -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)?,