Fix drag&drop

This commit is contained in:
Adrian Wozniak 2020-04-16 20:55:03 +02:00
parent 15095dc574
commit cf28ed7ef4
4 changed files with 767 additions and 755 deletions

View File

@ -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) => {

View File

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

View File

@ -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![])
}

View File

@ -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);
}
}