diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 13a7088b..01250ebd 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -87,6 +87,7 @@ pub enum Msg { IssueDragStarted(IssueId), IssueDragStopped(IssueId), ExchangePosition(IssueId), + IssueDragOverStatus(IssueStatus), IssueDropZone(IssueStatus), UnlockDragOver, diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 7bfcefc8..c892c183 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -118,7 +118,8 @@ pub struct ProjectPage { pub only_my_filter: bool, pub recently_updated_filter: bool, pub dragged_issue_id: Option, - pub drag_locked: bool, + pub last_drag_exchange_id: Option, + pub dirty_issues: Vec, } #[derive(Debug)] @@ -168,7 +169,8 @@ impl Default for Model { only_my_filter: false, recently_updated_filter: false, dragged_issue_id: None, - drag_locked: false, + last_drag_exchange_id: None, + dirty_issues: vec![], }, modals: vec![], project: None, diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 4147b4b4..9f4795d6 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -10,7 +10,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::{FieldId, Msg, APP}; +use crate::{FieldId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { match msg { @@ -92,112 +92,15 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order pp.recently_updated_filter = false; pp.only_my_filter = false; } - Msg::IssueDragStarted(issue_id) => { - model.project_page.dragged_issue_id = Some(issue_id); - } + Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model), Msg::IssueDragStopped(_) => { model.project_page.dragged_issue_id = None; } Msg::ExchangePosition(issue_bellow_id) => { - if model.project_page.drag_locked { - return; - } - log!(issue_bellow_id); - log!(model.project_page.dragged_issue_id); - let dragged_id = match model.project_page.dragged_issue_id { - Some(id) => id, - _ => return, - }; - - let mut below = None; - let mut dragged = None; - let mut issues = vec![]; - std::mem::swap(&mut issues, &mut model.issues); - - for issue in issues.into_iter() { - match issue.id { - id if id == issue_bellow_id => below = Some(issue), - id if id == dragged_id => dragged = Some(issue), - _ => model.issues.push(issue), - }; - } - - let mut below = match below { - Some(below) => below, - _ => return, - }; - let mut dragged = match dragged { - Some(issue) => issue, - _ => { - model.issues.push(below); - return; - } - }; - if dragged.status == below.status { - std::mem::swap(&mut dragged.list_position, &mut below.list_position); - below.status = dragged.status.clone(); - } else { - below.list_position = model - .issues - .iter() - .map(|i| i.list_position) - .max() - .unwrap_or(0) - + 1; - std::mem::swap(&mut dragged.list_position, &mut below.list_position); - below.status = dragged.status.clone(); - } - model.issues.push(below); - model.issues.push(dragged); - model - .issues - .sort_by(|a, b| a.list_position.cmp(&b.list_position)); - model.project_page.drag_locked = true; - // log!(model.issues); + crate::ws::issue::exchange_position(issue_bellow_id, model) } - Msg::UnlockDragOver => { - model.project_page.drag_locked = false; - } - Msg::IssueDropZone(status) => match model.project_page.dragged_issue_id.as_ref().cloned() { - Some(issue_id) => { - let mut position = 0; - let mut found: Option<&mut Issue> = None; - for issue in model.issues.iter_mut() { - if issue.status == status { - position += 1; - } - if issue.id == issue_id { - found = Some(issue); - break; - } - } - let issue = match found { - Some(i) => i, - _ => return, - }; - - issue.status = status.clone(); - issue.list_position = position + 1; - - let payload = UpdateIssuePayload { - title: Some(issue.title.clone()), - issue_type: Some(issue.issue_type.clone()), - status: Some(status), - priority: Some(issue.priority.clone()), - list_position: Some(issue.list_position), - description: Some(issue.description.clone()), - description_text: Some(issue.description_text.clone()), - estimate: Some(issue.estimate), - time_spent: Some(issue.time_spent), - time_remaining: Some(issue.time_remaining), - project_id: Some(issue.project_id), - user_ids: Some(issue.user_ids.clone()), - }; - model.project_page.dragged_issue_id = None; - send_ws_msg(WsMsg::IssueUpdateRequest(issue_id, payload)); - } - _ => error!("Drag stopped before drop :("), - }, + Msg::IssueDragOverStatus(status) => crate::ws::issue::change_status(status, model), + Msg::IssueDropZone(status) => crate::ws::issue::dropped(status, model), Msg::DeleteIssue(issue_id) => { send_ws_msg(jirs_data::WsMsg::IssueDeleteRequest(issue_id)); } @@ -357,9 +260,11 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node Node { let drag_over_handler = drag_ev(Ev::DragOver, move |ev| { ev.prevent_default(); ev.stop_propagation(); - seed::set_timeout( - Box::new(|| { - let app = match unsafe { APP.as_mut().unwrap() }.write() { - Ok(app) => app, - _ => return, - }; - app.update(Msg::UnlockDragOver); - }), - 3000, - ); Msg::ExchangePosition(issue_id) }); let class_list = vec!["issue"]; - if Some(issue_id) == model.project_page.dragged_issue_id { - // class_list.push("hidden"); - } let href = format!("/issues/{id}", id = issue_id); diff --git a/jirs-client/src/ws/issue.rs b/jirs-client/src/ws/issue.rs new file mode 100644 index 00000000..81ac78e5 --- /dev/null +++ b/jirs-client/src/ws/issue.rs @@ -0,0 +1,149 @@ +use seed::*; + +use jirs_data::*; + +use crate::api::send_ws_msg; +use crate::model::Model; +use crate::IssueId; + +pub fn drag_started(issue_id: IssueId, model: &mut Model) { + model.project_page.dragged_issue_id = Some(issue_id); + + mark_dirty(issue_id, model); +} + +pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) { + if model.project_page.dragged_issue_id == Some(issue_bellow_id) + || model.project_page.last_drag_exchange_id == Some(issue_bellow_id) + { + return; + } + let dragged_id = match model.project_page.dragged_issue_id { + Some(id) => id, + _ => return, + }; + + let mut below = None; + let mut dragged = None; + let mut issues = vec![]; + std::mem::swap(&mut issues, &mut model.issues); + + for issue in issues.into_iter() { + match issue.id { + id if id == issue_bellow_id => below = Some(issue), + id if id == dragged_id => dragged = Some(issue), + _ => model.issues.push(issue), + }; + } + + let mut below = match below { + Some(below) => below, + _ => return, + }; + let mut dragged = match dragged { + Some(issue) => issue, + _ => { + model.issues.push(below); + return; + } + }; + if dragged.status != below.status { + let mut issues = vec![]; + std::mem::swap(&mut issues, &mut model.issues); + for mut c in issues.into_iter() { + if c.status == below.status && c.list_position > below.list_position { + c.list_position += 1; + mark_dirty(c.id, model); + } + model.issues.push(c); + } + dragged.list_position = below.list_position + 1; + dragged.status = below.status.clone(); + } + std::mem::swap(&mut dragged.list_position, &mut below.list_position); + + mark_dirty(dragged.id, model); + mark_dirty(below.id, model); + + model.issues.push(below); + model.issues.push(dragged); + model + .issues + .sort_by(|a, b| a.list_position.cmp(&b.list_position)); + model.project_page.last_drag_exchange_id = Some(issue_bellow_id); +} + +pub fn dropped(_status: IssueStatus, model: &mut Model) { + for issue in model.issues.iter() { + if !model.project_page.dirty_issues.contains(&issue.id) { + continue; + } + + let payload = UpdateIssuePayload { + title: Some(issue.title.clone()), + issue_type: Some(issue.issue_type.clone()), + status: Some(issue.status.clone()), + priority: Some(issue.priority.clone()), + list_position: Some(issue.list_position), + description: Some(issue.description.clone()), + description_text: Some(issue.description_text.clone()), + estimate: Some(issue.estimate), + time_spent: Some(issue.time_spent), + time_remaining: Some(issue.time_remaining), + project_id: Some(issue.project_id), + user_ids: Some(issue.user_ids.clone()), + }; + model.project_page.dragged_issue_id = None; + send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload)); + model.project_page.last_drag_exchange_id = None; + } +} + +pub fn change_status(status: IssueStatus, model: &mut Model) { + let issue_id = match model.project_page.dragged_issue_id.as_ref().cloned() { + Some(issue_id) => issue_id, + _ => return, + }; + + let mut old: Vec = vec![]; + let mut pos = 0; + let mut found: Option = None; + std::mem::swap(&mut old, &mut model.issues); + old.sort_by(|a, b| a.list_position.cmp(&b.list_position)); + + for mut issue in old.into_iter() { + if issue.status == status { + issue.list_position = pos; + pos += 1; + } + if issue.id != issue_id { + model.issues.push(issue); + } else { + found = Some(issue); + } + } + + let mut issue = match found { + Some(i) => i, + _ => { + return; + } + }; + + if issue.status == status { + model.issues.push(issue); + return; + } + issue.status = status.clone(); + issue.list_position = pos + 1; + model.issues.push(issue); + + mark_dirty(issue_id, model); +} + +#[inline] +fn mark_dirty(id: IssueId, model: &mut Model) { + if !model.project_page.dirty_issues.contains(&id) { + model.project_page.dirty_issues.push(id); + } +} diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 9b418558..46190b75 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -4,6 +4,8 @@ use jirs_data::WsMsg; use crate::{model, Msg, APP}; +pub mod issue; + pub fn handle(msg: WsMsg) { let app = match unsafe { APP.as_mut().unwrap() }.write() { Ok(app) => app, diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index b37a11e6..0ff168ac 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -12,6 +12,11 @@ pub use sql::*; #[cfg(feature = "backend")] pub mod sql; +pub trait ToVec { + type Item; + fn ordered() -> Vec; +} + #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] #[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] @@ -22,6 +27,14 @@ pub enum IssueType { Story, } +impl ToVec for IssueType { + type Item = IssueType; + + fn ordered() -> Vec { + vec![IssueType::Task, IssueType::Bug, IssueType::Story] + } +} + impl Default for IssueType { fn default() -> Self { IssueType::Task @@ -72,7 +85,6 @@ impl std::fmt::Display for IssueType { #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", sql_type = "IssueStatusType")] #[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] -#[serde(rename_all = "lowercase")] pub enum IssueStatus { Backlog, Selected, @@ -100,6 +112,19 @@ impl FromStr for IssueStatus { } } +impl ToVec for IssueStatus { + type Item = IssueStatus; + + fn ordered() -> Vec { + vec![ + IssueStatus::Backlog, + IssueStatus::Selected, + IssueStatus::InProgress, + IssueStatus::Done, + ] + } +} + impl IssueStatus { pub fn to_label(&self) -> &str { match self { @@ -135,16 +160,30 @@ pub enum IssuePriority { Lowest, } +impl ToVec for IssuePriority { + type Item = IssuePriority; + + fn ordered() -> Vec { + vec![ + IssuePriority::Highest, + IssuePriority::High, + IssuePriority::Medium, + IssuePriority::Low, + IssuePriority::Lowest, + ] + } +} + impl FromStr for IssuePriority { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().trim() { - "5" | "highest" => Ok(IssuePriority::Highest), - "4" | "high" => Ok(IssuePriority::High), - "3" | "medium" => Ok(IssuePriority::Medium), - "2" | "low" => Ok(IssuePriority::Low), - "1" | "lowest" => Ok(IssuePriority::Lowest), + "highest" => Ok(IssuePriority::Highest), + "high" => Ok(IssuePriority::High), + "medium" => Ok(IssuePriority::Medium), + "low" => Ok(IssuePriority::Low), + "lowest" => Ok(IssuePriority::Lowest), _ => Err(format!("Unknown priority {}", s)), } } @@ -194,13 +233,11 @@ impl Into for u32 { } #[derive(Clone, Serialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct ErrorResponse { pub errors: Vec, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct FullProject { pub id: i32, pub name: String, @@ -215,11 +252,9 @@ pub struct FullProject { } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct FullIssue { pub id: i32, pub title: String, - #[serde(rename = "type")] pub issue_type: IssueType, pub status: IssueStatus, pub priority: IssuePriority, @@ -262,7 +297,6 @@ impl Into for FullIssue { } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct Project { pub id: i32, pub name: String, @@ -274,11 +308,9 @@ pub struct Project { } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct Issue { pub id: i32, pub title: String, - #[serde(rename = "type")] pub issue_type: IssueType, pub status: IssueStatus, pub priority: IssuePriority, @@ -297,7 +329,6 @@ pub struct Issue { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct Comment { pub id: i32, pub body: String, @@ -310,7 +341,6 @@ pub struct Comment { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct User { pub id: i32, pub name: String, @@ -322,7 +352,6 @@ pub struct User { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct Token { pub id: i32, pub user_id: i32, @@ -333,10 +362,8 @@ pub struct Token { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct UpdateIssuePayload { pub title: Option, - #[serde(rename = "type")] pub issue_type: Option, pub status: Option, pub priority: Option, @@ -351,7 +378,6 @@ pub struct UpdateIssuePayload { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct CreateCommentPayload { pub user_id: Option, pub issue_id: i32, @@ -359,16 +385,13 @@ pub struct CreateCommentPayload { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct UpdateCommentPayload { pub body: String, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct CreateIssuePayload { pub title: String, - #[serde(rename = "type")] pub issue_type: IssueType, pub status: IssueStatus, pub priority: IssuePriority, @@ -383,7 +406,6 @@ pub struct CreateIssuePayload { } #[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct UpdateProjectPayload { pub name: Option, pub url: Option,