Add custom issue statuses
This commit is contained in:
parent
8ee6566e3b
commit
4f1148dd1a
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
|
@ -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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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>),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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";
|
98
jirs-server/src/db/issue_statuses.rs
Normal file
98
jirs-server/src/db/issue_statuses.rs
Normal 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()))
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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)]
|
||||
|
@ -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,
|
||||
|
23
jirs-server/src/ws/issue_statuses.rs
Normal file
23
jirs-server/src/ws/issue_statuses.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)?,
|
||||
|
Loading…
Reference in New Issue
Block a user