Valid implementation of drag&drop issues

This commit is contained in:
Adrian Woźniak 2020-04-09 11:10:52 +02:00
parent dc3be999be
commit c3c8d1b66e
6 changed files with 209 additions and 141 deletions

View File

@ -87,6 +87,7 @@ pub enum Msg {
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatus),
IssueDropZone(IssueStatus),
UnlockDragOver,

View File

@ -118,7 +118,8 @@ pub struct ProjectPage {
pub only_my_filter: bool,
pub recently_updated_filter: bool,
pub dragged_issue_id: Option<IssueId>,
pub drag_locked: bool,
pub last_drag_exchange_id: Option<IssueId>,
pub dirty_issues: Vec<IssueId>,
}
#[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,

View File

@ -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<Msg>) {
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<Msg
ev.prevent_default();
Msg::IssueDropZone(send_status)
});
let send_status = status.clone();
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
ev.prevent_default();
Msg::NoOp
Msg::IssueDragOverStatus(send_status)
});
div![
@ -420,23 +325,10 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
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);

149
jirs-client/src/ws/issue.rs Normal file
View File

@ -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<Issue> = vec![];
let mut pos = 0;
let mut found: Option<Issue> = 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);
}
}

View File

@ -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,

View File

@ -12,6 +12,11 @@ pub use sql::*;
#[cfg(feature = "backend")]
pub mod sql;
pub trait ToVec {
type Item;
fn ordered() -> Vec<Self::Item>;
}
#[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<Self> {
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<Self> {
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<Self> {
vec![
IssuePriority::Highest,
IssuePriority::High,
IssuePriority::Medium,
IssuePriority::Low,
IssuePriority::Lowest,
]
}
}
impl FromStr for IssuePriority {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<IssuePriority> for u32 {
}
#[derive(Clone, Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse {
pub errors: Vec<String>,
}
#[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<Issue> 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<String>,
#[serde(rename = "type")]
pub issue_type: Option<IssueType>,
pub status: Option<IssueStatus>,
pub priority: Option<IssuePriority>,
@ -351,7 +378,6 @@ pub struct UpdateIssuePayload {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateCommentPayload {
pub user_id: Option<i32>,
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<String>,
pub url: Option<String>,