Add issue status management

This commit is contained in:
Adrian Woźniak 2020-05-07 17:08:40 +02:00
parent 8178483019
commit b47009635c
18 changed files with 713 additions and 120 deletions

95
jirs-client/dev/logo.svg Normal file
View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet" viewBox="0 0 640 640" width="640" height="640">
<defs>
<path d="M500 300C500 410.38 410.38 500 300 500C189.62 500 100 410.38 100 300C100 189.61 189.62 100 300 100C410.38 100 500 189.61 500 300Z"
id="b6KNxjEO2"></path>
<mask id="maskb3oIvRAxi1" x="78" y="78" width="444" height="444" maskUnits="userSpaceOnUse">
<rect x="78" y="78" width="444" height="444" fill="white"></rect>
<use xlink:href="#b6KNxjEO2" opacity="0.46" fill="black"></use>
</mask>
<path d="M520 338.18C520 448.56 430.38 538.18 320 538.18C209.62 538.18 120 448.56 120 338.18C120 227.8 209.62 138.18 320 138.18C430.38 138.18 520 227.8 520 338.18Z"
id="bDKGuSkBj"></path>
<mask id="maskb4LRewzUS" x="98" y="116.18" width="444" height="444" maskUnits="userSpaceOnUse">
<rect x="98" y="116.18" width="444" height="444" fill="white"></rect>
<use xlink:href="#bDKGuSkBj" opacity="0.46" fill="black"></use>
</mask>
<path d="M543.03 374.84C543.03 485.23 453.41 574.84 343.03 574.84C232.65 574.84 143.03 485.23 143.03 374.84C143.03 264.46 232.65 174.84 343.03 174.84C453.41 174.84 543.03 264.46 543.03 374.84Z"
id="a1k8tJrWR3"></path>
<mask id="maskbctn2Bw0" x="121.03" y="152.84" width="444" height="444" maskUnits="userSpaceOnUse">
<rect x="121.03" y="152.84" width="444" height="444" fill="white"></rect>
<use xlink:href="#a1k8tJrWR3" opacity="0.46" fill="black"></use>
</mask>
</defs>
<g>
<g>
<g>
<g>
<filter id="shadow16022051" x="78" y="78" width="458" height="454" filterUnits="userSpaceOnUse"
primitiveUnits="userSpaceOnUse">
<feFlood></feFlood>
<feComposite in2="SourceAlpha" operator="in"></feComposite>
<feGaussianBlur stdDeviation="1"></feGaussianBlur>
<feOffset dx="14" dy="10" result="afterOffset"></feOffset>
<feFlood flood-color="#0d0e44" flood-opacity="0.5"></feFlood>
<feComposite in2="afterOffset" operator="in"></feComposite>
<feMorphology operator="dilate" radius="1"></feMorphology>
<feComposite in2="SourceAlpha" operator="out"></feComposite>
</filter>
<path d="M500 300C500 410.38 410.38 500 300 500C189.62 500 100 410.38 100 300C100 189.61 189.62 100 300 100C410.38 100 500 189.61 500 300Z"
id="h3zi0EZA0A" fill="white" fill-opacity="1" filter="url(#shadow16022051)"></path>
</g>
<use xlink:href="#b6KNxjEO2" opacity="0.46" fill="#fefefe" fill-opacity="1"></use>
<g mask="url(#maskb3oIvRAxi1)">
<use xlink:href="#b6KNxjEO2" opacity="0.46" fill-opacity="0" stroke="#06697d" stroke-width="22"
stroke-opacity="1"></use>
</g>
</g>
<g>
<g>
<filter id="shadow11664684" x="98" y="116.18" width="458" height="454" filterUnits="userSpaceOnUse"
primitiveUnits="userSpaceOnUse">
<feFlood></feFlood>
<feComposite in2="SourceAlpha" operator="in"></feComposite>
<feGaussianBlur stdDeviation="1"></feGaussianBlur>
<feOffset dx="14" dy="10" result="afterOffset"></feOffset>
<feFlood flood-color="#0c0e43" flood-opacity="0.5"></feFlood>
<feComposite in2="afterOffset" operator="in"></feComposite>
<feMorphology operator="dilate" radius="1"></feMorphology>
<feComposite in2="SourceAlpha" operator="out"></feComposite>
</filter>
<path d="M520 338.18C520 448.56 430.38 538.18 320 538.18C209.62 538.18 120 448.56 120 338.18C120 227.8 209.62 138.18 320 138.18C430.38 138.18 520 227.8 520 338.18Z"
id="mzwSl44s1" fill="white" fill-opacity="1" filter="url(#shadow11664684)"></path>
</g>
<use xlink:href="#bDKGuSkBj" opacity="0.46" fill="#fefefe" fill-opacity="1"></use>
<g mask="url(#maskb4LRewzUS)">
<use xlink:href="#bDKGuSkBj" opacity="0.46" fill-opacity="0" stroke="#06697d" stroke-width="22"
stroke-opacity="1"></use>
</g>
</g>
<g>
<g>
<filter id="shadow13224340" x="121.03" y="152.84" width="458" height="454"
filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse">
<feFlood></feFlood>
<feComposite in2="SourceAlpha" operator="in"></feComposite>
<feGaussianBlur stdDeviation="1"></feGaussianBlur>
<feOffset dx="14" dy="10" result="afterOffset"></feOffset>
<feFlood flood-color="#0c0e43" flood-opacity="0.5"></feFlood>
<feComposite in2="afterOffset" operator="in"></feComposite>
<feMorphology operator="dilate" radius="1"></feMorphology>
<feComposite in2="SourceAlpha" operator="out"></feComposite>
</filter>
<path d="M543.03 374.84C543.03 485.23 453.41 574.84 343.03 574.84C232.65 574.84 143.03 485.23 143.03 374.84C143.03 264.46 232.65 174.84 343.03 174.84C453.41 174.84 543.03 264.46 543.03 374.84Z"
id="c16Px1v9IX" fill="white" fill-opacity="1" filter="url(#shadow13224340)"></path>
</g>
<use xlink:href="#a1k8tJrWR3" opacity="0.46" fill="#fefefe" fill-opacity="1"></use>
<g mask="url(#maskbctn2Bw0)">
<use xlink:href="#a1k8tJrWR3" opacity="0.46" fill-opacity="0" stroke="#06697d" stroke-width="22"
stroke-opacity="1"></use>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -4,10 +4,48 @@
} }
#projectSettings > .formContainer .styledForm { #projectSettings > .formContainer .styledForm {
max-width: 720px; max-width: 1024px;
width: 100%; width: 100%;
} }
#projectSettings > .formContainer .styledForm > .formElement > .actionButton { #projectSettings > .formContainer .styledForm > .formElement > .actionButton {
margin-top: 30px; margin-top: 30px;
} }
#projectSettings > .formContainer .styledForm > .formElement > .styledField.columnsField > .styledLabel {
font-size: 14px;
}
#projectSettings > .formContainer .styledForm > .formElement > .styledField > .columnsSection > .columns {
display: flex;
flex-direction: row;
justify-content: space-around;
}
#projectSettings > .formContainer .styledForm > .formElement > .styledField > .columnsSection > .columns > .columnPreview {
width: auto;
min-height: 60px;
background: var(--backgroundLightest);
margin: 0 5px;
}
#projectSettings > .formContainer .styledForm > .formElement > .styledField > .columnsSection > .columns > .columnPreview > .columnName {
display: block;
margin: 0 5px;
padding: 13px 10px 17px;
text-transform: uppercase;
color: var(--textMedium);
font-size: 12.5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
user-select: none;
}
#projectSettings > .formContainer .styledForm > .formElement > .styledField > .columnsSection > .columns > .columnPreview > .columnName.addColumn,
#projectSettings > .formContainer .styledForm > .formElement > .styledField > .columnsSection > .columns > .columnPreview > .columnName.addColumn > i {
color: var(--textMedium);
text-align: center;
cursor: pointer;
}

View File

@ -112,6 +112,7 @@ impl std::fmt::Display for FieldId {
ProjectFieldId::Description => f.write_str("projectSettings-description"), ProjectFieldId::Description => f.write_str("projectSettings-description"),
ProjectFieldId::Category => f.write_str("projectSettings-category"), ProjectFieldId::Category => f.write_str("projectSettings-category"),
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"), ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"),
ProjectFieldId::IssueStatusName => f.write_str("projectSettings-issueStatusName"),
}, },
FieldId::SignIn(sub) => match sub { FieldId::SignIn(sub) => match sub {
SignInFieldId::Email => f.write_str("login-email"), SignInFieldId::Email => f.write_str("login-email"),
@ -149,6 +150,17 @@ pub enum FieldChange {
EditComment(FieldId, i32), EditComment(FieldId, i32),
} }
#[derive(Clone, Debug, PartialEq)]
pub enum BoardPageChange {
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
DragLeave(IssueId),
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId),
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum UsersPageChange { pub enum UsersPageChange {
ResetForm, ResetForm,
@ -158,6 +170,15 @@ pub enum UsersPageChange {
pub enum ProjectPageChange { pub enum ProjectPageChange {
ResetForm, ResetForm,
SubmitForm, SubmitForm,
// dragging
ColumnDragStarted(IssueStatusId),
ColumnDragStopped(IssueStatusId),
ColumnDragLeave(IssueStatusId),
ColumnExchangePosition(IssueStatusId),
ColumnDragOverStatus(IssueStatusId),
ColumnDropZone(IssueStatusId),
// edit issue status name
EditIssueStatusName(Option<IssueStatusId>),
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -170,6 +191,7 @@ pub enum PageChanged {
Users(UsersPageChange), Users(UsersPageChange),
ProjectSettings(ProjectPageChange), ProjectSettings(ProjectPageChange),
Profile(ProfilePageChange), Profile(ProfilePageChange),
Board(BoardPageChange),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -209,15 +231,6 @@ pub enum Msg {
ProjectToggleRecentlyUpdated, ProjectToggleRecentlyUpdated,
ProjectClearFilters, ProjectClearFilters,
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
DragLeave(IssueId),
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId),
UnlockDragOver,
// inputs // inputs
StrInputChanged(FieldId, String), StrInputChanged(FieldId, String),
U32InputChanged(FieldId, u32), U32InputChanged(FieldId, u32),

View File

@ -6,6 +6,7 @@ use uuid::Uuid;
use jirs_data::*; use jirs_data::*;
use crate::modal::time_tracking::value_for_time_tracking; use crate::modal::time_tracking::value_for_time_tracking;
use crate::shared::drag::DragState;
use crate::shared::styled_checkbox::StyledCheckboxState; use crate::shared::styled_checkbox::StyledCheckboxState;
use crate::shared::styled_editor::Mode; use crate::shared::styled_editor::Mode;
use crate::shared::styled_image_input::StyledImageInputState; use crate::shared::styled_image_input::StyledImageInputState;
@ -235,9 +236,7 @@ pub struct ProjectPage {
pub active_avatar_filters: Vec<UserId>, pub active_avatar_filters: Vec<UserId>,
pub only_my_filter: bool, pub only_my_filter: bool,
pub recently_updated_filter: bool, pub recently_updated_filter: bool,
pub dragged_issue_id: Option<IssueId>, pub issue_drag: DragState,
pub last_drag_exchange_id: Option<IssueId>,
pub dirty_issues: Vec<IssueId>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -252,6 +251,9 @@ pub struct ProjectSettingsPage {
pub project_category_state: StyledSelectState, pub project_category_state: StyledSelectState,
pub description_mode: crate::shared::styled_editor::Mode, pub description_mode: crate::shared::styled_editor::Mode,
pub time_tracking: StyledCheckboxState, pub time_tracking: StyledCheckboxState,
pub column_drag: DragState,
pub edit_column_id: Option<IssueStatusId>,
pub name: StyledInputState,
} }
impl ProjectSettingsPage { impl ProjectSettingsPage {
@ -284,6 +286,12 @@ impl ProjectSettingsPage {
FieldId::ProjectSettings(ProjectFieldId::TimeTracking), FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
(*time_tracking).into(), (*time_tracking).into(),
), ),
column_drag: Default::default(),
edit_column_id: None,
name: StyledInputState::new(
FieldId::ProjectSettings(ProjectFieldId::IssueStatusName),
"",
),
} }
} }
} }

View File

@ -11,7 +11,7 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{drag_ev, inner_layout, ToNode}; use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{EditIssueModalSection, FieldId, Msg}; use crate::{BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() { if model.user.is_none() {
@ -104,16 +104,24 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
project_page.recently_updated_filter = false; project_page.recently_updated_filter = false;
project_page.only_my_filter = false; project_page.only_my_filter = false;
} }
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model), Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => {
Msg::IssueDragStopped(_) => { crate::ws::issue::drag_started(issue_id, model)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(_))) => {
crate::ws::issue::sync(model); crate::ws::issue::sync(model);
} }
Msg::ExchangePosition(issue_bellow_id) => { Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
crate::ws::issue::exchange_position(issue_bellow_id, model) issue_bellow_id,
))) => crate::ws::issue::exchange_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(status))) => {
crate::ws::issue::change_status(status, model)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => {
crate::ws::issue::sync(model)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(_id))) => {
project_page.issue_drag.clear_last();
} }
Msg::IssueDragOverStatus(status) => crate::ws::issue::change_status(status, model),
Msg::IssueDropZone(_status) => crate::ws::issue::sync(model),
Msg::DragLeave(_id) => project_page.last_drag_exchange_id = None,
Msg::DeleteIssue(issue_id) => { Msg::DeleteIssue(issue_id) => {
send_ws_msg(jirs_data::WsMsg::IssueDeleteRequest(issue_id)); send_ws_msg(jirs_data::WsMsg::IssueDeleteRequest(issue_id));
} }
@ -298,13 +306,17 @@ fn project_issue_list(model: &Model, status: &jirs_data::IssueStatus) -> Node<Ms
let send_status = status.id; 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::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(
send_status,
)))
}); });
let send_status = status.id; 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::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(
send_status,
)))
}); });
div![ div![
@ -382,15 +394,27 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
}; };
let issue_id = issue.id; let issue_id = issue.id;
let drag_started = drag_ev(Ev::DragStart, move |_| Msg::IssueDragStarted(issue_id)); let drag_started = drag_ev(Ev::DragStart, move |_| {
let drag_stopped = drag_ev(Ev::DragEnd, move |_| Msg::IssueDragStopped(issue_id)); Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(
issue_id,
)))
});
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(
issue_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();
ev.stop_propagation(); ev.stop_propagation();
Msg::ExchangePosition(issue_id) Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
issue_id,
)))
}); });
let issue_id = issue.id; let issue_id = issue.id;
let drag_out = drag_ev(Ev::DragLeave, move |_| Msg::DragLeave(issue_id)); let drag_out = drag_ev(Ev::DragLeave, move |_| {
Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(issue_id)))
});
let class_list = vec!["issue"]; let class_list = vec!["issue"];

View File

@ -1,6 +1,8 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{ProjectCategory, TimeTracking, ToVec, UpdateProjectPayload, WsMsg}; use jirs_data::{
IssueStatus, IssueStatusId, ProjectCategory, TimeTracking, ToVec, UpdateProjectPayload, WsMsg,
};
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{Model, Page, PageContent, ProjectSettingsPage}; use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
@ -9,9 +11,11 @@ use crate::shared::styled_checkbox::StyledCheckbox;
use crate::shared::styled_editor::StyledEditor; use crate::shared::styled_editor::StyledEditor;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange}; use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{inner_layout, ToChild, ToNode}; use crate::shared::{drag_ev, inner_layout, ToChild, ToNode};
use crate::FieldChange::TabChanged; use crate::FieldChange::TabChanged;
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange}; use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
@ -26,11 +30,13 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => { Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest); send_ws_msg(WsMsg::ProjectRequest);
send_ws_msg(WsMsg::IssueStatusesRequest);
} }
Msg::ChangePage(Page::ProjectSettings) => { Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model); build_page_content(model);
if model.user.is_some() { if model.user.is_some() {
send_ws_msg(WsMsg::ProjectRequest); send_ws_msg(WsMsg::ProjectRequest);
send_ws_msg(WsMsg::IssueStatusesRequest);
} }
} }
Msg::WsMsg(WsMsg::ProjectLoaded(..)) => { Msg::WsMsg(WsMsg::ProjectLoaded(..)) => {
@ -83,18 +89,47 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
time_tracking: Some(page.time_tracking.value.into()), time_tracking: Some(page.time_tracking.value.into()),
})); }));
} }
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
issue_status_id,
))) => {
page.column_drag.drag(issue_status_id);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(
_issue_status_id,
))) => page.column_drag.clear_last(),
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnExchangePosition(issue_bellow_id),
)) => exchange_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(
id,
))) => {
page.name.value = model
.issue_statuses
.iter()
.find_map(|is| {
if Some(is.id) == id {
Some(is.name.clone())
} else {
None
}
})
.unwrap_or_default();
page.edit_column_id = id;
}
_ => (), _ => (),
} }
} }
fn build_page_content(model: &mut Model) {
let project = match &model.project {
Some(project) => project,
_ => return,
};
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
}
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::ProjectSettings(page) => page, PageContent::ProjectSettings(page) => page,
@ -195,6 +230,83 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let width = 100f64 / (model.issue_statuses.len() + 1) as f64;
let column_style = format!("width: calc({width}% - 10px)", width = width);
let columns: Vec<Node<Msg>> = model
.issue_statuses
.iter()
.map(|is| {
let id = is.id;
let drag_started = drag_ev(Ev::DragStart, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
id,
)))
});
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
id,
)))
});
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
ev.stop_propagation();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnExchangePosition(
id,
)))
});
let drag_out = drag_ev(Ev::DragLeave, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(id)))
});
let name = if page.edit_column_id == Some(id) {
let blur = ev("focusout", |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(None)))
});
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.state(&page.name)
.primary()
.auto_focus()
.build()
.into_node();
div![class!["columnName"], input, blur]
} else {
let edit = mouse_ev(Ev::Click, move |_| {
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(Some(id))))
});
div![class!["columnName"], is.name, edit]
};
div![
class!["columnPreview"],
attrs![At::Style => column_style.as_str(), At::Draggable => "true", At::DropZone => "true"],
name,
drag_started,
drag_stopped,
drag_over_handler,
drag_out,
]
})
.collect();
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
let columns_section = section![
class!["columnsSection"],
div![
class!["columns"],
columns,
div![
class!["columnPreview"],
attrs![At::Style => column_style.as_str()],
div![class!["columnName addColumn"], add_column]
]
]
];
let columns_field = StyledField::build()
.add_class("columnsField")
.input(columns_section)
.label("Columns")
.build()
.into_node();
let save_button = StyledButton::build() let save_button = StyledButton::build()
.add_class("actionButton") .add_class("actionButton")
.on_click(mouse_ev(Ev::Click, |ev| { .on_click(mouse_ev(Ev::Click, |ev| {
@ -217,6 +329,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.add_field(category_field) .add_field(category_field)
.add_field(time_tracking_field) .add_field(time_tracking_field)
.add_field(save_button) .add_field(save_button)
.add_field(columns_field)
.build() .build()
.into_node(); .into_node();
@ -224,3 +337,76 @@ pub fn view(model: &model::Model) -> Node<Msg> {
inner_layout(model, "projectSettings", project_section, empty![]) inner_layout(model, "projectSettings", project_section, empty![])
} }
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return,
};
if page.column_drag.dragged_or_last(bellow_id) {
return;
}
let dragged_id = match page.column_drag.dragged_id.as_ref().cloned() {
Some(id) => id,
_ => return error!("Nothing is dragged"),
};
let mut below = None;
let mut dragged = None;
let mut issues_statuses = vec![];
std::mem::swap(&mut issues_statuses, &mut model.issue_statuses);
for issue_status in issues_statuses.into_iter() {
match issue_status.id {
id if id == bellow_id => below = Some(issue_status),
id if id == dragged_id => dragged = Some(issue_status),
_ => model.issue_statuses.push(issue_status),
};
}
let mut below = match below {
Some(below) => below,
_ => return,
};
let mut dragged = match dragged {
Some(issue_status) => issue_status,
_ => {
model.issue_statuses.push(below);
return;
}
};
std::mem::swap(&mut dragged.position, &mut below.position);
page.column_drag.mark_dirty(dragged.id);
page.column_drag.mark_dirty(below.id);
model.issue_statuses.push(below);
model.issue_statuses.push(dragged);
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
page.column_drag.last_id = Some(bellow_id);
}
fn sync(model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return error!("bad content type"),
};
for id in page.column_drag.dirty.iter() {
let IssueStatus { name, position, .. } =
match model.issue_statuses.iter().find(|is| is.id == *id) {
Some(is) => is,
_ => continue,
};
send_ws_msg(WsMsg::IssueStatusUpdate(*id, name.clone(), *position))
}
}
fn build_page_content(model: &mut Model) {
let project = match &model.project {
Some(project) => project,
_ => return,
};
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
}

View File

@ -0,0 +1,38 @@
use std::collections::HashSet;
#[derive(Default, Debug)]
pub struct DragState {
pub last_id: Option<i32>,
pub dragged_id: Option<i32>,
pub dirty: HashSet<i32>,
}
impl DragState {
#[inline]
pub fn mark_dirty(&mut self, id: i32) {
self.dirty.insert(id);
}
#[inline]
pub fn drag(&mut self, id: i32) {
self.dragged_id = Some(id);
self.mark_dirty(id);
}
#[inline]
pub fn dragged_or_last(&self, id: i32) -> bool {
self.dragged_id == Some(id) || self.last_id == Some(id)
}
#[inline]
pub fn clear_last(&mut self) {
self.last_id = None;
}
#[inline]
pub fn clear(&mut self) {
self.last_id = None;
self.dragged_id = None;
self.dirty.clear();
}
}

View File

@ -7,6 +7,7 @@ use crate::model::Model;
use crate::Msg; use crate::Msg;
pub mod aside; pub mod aside;
pub mod drag;
pub mod navbar_left; pub mod navbar_left;
pub mod styled_avatar; pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;

View File

@ -8,6 +8,7 @@ pub struct StyledField {
label: String, label: String,
tip: Option<String>, tip: Option<String>,
input: Node<Msg>, input: Node<Msg>,
class_list: Vec<String>,
} }
impl StyledField { impl StyledField {
@ -27,6 +28,7 @@ pub struct StyledFieldBuilder {
label: Option<String>, label: Option<String>,
tip: Option<String>, tip: Option<String>,
input: Option<Node<Msg>>, input: Option<Node<Msg>>,
class_list: Vec<String>,
} }
impl StyledFieldBuilder { impl StyledFieldBuilder {
@ -51,24 +53,39 @@ impl StyledFieldBuilder {
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn build(self) -> StyledField { pub fn build(self) -> StyledField {
StyledField { StyledField {
label: self.label.unwrap_or_default(), label: self.label.unwrap_or_default(),
tip: self.tip, tip: self.tip,
input: self.input.unwrap_or_else(|| empty![]), input: self.input.unwrap_or_else(|| empty![]),
class_list: self.class_list,
} }
} }
} }
pub fn render(values: StyledField) -> Node<Msg> { pub fn render(values: StyledField) -> Node<Msg> {
let StyledField { label, tip, input } = values; let StyledField {
label,
tip,
input,
mut class_list,
} = values;
let tip_node = match tip { let tip_node = match tip {
Some(s) => div![attrs![At::Class => "styledTip"], s], Some(s) => div![attrs![At::Class => "styledTip"], s],
_ => empty![], _ => empty![],
}; };
class_list.push("styledField".to_string());
div![ div![
attrs![At::Class => "styledField"], attrs![At::Class => class_list.join(" ")],
seed::label![attrs![At::Class => "styledLabel"], label], seed::label![attrs![At::Class => "styledLabel"], label],
input, input,
tip_node, tip_node,

View File

@ -69,6 +69,7 @@ pub struct StyledInput {
input_class_list: Vec<String>, input_class_list: Vec<String>,
wrapper_class_list: Vec<String>, wrapper_class_list: Vec<String>,
variant: Variant, variant: Variant,
auto_focus: bool,
} }
impl StyledInput { impl StyledInput {
@ -82,6 +83,7 @@ impl StyledInput {
input_class_list: vec![], input_class_list: vec![],
wrapper_class_list: vec![], wrapper_class_list: vec![],
variant: Variant::Normal, variant: Variant::Normal,
auto_focus: false,
} }
} }
} }
@ -96,6 +98,7 @@ pub struct StyledInputBuilder {
input_class_list: Vec<String>, input_class_list: Vec<String>,
wrapper_class_list: Vec<String>, wrapper_class_list: Vec<String>,
variant: Variant, variant: Variant,
auto_focus: bool,
} }
impl StyledInputBuilder { impl StyledInputBuilder {
@ -119,6 +122,7 @@ impl StyledInputBuilder {
pub fn state(self, state: &StyledInputState) -> Self { pub fn state(self, state: &StyledInputState) -> Self {
self.value(state.value.as_str()) self.value(state.value.as_str())
.valid(!state.value.is_empty())
} }
pub fn add_input_class<S>(mut self, name: S) -> Self pub fn add_input_class<S>(mut self, name: S) -> Self
@ -142,6 +146,11 @@ impl StyledInputBuilder {
self self
} }
pub fn auto_focus(mut self) -> Self {
self.auto_focus = true;
self
}
pub fn build(self) -> StyledInput { pub fn build(self) -> StyledInput {
StyledInput { StyledInput {
id: self.id, id: self.id,
@ -152,6 +161,7 @@ impl StyledInputBuilder {
input_class_list: self.input_class_list, input_class_list: self.input_class_list,
wrapper_class_list: self.wrapper_class_list, wrapper_class_list: self.wrapper_class_list,
variant: self.variant, variant: self.variant,
auto_focus: self.auto_focus,
} }
} }
} }
@ -172,6 +182,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
mut input_class_list, mut input_class_list,
mut wrapper_class_list, mut wrapper_class_list,
variant, variant,
auto_focus,
} = values; } = values;
wrapper_class_list.push("styledInput".to_string()); wrapper_class_list.push("styledInput".to_string());
@ -213,6 +224,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
At::Class => input_class_list.join(" "), At::Class => input_class_list.join(" "),
At::Value => value.unwrap_or_default(), At::Value => value.unwrap_or_default(),
At::Type => input_type.unwrap_or_else(|| "text".to_string()), At::Type => input_type.unwrap_or_else(|| "text".to_string()),
At::AutoFocus => auto_focus,
], ],
change_handler, change_handler,
key_handler, key_handler,

View File

@ -3,15 +3,14 @@ use seed::*;
use jirs_data::*; use jirs_data::*;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{Model, PageContent, ProjectPage}; use crate::model::{Model, PageContent};
pub fn drag_started(issue_id: IssueId, model: &mut Model) { pub fn drag_started(issue_id: IssueId, 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,
}; };
project_page.dragged_issue_id = Some(issue_id); project_page.issue_drag.drag(issue_id);
mark_dirty(issue_id, project_page);
} }
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) { pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
@ -19,12 +18,10 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
PageContent::Project(project_page) => project_page, PageContent::Project(project_page) => project_page,
_ => return, _ => return,
}; };
if project_page.dragged_issue_id == Some(issue_bellow_id) if project_page.issue_drag.dragged_or_last(issue_bellow_id) {
|| project_page.last_drag_exchange_id == Some(issue_bellow_id)
{
return; return;
} }
let dragged_id = match project_page.dragged_issue_id.as_ref().cloned() { let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
Some(id) => id, Some(id) => id,
_ => return error!("Nothing is dragged"), _ => return error!("Nothing is dragged"),
}; };
@ -59,7 +56,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
for mut c in issues.into_iter() { for mut c in issues.into_iter() {
if c.issue_status_id == below.issue_status_id && 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); project_page.issue_drag.mark_dirty(c.id);
} }
model.issues.push(c); model.issues.push(c);
} }
@ -68,15 +65,15 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
} }
std::mem::swap(&mut dragged.list_position, &mut below.list_position); std::mem::swap(&mut dragged.list_position, &mut below.list_position);
mark_dirty(dragged.id, project_page); project_page.issue_drag.mark_dirty(dragged.id);
mark_dirty(below.id, project_page); project_page.issue_drag.mark_dirty(below.id);
model.issues.push(below); model.issues.push(below);
model.issues.push(dragged); model.issues.push(dragged);
model model
.issues .issues
.sort_by(|a, b| a.list_position.cmp(&b.list_position)); .sort_by(|a, b| a.list_position.cmp(&b.list_position));
project_page.last_drag_exchange_id = Some(issue_bellow_id); project_page.issue_drag.last_id = Some(issue_bellow_id);
} }
pub fn sync(model: &mut Model) { pub fn sync(model: &mut Model) {
@ -89,7 +86,7 @@ pub fn sync(model: &mut Model) {
}; };
for issue in model.issues.iter() { for issue in model.issues.iter() {
if !project_page.dirty_issues.contains(&issue.id) { if !project_page.issue_drag.dirty.contains(&issue.id) {
continue; continue;
} }
@ -104,9 +101,7 @@ pub fn sync(model: &mut Model) {
PayloadVariant::I32(issue.list_position), PayloadVariant::I32(issue.list_position),
)); ));
} }
project_page.dragged_issue_id = None; project_page.issue_drag.clear();
project_page.last_drag_exchange_id = None;
project_page.dirty_issues.clear();
} }
pub fn change_status(status_id: IssueStatusId, model: &mut Model) { pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
@ -115,7 +110,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
_ => return, _ => return,
}; };
let issue_id = match project_page.dragged_issue_id.as_ref().cloned() { let issue_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
Some(issue_id) => issue_id, Some(issue_id) => issue_id,
_ => return error!("Nothing is dragged"), _ => return error!("Nothing is dragged"),
}; };
@ -130,7 +125,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
if issue.issue_status_id == status_id { 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); project_page.issue_drag.mark_dirty(issue.id);
} }
pos += 1; pos += 1;
} }
@ -154,13 +149,6 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
issue.issue_status_id = status_id; 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); project_page.issue_drag.mark_dirty(issue_id);
}
}
#[inline]
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
if !project_page.dirty_issues.contains(&id) {
project_page.dirty_issues.push(id);
} }
} }

View File

@ -48,6 +48,39 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
.issue_statuses .issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position)); .sort_by(|a, b| a.position.cmp(&b.position));
} }
Msg::WsMsg(WsMsg::IssueStatusCreated(is)) => {
model.issue_statuses.push(is.clone());
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
Msg::WsMsg(WsMsg::IssueStatusUpdated(changed)) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
if is.id == changed.id {
model.issue_statuses.push(changed.clone());
} else {
model.issue_statuses.push(is);
}
}
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
Msg::WsMsg(WsMsg::IssueDeleted(id)) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
if is.id == *id {
continue;
}
model.issue_statuses.push(is);
}
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();

View File

@ -25,8 +25,10 @@ pub type CommentId = i32;
pub type TokenId = i32; pub type TokenId = i32;
pub type IssueStatusId = i32; pub type IssueStatusId = i32;
pub type InvitationId = i32; pub type InvitationId = i32;
pub type Position = i32;
pub type EmailString = String; pub type EmailString = String;
pub type UsernameString = String; pub type UsernameString = String;
pub type TitleString = String;
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
@ -587,6 +589,7 @@ pub enum ProjectFieldId {
Description, Description,
Category, Category,
TimeTracking, TimeTracking,
IssueStatusName,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
@ -697,6 +700,12 @@ pub enum WsMsg {
// issue status // issue status
IssueStatusesRequest, IssueStatusesRequest,
IssueStatusesResponse(Vec<IssueStatus>), IssueStatusesResponse(Vec<IssueStatus>),
IssueStatusUpdate(IssueStatusId, TitleString, Position),
IssueStatusUpdated(IssueStatus),
IssueStatusCreate(TitleString, Position),
IssueStatusCreated(IssueStatus),
IssueStatusDelete(IssueStatusId),
IssueStatusDeleted(IssueStatusId),
// comments // comments
IssueCommentsRequest(IssueId), IssueCommentsRequest(IssueId),

View File

@ -13,6 +13,11 @@ rustflags = [
"-C", "link-arg=-fuse-ld=lld", "-C", "link-arg=-fuse-ld=lld",
] ]
[target.nightly-x86_64-unknown-linux-gnu]
rustflags = [
"-C", "link-arg=-fuse-ld=lld",
]
[[bin]] [[bin]]
name = "jirs_server" name = "jirs_server"
path = "./src/main.rs" path = "./src/main.rs"

View File

@ -2,7 +2,7 @@ use actix::{Handler, Message};
use diesel::pg::Pg; use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use jirs_data::{IssueStatus, IssueStatusId, ProjectId}; use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
@ -39,7 +39,7 @@ impl Handler<LoadIssueStatuses> for DbExecutor {
pub struct CreateIssueStatus { pub struct CreateIssueStatus {
pub project_id: ProjectId, pub project_id: ProjectId,
pub position: i32, pub position: i32,
pub name: String, pub name: TitleString,
} }
impl Message for CreateIssueStatus { impl Message for CreateIssueStatus {
@ -70,29 +70,71 @@ impl Handler<CreateIssueStatus> for DbExecutor {
} }
pub struct DeleteIssueStatus { pub struct DeleteIssueStatus {
pub project_id: ProjectId,
pub issue_status_id: IssueStatusId, pub issue_status_id: IssueStatusId,
} }
impl Message for DeleteIssueStatus { impl Message for DeleteIssueStatus {
type Result = Result<usize, ServiceErrors>; type Result = Result<IssueStatusId, ServiceErrors>;
} }
impl Handler<DeleteIssueStatus> for DbExecutor { impl Handler<DeleteIssueStatus> for DbExecutor {
type Result = Result<usize, ServiceErrors>; type Result = Result<IssueStatusId, ServiceErrors>;
fn handle(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_statuses::dsl::{id, issue_statuses}; use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id};
let conn = &self let conn = &self
.pool .pool
.get() .get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?; .map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = let issue_assignees_query = diesel::delete(issue_statuses)
diesel::delete(issue_statuses).filter(id.eq(msg.issue_status_id)); .filter(id.eq(msg.issue_status_id))
.filter(project_id.eq(msg.project_id));
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query)); debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query));
issue_assignees_query issue_assignees_query
.execute(conn) .execute(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))?;
Ok(msg.issue_status_id)
}
}
pub struct UpdateIssueStatus {
pub issue_status_id: IssueStatusId,
pub project_id: ProjectId,
pub position: Position,
pub name: TitleString,
}
impl Message for UpdateIssueStatus {
type Result = Result<IssueStatus, ServiceErrors>;
}
impl Handler<UpdateIssueStatus> for DbExecutor {
type Result = Result<IssueStatus, ServiceErrors>;
fn handle(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::issue_statuses::dsl::{
id, issue_statuses, name, position, project_id, updated_at,
};
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let issue_assignees_query = diesel::update(issue_statuses)
.set((
name.eq(msg.name),
position.eq(msg.position),
updated_at.eq(chrono::Utc::now().naive_utc()),
))
.filter(id.eq(msg.issue_status_id))
.filter(project_id.eq(msg.project_id));
debug!("{}", diesel::debug_query::<Pg, _>(&issue_assignees_query));
issue_assignees_query
.get_result::<IssueStatus>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string())) .map_err(|_| ServiceErrors::RecordNotFound("issue users".to_string()))
} }
} }

View File

@ -153,6 +153,53 @@ table! {
} }
} }
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,
}
}
table! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use jirs_data::sql::*; use jirs_data::sql::*;
@ -254,53 +301,6 @@ table! {
} }
} }
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,
}
}
table! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use jirs_data::sql::*; use jirs_data::sql::*;
@ -489,8 +489,8 @@ allow_tables_to_appear_in_same_query!(
comments, comments,
invitations, invitations,
issue_assignees, issue_assignees,
issues,
issue_statuses, issue_statuses,
issues,
projects, projects,
tokens, tokens,
users, users,

View File

@ -1,6 +1,6 @@
use futures::executor::block_on; use futures::executor::block_on;
use jirs_data::WsMsg; use jirs_data::{IssueStatusId, Position, TitleString, WsMsg};
use crate::db::issue_statuses; use crate::db::issue_statuses;
use crate::ws::{WebSocketActor, WsHandler, WsResult}; use crate::ws::{WebSocketActor, WsHandler, WsResult};
@ -21,3 +21,73 @@ impl WsHandler<LoadIssueStatuses> for WebSocketActor {
Ok(msg) Ok(msg)
} }
} }
pub struct CreateIssueStatus {
pub position: i32,
pub name: TitleString,
}
impl WsHandler<CreateIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: CreateIssueStatus, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user()?.project_id;
let CreateIssueStatus { position, name } = msg;
let msg = match block_on(self.db.send(issue_statuses::CreateIssueStatus {
project_id,
position,
name,
})) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusCreated(is)),
_ => None,
};
Ok(msg)
}
}
pub struct DeleteIssueStatus {
pub issue_status_id: IssueStatusId,
}
impl WsHandler<DeleteIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteIssueStatus, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user()?.project_id;
let DeleteIssueStatus { issue_status_id } = msg;
let msg = match block_on(self.db.send(issue_statuses::DeleteIssueStatus {
issue_status_id,
project_id,
})) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusDeleted(is)),
_ => None,
};
Ok(msg)
}
}
pub struct UpdateIssueStatus {
pub issue_status_id: IssueStatusId,
pub position: Position,
pub name: TitleString,
}
impl WsHandler<UpdateIssueStatus> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateIssueStatus, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user()?.project_id;
let UpdateIssueStatus {
issue_status_id,
position,
name,
} = msg;
let msg = match block_on(self.db.send(issue_statuses::UpdateIssueStatus {
issue_status_id,
position,
name,
project_id,
})) {
Ok(Ok(is)) => Some(WsMsg::IssueStatusUpdated(is)),
_ => None,
};
Ok(msg)
}
}

View File

@ -98,6 +98,20 @@ impl WebSocketActor {
// issue statuses // issue statuses
WsMsg::IssueStatusesRequest => self.handle_msg(LoadIssueStatuses, ctx)?, WsMsg::IssueStatusesRequest => self.handle_msg(LoadIssueStatuses, ctx)?,
WsMsg::IssueStatusDelete(issue_status_id) => {
self.handle_msg(DeleteIssueStatus { issue_status_id }, ctx)?
}
WsMsg::IssueStatusUpdate(issue_status_id, name, position) => self.handle_msg(
UpdateIssueStatus {
issue_status_id,
name,
position,
},
ctx,
)?,
WsMsg::IssueStatusCreate(name, position) => {
self.handle_msg(CreateIssueStatus { name, position }, ctx)?
}
// projects // projects
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?, WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,