Fix drag&drop
This commit is contained in:
parent
15095dc574
commit
cf28ed7ef4
@ -192,11 +192,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::AuthTokenStored => {
|
||||
seed::push_route(vec!["dashboard"]);
|
||||
orders.skip().send_msg(Msg::ChangePage(Page::Project));
|
||||
authorize_or_redirect();
|
||||
return;
|
||||
}
|
||||
Msg::AuthTokenErased => {
|
||||
seed::push_route(vec!["login"]);
|
||||
orders.skip().send_msg(Msg::ChangePage(Page::Login));
|
||||
authorize_or_redirect();
|
||||
return;
|
||||
}
|
||||
Msg::ChangePage(page) => {
|
||||
|
@ -1,407 +1,409 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{ModalType, Model, Page, PageContent, ProjectPage};
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
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::{EditIssueModalFieldId, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.user.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
match &model.page {
|
||||
Page::Project | Page::AddIssue | Page::EditIssue(..) => {
|
||||
model.page_content = PageContent::Project(ProjectPage::default());
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(..))
|
||||
| Msg::ChangePage(Page::Project)
|
||||
| Msg::ChangePage(Page::AddIssue)
|
||||
| Msg::ChangePage(Page::EditIssue(..)) => {
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectRequest);
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest);
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest);
|
||||
}
|
||||
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id == issue.id {
|
||||
model.issues.push(issue.clone())
|
||||
} else {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::WsMsg(WsMsg::IssueDeleted(id)) => {
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id != id {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Text(text),
|
||||
) => {
|
||||
let modal = model
|
||||
.modals
|
||||
.iter_mut()
|
||||
.filter_map(|modal| match modal {
|
||||
ModalType::EditIssue(_, modal) => Some(modal),
|
||||
_ => None,
|
||||
})
|
||||
.last();
|
||||
if let Some(m) = modal {
|
||||
m.top_type_state.text_filter = text;
|
||||
}
|
||||
}
|
||||
Msg::InputChanged(FieldId::TextFilterBoard, text) => {
|
||||
project_page.text_filter = text;
|
||||
}
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active) => {
|
||||
if active {
|
||||
project_page.active_avatar_filters = project_page
|
||||
.active_avatar_filters
|
||||
.iter()
|
||||
.filter_map(|id| if *id != user_id { Some(*id) } else { None })
|
||||
.collect();
|
||||
} else {
|
||||
project_page.active_avatar_filters.push(user_id);
|
||||
}
|
||||
}
|
||||
Msg::ProjectToggleOnlyMy => {
|
||||
project_page.only_my_filter = !project_page.only_my_filter;
|
||||
}
|
||||
Msg::ProjectToggleRecentlyUpdated => {
|
||||
project_page.recently_updated_filter = !project_page.recently_updated_filter;
|
||||
}
|
||||
Msg::ProjectClearFilters => {
|
||||
project_page.active_avatar_filters = vec![];
|
||||
project_page.recently_updated_filter = false;
|
||||
project_page.only_my_filter = false;
|
||||
}
|
||||
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model),
|
||||
Msg::IssueDragStopped(_) => {
|
||||
project_page.dragged_issue_id = None;
|
||||
}
|
||||
Msg::ExchangePosition(issue_bellow_id) => {
|
||||
crate::ws::issue::exchange_position(issue_bellow_id, model)
|
||||
}
|
||||
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));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let project_section = vec![
|
||||
breadcrumbs(model),
|
||||
header(),
|
||||
project_board_filters(model),
|
||||
project_board_lists(model),
|
||||
];
|
||||
|
||||
inner_layout(
|
||||
model,
|
||||
"projectPage",
|
||||
project_section,
|
||||
crate::modal::view(model),
|
||||
)
|
||||
}
|
||||
|
||||
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||
let project_name = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default();
|
||||
div![
|
||||
attrs![At::Class => "breadcrumbsContainer"],
|
||||
span!["Projects"],
|
||||
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
|
||||
span![project_name],
|
||||
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
|
||||
span!["Kanban Board"]
|
||||
]
|
||||
}
|
||||
|
||||
fn header() -> Node<Msg> {
|
||||
let button = StyledButton::build()
|
||||
.secondary()
|
||||
.text("Github Repo".to_string())
|
||||
.icon(Icon::Github)
|
||||
.build()
|
||||
.into_node();
|
||||
div![
|
||||
id!["projectBoardHeader"],
|
||||
div![id!["boardName"], "Kanban board"],
|
||||
a![
|
||||
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
|
||||
button
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
let search_input = StyledInput::build(FieldId::TextFilterBoard)
|
||||
.icon(Icon::Search)
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(page_content) => page_content,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let only_my = StyledButton::build()
|
||||
.empty()
|
||||
.active(project_page.only_my_filter)
|
||||
.text("Only My Issues")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let recently_updated = StyledButton::build()
|
||||
.empty()
|
||||
.text("Recently Updated")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated))
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let clear_all = if project_page.only_my_filter
|
||||
|| project_page.recently_updated_filter
|
||||
|| !project_page.active_avatar_filters.is_empty()
|
||||
{
|
||||
seed::button![
|
||||
id!["clearAllFilters"],
|
||||
"Clear all",
|
||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||
]
|
||||
} else {
|
||||
empty![]
|
||||
};
|
||||
|
||||
div![
|
||||
id!["projectBoardFilters"],
|
||||
search_input,
|
||||
avatars_filters(model),
|
||||
only_my,
|
||||
recently_updated,
|
||||
clear_all
|
||||
]
|
||||
}
|
||||
|
||||
fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let active_avatar_filters = &project_page.active_avatar_filters;
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| {
|
||||
let mut class_list = vec!["avatarIsActiveBorder"];
|
||||
let user_id = user.id;
|
||||
let active = active_avatar_filters.contains(&user_id);
|
||||
if active {
|
||||
class_list.push("isActive");
|
||||
}
|
||||
let styled_avatar = StyledAvatar::build()
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.on_click(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active)
|
||||
}))
|
||||
.name(user.name.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
div![attrs![At::Class => class_list.join(" ")], styled_avatar]
|
||||
})
|
||||
.collect();
|
||||
|
||||
div![id!["avatars"], avatars]
|
||||
}
|
||||
|
||||
fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
div![
|
||||
id!["projectBoardLists"],
|
||||
project_issue_list(model, IssueStatus::Backlog),
|
||||
project_issue_list(model, IssueStatus::Selected),
|
||||
project_issue_list(model, IssueStatus::InProgress),
|
||||
project_issue_list(model, IssueStatus::Done),
|
||||
]
|
||||
}
|
||||
|
||||
fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let ids: Vec<IssueId> = if project_page.recently_updated_filter {
|
||||
let mut v: Vec<(IssueId, NaiveDateTime)> = model
|
||||
.issues
|
||||
.iter()
|
||||
.map(|issue| (issue.id, issue.updated_at))
|
||||
.collect();
|
||||
v.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
if v.len() > 10 { v[0..10].to_vec() } else { v }
|
||||
.into_iter()
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
} else {
|
||||
model.issues.iter().map(|issue| issue.id).collect()
|
||||
};
|
||||
let issues: Vec<Node<Msg>> = model
|
||||
.issues
|
||||
.iter()
|
||||
.filter(|issue| {
|
||||
issue_filter_status(issue, &status)
|
||||
&& issue_filter_with_text(issue, project_page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
|
||||
&& issue_filter_with_only_recent(issue, ids.as_slice())
|
||||
})
|
||||
.map(|issue| project_issue(model, 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 send_status = status.clone();
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDragOverStatus(send_status)
|
||||
});
|
||||
|
||||
div![
|
||||
attrs![At::Class => "list";],
|
||||
div![
|
||||
attrs![At::Class => "title"],
|
||||
label,
|
||||
div![attrs![At::Class => "issuesCount"]]
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "issues"; At::DropZone => "link"],
|
||||
drop_handler,
|
||||
drag_over_handler,
|
||||
issues
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
|
||||
&issue.status == status
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_text(issue: &Issue, text: &str) -> bool {
|
||||
text.is_empty() || issue.title.contains(text)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_my(issue: &Issue, only_my: bool, user: &Option<User>) -> bool {
|
||||
let my_id = user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
!only_my || issue.user_ids.contains(&my_id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_recent(issue: &Issue, ids: &[IssueId]) -> bool {
|
||||
ids.is_empty() || ids.contains(&issue.id)
|
||||
}
|
||||
|
||||
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| issue.user_ids.contains(&user.id))
|
||||
.map(|user| {
|
||||
StyledAvatar::build()
|
||||
.size(24)
|
||||
.name(user.name.as_str())
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.build()
|
||||
.into_node()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let issue_type_icon = {
|
||||
StyledIcon::build(issue.issue_type.clone().into())
|
||||
.add_style(format!(
|
||||
"color: var(--{issue_type})",
|
||||
issue_type = issue.issue_type.to_string()
|
||||
))
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
let priority_icon = {
|
||||
let icon = match issue.priority {
|
||||
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
||||
_ => Icon::ArrowUp,
|
||||
};
|
||||
StyledIcon::build(icon)
|
||||
.add_style(format!("color: var(--{})", issue.priority))
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
|
||||
let issue_id = issue.id;
|
||||
let drag_started = drag_ev(Ev::DragStart, move |_| Msg::IssueDragStarted(issue_id));
|
||||
let drag_stopped = drag_ev(Ev::DragEnd, move |_| Msg::IssueDragStopped(issue_id));
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
Msg::ExchangePosition(issue_id)
|
||||
});
|
||||
|
||||
let class_list = vec!["issue"];
|
||||
|
||||
let href = format!("/issues/{id}", id = issue_id);
|
||||
|
||||
a![
|
||||
drag_started,
|
||||
attrs![At::Class => "issueLink"; At::Href => href],
|
||||
div![
|
||||
attrs![At::Class => class_list.join(" "), At::Draggable => true],
|
||||
drag_stopped,
|
||||
drag_over_handler,
|
||||
p![attrs![At::Class => "title"], issue.title,],
|
||||
div![
|
||||
attrs![At::Class => "bottom"],
|
||||
div![
|
||||
div![attrs![At::Class => "issueTypeIcon"], issue_type_icon],
|
||||
div![attrs![At::Class => "issuePriorityIcon"], priority_icon]
|
||||
],
|
||||
div![attrs![At::Class => "assignees"], avatars,],
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
use chrono::NaiveDateTime;
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{ModalType, Model, Page, PageContent, ProjectPage};
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
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::{EditIssueModalFieldId, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.user.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Project)
|
||||
| Msg::ChangePage(Page::AddIssue)
|
||||
| Msg::ChangePage(Page::EditIssue(..)) => {
|
||||
model.page_content = PageContent::Project(ProjectPage::default());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(..))
|
||||
| Msg::ChangePage(Page::Project)
|
||||
| Msg::ChangePage(Page::AddIssue)
|
||||
| Msg::ChangePage(Page::EditIssue(..)) => {
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectRequest);
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest);
|
||||
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest);
|
||||
}
|
||||
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id == issue.id {
|
||||
model.issues.push(issue.clone())
|
||||
} else {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
}
|
||||
Msg::WsMsg(WsMsg::IssueDeleted(id)) => {
|
||||
let mut old: Vec<Issue> = vec![];
|
||||
std::mem::swap(&mut old, &mut model.issues);
|
||||
for is in old {
|
||||
if is.id != id {
|
||||
model.issues.push(is);
|
||||
}
|
||||
}
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Text(text),
|
||||
) => {
|
||||
let modal = model
|
||||
.modals
|
||||
.iter_mut()
|
||||
.filter_map(|modal| match modal {
|
||||
ModalType::EditIssue(_, modal) => Some(modal),
|
||||
_ => None,
|
||||
})
|
||||
.last();
|
||||
if let Some(m) = modal {
|
||||
m.top_type_state.text_filter = text;
|
||||
}
|
||||
}
|
||||
Msg::InputChanged(FieldId::TextFilterBoard, text) => {
|
||||
project_page.text_filter = text;
|
||||
}
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active) => {
|
||||
if active {
|
||||
project_page.active_avatar_filters = project_page
|
||||
.active_avatar_filters
|
||||
.iter()
|
||||
.filter_map(|id| if *id != user_id { Some(*id) } else { None })
|
||||
.collect();
|
||||
} else {
|
||||
project_page.active_avatar_filters.push(user_id);
|
||||
}
|
||||
}
|
||||
Msg::ProjectToggleOnlyMy => {
|
||||
project_page.only_my_filter = !project_page.only_my_filter;
|
||||
}
|
||||
Msg::ProjectToggleRecentlyUpdated => {
|
||||
project_page.recently_updated_filter = !project_page.recently_updated_filter;
|
||||
}
|
||||
Msg::ProjectClearFilters => {
|
||||
project_page.active_avatar_filters = vec![];
|
||||
project_page.recently_updated_filter = false;
|
||||
project_page.only_my_filter = false;
|
||||
}
|
||||
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model),
|
||||
Msg::IssueDragStopped(_) => {
|
||||
crate::ws::issue::sync(model);
|
||||
}
|
||||
Msg::ExchangePosition(issue_bellow_id) => {
|
||||
crate::ws::issue::exchange_position(issue_bellow_id, model)
|
||||
}
|
||||
Msg::IssueDragOverStatus(status) => crate::ws::issue::change_status(status, model),
|
||||
Msg::IssueDropZone(_status) => crate::ws::issue::sync(model),
|
||||
Msg::DeleteIssue(issue_id) => {
|
||||
send_ws_msg(jirs_data::WsMsg::IssueDeleteRequest(issue_id));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let project_section = vec![
|
||||
breadcrumbs(model),
|
||||
header(),
|
||||
project_board_filters(model),
|
||||
project_board_lists(model),
|
||||
];
|
||||
|
||||
inner_layout(
|
||||
model,
|
||||
"projectPage",
|
||||
project_section,
|
||||
crate::modal::view(model),
|
||||
)
|
||||
}
|
||||
|
||||
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||
let project_name = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default();
|
||||
div![
|
||||
attrs![At::Class => "breadcrumbsContainer"],
|
||||
span!["Projects"],
|
||||
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
|
||||
span![project_name],
|
||||
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
|
||||
span!["Kanban Board"]
|
||||
]
|
||||
}
|
||||
|
||||
fn header() -> Node<Msg> {
|
||||
let button = StyledButton::build()
|
||||
.secondary()
|
||||
.text("Github Repo".to_string())
|
||||
.icon(Icon::Github)
|
||||
.build()
|
||||
.into_node();
|
||||
div![
|
||||
id!["projectBoardHeader"],
|
||||
div![id!["boardName"], "Kanban board"],
|
||||
a![
|
||||
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
|
||||
button
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
let search_input = StyledInput::build(FieldId::TextFilterBoard)
|
||||
.icon(Icon::Search)
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(page_content) => page_content,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let only_my = StyledButton::build()
|
||||
.empty()
|
||||
.active(project_page.only_my_filter)
|
||||
.text("Only My Issues")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let recently_updated = StyledButton::build()
|
||||
.empty()
|
||||
.text("Recently Updated")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated))
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let clear_all = if project_page.only_my_filter
|
||||
|| project_page.recently_updated_filter
|
||||
|| !project_page.active_avatar_filters.is_empty()
|
||||
{
|
||||
seed::button![
|
||||
id!["clearAllFilters"],
|
||||
"Clear all",
|
||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||
]
|
||||
} else {
|
||||
empty![]
|
||||
};
|
||||
|
||||
div![
|
||||
id!["projectBoardFilters"],
|
||||
search_input,
|
||||
avatars_filters(model),
|
||||
only_my,
|
||||
recently_updated,
|
||||
clear_all
|
||||
]
|
||||
}
|
||||
|
||||
fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let active_avatar_filters = &project_page.active_avatar_filters;
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| {
|
||||
let mut class_list = vec!["avatarIsActiveBorder"];
|
||||
let user_id = user.id;
|
||||
let active = active_avatar_filters.contains(&user_id);
|
||||
if active {
|
||||
class_list.push("isActive");
|
||||
}
|
||||
let styled_avatar = StyledAvatar::build()
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.on_click(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active)
|
||||
}))
|
||||
.name(user.name.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
div![attrs![At::Class => class_list.join(" ")], styled_avatar]
|
||||
})
|
||||
.collect();
|
||||
|
||||
div![id!["avatars"], avatars]
|
||||
}
|
||||
|
||||
fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
div![
|
||||
id!["projectBoardLists"],
|
||||
project_issue_list(model, IssueStatus::Backlog),
|
||||
project_issue_list(model, IssueStatus::Selected),
|
||||
project_issue_list(model, IssueStatus::InProgress),
|
||||
project_issue_list(model, IssueStatus::Done),
|
||||
]
|
||||
}
|
||||
|
||||
fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let ids: Vec<IssueId> = if project_page.recently_updated_filter {
|
||||
let mut v: Vec<(IssueId, NaiveDateTime)> = model
|
||||
.issues
|
||||
.iter()
|
||||
.map(|issue| (issue.id, issue.updated_at))
|
||||
.collect();
|
||||
v.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
if v.len() > 10 { v[0..10].to_vec() } else { v }
|
||||
.into_iter()
|
||||
.map(|(id, _)| id)
|
||||
.collect()
|
||||
} else {
|
||||
model.issues.iter().map(|issue| issue.id).collect()
|
||||
};
|
||||
let issues: Vec<Node<Msg>> = model
|
||||
.issues
|
||||
.iter()
|
||||
.filter(|issue| {
|
||||
issue_filter_status(issue, &status)
|
||||
&& issue_filter_with_text(issue, project_page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
|
||||
&& issue_filter_with_only_recent(issue, ids.as_slice())
|
||||
})
|
||||
.map(|issue| project_issue(model, 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 send_status = status.clone();
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::IssueDragOverStatus(send_status)
|
||||
});
|
||||
|
||||
div![
|
||||
attrs![At::Class => "list";],
|
||||
div![
|
||||
attrs![At::Class => "title"],
|
||||
label,
|
||||
div![attrs![At::Class => "issuesCount"]]
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "issues"; At::DropZone => "link"],
|
||||
drop_handler,
|
||||
drag_over_handler,
|
||||
issues
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_status(issue: &Issue, status: &IssueStatus) -> bool {
|
||||
&issue.status == status
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_text(issue: &Issue, text: &str) -> bool {
|
||||
text.is_empty() || issue.title.contains(text)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_my(issue: &Issue, only_my: bool, user: &Option<User>) -> bool {
|
||||
let my_id = user.as_ref().map(|u| u.id).unwrap_or_default();
|
||||
!only_my || issue.user_ids.contains(&my_id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn issue_filter_with_only_recent(issue: &Issue, ids: &[IssueId]) -> bool {
|
||||
ids.is_empty() || ids.contains(&issue.id)
|
||||
}
|
||||
|
||||
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| issue.user_ids.contains(&user.id))
|
||||
.map(|user| {
|
||||
StyledAvatar::build()
|
||||
.size(24)
|
||||
.name(user.name.as_str())
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.build()
|
||||
.into_node()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let issue_type_icon = {
|
||||
StyledIcon::build(issue.issue_type.clone().into())
|
||||
.add_style(format!(
|
||||
"color: var(--{issue_type})",
|
||||
issue_type = issue.issue_type.to_string()
|
||||
))
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
let priority_icon = {
|
||||
let icon = match issue.priority {
|
||||
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
||||
_ => Icon::ArrowUp,
|
||||
};
|
||||
StyledIcon::build(icon)
|
||||
.add_style(format!("color: var(--{})", issue.priority))
|
||||
.build()
|
||||
.into_node()
|
||||
};
|
||||
|
||||
let issue_id = issue.id;
|
||||
let drag_started = drag_ev(Ev::DragStart, move |_| Msg::IssueDragStarted(issue_id));
|
||||
let drag_stopped = drag_ev(Ev::DragEnd, move |_| Msg::IssueDragStopped(issue_id));
|
||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
Msg::ExchangePosition(issue_id)
|
||||
});
|
||||
|
||||
let class_list = vec!["issue"];
|
||||
|
||||
let href = format!("/issues/{id}", id = issue_id);
|
||||
|
||||
a![
|
||||
drag_started,
|
||||
attrs![At::Class => "issueLink"; At::Href => href],
|
||||
div![
|
||||
attrs![At::Class => class_list.join(" "), At::Draggable => true],
|
||||
drag_stopped,
|
||||
drag_over_handler,
|
||||
p![attrs![At::Class => "title"], issue.title,],
|
||||
div![
|
||||
attrs![At::Class => "bottom"],
|
||||
div![
|
||||
div![attrs![At::Class => "issueTypeIcon"], issue_type_icon],
|
||||
div![attrs![At::Class => "issuePriorityIcon"], priority_icon]
|
||||
],
|
||||
div![attrs![At::Class => "assignees"], avatars,],
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
@ -1,183 +1,182 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{ProjectCategory, ToVec, WsMsg};
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_editor::StyledEditor;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
|
||||
use crate::shared::styled_select_child::ToStyledSelectChild;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::FieldChange::TabChanged;
|
||||
use crate::{model, FieldId, Msg, ProjectSettingsFieldId};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.user.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if model.page != Page::ProjectSettings {
|
||||
log!("not settings page");
|
||||
return;
|
||||
}
|
||||
|
||||
match msg {
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
}
|
||||
Msg::ChangePage(Page::ProjectSettings) => {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
build_page_content(model);
|
||||
}
|
||||
Msg::WsMsg(WsMsg::ProjectLoaded(..)) => {
|
||||
build_page_content(model);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::ProjectSettings(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
page.project_category_state.update(&msg, orders);
|
||||
match msg {
|
||||
Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())),
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Name), text) => {
|
||||
page.payload.name = Some(text);
|
||||
}
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Url), text) => {
|
||||
page.payload.url = Some(text);
|
||||
}
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Description), text) => {
|
||||
page.payload.description = Some(text);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::ProjectSettings(ProjectSettingsFieldId::Category),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
let category = value.into();
|
||||
page.payload.category = Some(category);
|
||||
}
|
||||
Msg::ModalChanged(TabChanged(
|
||||
FieldId::ProjectSettings(ProjectSettingsFieldId::Description),
|
||||
mode,
|
||||
)) => {
|
||||
page.description_mode = mode;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
let project = match &model.project {
|
||||
Some(project) => project,
|
||||
_ => return,
|
||||
};
|
||||
model.page_content = PageContent::ProjectSettings(ProjectSettingsPage::new(project));
|
||||
}
|
||||
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::ProjectSettings(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let name = StyledTextarea::build()
|
||||
.value(page.payload.name.as_ref().cloned().unwrap_or_default())
|
||||
.height(39)
|
||||
.max_height(39)
|
||||
.disable_auto_resize()
|
||||
.build(FieldId::ProjectSettings(ProjectSettingsFieldId::Name))
|
||||
.into_node();
|
||||
let name_field = StyledField::build()
|
||||
.label("Name")
|
||||
.input(name)
|
||||
.tip("")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let url = StyledTextarea::build()
|
||||
.height(39)
|
||||
.max_height(39)
|
||||
.disable_auto_resize()
|
||||
.value(page.payload.url.as_ref().cloned().unwrap_or_default())
|
||||
.build(FieldId::ProjectSettings(ProjectSettingsFieldId::Url))
|
||||
.into_node();
|
||||
let url_field = StyledField::build()
|
||||
.label("Url")
|
||||
.input(url)
|
||||
.tip("")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let description = StyledEditor::build(FieldId::ProjectSettings(
|
||||
ProjectSettingsFieldId::Description,
|
||||
))
|
||||
.text(
|
||||
page.payload
|
||||
.description
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.update_on(Ev::Change)
|
||||
.mode(page.description_mode.clone())
|
||||
.build()
|
||||
.into_node();
|
||||
let description_field = StyledField::build()
|
||||
.input(description)
|
||||
.label("Description")
|
||||
.tip("Describe the project in as much detail as you'd like.")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let category = StyledSelect::build(FieldId::ProjectSettings(ProjectSettingsFieldId::Category))
|
||||
.opened(page.project_category_state.opened)
|
||||
.text_filter(page.project_category_state.text_filter.as_str())
|
||||
.valid(true)
|
||||
.normal()
|
||||
.options(
|
||||
ProjectCategory::ordered()
|
||||
.into_iter()
|
||||
.map(|c| c.to_select_child())
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![page
|
||||
.payload
|
||||
.category
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.to_select_child()])
|
||||
.build()
|
||||
.into_node();
|
||||
let category_field = StyledField::build()
|
||||
.label("Project Category")
|
||||
.input(category)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let save_button = StyledButton::build()
|
||||
.add_class("actionButton")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectSaveChanges))
|
||||
.text("Save changes")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Project Details")
|
||||
.add_field(name_field)
|
||||
.add_field(url_field)
|
||||
.add_field(description_field)
|
||||
.add_field(category_field)
|
||||
.add_field(save_button)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_section = vec![div![class!["formContainer"], form]];
|
||||
|
||||
inner_layout(model, "projectSettings", project_section, empty![])
|
||||
}
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{ProjectCategory, ToVec, WsMsg};
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_editor::StyledEditor;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
|
||||
use crate::shared::styled_select_child::ToStyledSelectChild;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::FieldChange::TabChanged;
|
||||
use crate::{model, FieldId, Msg, ProjectSettingsFieldId};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if model.user.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if model.page != Page::ProjectSettings {
|
||||
return;
|
||||
}
|
||||
|
||||
match msg {
|
||||
Msg::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
}
|
||||
Msg::ChangePage(Page::ProjectSettings) => {
|
||||
send_ws_msg(WsMsg::ProjectRequest);
|
||||
build_page_content(model);
|
||||
}
|
||||
Msg::WsMsg(WsMsg::ProjectLoaded(..)) => {
|
||||
build_page_content(model);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let page = match &mut model.page_content {
|
||||
PageContent::ProjectSettings(page) => page,
|
||||
_ => return,
|
||||
};
|
||||
page.project_category_state.update(&msg, orders);
|
||||
match msg {
|
||||
Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())),
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Name), text) => {
|
||||
page.payload.name = Some(text);
|
||||
}
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Url), text) => {
|
||||
page.payload.url = Some(text);
|
||||
}
|
||||
Msg::InputChanged(FieldId::ProjectSettings(ProjectSettingsFieldId::Description), text) => {
|
||||
page.payload.description = Some(text);
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::ProjectSettings(ProjectSettingsFieldId::Category),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
let category = value.into();
|
||||
page.payload.category = Some(category);
|
||||
}
|
||||
Msg::ModalChanged(TabChanged(
|
||||
FieldId::ProjectSettings(ProjectSettingsFieldId::Description),
|
||||
mode,
|
||||
)) => {
|
||||
page.description_mode = mode;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_page_content(model: &mut Model) {
|
||||
let project = match &model.project {
|
||||
Some(project) => project,
|
||||
_ => return,
|
||||
};
|
||||
model.page_content = PageContent::ProjectSettings(ProjectSettingsPage::new(project));
|
||||
}
|
||||
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match &model.page_content {
|
||||
PageContent::ProjectSettings(page) => page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let name = StyledTextarea::build()
|
||||
.value(page.payload.name.as_ref().cloned().unwrap_or_default())
|
||||
.height(39)
|
||||
.max_height(39)
|
||||
.disable_auto_resize()
|
||||
.build(FieldId::ProjectSettings(ProjectSettingsFieldId::Name))
|
||||
.into_node();
|
||||
let name_field = StyledField::build()
|
||||
.label("Name")
|
||||
.input(name)
|
||||
.tip("")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let url = StyledTextarea::build()
|
||||
.height(39)
|
||||
.max_height(39)
|
||||
.disable_auto_resize()
|
||||
.value(page.payload.url.as_ref().cloned().unwrap_or_default())
|
||||
.build(FieldId::ProjectSettings(ProjectSettingsFieldId::Url))
|
||||
.into_node();
|
||||
let url_field = StyledField::build()
|
||||
.label("Url")
|
||||
.input(url)
|
||||
.tip("")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let description = StyledEditor::build(FieldId::ProjectSettings(
|
||||
ProjectSettingsFieldId::Description,
|
||||
))
|
||||
.text(
|
||||
page.payload
|
||||
.description
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.update_on(Ev::Change)
|
||||
.mode(page.description_mode.clone())
|
||||
.build()
|
||||
.into_node();
|
||||
let description_field = StyledField::build()
|
||||
.input(description)
|
||||
.label("Description")
|
||||
.tip("Describe the project in as much detail as you'd like.")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let category = StyledSelect::build(FieldId::ProjectSettings(ProjectSettingsFieldId::Category))
|
||||
.opened(page.project_category_state.opened)
|
||||
.text_filter(page.project_category_state.text_filter.as_str())
|
||||
.valid(true)
|
||||
.normal()
|
||||
.options(
|
||||
ProjectCategory::ordered()
|
||||
.into_iter()
|
||||
.map(|c| c.to_select_child())
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![page
|
||||
.payload
|
||||
.category
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.to_select_child()])
|
||||
.build()
|
||||
.into_node();
|
||||
let category_field = StyledField::build()
|
||||
.label("Project Category")
|
||||
.input(category)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let save_button = StyledButton::build()
|
||||
.add_class("actionButton")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectSaveChanges))
|
||||
.text("Save changes")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let form = StyledForm::build()
|
||||
.heading("Project Details")
|
||||
.add_field(name_field)
|
||||
.add_field(url_field)
|
||||
.add_field(description_field)
|
||||
.add_field(category_field)
|
||||
.add_field(save_button)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_section = vec![div![class!["formContainer"], form]];
|
||||
|
||||
inner_layout(model, "projectSettings", project_section, empty![])
|
||||
}
|
||||
|
@ -1,165 +1,174 @@
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{Model, PageContent, ProjectPage};
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.dragged_issue_id = Some(issue_id);
|
||||
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
|
||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.dragged_issue_id == Some(issue_bellow_id)
|
||||
|| project_page.last_drag_exchange_id == Some(issue_bellow_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let dragged_id = match 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, project_page);
|
||||
}
|
||||
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, project_page);
|
||||
mark_dirty(below.id, project_page);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
project_page.last_drag_exchange_id = Some(issue_bellow_id);
|
||||
}
|
||||
|
||||
pub fn dropped(_status: IssueStatus, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !project_page.dirty_issues.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let payload = UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type.clone(),
|
||||
status: issue.status.clone(),
|
||||
priority: issue.priority.clone(),
|
||||
list_position: issue.list_position,
|
||||
description: issue.description.clone(),
|
||||
description_text: issue.description_text.clone(),
|
||||
estimate: issue.estimate,
|
||||
time_spent: issue.time_spent,
|
||||
time_remaining: issue.time_remaining,
|
||||
project_id: issue.project_id,
|
||||
reporter_id: issue.reporter_id,
|
||||
user_ids: issue.user_ids.clone(),
|
||||
};
|
||||
project_page.dragged_issue_id = None;
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload));
|
||||
project_page.last_drag_exchange_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_status(status: IssueStatus, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let issue_id = match 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;
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
|
||||
if !project_page.dirty_issues.contains(&id) {
|
||||
project_page.dirty_issues.push(id);
|
||||
}
|
||||
}
|
||||
use seed::*;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{Model, PageContent, ProjectPage};
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.dragged_issue_id = Some(issue_id);
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
|
||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.dragged_issue_id == Some(issue_bellow_id)
|
||||
|| project_page.last_drag_exchange_id == Some(issue_bellow_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let dragged_id = match project_page.dragged_issue_id.as_ref().cloned() {
|
||||
Some(id) => id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
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, project_page);
|
||||
}
|
||||
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, project_page);
|
||||
mark_dirty(below.id, project_page);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
project_page.last_drag_exchange_id = Some(issue_bellow_id);
|
||||
}
|
||||
|
||||
pub fn sync(model: &mut Model) {
|
||||
log!("------------------------------------------------------------------");
|
||||
log!("| SYNC |");
|
||||
log!("------------------------------------------------------------------");
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !project_page.dirty_issues.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let payload = UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type.clone(),
|
||||
status: issue.status.clone(),
|
||||
priority: issue.priority.clone(),
|
||||
list_position: issue.list_position,
|
||||
description: issue.description.clone(),
|
||||
description_text: issue.description_text.clone(),
|
||||
estimate: issue.estimate,
|
||||
time_spent: issue.time_spent,
|
||||
time_remaining: issue.time_remaining,
|
||||
project_id: issue.project_id,
|
||||
reporter_id: issue.reporter_id,
|
||||
user_ids: issue.user_ids.clone(),
|
||||
};
|
||||
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload));
|
||||
}
|
||||
project_page.dragged_issue_id = None;
|
||||
project_page.last_drag_exchange_id = None;
|
||||
project_page.dirty_issues.clear();
|
||||
}
|
||||
|
||||
pub fn change_status(status: IssueStatus, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let issue_id = match project_page.dragged_issue_id.as_ref().cloned() {
|
||||
Some(issue_id) => issue_id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
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 {
|
||||
if issue.list_position != pos {
|
||||
issue.list_position = pos;
|
||||
mark_dirty(issue.id, project_page);
|
||||
}
|
||||
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;
|
||||
} else {
|
||||
issue.status = status;
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
|
||||
if !project_page.dirty_issues.contains(&id) {
|
||||
project_page.dirty_issues.push(id);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user