Add issue status management
This commit is contained in:
parent
8178483019
commit
b47009635c
95
jirs-client/dev/logo.svg
Normal file
95
jirs-client/dev/logo.svg
Normal 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 |
@ -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;
|
||||||
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
"",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"];
|
||||||
|
|
||||||
|
@ -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)));
|
||||||
|
}
|
||||||
|
38
jirs-client/src/shared/drag.rs
Normal file
38
jirs-client/src/shared/drag.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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),
|
||||||
|
@ -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"
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)?,
|
||||||
|
Loading…
Reference in New Issue
Block a user