Add drag&drop
This commit is contained in:
parent
8ee9cc8957
commit
d272b6a1dc
@ -27,5 +27,9 @@ futures = "^0.1.26"
|
||||
[dependencies.web-sys]
|
||||
version = "*"
|
||||
features = [
|
||||
"Window"
|
||||
"Window",
|
||||
"DataTransfer",
|
||||
"DragEvent",
|
||||
"HtmlDivElement",
|
||||
"DomRect"
|
||||
]
|
||||
|
@ -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) {
|
||||
|
@ -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<Msg, Msg> {
|
||||
Err(e) => Err(Msg::InternalFailure(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_issue(
|
||||
host_url: String,
|
||||
id: i32,
|
||||
payload: UpdateIssuePayload,
|
||||
) -> Result<Msg, Msg> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
84
jirs-client/src/api_handlers.rs
Normal file
84
jirs-client/src/api_handlers.rs
Normal file
@ -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<String>, 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<String>, 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<String>, 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<Issue> = vec![];
|
||||
for i in project.issues.iter() {
|
||||
if i.id != issue.id {
|
||||
issues.push(i.clone());
|
||||
}
|
||||
}
|
||||
issues.push(issue);
|
||||
project.issues = issues;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String>),
|
||||
CurrentUserResult(FetchObject<String>),
|
||||
@ -28,6 +33,15 @@ pub enum Msg {
|
||||
ProjectToggleOnlyMy,
|
||||
ProjectToggleRecentlyUpdated,
|
||||
ProjectClearFilters,
|
||||
|
||||
// dragging
|
||||
IssueDragStarted(IssueId),
|
||||
IssueDragStopped(IssueId),
|
||||
IssueDragOver(f64, f64),
|
||||
IssueDropZone(IssueStatus),
|
||||
|
||||
// issues
|
||||
IssueUpdateResult(FetchObject<String>),
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
@ -48,7 +62,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
Page::Register => register::update(msg, model, orders),
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
log!(model);
|
||||
// debug!(model);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<i32>,
|
||||
pub active_avatar_filters: Vec<UserId>,
|
||||
pub only_my_filter: bool,
|
||||
pub recenlty_updated_filter: bool,
|
||||
pub recently_updated_filter: bool,
|
||||
pub dragged_issue_id: Option<IssueId>,
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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<Msg>) {
|
||||
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<Msg> {
|
||||
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<Msg> {
|
||||
.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<Msg
|
||||
.map(|issue| project_issue(model, project, issue))
|
||||
.collect();
|
||||
let label = status.to_label();
|
||||
|
||||
let send_status = status.clone();
|
||||
let drop_handler = drag_ev(Ev::Drop, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDropZone(send_status)
|
||||
});
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::NoOp
|
||||
});
|
||||
|
||||
div![
|
||||
attrs![At::Class => "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<Msg> {
|
||||
fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node<Msg> {
|
||||
let avatars: Vec<Node<Msg>> = project
|
||||
.users
|
||||
.iter()
|
||||
@ -240,6 +315,7 @@ fn project_issue(_model: &Model, project: &FullProject, issue: &Issue) -> Node<M
|
||||
.into_node()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut issue_type_icon = match issue.issue_type.parse::<IssueType>() {
|
||||
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<M
|
||||
Err(e) => span![format!("{}", e)],
|
||||
};
|
||||
let priority_icon = match issue.priority.parse::<IssuePriority>() {
|
||||
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"],
|
||||
|
@ -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<Request, String> {
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders<Msg>) {
|
||||
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::CurrentProjectResult(fetched) => {
|
||||
crate::api_handlers::current_project_response(fetched, model);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
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::CurrentUserResult(fetched) => {
|
||||
crate::api_handlers::current_user_response(fetched, model);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drag_ev<Ms>(
|
||||
trigger: impl Into<Ev>,
|
||||
handler: impl FnOnce(web_sys::DragEvent) -> Ms + 'static + Clone,
|
||||
) -> EventHandler<Ms> {
|
||||
let closure_handler = move |event: web_sys::Event| {
|
||||
(handler.clone())(event.dyn_ref::<web_sys::DragEvent>().unwrap().clone())
|
||||
};
|
||||
EventHandler::new(trigger, closure_handler)
|
||||
}
|
||||
|
@ -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<Self, Self::Err> {
|
||||
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,
|
||||
|
@ -3,9 +3,9 @@ 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,
|
||||
@ -19,36 +19,37 @@ const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
|
||||
const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId));
|
||||
|
||||
return (
|
||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<Draggable draggableId={ issue.id.toString() } index={ index }>
|
||||
{ (provided, snapshot) => (
|
||||
<IssueLink
|
||||
to={`${match.url}/issues/${issue.id}`}
|
||||
ref={provided.innerRef}
|
||||
data-id="IssueLink"
|
||||
to={ `${ match.url }/issues/${ issue.id }` }
|
||||
ref={ provided.innerRef }
|
||||
data-testid="list-issue"
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
{ ...provided.draggableProps }
|
||||
{ ...provided.dragHandleProps }
|
||||
>
|
||||
<Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
|
||||
<Title>{issue.title}</Title>
|
||||
<Issue data-id='Issue' isBeingDragged={ snapshot.isDragging && !snapshot.isDropAnimating }>
|
||||
<Title>{ issue.title }</Title>
|
||||
<Bottom>
|
||||
<div>
|
||||
<IssueTypeIcon type={issue.type} />
|
||||
<IssuePriorityIcon priority={issue.priority} top={-1} left={4} />
|
||||
<IssueTypeIcon type={ issue.type }/>
|
||||
<IssuePriorityIcon priority={ issue.priority } top={ -1 } left={ 4 }/>
|
||||
</div>
|
||||
<Assignees>
|
||||
{assignees.map(user => (
|
||||
{ assignees.map(user => (
|
||||
<AssigneeAvatar
|
||||
key={user.id}
|
||||
size={24}
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.name}
|
||||
key={ user.id }
|
||||
size={ 24 }
|
||||
avatarUrl={ user.avatarUrl }
|
||||
name={ user.name }
|
||||
/>
|
||||
))}
|
||||
)) }
|
||||
</Assignees>
|
||||
</Bottom>
|
||||
</Issue>
|
||||
</IssueLink>
|
||||
)}
|
||||
) }
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ 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);
|
||||
@ -15,25 +15,26 @@ const ProjectBoardList = ({ status, project, filters, currentUserId }) => {
|
||||
const allListIssues = getSortedListIssues(project.issues, status);
|
||||
|
||||
return (
|
||||
<Droppable key={status} droppableId={status}>
|
||||
{provided => (
|
||||
<List>
|
||||
<Title>
|
||||
{`${IssueStatusCopy[status]} `}
|
||||
<IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
|
||||
<Droppable key={ status } droppableId={ status }>
|
||||
{ provided => (
|
||||
<List data-id='List'>
|
||||
<Title data-id="Title">
|
||||
{ `${ IssueStatusCopy[status] } ` }
|
||||
<IssuesCount>{ formatIssuesCount(allListIssues, filteredListIssues) }</IssuesCount>
|
||||
</Title>
|
||||
<Issues
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
data-testid={`board-list:${status}`}
|
||||
data-id="Issues"
|
||||
{ ...provided.droppableProps }
|
||||
ref={ provided.innerRef }
|
||||
data-testid={ `board-list:${ status }` }
|
||||
>
|
||||
{filteredListIssues.map((issue, index) => (
|
||||
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />
|
||||
))}
|
||||
{provided.placeholder}
|
||||
{ filteredListIssues.map((issue, index) => (
|
||||
<Issue key={ issue.id } projectUsers={ project.users } issue={ issue } index={ index }/>
|
||||
)) }
|
||||
{ provided.placeholder }
|
||||
</Issues>
|
||||
</List>
|
||||
)}
|
||||
) }
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
@ -62,7 +63,7 @@ const getSortedListIssues = (issues, status) =>
|
||||
|
||||
const formatIssuesCount = (allListIssues, filteredListIssues) => {
|
||||
if (allListIssues.length !== filteredListIssues.length) {
|
||||
return `${filteredListIssues.length} of ${allListIssues.length}`;
|
||||
return `${ filteredListIssues.length } of ${ allListIssues.length }`;
|
||||
}
|
||||
return allListIssues.length;
|
||||
};
|
||||
|
@ -4,18 +4,12 @@ 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();
|
||||
|
||||
@ -24,7 +18,7 @@ const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
||||
|
||||
const issueId = Number(draggableId);
|
||||
|
||||
api.optimisticUpdate(`/issues/${issueId}`, {
|
||||
api.optimisticUpdate(`/issues/${ issueId }`, {
|
||||
updatedFields: {
|
||||
status: destination.droppableId,
|
||||
listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),
|
||||
@ -35,17 +29,18 @@ const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={handleIssueDrop}>
|
||||
<Lists>
|
||||
{Object.values(IssueStatus).map(status => (
|
||||
<DragDropContext onDragEnd={ handleIssueDrop }>
|
||||
<Lists id='Lists'>
|
||||
{ Object.values(IssueStatus).map(status => (
|
||||
<List
|
||||
key={status}
|
||||
status={status}
|
||||
project={project}
|
||||
filters={filters}
|
||||
currentUserId={currentUserId}
|
||||
data-name='List'
|
||||
key={ status }
|
||||
status={ status }
|
||||
project={ project }
|
||||
filters={ filters }
|
||||
currentUserId={ currentUserId }
|
||||
/>
|
||||
))}
|
||||
)) }
|
||||
</Lists>
|
||||
</DragDropContext>
|
||||
);
|
||||
@ -92,6 +87,10 @@ const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueI
|
||||
const getSortedListIssues = (issues, status) =>
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user