Add drag&drop
This commit is contained in:
parent
8ee9cc8957
commit
d272b6a1dc
@ -27,5 +27,9 @@ futures = "^0.1.26"
|
|||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "*"
|
version = "*"
|
||||||
features = [
|
features = [
|
||||||
"Window"
|
"Window",
|
||||||
|
"DataTransfer",
|
||||||
|
"DragEvent",
|
||||||
|
"HtmlDivElement",
|
||||||
|
"DomRect"
|
||||||
]
|
]
|
||||||
|
@ -87,6 +87,7 @@
|
|||||||
#projectPage > #projectBoardLists {
|
#projectPage > #projectBoardLists {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 26px -5px 0;
|
margin: 26px -5px 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#projectPage > #projectBoardLists > .list {
|
#projectPage > #projectBoardLists > .list {
|
||||||
@ -134,9 +135,18 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
#projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue.isBeingDragged {
|
#projectPage > #projectBoardLists > .list > .issues > .issueLink > .issue.isBeingDragged {
|
||||||
transform: rotate(3deg);
|
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) {
|
@media (max-width: 1100px) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use jirs_data::UpdateIssuePayload;
|
||||||
|
|
||||||
use crate::shared::host_client;
|
use crate::shared::host_client;
|
||||||
use crate::Msg;
|
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)),
|
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::fetch::FetchObject;
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
|
use jirs_data::IssueStatus;
|
||||||
|
|
||||||
use crate::model::Page;
|
use crate::model::Page;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
mod api_handlers;
|
||||||
mod login;
|
mod login;
|
||||||
mod model;
|
mod model;
|
||||||
mod project;
|
mod project;
|
||||||
@ -12,10 +15,12 @@ mod register;
|
|||||||
mod shared;
|
mod shared;
|
||||||
|
|
||||||
pub type UserId = i32;
|
pub type UserId = i32;
|
||||||
|
pub type IssueId = i32;
|
||||||
pub type AvatarFilterActive = bool;
|
pub type AvatarFilterActive = bool;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Msg {
|
pub enum Msg {
|
||||||
|
NoOp,
|
||||||
ChangePage(model::Page),
|
ChangePage(model::Page),
|
||||||
CurrentProjectResult(FetchObject<String>),
|
CurrentProjectResult(FetchObject<String>),
|
||||||
CurrentUserResult(FetchObject<String>),
|
CurrentUserResult(FetchObject<String>),
|
||||||
@ -28,6 +33,15 @@ pub enum Msg {
|
|||||||
ProjectToggleOnlyMy,
|
ProjectToggleOnlyMy,
|
||||||
ProjectToggleRecentlyUpdated,
|
ProjectToggleRecentlyUpdated,
|
||||||
ProjectClearFilters,
|
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>) {
|
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),
|
Page::Register => register::update(msg, model, orders),
|
||||||
}
|
}
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
log!(model);
|
// debug!(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use jirs_data::*;
|
use jirs_data::*;
|
||||||
|
|
||||||
use crate::HOST_URL;
|
use crate::{IssueId, UserId, HOST_URL};
|
||||||
|
|
||||||
pub type ProjectId = i32;
|
pub type ProjectId = i32;
|
||||||
pub type StatusCode = u32;
|
pub type StatusCode = u32;
|
||||||
@ -47,13 +47,21 @@ pub struct UpdateProjectForm {
|
|||||||
pub fields: UpdateProjectPayload,
|
pub fields: UpdateProjectPayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
pub struct Point {
|
||||||
|
pub x: f64,
|
||||||
|
pub y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ProjectPage {
|
pub struct ProjectPage {
|
||||||
pub about_tooltip_visible: bool,
|
pub about_tooltip_visible: bool,
|
||||||
pub text_filter: String,
|
pub text_filter: String,
|
||||||
pub active_avatar_filters: Vec<i32>,
|
pub active_avatar_filters: Vec<UserId>,
|
||||||
pub only_my_filter: bool,
|
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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@ -90,7 +98,9 @@ impl Default for Model {
|
|||||||
text_filter: "".to_string(),
|
text_filter: "".to_string(),
|
||||||
active_avatar_filters: vec![],
|
active_avatar_filters: vec![],
|
||||||
only_my_filter: false,
|
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 seed::{prelude::*, *};
|
||||||
|
|
||||||
|
use jirs_data::{FullProject, Issue, IssuePriority, IssueType, UpdateIssuePayload};
|
||||||
|
|
||||||
use crate::model::{Icon, Model, Page};
|
use crate::model::{Icon, Model, Page};
|
||||||
use crate::shared::styled_avatar::StyledAvatar;
|
use crate::shared::styled_avatar::StyledAvatar;
|
||||||
use crate::shared::styled_button::{StyledButton, Variant};
|
use crate::shared::styled_button::{StyledButton, Variant};
|
||||||
use crate::shared::styled_input::StyledInput;
|
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 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>) {
|
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||||
match 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;
|
model.project_page.only_my_filter = !model.project_page.only_my_filter;
|
||||||
}
|
}
|
||||||
Msg::ProjectToggleRecentlyUpdated => {
|
Msg::ProjectToggleRecentlyUpdated => {
|
||||||
model.project_page.recenlty_updated_filter =
|
model.project_page.recently_updated_filter =
|
||||||
!model.project_page.recenlty_updated_filter;
|
!model.project_page.recently_updated_filter;
|
||||||
}
|
}
|
||||||
Msg::ProjectClearFilters => {
|
Msg::ProjectClearFilters => {
|
||||||
let pp = &mut model.project_page;
|
let pp = &mut model.project_page;
|
||||||
pp.active_avatar_filters = vec![];
|
pp.active_avatar_filters = vec![];
|
||||||
pp.recenlty_updated_filter = false;
|
pp.recently_updated_filter = false;
|
||||||
pp.only_my_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,
|
variant: Variant::Empty,
|
||||||
icon_only: false,
|
icon_only: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
active: model.project_page.recenlty_updated_filter,
|
active: model.project_page.recently_updated_filter,
|
||||||
text: Some("Recently Updated".to_string()),
|
text: Some("Recently Updated".to_string()),
|
||||||
icon: None,
|
icon: None,
|
||||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)),
|
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)),
|
||||||
@ -137,7 +196,7 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|
|||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let clear_all = match project_page.only_my_filter
|
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()
|
|| !project_page.active_avatar_filters.is_empty()
|
||||||
{
|
{
|
||||||
true => button![
|
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))
|
.map(|issue| project_issue(model, project, issue))
|
||||||
.collect();
|
.collect();
|
||||||
let label = status.to_label();
|
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![
|
div![
|
||||||
attrs![At::Class => "list"],
|
attrs![At::Class => "list";],
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "title"],
|
attrs![At::Class => "title"],
|
||||||
label,
|
label,
|
||||||
div![attrs![At::Class => "issuesCount"]]
|
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
|
let avatars: Vec<Node<Msg>> = project
|
||||||
.users
|
.users
|
||||||
.iter()
|
.iter()
|
||||||
@ -240,6 +315,7 @@ fn project_issue(_model: &Model, project: &FullProject, issue: &Issue) -> Node<M
|
|||||||
.into_node()
|
.into_node()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut issue_type_icon = match issue.issue_type.parse::<IssueType>() {
|
let mut issue_type_icon = match issue.issue_type.parse::<IssueType>() {
|
||||||
Ok(icon) => {
|
Ok(icon) => {
|
||||||
let mut node = crate::shared::styled_icon(icon.into());
|
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)],
|
Err(e) => span![format!("{}", e)],
|
||||||
};
|
};
|
||||||
let priority_icon = match issue.priority.parse::<IssuePriority>() {
|
let priority_icon = match issue.priority.parse::<IssuePriority>() {
|
||||||
Ok(IssuePriority::Low) | Ok(IssuePriority::Lowest) => {
|
Ok(p) => {
|
||||||
crate::shared::styled_icon(Icon::ArrowDown)
|
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()],
|
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![
|
a![
|
||||||
attrs![At::Class => "issueLink"],
|
attrs![At::Class => "issueLink"],
|
||||||
div![
|
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,],
|
p![attrs![At::Class => "title"], issue.title,],
|
||||||
div![
|
div![
|
||||||
attrs![At::Class => "bottom"],
|
attrs![At::Class => "bottom"],
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use seed::fetch::{FetchObject, ResponseWithDataResult};
|
use seed::fetch::{FetchObject, ResponseWithDataResult};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
use jirs_data::FullProjectResponse;
|
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>) {
|
pub fn update(msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
Msg::CurrentProjectResult(FetchObject {
|
Msg::CurrentProjectResult(fetched) => {
|
||||||
result:
|
crate::api_handlers::current_project_response(fetched, model);
|
||||||
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(fetched) => {
|
||||||
},
|
crate::api_handlers::current_user_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);
|
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
},
|
|
||||||
_ => (),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::str::FromStr;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub trait ResponseData {
|
pub trait ResponseData {
|
||||||
@ -96,12 +97,12 @@ impl FromStr for IssuePriority {
|
|||||||
type Err = String;
|
type Err = String;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().trim() {
|
||||||
"highest" => Ok(IssuePriority::Highest),
|
"5" | "highest" => Ok(IssuePriority::Highest),
|
||||||
"high" => Ok(IssuePriority::High),
|
"4" | "high" => Ok(IssuePriority::High),
|
||||||
"medium" => Ok(IssuePriority::Medium),
|
"3" | "medium" => Ok(IssuePriority::Medium),
|
||||||
"low" => Ok(IssuePriority::Low),
|
"2" | "low" => Ok(IssuePriority::Low),
|
||||||
"lowest" => Ok(IssuePriority::Lowest),
|
"1" | "lowest" => Ok(IssuePriority::Lowest),
|
||||||
_ => Err(format!("Unknown priority {}", s)),
|
_ => 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 {
|
pub fn to_value(&self) -> i32 {
|
||||||
match self {
|
match self {
|
||||||
IssuePriority::Highest => 5,
|
IssuePriority::Highest => 5,
|
||||||
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
|||||||
import { useRouteMatch } from 'react-router-dom';
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
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 = {
|
const propTypes = {
|
||||||
projectUsers: PropTypes.array.isRequired,
|
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));
|
const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
<Draggable draggableId={ issue.id.toString() } index={ index }>
|
||||||
{(provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
<IssueLink
|
<IssueLink
|
||||||
to={`${match.url}/issues/${issue.id}`}
|
data-id="IssueLink"
|
||||||
ref={provided.innerRef}
|
to={ `${ match.url }/issues/${ issue.id }` }
|
||||||
|
ref={ provided.innerRef }
|
||||||
data-testid="list-issue"
|
data-testid="list-issue"
|
||||||
{...provided.draggableProps}
|
{ ...provided.draggableProps }
|
||||||
{...provided.dragHandleProps}
|
{ ...provided.dragHandleProps }
|
||||||
>
|
>
|
||||||
<Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
|
<Issue data-id='Issue' isBeingDragged={ snapshot.isDragging && !snapshot.isDropAnimating }>
|
||||||
<Title>{issue.title}</Title>
|
<Title>{ issue.title }</Title>
|
||||||
<Bottom>
|
<Bottom>
|
||||||
<div>
|
<div>
|
||||||
<IssueTypeIcon type={issue.type} />
|
<IssueTypeIcon type={ issue.type }/>
|
||||||
<IssuePriorityIcon priority={issue.priority} top={-1} left={4} />
|
<IssuePriorityIcon priority={ issue.priority } top={ -1 } left={ 4 }/>
|
||||||
</div>
|
</div>
|
||||||
<Assignees>
|
<Assignees>
|
||||||
{assignees.map(user => (
|
{ assignees.map(user => (
|
||||||
<AssigneeAvatar
|
<AssigneeAvatar
|
||||||
key={user.id}
|
key={ user.id }
|
||||||
size={24}
|
size={ 24 }
|
||||||
avatarUrl={user.avatarUrl}
|
avatarUrl={ user.avatarUrl }
|
||||||
name={user.name}
|
name={ user.name }
|
||||||
/>
|
/>
|
||||||
))}
|
)) }
|
||||||
</Assignees>
|
</Assignees>
|
||||||
</Bottom>
|
</Bottom>
|
||||||
</Issue>
|
</Issue>
|
||||||
</IssueLink>
|
</IssueLink>
|
||||||
)}
|
) }
|
||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { intersection } from 'lodash';
|
|||||||
import { IssueStatusCopy } from 'shared/constants/issues';
|
import { IssueStatusCopy } from 'shared/constants/issues';
|
||||||
|
|
||||||
import Issue from './Issue';
|
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 ProjectBoardList = ({ status, project, filters, currentUserId }) => {
|
||||||
const filteredIssues = filterIssues(project.issues, 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);
|
const allListIssues = getSortedListIssues(project.issues, status);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Droppable key={status} droppableId={status}>
|
<Droppable key={ status } droppableId={ status }>
|
||||||
{provided => (
|
{ provided => (
|
||||||
<List>
|
<List data-id='List'>
|
||||||
<Title>
|
<Title data-id="Title">
|
||||||
{`${IssueStatusCopy[status]} `}
|
{ `${ IssueStatusCopy[status] } ` }
|
||||||
<IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
|
<IssuesCount>{ formatIssuesCount(allListIssues, filteredListIssues) }</IssuesCount>
|
||||||
</Title>
|
</Title>
|
||||||
<Issues
|
<Issues
|
||||||
{...provided.droppableProps}
|
data-id="Issues"
|
||||||
ref={provided.innerRef}
|
{ ...provided.droppableProps }
|
||||||
data-testid={`board-list:${status}`}
|
ref={ provided.innerRef }
|
||||||
|
data-testid={ `board-list:${ status }` }
|
||||||
>
|
>
|
||||||
{filteredListIssues.map((issue, index) => (
|
{ filteredListIssues.map((issue, index) => (
|
||||||
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />
|
<Issue key={ issue.id } projectUsers={ project.users } issue={ issue } index={ index }/>
|
||||||
))}
|
)) }
|
||||||
{provided.placeholder}
|
{ provided.placeholder }
|
||||||
</Issues>
|
</Issues>
|
||||||
</List>
|
</List>
|
||||||
)}
|
) }
|
||||||
</Droppable>
|
</Droppable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -62,7 +63,7 @@ const getSortedListIssues = (issues, status) =>
|
|||||||
|
|
||||||
const formatIssuesCount = (allListIssues, filteredListIssues) => {
|
const formatIssuesCount = (allListIssues, filteredListIssues) => {
|
||||||
if (allListIssues.length !== filteredListIssues.length) {
|
if (allListIssues.length !== filteredListIssues.length) {
|
||||||
return `${filteredListIssues.length} of ${allListIssues.length}`;
|
return `${ filteredListIssues.length } of ${ allListIssues.length }`;
|
||||||
}
|
}
|
||||||
return allListIssues.length;
|
return allListIssues.length;
|
||||||
};
|
};
|
||||||
|
@ -4,18 +4,12 @@ import { DragDropContext } from 'react-beautiful-dnd';
|
|||||||
|
|
||||||
import useCurrentUser from 'shared/hooks/currentUser';
|
import useCurrentUser from 'shared/hooks/currentUser';
|
||||||
import api from 'shared/utils/api';
|
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 { IssueStatus } from 'shared/constants/issues';
|
||||||
|
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import { Lists } from './Styles';
|
import { Lists } from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
project: PropTypes.object.isRequired,
|
|
||||||
filters: PropTypes.object.isRequired,
|
|
||||||
updateLocalProjectIssues: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
||||||
const { currentUserId } = useCurrentUser();
|
const { currentUserId } = useCurrentUser();
|
||||||
|
|
||||||
@ -24,7 +18,7 @@ const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
|||||||
|
|
||||||
const issueId = Number(draggableId);
|
const issueId = Number(draggableId);
|
||||||
|
|
||||||
api.optimisticUpdate(`/issues/${issueId}`, {
|
api.optimisticUpdate(`/issues/${ issueId }`, {
|
||||||
updatedFields: {
|
updatedFields: {
|
||||||
status: destination.droppableId,
|
status: destination.droppableId,
|
||||||
listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),
|
listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),
|
||||||
@ -35,17 +29,18 @@ const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleIssueDrop}>
|
<DragDropContext onDragEnd={ handleIssueDrop }>
|
||||||
<Lists>
|
<Lists id='Lists'>
|
||||||
{Object.values(IssueStatus).map(status => (
|
{ Object.values(IssueStatus).map(status => (
|
||||||
<List
|
<List
|
||||||
key={status}
|
data-name='List'
|
||||||
status={status}
|
key={ status }
|
||||||
project={project}
|
status={ status }
|
||||||
filters={filters}
|
project={ project }
|
||||||
currentUserId={currentUserId}
|
filters={ filters }
|
||||||
|
currentUserId={ currentUserId }
|
||||||
/>
|
/>
|
||||||
))}
|
)) }
|
||||||
</Lists>
|
</Lists>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
);
|
);
|
||||||
@ -92,6 +87,10 @@ const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueI
|
|||||||
const getSortedListIssues = (issues, status) =>
|
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;
|
export default ProjectBoardLists;
|
||||||
|
Loading…
Reference in New Issue
Block a user