diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index dad70828..f75a8365 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -27,5 +27,9 @@ futures = "^0.1.26" [dependencies.web-sys] version = "*" features = [ - "Window" + "Window", + "DataTransfer", + "DragEvent", + "HtmlDivElement", + "DomRect" ] diff --git a/jirs-client/js/css/project.css b/jirs-client/js/css/project.css index 390977f8..71cc2b52 100644 --- a/jirs-client/js/css/project.css +++ b/jirs-client/js/css/project.css @@ -87,6 +87,7 @@ #projectPage > #projectBoardLists { display: flex; margin: 26px -5px 0; + position: relative; } #projectPage > #projectBoardLists > .list { @@ -134,9 +135,18 @@ user-select: none; } +#projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue.hidden { + display: none; +} + #projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue.isBeingDragged { transform: rotate(3deg); - box-shadow: 5px 10px 30px 0 rgba(9, 30, 66, 0.15); + /*box-shadow: 5px 10px 30px 0 rgba(9, 30, 66, 0.15);*/ + position: absolute; + top: 0; + left: 0; + z-index: 2; + width: 90px; } @media (max-width: 1100px) { diff --git a/jirs-client/src/api.rs b/jirs-client/src/api.rs index 6ac8b342..a3c78689 100644 --- a/jirs-client/src/api.rs +++ b/jirs-client/src/api.rs @@ -1,3 +1,5 @@ +use jirs_data::UpdateIssuePayload; + use crate::shared::host_client; use crate::Msg; @@ -14,3 +16,19 @@ pub async fn fetch_current_user(host_url: String) -> Result { Err(e) => Err(Msg::InternalFailure(e)), } } + +pub async fn update_issue( + host_url: String, + id: i32, + payload: UpdateIssuePayload, +) -> Result { + match host_client(host_url, format!("/issue/{id}", id = id).as_str()) { + Ok(client) => { + client + .body_json(&payload) + .fetch_json(Msg::IssueUpdateResult) + .await + } + Err(e) => return Ok(Msg::InternalFailure(e)), + } +} diff --git a/jirs-client/src/api_handlers.rs b/jirs-client/src/api_handlers.rs new file mode 100644 index 00000000..bfc8acc6 --- /dev/null +++ b/jirs-client/src/api_handlers.rs @@ -0,0 +1,84 @@ +use seed::fetch::{FetchObject, ResponseWithDataResult}; + +use jirs_data::{FullProjectResponse, Issue}; + +use crate::model::Model; + +pub fn current_user_response(fetched: &FetchObject, model: &mut Model) { + if let FetchObject { + result: + Ok(ResponseWithDataResult { + data: Ok(body), + status, + .. + }), + .. + } = fetched + { + if status.is_error() { + return; + } + match serde_json::from_str::<'_, jirs_data::User>(body.as_str()) { + Ok(user) => { + model.user = Some(user); + } + _ => (), + } + } +} + +pub fn current_project_response(fetched: &FetchObject, model: &mut Model) { + if let FetchObject { + result: + Ok(ResponseWithDataResult { + data: Ok(body), + status, + .. + }), + .. + } = fetched + { + if status.is_error() { + return; + } + match serde_json::from_str::<'_, FullProjectResponse>(body.as_str()) { + Ok(project_response) => { + model.project = Some(project_response.project); + } + _ => (), + } + } +} + +pub fn update_issue_response(fetched: &FetchObject, model: &mut Model) { + if let FetchObject { + result: + Ok(ResponseWithDataResult { + data: Ok(body), + status, + .. + }), + .. + } = fetched + { + if status.is_error() { + return; + } + match ( + serde_json::from_str::<'_, Issue>(body.as_str()), + model.project.as_mut(), + ) { + (Ok(issue), Some(project)) => { + let mut issues: Vec = vec![]; + for i in project.issues.iter() { + if i.id != issue.id { + issues.push(i.clone()); + } + } + issues.push(issue); + project.issues = issues; + } + _ => (), + } + } +} diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 9f3c4874..f0a88b88 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -1,9 +1,12 @@ use seed::fetch::FetchObject; use seed::{prelude::*, *}; +use jirs_data::IssueStatus; + use crate::model::Page; mod api; +mod api_handlers; mod login; mod model; mod project; @@ -12,10 +15,12 @@ mod register; mod shared; pub type UserId = i32; +pub type IssueId = i32; pub type AvatarFilterActive = bool; #[derive(Clone, Debug)] pub enum Msg { + NoOp, ChangePage(model::Page), CurrentProjectResult(FetchObject), CurrentUserResult(FetchObject), @@ -28,6 +33,15 @@ pub enum Msg { ProjectToggleOnlyMy, ProjectToggleRecentlyUpdated, ProjectClearFilters, + + // dragging + IssueDragStarted(IssueId), + IssueDragStopped(IssueId), + IssueDragOver(f64, f64), + IssueDropZone(IssueStatus), + + // issues + IssueUpdateResult(FetchObject), } fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { @@ -48,7 +62,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { Page::Register => register::update(msg, model, orders), } if cfg!(debug_assertions) { - log!(model); + // debug!(model); } } diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 40adf316..fdbc7092 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use jirs_data::*; -use crate::HOST_URL; +use crate::{IssueId, UserId, HOST_URL}; pub type ProjectId = i32; pub type StatusCode = u32; @@ -47,13 +47,21 @@ pub struct UpdateProjectForm { pub fields: UpdateProjectPayload, } +#[derive(Serialize, Deserialize, Debug, Default)] +pub struct Point { + pub x: f64, + pub y: f64, +} + #[derive(Serialize, Deserialize, Debug)] pub struct ProjectPage { pub about_tooltip_visible: bool, pub text_filter: String, - pub active_avatar_filters: Vec, + pub active_avatar_filters: Vec, pub only_my_filter: bool, - pub recenlty_updated_filter: bool, + pub recently_updated_filter: bool, + pub dragged_issue_id: Option, + pub drag_point: Point, } #[derive(Serialize, Deserialize, Debug)] @@ -90,7 +98,9 @@ impl Default for Model { text_filter: "".to_string(), active_avatar_filters: vec![], only_my_filter: false, - recenlty_updated_filter: false, + recently_updated_filter: false, + dragged_issue_id: None, + drag_point: Point::default(), }, } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index f30a8681..b3bf9987 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -1,12 +1,13 @@ use seed::{prelude::*, *}; +use jirs_data::{FullProject, Issue, IssuePriority, IssueType, UpdateIssuePayload}; + use crate::model::{Icon, Model, Page}; use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_button::{StyledButton, Variant}; use crate::shared::styled_input::StyledInput; -use crate::shared::{host_client, inner_layout, ToNode}; +use crate::shared::{drag_ev, host_client, inner_layout, ToNode}; use crate::Msg; -use jirs_data::{FullProject, Issue, IssuePriority, IssueStatus, IssueType}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { match msg { @@ -42,15 +43,73 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order model.project_page.only_my_filter = !model.project_page.only_my_filter; } Msg::ProjectToggleRecentlyUpdated => { - model.project_page.recenlty_updated_filter = - !model.project_page.recenlty_updated_filter; + model.project_page.recently_updated_filter = + !model.project_page.recently_updated_filter; } Msg::ProjectClearFilters => { let pp = &mut model.project_page; pp.active_avatar_filters = vec![]; - pp.recenlty_updated_filter = false; + pp.recently_updated_filter = false; pp.only_my_filter = false; } + Msg::IssueDragStarted(issue_id) => { + model.project_page.dragged_issue_id = Some(issue_id); + } + Msg::IssueDragStopped(_) => { + model.project_page.dragged_issue_id = None; + } + Msg::IssueDragOver(x, y) => { + model.project_page.drag_point.x = x; + model.project_page.drag_point.y = y; + } + Msg::IssueDropZone(status) => { + match ( + model.project_page.dragged_issue_id.as_ref().cloned(), + model.project.as_mut(), + ) { + (Some(issue_id), Some(project)) => { + let mut position = 0f64; + let mut found: Option<&mut Issue> = None; + for issue in project.issues.iter_mut() { + if issue.status == status.to_payload() { + position += 1f64; + } + if issue.id == issue_id { + found = Some(issue); + break; + } + } + if let Some(issue) = found { + issue.status = status.to_payload().to_string(); + issue.list_position = position + 1f64; + let payload = UpdateIssuePayload { + title: None, + issue_type: None, + status: Some(status.to_payload().to_string()), + priority: None, + list_position: Some(position + 1f64), + description: None, + description_text: None, + estimate: None, + time_spent: None, + time_remaining: None, + project_id: None, + users: None, + user_ids: None, + }; + orders.skip().perform_cmd(crate::api::update_issue( + model.host_url.clone(), + issue.id, + payload, + )); + } + } + _ => error!("Drag stopped before drop :("), + } + } + Msg::IssueUpdateResult(fetched) => { + crate::api_handlers::update_issue_response(&fetched, model); + } _ => (), } } @@ -129,7 +188,7 @@ fn project_board_filters(model: &Model) -> Node { variant: Variant::Empty, icon_only: false, disabled: false, - active: model.project_page.recenlty_updated_filter, + active: model.project_page.recently_updated_filter, text: Some("Recently Updated".to_string()), icon: None, on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)), @@ -137,7 +196,7 @@ fn project_board_filters(model: &Model) -> Node { .into_node(); let clear_all = match project_page.only_my_filter - || project_page.recenlty_updated_filter + || project_page.recently_updated_filter || !project_page.active_avatar_filters.is_empty() { true => button![ @@ -214,18 +273,34 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node "list"], + attrs![At::Class => "list";], div![ attrs![At::Class => "title"], label, div![attrs![At::Class => "issuesCount"]] ], - div![attrs![At::Class => "issues"], issues] + div![ + attrs![At::Class => "issues"; At::DropZone => "link"], + drop_handler, + drag_over_handler, + issues + ] ] } -fn project_issue(_model: &Model, project: &FullProject, issue: &Issue) -> Node { +fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node { let avatars: Vec> = project .users .iter() @@ -240,6 +315,7 @@ fn project_issue(_model: &Model, project: &FullProject, issue: &Issue) -> Node() { Ok(icon) => { let mut node = crate::shared::styled_icon(icon.into()); @@ -252,16 +328,33 @@ fn project_issue(_model: &Model, project: &FullProject, issue: &Issue) -> Node span![format!("{}", e)], }; let priority_icon = match issue.priority.parse::() { - Ok(IssuePriority::Low) | Ok(IssuePriority::Lowest) => { - crate::shared::styled_icon(Icon::ArrowDown) + Ok(p) => { + let icon = match p { + IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown, + _ => Icon::ArrowUp, + }; + let mut node = crate::shared::styled_icon(icon); + node.add_style(St::Color, format!("var(--{})", p.to_lower_name())); + node } - Ok(_) => crate::shared::styled_icon(Icon::ArrowUp), Err(e) => span![e.clone()], }; + + let issue_id = issue.id; + let drag_started = drag_ev(Ev::DragStart, move |event| Msg::IssueDragStarted(issue_id)); + let drag_stopped = drag_ev(Ev::DragEnd, move |_| Msg::IssueDragStopped(issue_id)); + + let mut class_list = vec!["issue"]; + if Some(issue_id) == model.project_page.dragged_issue_id { + class_list.push("hidden"); + } + a![ attrs![At::Class => "issueLink"], div![ - attrs![At::Class => "issue"], + attrs![At::Class => class_list.join(" "), At::Draggable => true], + drag_started, + drag_stopped, p![attrs![At::Class => "title"], issue.title,], div![ attrs![At::Class => "bottom"], diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index ed0e63c3..a6f15cfe 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -1,5 +1,6 @@ use seed::fetch::{FetchObject, ResponseWithDataResult}; use seed::{prelude::*, *}; +use wasm_bindgen::JsCast; use jirs_data::FullProjectResponse; @@ -51,34 +52,22 @@ pub fn host_client(host_url: String, path: &str) -> Result { pub fn update(msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders) { match msg { - Msg::CurrentProjectResult(FetchObject { - result: - Ok(ResponseWithDataResult { - data: Ok(body), - status, - .. - }), - .. - }) if status.is_ok() => match serde_json::from_str::<'_, FullProjectResponse>(body) { - Ok(project_response) => { - model.project = Some(project_response.project); - } - _ => (), - }, - Msg::CurrentUserResult(FetchObject { - result: - Ok(ResponseWithDataResult { - data: Ok(body), - status, - .. - }), - .. - }) if status.is_ok() => match serde_json::from_str::<'_, jirs_data::User>(body) { - Ok(user) => { - model.user = Some(user); - } - _ => (), - }, + Msg::CurrentProjectResult(fetched) => { + crate::api_handlers::current_project_response(fetched, model); + } + Msg::CurrentUserResult(fetched) => { + crate::api_handlers::current_user_response(fetched, model); + } _ => (), } } + +pub fn drag_ev( + trigger: impl Into, + handler: impl FnOnce(web_sys::DragEvent) -> Ms + 'static + Clone, +) -> EventHandler { + let closure_handler = move |event: web_sys::Event| { + (handler.clone())(event.dyn_ref::().unwrap().clone()) + }; + EventHandler::new(trigger, closure_handler) +} diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 8323b8eb..7eb1c920 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -1,6 +1,7 @@ +use std::str::FromStr; + use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; -use std::str::FromStr; use uuid::Uuid; pub trait ResponseData { @@ -96,12 +97,12 @@ impl FromStr for IssuePriority { type Err = String; fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "highest" => Ok(IssuePriority::Highest), - "high" => Ok(IssuePriority::High), - "medium" => Ok(IssuePriority::Medium), - "low" => Ok(IssuePriority::Low), - "lowest" => Ok(IssuePriority::Lowest), + 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), _ => Err(format!("Unknown priority {}", s)), } } @@ -118,6 +119,16 @@ impl IssuePriority { } } + pub fn to_lower_name(&self) -> &str { + match self { + IssuePriority::Highest => "highest", + IssuePriority::High => "high", + IssuePriority::Medium => "medium", + IssuePriority::Low => "low", + IssuePriority::Lowest => "lowest", + } + } + pub fn to_value(&self) -> i32 { match self { IssuePriority::Highest => 5, diff --git a/react-client/src/Project/Board/Lists/List/Issue/index.jsx b/react-client/src/Project/Board/Lists/List/Issue/index.jsx index 7c9820a9..a1cd4315 100644 --- a/react-client/src/Project/Board/Lists/List/Issue/index.jsx +++ b/react-client/src/Project/Board/Lists/List/Issue/index.jsx @@ -3,54 +3,55 @@ import PropTypes from 'prop-types'; import { useRouteMatch } from 'react-router-dom'; import { Draggable } from 'react-beautiful-dnd'; -import { IssueTypeIcon, IssuePriorityIcon } from '../../../../../shared/components'; +import { IssuePriorityIcon, IssueTypeIcon } from '../../../../../shared/components'; -import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles'; +import { AssigneeAvatar, Assignees, Bottom, Issue, IssueLink, Title } from './Styles'; const propTypes = { - projectUsers: PropTypes.array.isRequired, - issue: PropTypes.object.isRequired, - index: PropTypes.number.isRequired, + projectUsers: PropTypes.array.isRequired, + issue: PropTypes.object.isRequired, + index: PropTypes.number.isRequired, }; const ProjectBoardListIssue = ({ projectUsers, issue, index }) => { - const match = useRouteMatch(); + const match = useRouteMatch(); - const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId)); + const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId)); - return ( - - {(provided, snapshot) => ( - - - {issue.title} - -
- - -
- - {assignees.map(user => ( - - ))} - -
-
-
- )} -
- ); + return ( + + { (provided, snapshot) => ( + + + { issue.title } + +
+ + +
+ + { assignees.map(user => ( + + )) } + +
+
+
+ ) } +
+ ); }; ProjectBoardListIssue.propTypes = propTypes; diff --git a/react-client/src/Project/Board/Lists/List/index.jsx b/react-client/src/Project/Board/Lists/List/index.jsx index 9db2b3a8..a5c8baff 100644 --- a/react-client/src/Project/Board/Lists/List/index.jsx +++ b/react-client/src/Project/Board/Lists/List/index.jsx @@ -7,74 +7,75 @@ import { intersection } from 'lodash'; import { IssueStatusCopy } from 'shared/constants/issues'; import Issue from './Issue'; -import { List, Title, IssuesCount, Issues } from './Styles'; +import { Issues, IssuesCount, List, Title } from './Styles'; const ProjectBoardList = ({ status, project, filters, currentUserId }) => { - const filteredIssues = filterIssues(project.issues, filters, currentUserId); - const filteredListIssues = getSortedListIssues(filteredIssues, status); - const allListIssues = getSortedListIssues(project.issues, status); + const filteredIssues = filterIssues(project.issues, filters, currentUserId); + const filteredListIssues = getSortedListIssues(filteredIssues, status); + const allListIssues = getSortedListIssues(project.issues, status); - return ( - - {provided => ( - - - {`${IssueStatusCopy[status]} `} - <IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount> - - - {filteredListIssues.map((issue, index) => ( - - ))} - {provided.placeholder} - - - )} - - ); + return ( + + { provided => ( + + + { `${ IssueStatusCopy[status] } ` } + <IssuesCount>{ formatIssuesCount(allListIssues, filteredListIssues) }</IssuesCount> + + + { filteredListIssues.map((issue, index) => ( + + )) } + { provided.placeholder } + + + ) } + + ); }; const filterIssues = (projectIssues, filters, currentUserId) => { - const { searchTerm, userIds, myOnly, recent } = filters; - let issues = projectIssues; + const { searchTerm, userIds, myOnly, recent } = filters; + let issues = projectIssues; - if (searchTerm) { - issues = issues.filter(issue => issue.title.toLowerCase().includes(searchTerm.toLowerCase())); - } - if (userIds.length > 0) { - issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0); - } - if (myOnly && currentUserId) { - issues = issues.filter(issue => issue.userIds.includes(currentUserId)); - } - if (recent) { - issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days'))); - } - return issues; + if (searchTerm) { + issues = issues.filter(issue => issue.title.toLowerCase().includes(searchTerm.toLowerCase())); + } + if (userIds.length > 0) { + issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0); + } + if (myOnly && currentUserId) { + issues = issues.filter(issue => issue.userIds.includes(currentUserId)); + } + if (recent) { + issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days'))); + } + return issues; }; const getSortedListIssues = (issues, status) => - issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); + issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); const formatIssuesCount = (allListIssues, filteredListIssues) => { - if (allListIssues.length !== filteredListIssues.length) { - return `${filteredListIssues.length} of ${allListIssues.length}`; - } - return allListIssues.length; + if (allListIssues.length !== filteredListIssues.length) { + return `${ filteredListIssues.length } of ${ allListIssues.length }`; + } + return allListIssues.length; }; ProjectBoardList.propTypes = { - status: PropTypes.string.isRequired, - project: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired, - currentUserId: PropTypes.number, + status: PropTypes.string.isRequired, + project: PropTypes.object.isRequired, + filters: PropTypes.object.isRequired, + currentUserId: PropTypes.number, }; ProjectBoardList.defaultProps = { - currentUserId: null, + currentUserId: null, }; export default ProjectBoardList; diff --git a/react-client/src/Project/Board/Lists/index.jsx b/react-client/src/Project/Board/Lists/index.jsx index 8c3edd60..3d817018 100644 --- a/react-client/src/Project/Board/Lists/index.jsx +++ b/react-client/src/Project/Board/Lists/index.jsx @@ -4,94 +4,93 @@ import { DragDropContext } from 'react-beautiful-dnd'; import useCurrentUser from 'shared/hooks/currentUser'; import api from 'shared/utils/api'; -import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript'; +import { insertItemIntoArray, moveItemWithinArray } from 'shared/utils/javascript'; import { IssueStatus } from 'shared/constants/issues'; import List from './List'; import { Lists } from './Styles'; -const propTypes = { - project: PropTypes.object.isRequired, - filters: PropTypes.object.isRequired, - updateLocalProjectIssues: PropTypes.func.isRequired, -}; - const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => { - const { currentUserId } = useCurrentUser(); + const { currentUserId } = useCurrentUser(); - const handleIssueDrop = ({ draggableId, destination, source }) => { - if (!isPositionChanged(source, destination)) return; + const handleIssueDrop = ({ draggableId, destination, source }) => { + if (!isPositionChanged(source, destination)) return; - const issueId = Number(draggableId); + const issueId = Number(draggableId); - api.optimisticUpdate(`/issues/${issueId}`, { - updatedFields: { - status: destination.droppableId, - listPosition: calculateIssueListPosition(project.issues, destination, source, issueId), - }, - currentFields: project.issues.find(({ id }) => id === issueId), - setLocalData: fields => updateLocalProjectIssues(issueId, fields), - }); - }; + api.optimisticUpdate(`/issues/${ issueId }`, { + updatedFields: { + status: destination.droppableId, + listPosition: calculateIssueListPosition(project.issues, destination, source, issueId), + }, + currentFields: project.issues.find(({ id }) => id === issueId), + setLocalData: fields => updateLocalProjectIssues(issueId, fields), + }); + }; - return ( - - - {Object.values(IssueStatus).map(status => ( - - ))} - - - ); + return ( + + + { Object.values(IssueStatus).map(status => ( + + )) } + + + ); }; const isPositionChanged = (destination, source) => { - if (!destination) return false; - const isSameList = destination.droppableId === source.droppableId; - const isSamePosition = destination.index === source.index; - return !isSameList || !isSamePosition; + if (!destination) return false; + const isSameList = destination.droppableId === source.droppableId; + const isSamePosition = destination.index === source.index; + return !isSameList || !isSamePosition; }; const calculateIssueListPosition = (...args) => { - const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args); - let position; + const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args); + let position; - if (!prevIssue && !nextIssue) { - position = 1; - } else if (!prevIssue) { - position = nextIssue.listPosition - 1; - } else if (!nextIssue) { - position = prevIssue.listPosition + 1; - } else { - position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2; - } - return position; + if (!prevIssue && !nextIssue) { + position = 1; + } else if (!prevIssue) { + position = nextIssue.listPosition - 1; + } else if (!nextIssue) { + position = prevIssue.listPosition + 1; + } else { + position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2; + } + return position; }; const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => { - const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId); - const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId); - const isSameList = destination.droppableId === source.droppableId; + const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId); + const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId); + const isSameList = destination.droppableId === source.droppableId; - const afterDropDestinationIssues = isSameList - ? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index) - : insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index); + const afterDropDestinationIssues = isSameList + ? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index) + : insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index); - return { - prevIssue: afterDropDestinationIssues[destination.index - 1], - nextIssue: afterDropDestinationIssues[destination.index + 1], - }; + return { + prevIssue: afterDropDestinationIssues[destination.index - 1], + nextIssue: afterDropDestinationIssues[destination.index + 1], + }; }; const getSortedListIssues = (issues, status) => - issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); + issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); -ProjectBoardLists.propTypes = propTypes; +ProjectBoardLists.propTypes = { + project: PropTypes.object.isRequired, + filters: PropTypes.object.isRequired, + updateLocalProjectIssues: PropTypes.func.isRequired, +}; export default ProjectBoardLists;