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) => {
|
EditIssueModalSection::Issue(IssueFieldId::Description) => {
|
||||||
f.write_str("descriptionIssueEditModal")
|
f.write_str("descriptionIssueEditModal")
|
||||||
}
|
}
|
||||||
EditIssueModalSection::Issue(IssueFieldId::Status) => {
|
EditIssueModalSection::Issue(IssueFieldId::IssueStatusId) => {
|
||||||
f.write_str("statusIssueEditModal")
|
f.write_str("statusIssueEditModal")
|
||||||
}
|
}
|
||||||
EditIssueModalSection::Issue(IssueFieldId::Assignees) => {
|
EditIssueModalSection::Issue(IssueFieldId::Assignees) => {
|
||||||
@ -98,7 +98,7 @@ impl std::fmt::Display for FieldId {
|
|||||||
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"),
|
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"),
|
||||||
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
|
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
|
||||||
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
|
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::Estimate => f.write_str("addIssueModal-estimate"),
|
||||||
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
|
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
|
||||||
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
|
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
|
||||||
@ -214,8 +214,8 @@ pub enum Msg {
|
|||||||
IssueDragStopped(IssueId),
|
IssueDragStopped(IssueId),
|
||||||
DragLeave(IssueId),
|
DragLeave(IssueId),
|
||||||
ExchangePosition(IssueId),
|
ExchangePosition(IssueId),
|
||||||
IssueDragOverStatus(IssueStatus),
|
IssueDragOverStatus(IssueStatusId),
|
||||||
IssueDropZone(IssueStatus),
|
IssueDropZone(IssueStatusId),
|
||||||
UnlockDragOver,
|
UnlockDragOver,
|
||||||
|
|
||||||
// inputs
|
// inputs
|
||||||
|
@ -39,7 +39,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
|||||||
let payload = jirs_data::CreateIssuePayload {
|
let payload = jirs_data::CreateIssuePayload {
|
||||||
title: modal.title.clone(),
|
title: modal.title.clone(),
|
||||||
issue_type: modal.issue_type,
|
issue_type: modal.issue_type,
|
||||||
status: modal.status,
|
issue_status_id: modal.issue_status_id,
|
||||||
priority: modal.priority,
|
priority: modal.priority,
|
||||||
description: modal.description.clone(),
|
description: modal.description.clone(),
|
||||||
description_text: modal.description_text.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(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Status)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||||
StyledSelectChange::Changed(value),
|
StyledSelectChange::Changed(value),
|
||||||
) => {
|
) => {
|
||||||
modal.payload.status = (*value).into();
|
modal.payload.issue_status_id = *value as IssueStatusId;
|
||||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||||
modal.id,
|
modal.id,
|
||||||
IssueFieldId::Status,
|
IssueFieldId::IssueStatusId,
|
||||||
PayloadVariant::IssueStatus(modal.payload.status),
|
PayloadVariant::I32(modal.payload.issue_status_id),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
@ -599,19 +599,27 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
|||||||
} = modal;
|
} = modal;
|
||||||
|
|
||||||
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||||
IssueFieldId::Status,
|
IssueFieldId::IssueStatusId,
|
||||||
)))
|
)))
|
||||||
.name("status")
|
.name("status")
|
||||||
.opened(status_state.opened)
|
.opened(status_state.opened)
|
||||||
.normal()
|
.normal()
|
||||||
.text_filter(status_state.text_filter.as_str())
|
.text_filter(status_state.text_filter.as_str())
|
||||||
.options(
|
.options(
|
||||||
IssueStatus::ordered()
|
model
|
||||||
.into_iter()
|
.issue_statuses
|
||||||
|
.iter()
|
||||||
.map(|opt| opt.to_child().name("status"))
|
.map(|opt| opt.to_child().name("status"))
|
||||||
.collect(),
|
.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)
|
.valid(true)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
@ -61,7 +61,7 @@ impl EditIssueModal {
|
|||||||
payload: UpdateIssuePayload {
|
payload: UpdateIssuePayload {
|
||||||
title: issue.title.clone(),
|
title: issue.title.clone(),
|
||||||
issue_type: issue.issue_type,
|
issue_type: issue.issue_type,
|
||||||
status: issue.status,
|
issue_status_id: issue.issue_status_id,
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
list_position: issue.list_position,
|
list_position: issue.list_position,
|
||||||
description: issue.description.clone(),
|
description: issue.description.clone(),
|
||||||
@ -78,8 +78,8 @@ impl EditIssueModal {
|
|||||||
issue.estimate.map(|v| vec![v as u32]).unwrap_or_default(),
|
issue.estimate.map(|v| vec![v as u32]).unwrap_or_default(),
|
||||||
),
|
),
|
||||||
status_state: StyledSelectState::new(
|
status_state: StyledSelectState::new(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Status)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||||
vec![issue.status.into()],
|
vec![issue.issue_status_id as u32],
|
||||||
),
|
),
|
||||||
reporter_state: StyledSelectState::new(
|
reporter_state: StyledSelectState::new(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||||
@ -134,7 +134,6 @@ impl EditIssueModal {
|
|||||||
pub struct AddIssueModal {
|
pub struct AddIssueModal {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub description_text: Option<String>,
|
pub description_text: Option<String>,
|
||||||
@ -144,6 +143,7 @@ pub struct AddIssueModal {
|
|||||||
pub project_id: Option<i32>,
|
pub project_id: Option<i32>,
|
||||||
pub user_ids: Vec<i32>,
|
pub user_ids: Vec<i32>,
|
||||||
pub reporter_id: Option<i32>,
|
pub reporter_id: Option<i32>,
|
||||||
|
pub issue_status_id: i32,
|
||||||
|
|
||||||
// modal fields
|
// modal fields
|
||||||
pub type_state: StyledSelectState,
|
pub type_state: StyledSelectState,
|
||||||
@ -157,7 +157,6 @@ impl Default for AddIssueModal {
|
|||||||
Self {
|
Self {
|
||||||
title: Default::default(),
|
title: Default::default(),
|
||||||
issue_type: Default::default(),
|
issue_type: Default::default(),
|
||||||
status: Default::default(),
|
|
||||||
priority: Default::default(),
|
priority: Default::default(),
|
||||||
description: Default::default(),
|
description: Default::default(),
|
||||||
description_text: Default::default(),
|
description_text: Default::default(),
|
||||||
@ -167,6 +166,7 @@ impl Default for AddIssueModal {
|
|||||||
project_id: Default::default(),
|
project_id: Default::default(),
|
||||||
user_ids: Default::default(),
|
user_ids: Default::default(),
|
||||||
reporter_id: Default::default(),
|
reporter_id: Default::default(),
|
||||||
|
issue_status_id: Default::default(),
|
||||||
type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]),
|
type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]),
|
||||||
reporter_state: StyledSelectState::new(
|
reporter_state: StyledSelectState::new(
|
||||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||||
@ -424,6 +424,7 @@ pub struct Model {
|
|||||||
pub issues: Vec<Issue>,
|
pub issues: Vec<Issue>,
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
pub comments: Vec<Comment>,
|
pub comments: Vec<Comment>,
|
||||||
|
pub issue_statuses: Vec<IssueStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Model {
|
impl Default for Model {
|
||||||
@ -445,6 +446,7 @@ impl Default for Model {
|
|||||||
project: None,
|
project: None,
|
||||||
comments: vec![],
|
comments: vec![],
|
||||||
about_tooltip_visible: false,
|
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::ProjectRequest);
|
||||||
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest);
|
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest);
|
||||||
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest);
|
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest);
|
||||||
|
send_ws_msg(jirs_data::WsMsg::IssueStatusesRequest);
|
||||||
}
|
}
|
||||||
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
|
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
|
||||||
let mut old: Vec<Issue> = vec![];
|
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> {
|
fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||||
div![
|
let columns: Vec<Node<Msg>> = model
|
||||||
id!["projectBoardLists"],
|
.issue_statuses
|
||||||
project_issue_list(model, IssueStatus::Backlog),
|
.iter()
|
||||||
project_issue_list(model, IssueStatus::Selected),
|
.map(|is| project_issue_list(model, is))
|
||||||
project_issue_list(model, IssueStatus::InProgress),
|
.collect();
|
||||||
project_issue_list(model, IssueStatus::Done),
|
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 {
|
let project_page = match &model.page_content {
|
||||||
PageContent::Project(project_page) => project_page,
|
PageContent::Project(project_page) => project_page,
|
||||||
_ => return empty![],
|
_ => return empty![],
|
||||||
@ -293,15 +293,15 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
|
|||||||
})
|
})
|
||||||
.map(|issue| project_issue(model, issue))
|
.map(|issue| project_issue(model, issue))
|
||||||
.collect();
|
.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| {
|
let drop_handler = drag_ev(Ev::Drop, move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Msg::IssueDropZone(send_status)
|
Msg::IssueDropZone(send_status)
|
||||||
});
|
});
|
||||||
|
|
||||||
let send_status = status;
|
let send_status = status.id;
|
||||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
Msg::IssueDragOverStatus(send_status)
|
Msg::IssueDragOverStatus(send_status)
|
||||||
@ -324,8 +324,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn issue_filter_status(issue: &Issue, status: IssueStatus) -> bool {
|
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
|
||||||
issue.status == status
|
issue.issue_status_id == status.id
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
@ -214,12 +214,12 @@ impl ToChild for jirs_data::IssueStatus {
|
|||||||
type Builder = StyledSelectChildBuilder;
|
type Builder = StyledSelectChildBuilder;
|
||||||
|
|
||||||
fn to_child(&self) -> StyledSelectChildBuilder {
|
fn to_child(&self) -> StyledSelectChildBuilder {
|
||||||
let text = self.to_label();
|
let text = &self.name;
|
||||||
|
|
||||||
StyledSelectChild::build()
|
StyledSelectChild::build()
|
||||||
.value(self.clone().into())
|
.value(self.id as u32)
|
||||||
.add_class(text)
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if dragged.status != below.status {
|
if dragged.issue_status_id != below.issue_status_id {
|
||||||
let mut issues = vec![];
|
let mut issues = vec![];
|
||||||
std::mem::swap(&mut issues, &mut model.issues);
|
std::mem::swap(&mut issues, &mut model.issues);
|
||||||
for mut c in issues.into_iter() {
|
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;
|
c.list_position += 1;
|
||||||
mark_dirty(c.id, project_page);
|
mark_dirty(c.id, project_page);
|
||||||
}
|
}
|
||||||
model.issues.push(c);
|
model.issues.push(c);
|
||||||
}
|
}
|
||||||
dragged.list_position = below.list_position + 1;
|
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);
|
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(
|
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||||
issue.id,
|
issue.id,
|
||||||
IssueFieldId::Status,
|
IssueFieldId::IssueStatusId,
|
||||||
PayloadVariant::IssueStatus(issue.status),
|
PayloadVariant::I32(issue.issue_status_id),
|
||||||
));
|
));
|
||||||
send_ws_msg(WsMsg::IssueUpdateRequest(
|
send_ws_msg(WsMsg::IssueUpdateRequest(
|
||||||
issue.id,
|
issue.id,
|
||||||
@ -109,7 +109,7 @@ pub fn sync(model: &mut Model) {
|
|||||||
project_page.dirty_issues.clear();
|
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 {
|
let project_page = match &mut model.page_content {
|
||||||
PageContent::Project(project_page) => project_page,
|
PageContent::Project(project_page) => project_page,
|
||||||
_ => return,
|
_ => 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));
|
old.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||||
|
|
||||||
for mut issue in old.into_iter() {
|
for mut issue in old.into_iter() {
|
||||||
if issue.status == status {
|
if issue.issue_status_id == status_id {
|
||||||
if issue.list_position != pos {
|
if issue.list_position != pos {
|
||||||
issue.list_position = pos;
|
issue.list_position = pos;
|
||||||
mark_dirty(issue.id, project_page);
|
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);
|
model.issues.push(issue);
|
||||||
} else {
|
} else {
|
||||||
issue.status = status;
|
issue.issue_status_id = status_id;
|
||||||
issue.list_position = pos + 1;
|
issue.list_position = pos + 1;
|
||||||
model.issues.push(issue);
|
model.issues.push(issue);
|
||||||
mark_dirty(issue_id, project_page);
|
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)));
|
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64)));
|
||||||
model.issues = v;
|
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
|
// users
|
||||||
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
|
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
|
||||||
model.users = v.clone();
|
model.users = v.clone();
|
||||||
|
@ -23,6 +23,7 @@ pub type ProjectId = i32;
|
|||||||
pub type UserId = i32;
|
pub type UserId = i32;
|
||||||
pub type CommentId = i32;
|
pub type CommentId = i32;
|
||||||
pub type TokenId = i32;
|
pub type TokenId = i32;
|
||||||
|
pub type IssueStatusId = i32;
|
||||||
pub type InvitationId = i32;
|
pub type InvitationId = i32;
|
||||||
pub type EmailString = String;
|
pub type EmailString = String;
|
||||||
pub type UsernameString = 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", derive(FromSqlRow, AsExpression))]
|
||||||
#[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")]
|
#[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")]
|
||||||
#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
|
#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
|
||||||
@ -482,7 +406,6 @@ pub struct Issue {
|
|||||||
pub id: IssueId,
|
pub id: IssueId,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub list_position: i32,
|
pub list_position: i32,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@ -494,10 +417,22 @@ pub struct Issue {
|
|||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
|
pub issue_status_id: IssueStatusId,
|
||||||
|
|
||||||
pub user_ids: Vec<i32>,
|
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))]
|
#[cfg_attr(feature = "backend", derive(Queryable))]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Invitation {
|
pub struct Invitation {
|
||||||
@ -552,7 +487,6 @@ pub struct Token {
|
|||||||
pub struct UpdateIssuePayload {
|
pub struct UpdateIssuePayload {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub list_position: i32,
|
pub list_position: i32,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@ -562,6 +496,7 @@ pub struct UpdateIssuePayload {
|
|||||||
pub time_remaining: Option<i32>,
|
pub time_remaining: Option<i32>,
|
||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub reporter_id: UserId,
|
pub reporter_id: UserId,
|
||||||
|
pub issue_status_id: IssueStatusId,
|
||||||
pub user_ids: Vec<UserId>,
|
pub user_ids: Vec<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,7 +515,6 @@ impl From<Issue> for UpdateIssuePayload {
|
|||||||
Self {
|
Self {
|
||||||
title: issue.title,
|
title: issue.title,
|
||||||
issue_type: issue.issue_type,
|
issue_type: issue.issue_type,
|
||||||
status: issue.status,
|
|
||||||
priority: issue.priority,
|
priority: issue.priority,
|
||||||
list_position: issue.list_position,
|
list_position: issue.list_position,
|
||||||
description: issue.description,
|
description: issue.description,
|
||||||
@ -591,6 +525,7 @@ impl From<Issue> for UpdateIssuePayload {
|
|||||||
project_id: issue.project_id,
|
project_id: issue.project_id,
|
||||||
reporter_id: issue.reporter_id,
|
reporter_id: issue.reporter_id,
|
||||||
user_ids: issue.user_ids,
|
user_ids: issue.user_ids,
|
||||||
|
issue_status_id: issue.issue_status_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -612,7 +547,6 @@ pub struct UpdateCommentPayload {
|
|||||||
pub struct CreateIssuePayload {
|
pub struct CreateIssuePayload {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub description_text: Option<String>,
|
pub description_text: Option<String>,
|
||||||
@ -622,6 +556,7 @@ pub struct CreateIssuePayload {
|
|||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub user_ids: Vec<UserId>,
|
pub user_ids: Vec<UserId>,
|
||||||
pub reporter_id: UserId,
|
pub reporter_id: UserId,
|
||||||
|
pub issue_status_id: IssueStatusId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
@ -641,7 +576,6 @@ pub enum PayloadVariant {
|
|||||||
I32(i32),
|
I32(i32),
|
||||||
String(String),
|
String(String),
|
||||||
IssueType(IssueType),
|
IssueType(IssueType),
|
||||||
IssueStatus(IssueStatus),
|
|
||||||
IssuePriority(IssuePriority),
|
IssuePriority(IssuePriority),
|
||||||
ProjectCategory(ProjectCategory),
|
ProjectCategory(ProjectCategory),
|
||||||
}
|
}
|
||||||
@ -691,7 +625,6 @@ pub enum IssueFieldId {
|
|||||||
Type,
|
Type,
|
||||||
Title,
|
Title,
|
||||||
Description,
|
Description,
|
||||||
Status,
|
|
||||||
ListPosition,
|
ListPosition,
|
||||||
Assignees,
|
Assignees,
|
||||||
Reporter,
|
Reporter,
|
||||||
@ -699,6 +632,7 @@ pub enum IssueFieldId {
|
|||||||
Estimate,
|
Estimate,
|
||||||
TimeSpent,
|
TimeSpent,
|
||||||
TimeRemaining,
|
TimeRemaining,
|
||||||
|
IssueStatusId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
@ -760,6 +694,10 @@ pub enum WsMsg {
|
|||||||
IssueCreateRequest(CreateIssuePayload),
|
IssueCreateRequest(CreateIssuePayload),
|
||||||
IssueCreated(Issue),
|
IssueCreated(Issue),
|
||||||
|
|
||||||
|
// issue status
|
||||||
|
IssueStatusesRequest,
|
||||||
|
IssueStatusesResponse(Vec<IssueStatus>),
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
IssueCommentsRequest(IssueId),
|
IssueCommentsRequest(IssueId),
|
||||||
IssueCommentsLoaded(Vec<Comment>),
|
IssueCommentsLoaded(Vec<Comment>),
|
||||||
|
@ -2,9 +2,7 @@ use std::io::Write;
|
|||||||
|
|
||||||
use diesel::{deserialize::*, pg::*, serialize::*, *};
|
use diesel::{deserialize::*, pg::*, serialize::*, *};
|
||||||
|
|
||||||
use crate::{
|
use crate::{InvitationState, IssuePriority, IssueType, ProjectCategory, TimeTracking, UserRole};
|
||||||
InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking, UserRole,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(SqlType)]
|
#[derive(SqlType)]
|
||||||
#[postgres(type_name = "IssuePriorityType")]
|
#[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)]
|
#[derive(SqlType)]
|
||||||
#[postgres(type_name = "ProjectCategoryType")]
|
#[postgres(type_name = "ProjectCategoryType")]
|
||||||
pub struct 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 diesel::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use jirs_data::{IssuePriority, IssueStatus, IssueType};
|
use jirs_data::{IssuePriority, IssueStatusId, IssueType};
|
||||||
|
|
||||||
use crate::db::DbExecutor;
|
use crate::db::DbExecutor;
|
||||||
use crate::errors::ServiceErrors;
|
use crate::errors::ServiceErrors;
|
||||||
@ -74,7 +74,6 @@ pub struct UpdateIssue {
|
|||||||
pub issue_id: i32,
|
pub issue_id: i32,
|
||||||
pub title: Option<String>,
|
pub title: Option<String>,
|
||||||
pub issue_type: Option<IssueType>,
|
pub issue_type: Option<IssueType>,
|
||||||
pub status: Option<IssueStatus>,
|
|
||||||
pub priority: Option<IssuePriority>,
|
pub priority: Option<IssuePriority>,
|
||||||
pub list_position: Option<i32>,
|
pub list_position: Option<i32>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@ -85,6 +84,7 @@ pub struct UpdateIssue {
|
|||||||
pub project_id: Option<i32>,
|
pub project_id: Option<i32>,
|
||||||
pub user_ids: Option<Vec<i32>>,
|
pub user_ids: Option<Vec<i32>>,
|
||||||
pub reporter_id: Option<i32>,
|
pub reporter_id: Option<i32>,
|
||||||
|
pub issue_status_id: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message for UpdateIssue {
|
impl Message for UpdateIssue {
|
||||||
@ -107,7 +107,7 @@ impl Handler<UpdateIssue> for DbExecutor {
|
|||||||
msg.title.map(|title| dsl::title.eq(title)),
|
msg.title.map(|title| dsl::title.eq(title)),
|
||||||
msg.issue_type
|
msg.issue_type
|
||||||
.map(|issue_type| dsl::issue_type.eq(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.priority.map(|priority| dsl::priority.eq(priority)),
|
||||||
msg.list_position
|
msg.list_position
|
||||||
.map(|list_position| dsl::list_position.eq(list_position)),
|
.map(|list_position| dsl::list_position.eq(list_position)),
|
||||||
@ -204,7 +204,7 @@ impl Handler<DeleteIssue> for DbExecutor {
|
|||||||
pub struct CreateIssue {
|
pub struct CreateIssue {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
pub issue_status_id: IssueStatusId,
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub description_text: 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 {
|
fn handle(&mut self, msg: CreateIssue, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
use crate::schema::issue_assignees::dsl;
|
use crate::schema::issue_assignees::dsl;
|
||||||
use crate::schema::issues::dsl::{issues, status};
|
use crate::schema::issues::dsl::issues;
|
||||||
|
|
||||||
let conn = &self
|
let conn = &self
|
||||||
.pool
|
.pool
|
||||||
@ -233,7 +233,7 @@ impl Handler<CreateIssue> for DbExecutor {
|
|||||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||||
|
|
||||||
let list_position = issues
|
let list_position = issues
|
||||||
.filter(status.eq(IssueStatus::Backlog))
|
// .filter(issue_status_id.eq(IssueStatus::Backlog))
|
||||||
.select(sql("max(list_position) + 1"))
|
.select(sql("max(list_position) + 1"))
|
||||||
.get_result::<i32>(conn)
|
.get_result::<i32>(conn)
|
||||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||||
@ -241,7 +241,7 @@ impl Handler<CreateIssue> for DbExecutor {
|
|||||||
let form = crate::models::CreateIssueForm {
|
let form = crate::models::CreateIssueForm {
|
||||||
title: msg.title,
|
title: msg.title,
|
||||||
issue_type: msg.issue_type,
|
issue_type: msg.issue_type,
|
||||||
status: msg.status,
|
issue_status_id: msg.issue_status_id,
|
||||||
priority: msg.priority,
|
priority: msg.priority,
|
||||||
list_position,
|
list_position,
|
||||||
description: msg.description,
|
description: msg.description,
|
||||||
|
@ -13,6 +13,7 @@ pub mod authorize_user;
|
|||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod invitations;
|
pub mod invitations;
|
||||||
pub mod issue_assignees;
|
pub mod issue_assignees;
|
||||||
|
pub mod issue_statuses;
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod tokens;
|
pub mod tokens;
|
||||||
|
@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use jirs_data::{
|
use jirs_data::{
|
||||||
InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking,
|
InvitationState, IssuePriority, IssueStatusId, IssueType, ProjectCategory, ProjectId,
|
||||||
|
TimeTracking, UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::schema::*;
|
use crate::schema::*;
|
||||||
@ -21,7 +22,6 @@ pub struct Issue {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub list_position: i32,
|
pub list_position: i32,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@ -33,6 +33,7 @@ pub struct Issue {
|
|||||||
pub project_id: i32,
|
pub project_id: i32,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
|
pub issue_status_id: IssueStatusId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<jirs_data::Issue> for Issue {
|
impl Into<jirs_data::Issue> for Issue {
|
||||||
@ -41,7 +42,6 @@ impl Into<jirs_data::Issue> for Issue {
|
|||||||
id: self.id,
|
id: self.id,
|
||||||
title: self.title,
|
title: self.title,
|
||||||
issue_type: self.issue_type,
|
issue_type: self.issue_type,
|
||||||
status: self.status,
|
|
||||||
priority: self.priority,
|
priority: self.priority,
|
||||||
list_position: self.list_position,
|
list_position: self.list_position,
|
||||||
description: self.description,
|
description: self.description,
|
||||||
@ -53,6 +53,7 @@ impl Into<jirs_data::Issue> for Issue {
|
|||||||
project_id: self.project_id,
|
project_id: self.project_id,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
updated_at: self.updated_at,
|
updated_at: self.updated_at,
|
||||||
|
issue_status_id: self.issue_status_id,
|
||||||
|
|
||||||
user_ids: vec![],
|
user_ids: vec![],
|
||||||
}
|
}
|
||||||
@ -64,7 +65,6 @@ impl Into<jirs_data::Issue> for Issue {
|
|||||||
pub struct CreateIssueForm {
|
pub struct CreateIssueForm {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub issue_type: IssueType,
|
pub issue_type: IssueType,
|
||||||
pub status: IssueStatus,
|
|
||||||
pub priority: IssuePriority,
|
pub priority: IssuePriority,
|
||||||
pub list_position: i32,
|
pub list_position: i32,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
@ -72,8 +72,9 @@ pub struct CreateIssueForm {
|
|||||||
pub estimate: Option<i32>,
|
pub estimate: Option<i32>,
|
||||||
pub time_spent: Option<i32>,
|
pub time_spent: Option<i32>,
|
||||||
pub time_remaining: Option<i32>,
|
pub time_remaining: Option<i32>,
|
||||||
pub reporter_id: i32,
|
pub reporter_id: UserId,
|
||||||
pub project_id: i32,
|
pub project_id: ProjectId,
|
||||||
|
pub issue_status_id: IssueStatusId,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
#[derive(Debug, Serialize, Deserialize, Insertable)]
|
||||||
|
@ -179,12 +179,6 @@ table! {
|
|||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
issue_type -> IssueTypeType,
|
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.
|
/// The `priority` column of the `issues` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `IssuePriorityType`.
|
/// Its SQL type is `IssuePriorityType`.
|
||||||
@ -251,6 +245,59 @@ table! {
|
|||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
updated_at -> Timestamp,
|
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!(invitations -> users (invited_by_id));
|
||||||
joinable!(issue_assignees -> issues (issue_id));
|
joinable!(issue_assignees -> issues (issue_id));
|
||||||
joinable!(issue_assignees -> users (user_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 -> projects (project_id));
|
||||||
joinable!(issues -> users (reporter_id));
|
joinable!(issues -> users (reporter_id));
|
||||||
joinable!(tokens -> users (user_id));
|
joinable!(tokens -> users (user_id));
|
||||||
@ -441,6 +490,7 @@ allow_tables_to_appear_in_same_query!(
|
|||||||
invitations,
|
invitations,
|
||||||
issue_assignees,
|
issue_assignees,
|
||||||
issues,
|
issues,
|
||||||
|
issue_statuses,
|
||||||
projects,
|
projects,
|
||||||
tokens,
|
tokens,
|
||||||
users,
|
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)) => {
|
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
||||||
msg.description = Some(s);
|
msg.description = Some(s);
|
||||||
}
|
}
|
||||||
(IssueFieldId::Status, PayloadVariant::IssueStatus(s)) => {
|
(IssueFieldId::IssueStatusId, PayloadVariant::I32(s)) => {
|
||||||
msg.status = Some(s);
|
msg.issue_status_id = Some(s);
|
||||||
}
|
}
|
||||||
(IssueFieldId::ListPosition, PayloadVariant::I32(i)) => {
|
(IssueFieldId::ListPosition, PayloadVariant::I32(i)) => {
|
||||||
msg.list_position = Some(i);
|
msg.list_position = Some(i);
|
||||||
@ -89,7 +89,7 @@ impl WsHandler<CreateIssuePayload> for WebSocketActor {
|
|||||||
let msg = crate::db::issues::CreateIssue {
|
let msg = crate::db::issues::CreateIssue {
|
||||||
title: msg.title,
|
title: msg.title,
|
||||||
issue_type: msg.issue_type,
|
issue_type: msg.issue_type,
|
||||||
status: msg.status,
|
issue_status_id: msg.issue_status_id,
|
||||||
priority: msg.priority,
|
priority: msg.priority,
|
||||||
description: msg.description,
|
description: msg.description,
|
||||||
description_text: msg.description_text,
|
description_text: msg.description_text,
|
||||||
|
@ -14,6 +14,7 @@ use crate::mail::MailExecutor;
|
|||||||
use crate::ws::auth::*;
|
use crate::ws::auth::*;
|
||||||
use crate::ws::comments::*;
|
use crate::ws::comments::*;
|
||||||
use crate::ws::invitations::*;
|
use crate::ws::invitations::*;
|
||||||
|
use crate::ws::issue_statuses::*;
|
||||||
use crate::ws::issues::*;
|
use crate::ws::issues::*;
|
||||||
use crate::ws::projects::*;
|
use crate::ws::projects::*;
|
||||||
use crate::ws::users::*;
|
use crate::ws::users::*;
|
||||||
@ -21,6 +22,7 @@ use crate::ws::users::*;
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod comments;
|
pub mod comments;
|
||||||
pub mod invitations;
|
pub mod invitations;
|
||||||
|
pub mod issue_statuses;
|
||||||
pub mod issues;
|
pub mod issues;
|
||||||
pub mod projects;
|
pub mod projects;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
@ -81,7 +83,7 @@ impl WebSocketActor {
|
|||||||
WsMsg::Ping => Some(WsMsg::Pong),
|
WsMsg::Ping => Some(WsMsg::Pong),
|
||||||
WsMsg::Pong => Some(WsMsg::Ping),
|
WsMsg::Pong => Some(WsMsg::Ping),
|
||||||
|
|
||||||
// Issues
|
// issues
|
||||||
WsMsg::IssueUpdateRequest(id, field_id, payload) => self.handle_msg(
|
WsMsg::IssueUpdateRequest(id, field_id, payload) => self.handle_msg(
|
||||||
UpdateIssueHandler {
|
UpdateIssueHandler {
|
||||||
id,
|
id,
|
||||||
@ -94,6 +96,9 @@ impl WebSocketActor {
|
|||||||
WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
|
WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
|
||||||
WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?,
|
WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?,
|
||||||
|
|
||||||
|
// issue statuses
|
||||||
|
WsMsg::IssueStatusesRequest => self.handle_msg(LoadIssueStatuses, ctx)?,
|
||||||
|
|
||||||
// projects
|
// projects
|
||||||
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
|
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
|
||||||
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
|
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
|
||||||
|
Loading…
Reference in New Issue
Block a user