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 {
|
||||
max-width: 720px;
|
||||
max-width: 1024px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#projectSettings > .formContainer .styledForm > .formElement > .actionButton {
|
||||
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::Category => f.write_str("projectSettings-category"),
|
||||
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"),
|
||||
ProjectFieldId::IssueStatusName => f.write_str("projectSettings-issueStatusName"),
|
||||
},
|
||||
FieldId::SignIn(sub) => match sub {
|
||||
SignInFieldId::Email => f.write_str("login-email"),
|
||||
@ -149,6 +150,17 @@ pub enum FieldChange {
|
||||
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)]
|
||||
pub enum UsersPageChange {
|
||||
ResetForm,
|
||||
@ -158,6 +170,15 @@ pub enum UsersPageChange {
|
||||
pub enum ProjectPageChange {
|
||||
ResetForm,
|
||||
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)]
|
||||
@ -170,6 +191,7 @@ pub enum PageChanged {
|
||||
Users(UsersPageChange),
|
||||
ProjectSettings(ProjectPageChange),
|
||||
Profile(ProfilePageChange),
|
||||
Board(BoardPageChange),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -209,15 +231,6 @@ pub enum Msg {
|
||||
ProjectToggleRecentlyUpdated,
|
||||
ProjectClearFilters,
|
||||
|
||||
// dragging
|
||||
IssueDragStarted(IssueId),
|
||||
IssueDragStopped(IssueId),
|
||||
DragLeave(IssueId),
|
||||
ExchangePosition(IssueId),
|
||||
IssueDragOverStatus(IssueStatusId),
|
||||
IssueDropZone(IssueStatusId),
|
||||
UnlockDragOver,
|
||||
|
||||
// inputs
|
||||
StrInputChanged(FieldId, String),
|
||||
U32InputChanged(FieldId, u32),
|
||||
|
@ -6,6 +6,7 @@ use uuid::Uuid;
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::modal::time_tracking::value_for_time_tracking;
|
||||
use crate::shared::drag::DragState;
|
||||
use crate::shared::styled_checkbox::StyledCheckboxState;
|
||||
use crate::shared::styled_editor::Mode;
|
||||
use crate::shared::styled_image_input::StyledImageInputState;
|
||||
@ -235,9 +236,7 @@ pub struct ProjectPage {
|
||||
pub active_avatar_filters: Vec<UserId>,
|
||||
pub only_my_filter: bool,
|
||||
pub recently_updated_filter: bool,
|
||||
pub dragged_issue_id: Option<IssueId>,
|
||||
pub last_drag_exchange_id: Option<IssueId>,
|
||||
pub dirty_issues: Vec<IssueId>,
|
||||
pub issue_drag: DragState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@ -252,6 +251,9 @@ pub struct ProjectSettingsPage {
|
||||
pub project_category_state: StyledSelectState,
|
||||
pub description_mode: crate::shared::styled_editor::Mode,
|
||||
pub time_tracking: StyledCheckboxState,
|
||||
pub column_drag: DragState,
|
||||
pub edit_column_id: Option<IssueStatusId>,
|
||||
pub name: StyledInputState,
|
||||
}
|
||||
|
||||
impl ProjectSettingsPage {
|
||||
@ -284,6 +286,12 @@ impl ProjectSettingsPage {
|
||||
FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
|
||||
(*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_select::StyledSelectChange;
|
||||
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>) {
|
||||
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.only_my_filter = false;
|
||||
}
|
||||
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model),
|
||||
Msg::IssueDragStopped(_) => {
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => {
|
||||
crate::ws::issue::drag_started(issue_id, model)
|
||||
}
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(_))) => {
|
||||
crate::ws::issue::sync(model);
|
||||
}
|
||||
Msg::ExchangePosition(issue_bellow_id) => {
|
||||
crate::ws::issue::exchange_position(issue_bellow_id, model)
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
|
||||
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) => {
|
||||
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 drop_handler = drag_ev(Ev::Drop, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDropZone(send_status)
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(
|
||||
send_status,
|
||||
)))
|
||||
});
|
||||
|
||||
let send_status = status.id;
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDragOverStatus(send_status)
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(
|
||||
send_status,
|
||||
)))
|
||||
});
|
||||
|
||||
div![
|
||||
@ -382,15 +394,27 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
};
|
||||
|
||||
let issue_id = issue.id;
|
||||
let drag_started = drag_ev(Ev::DragStart, move |_| Msg::IssueDragStarted(issue_id));
|
||||
let drag_stopped = drag_ev(Ev::DragEnd, move |_| Msg::IssueDragStopped(issue_id));
|
||||
let drag_started = drag_ev(Ev::DragStart, move |_| {
|
||||
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| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
Msg::ExchangePosition(issue_id)
|
||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
|
||||
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"];
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
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::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_field::StyledField;
|
||||
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_textarea::StyledTextarea;
|
||||
use crate::shared::{inner_layout, ToChild, ToNode};
|
||||
use crate::shared::{drag_ev, inner_layout, ToChild, ToNode};
|
||||
use crate::FieldChange::TabChanged;
|
||||
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 {
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
send_ws_msg(WsMsg::IssueStatusesRequest);
|
||||
}
|
||||
Msg::ChangePage(Page::ProjectSettings) => {
|
||||
build_page_content(model);
|
||||
if model.user.is_some() {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
send_ws_msg(WsMsg::IssueStatusesRequest);
|
||||
}
|
||||
}
|
||||
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()),
|
||||
}));
|
||||
}
|
||||
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> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::ProjectSettings(page) => page,
|
||||
@ -195,6 +230,83 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.build()
|
||||
.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()
|
||||
.add_class("actionButton")
|
||||
.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(time_tracking_field)
|
||||
.add_field(save_button)
|
||||
.add_field(columns_field)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
@ -224,3 +337,76 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
|
||||
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;
|
||||
|
||||
pub mod aside;
|
||||
pub mod drag;
|
||||
pub mod navbar_left;
|
||||
pub mod styled_avatar;
|
||||
pub mod styled_button;
|
||||
|
@ -8,6 +8,7 @@ pub struct StyledField {
|
||||
label: String,
|
||||
tip: Option<String>,
|
||||
input: Node<Msg>,
|
||||
class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl StyledField {
|
||||
@ -27,6 +28,7 @@ pub struct StyledFieldBuilder {
|
||||
label: Option<String>,
|
||||
tip: Option<String>,
|
||||
input: Option<Node<Msg>>,
|
||||
class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl StyledFieldBuilder {
|
||||
@ -51,24 +53,39 @@ impl StyledFieldBuilder {
|
||||
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 {
|
||||
StyledField {
|
||||
label: self.label.unwrap_or_default(),
|
||||
tip: self.tip,
|
||||
input: self.input.unwrap_or_else(|| empty![]),
|
||||
class_list: self.class_list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Some(s) => div![attrs![At::Class => "styledTip"], s],
|
||||
_ => empty![],
|
||||
};
|
||||
class_list.push("styledField".to_string());
|
||||
|
||||
div![
|
||||
attrs![At::Class => "styledField"],
|
||||
attrs![At::Class => class_list.join(" ")],
|
||||
seed::label![attrs![At::Class => "styledLabel"], label],
|
||||
input,
|
||||
tip_node,
|
||||
|
@ -69,6 +69,7 @@ pub struct StyledInput {
|
||||
input_class_list: Vec<String>,
|
||||
wrapper_class_list: Vec<String>,
|
||||
variant: Variant,
|
||||
auto_focus: bool,
|
||||
}
|
||||
|
||||
impl StyledInput {
|
||||
@ -82,6 +83,7 @@ impl StyledInput {
|
||||
input_class_list: vec![],
|
||||
wrapper_class_list: vec![],
|
||||
variant: Variant::Normal,
|
||||
auto_focus: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,6 +98,7 @@ pub struct StyledInputBuilder {
|
||||
input_class_list: Vec<String>,
|
||||
wrapper_class_list: Vec<String>,
|
||||
variant: Variant,
|
||||
auto_focus: bool,
|
||||
}
|
||||
|
||||
impl StyledInputBuilder {
|
||||
@ -119,6 +122,7 @@ impl StyledInputBuilder {
|
||||
|
||||
pub fn state(self, state: &StyledInputState) -> Self {
|
||||
self.value(state.value.as_str())
|
||||
.valid(!state.value.is_empty())
|
||||
}
|
||||
|
||||
pub fn add_input_class<S>(mut self, name: S) -> Self
|
||||
@ -142,6 +146,11 @@ impl StyledInputBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn auto_focus(mut self) -> Self {
|
||||
self.auto_focus = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledInput {
|
||||
StyledInput {
|
||||
id: self.id,
|
||||
@ -152,6 +161,7 @@ impl StyledInputBuilder {
|
||||
input_class_list: self.input_class_list,
|
||||
wrapper_class_list: self.wrapper_class_list,
|
||||
variant: self.variant,
|
||||
auto_focus: self.auto_focus,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,6 +182,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
mut input_class_list,
|
||||
mut wrapper_class_list,
|
||||
variant,
|
||||
auto_focus,
|
||||
} = values;
|
||||
|
||||
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::Value => value.unwrap_or_default(),
|
||||
At::Type => input_type.unwrap_or_else(|| "text".to_string()),
|
||||
At::AutoFocus => auto_focus,
|
||||
],
|
||||
change_handler,
|
||||
key_handler,
|
||||
|
@ -3,15 +3,14 @@ use seed::*;
|
||||
use jirs_data::*;
|
||||
|
||||
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) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.dragged_issue_id = Some(issue_id);
|
||||
mark_dirty(issue_id, project_page);
|
||||
project_page.issue_drag.drag(issue_id);
|
||||
}
|
||||
|
||||
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,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.dragged_issue_id == Some(issue_bellow_id)
|
||||
|| project_page.last_drag_exchange_id == Some(issue_bellow_id)
|
||||
{
|
||||
if project_page.issue_drag.dragged_or_last(issue_bellow_id) {
|
||||
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,
|
||||
_ => 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() {
|
||||
if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position {
|
||||
c.list_position += 1;
|
||||
mark_dirty(c.id, project_page);
|
||||
project_page.issue_drag.mark_dirty(c.id);
|
||||
}
|
||||
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);
|
||||
|
||||
mark_dirty(dragged.id, project_page);
|
||||
mark_dirty(below.id, project_page);
|
||||
project_page.issue_drag.mark_dirty(dragged.id);
|
||||
project_page.issue_drag.mark_dirty(below.id);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.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) {
|
||||
@ -89,7 +86,7 @@ pub fn sync(model: &mut Model) {
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !project_page.dirty_issues.contains(&issue.id) {
|
||||
if !project_page.issue_drag.dirty.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -104,9 +101,7 @@ pub fn sync(model: &mut Model) {
|
||||
PayloadVariant::I32(issue.list_position),
|
||||
));
|
||||
}
|
||||
project_page.dragged_issue_id = None;
|
||||
project_page.last_drag_exchange_id = None;
|
||||
project_page.dirty_issues.clear();
|
||||
project_page.issue_drag.clear();
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
_ => 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.list_position != pos {
|
||||
issue.list_position = pos;
|
||||
mark_dirty(issue.id, project_page);
|
||||
project_page.issue_drag.mark_dirty(issue.id);
|
||||
}
|
||||
pos += 1;
|
||||
}
|
||||
@ -154,13 +149,6 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
|
||||
issue.issue_status_id = status_id;
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
|
||||
if !project_page.dirty_issues.contains(&id) {
|
||||
project_page.dirty_issues.push(id);
|
||||
project_page.issue_drag.mark_dirty(issue_id);
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,39 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
.issue_statuses
|
||||
.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
|
||||
Msg::WsMsg(WsMsg::ProjectUsersLoaded(v)) => {
|
||||
model.users = v.clone();
|
||||
|
@ -25,8 +25,10 @@ pub type CommentId = i32;
|
||||
pub type TokenId = i32;
|
||||
pub type IssueStatusId = i32;
|
||||
pub type InvitationId = i32;
|
||||
pub type Position = i32;
|
||||
pub type EmailString = String;
|
||||
pub type UsernameString = String;
|
||||
pub type TitleString = String;
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
#[cfg_attr(feature = "backend", sql_type = "IssueTypeType")]
|
||||
@ -587,6 +589,7 @@ pub enum ProjectFieldId {
|
||||
Description,
|
||||
Category,
|
||||
TimeTracking,
|
||||
IssueStatusName,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
@ -697,6 +700,12 @@ pub enum WsMsg {
|
||||
// issue status
|
||||
IssueStatusesRequest,
|
||||
IssueStatusesResponse(Vec<IssueStatus>),
|
||||
IssueStatusUpdate(IssueStatusId, TitleString, Position),
|
||||
IssueStatusUpdated(IssueStatus),
|
||||
IssueStatusCreate(TitleString, Position),
|
||||
IssueStatusCreated(IssueStatus),
|
||||
IssueStatusDelete(IssueStatusId),
|
||||
IssueStatusDeleted(IssueStatusId),
|
||||
|
||||
// comments
|
||||
IssueCommentsRequest(IssueId),
|
||||
|
@ -13,6 +13,11 @@ rustflags = [
|
||||
"-C", "link-arg=-fuse-ld=lld",
|
||||
]
|
||||
|
||||
[target.nightly-x86_64-unknown-linux-gnu]
|
||||
rustflags = [
|
||||
"-C", "link-arg=-fuse-ld=lld",
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
name = "jirs_server"
|
||||
path = "./src/main.rs"
|
||||
|
@ -2,7 +2,7 @@ use actix::{Handler, Message};
|
||||
use diesel::pg::Pg;
|
||||
use diesel::prelude::*;
|
||||
|
||||
use jirs_data::{IssueStatus, IssueStatusId, ProjectId};
|
||||
use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
|
||||
|
||||
use crate::db::DbExecutor;
|
||||
use crate::errors::ServiceErrors;
|
||||
@ -39,7 +39,7 @@ impl Handler<LoadIssueStatuses> for DbExecutor {
|
||||
pub struct CreateIssueStatus {
|
||||
pub project_id: ProjectId,
|
||||
pub position: i32,
|
||||
pub name: String,
|
||||
pub name: TitleString,
|
||||
}
|
||||
|
||||
impl Message for CreateIssueStatus {
|
||||
@ -70,29 +70,71 @@ impl Handler<CreateIssueStatus> for DbExecutor {
|
||||
}
|
||||
|
||||
pub struct DeleteIssueStatus {
|
||||
pub project_id: ProjectId,
|
||||
pub issue_status_id: IssueStatusId,
|
||||
}
|
||||
|
||||
impl Message for DeleteIssueStatus {
|
||||
type Result = Result<usize, ServiceErrors>;
|
||||
type Result = Result<IssueStatusId, ServiceErrors>;
|
||||
}
|
||||
|
||||
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 {
|
||||
use crate::schema::issue_statuses::dsl::{id, issue_statuses};
|
||||
use crate::schema::issue_statuses::dsl::{id, issue_statuses, project_id};
|
||||
|
||||
let conn = &self
|
||||
.pool
|
||||
.get()
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
|
||||
let issue_assignees_query =
|
||||
diesel::delete(issue_statuses).filter(id.eq(msg.issue_status_id));
|
||||
let issue_assignees_query = diesel::delete(issue_statuses)
|
||||
.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
|
||||
.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()))
|
||||
}
|
||||
}
|
||||
|
@ -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! {
|
||||
use diesel::sql_types::*;
|
||||
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! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::sql::*;
|
||||
@ -489,8 +489,8 @@ allow_tables_to_appear_in_same_query!(
|
||||
comments,
|
||||
invitations,
|
||||
issue_assignees,
|
||||
issues,
|
||||
issue_statuses,
|
||||
issues,
|
||||
projects,
|
||||
tokens,
|
||||
users,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use futures::executor::block_on;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
use jirs_data::{IssueStatusId, Position, TitleString, WsMsg};
|
||||
|
||||
use crate::db::issue_statuses;
|
||||
use crate::ws::{WebSocketActor, WsHandler, WsResult};
|
||||
@ -21,3 +21,73 @@ impl WsHandler<LoadIssueStatuses> for WebSocketActor {
|
||||
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
|
||||
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
|
||||
WsMsg::ProjectRequest => self.handle_msg(CurrentProject, ctx)?,
|
||||
|
Loading…
Reference in New Issue
Block a user