Save estimated time. Change path on drop add issue. Better support for multi-page

This commit is contained in:
Adrian Wozniak 2020-04-13 19:55:21 +02:00
parent ff5a43efc6
commit cb6f41fe0a
9 changed files with 140 additions and 61 deletions

View File

@ -177,6 +177,9 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(page) => { Msg::ChangePage(page) => {
model.page = page.clone(); model.page = page.clone();
} }
Msg::ToggleAboutTooltip => {
model.about_tooltip_visible = !model.about_tooltip_visible;
}
_ => (), _ => (),
} }
crate::ws::update(&msg, model, orders); crate::ws::update(&msg, model, orders);

View File

@ -109,6 +109,19 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), text) => { Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), text) => {
modal.comment_form.body = text.clone(); modal.comment_form.body = text.clone();
} }
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate), value) => {
match value.parse::<i32>() {
Ok(n) if !value.is_empty() => {
modal.payload.estimate = Some(n);
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
_ if value.is_empty() => {
modal.payload.estimate = None;
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
_ => {}
}
}
Msg::SaveComment => { Msg::SaveComment => {
let msg = match modal.comment_form.id { let msg = match modal.comment_form.id {
Some(id) => WsMsg::UpdateComment(UpdateCommentPayload { Some(id) => WsMsg::UpdateComment(UpdateCommentPayload {
@ -564,6 +577,14 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate)) let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
.valid(true) .valid(true)
.value(
payload
.estimate
.as_ref()
.map(|n| n.to_string())
.clone()
.unwrap_or_default(),
)
.build() .build()
.into_node(); .into_node();
let estimate_field = StyledField::build() let estimate_field = StyledField::build()

View File

@ -16,7 +16,7 @@ mod issue_details;
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::ModalDropped => match model.modals.pop() { Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) => { Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
seed::push_route(vec!["board"]); seed::push_route(vec!["board"]);
orders.send_msg(Msg::ChangePage(Page::Project)); orders.send_msg(Msg::ChangePage(Page::Project));
} }

View File

@ -76,7 +76,7 @@ impl EditIssueModal {
priority_state: StyledSelectState::new(FieldId::EditIssueModal( priority_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::Priority, EditIssueModalFieldId::Priority,
)), )),
description_editor_mode: Mode::Editor, description_editor_mode: Mode::View,
comment_form: CommentForm { comment_form: CommentForm {
id: None, id: None,
body: String::new(), body: String::new(),
@ -178,9 +178,8 @@ pub struct UpdateProjectForm {
pub fields: UpdateProjectPayload, pub fields: UpdateProjectPayload,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Debug, Default)]
pub struct ProjectPage { pub struct ProjectPage {
pub about_tooltip_visible: bool,
pub text_filter: String, pub text_filter: String,
pub active_avatar_filters: Vec<UserId>, pub active_avatar_filters: Vec<UserId>,
pub only_my_filter: bool, pub only_my_filter: bool,
@ -190,10 +189,23 @@ pub struct ProjectPage {
pub dirty_issues: Vec<IssueId>, pub dirty_issues: Vec<IssueId>,
} }
#[derive(Debug)]
pub struct ProjectSettingsPage {
pub payload: UpdateProjectPayload,
pub project_type_state: StyledSelectState,
}
#[derive(Debug)]
pub enum PageContent {
Project(ProjectPage),
ProjectSettings(ProjectSettingsPage),
}
#[derive(Debug)] #[derive(Debug)]
pub struct Model { pub struct Model {
pub host_url: String, pub host_url: String,
pub access_token: Option<Uuid>, pub access_token: Option<Uuid>,
pub about_tooltip_visible: bool,
// mapped // mapped
pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>, pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>,
@ -208,7 +220,7 @@ pub struct Model {
// pages // pages
pub page: Page, pub page: Page,
pub project_page: ProjectPage, pub page_content: PageContent,
pub project: Option<Project>, pub project: Option<Project>,
pub user: Option<User>, pub user: Option<User>,
@ -231,19 +243,11 @@ impl Default for Model {
comments_by_project_id: Default::default(), comments_by_project_id: Default::default(),
page: Page::Project, page: Page::Project,
host_url, host_url,
project_page: ProjectPage { page_content: PageContent::Project(ProjectPage::default()),
about_tooltip_visible: false,
text_filter: "".to_string(),
active_avatar_filters: vec![],
only_my_filter: false,
recently_updated_filter: false,
dragged_issue_id: None,
last_drag_exchange_id: None,
dirty_issues: vec![],
},
modals: vec![], modals: vec![],
project: None, project: None,
comments: vec![], comments: vec![],
about_tooltip_visible: false,
} }
} }
} }

View File

@ -4,7 +4,7 @@ use seed::{prelude::*, *};
use jirs_data::*; use jirs_data::*;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page, PageContent};
use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
@ -14,6 +14,11 @@ use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{EditIssueModalFieldId, FieldId, Msg}; use crate::{EditIssueModalFieldId, FieldId, Msg};
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>) {
let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page,
_ => return,
};
match msg { match msg {
Msg::ChangePage(Page::Project) Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::AddIssue)
@ -44,9 +49,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
} }
orders.skip().send_msg(Msg::ModalDropped); orders.skip().send_msg(Msg::ModalDropped);
} }
Msg::ToggleAboutTooltip => {
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
}
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType), FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
StyledSelectChange::Text(text), StyledSelectChange::Text(text),
@ -64,12 +66,11 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
} }
} }
Msg::InputChanged(FieldId::TextFilterBoard, text) => { Msg::InputChanged(FieldId::TextFilterBoard, text) => {
model.project_page.text_filter = text; project_page.text_filter = text;
} }
Msg::ProjectAvatarFilterChanged(user_id, active) => match active { Msg::ProjectAvatarFilterChanged(user_id, active) => match active {
true => { true => {
model.project_page.active_avatar_filters = model project_page.active_avatar_filters = project_page
.project_page
.active_avatar_filters .active_avatar_filters
.iter() .iter()
.filter(|id| **id != user_id) .filter(|id| **id != user_id)
@ -77,25 +78,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
.collect(); .collect();
} }
false => { false => {
model.project_page.active_avatar_filters.push(user_id); project_page.active_avatar_filters.push(user_id);
} }
}, },
Msg::ProjectToggleOnlyMy => { Msg::ProjectToggleOnlyMy => {
model.project_page.only_my_filter = !model.project_page.only_my_filter; project_page.only_my_filter = !project_page.only_my_filter;
} }
Msg::ProjectToggleRecentlyUpdated => { Msg::ProjectToggleRecentlyUpdated => {
model.project_page.recently_updated_filter = project_page.recently_updated_filter = !project_page.recently_updated_filter;
!model.project_page.recently_updated_filter;
} }
Msg::ProjectClearFilters => { Msg::ProjectClearFilters => {
let pp = &mut model.project_page; project_page.active_avatar_filters = vec![];
pp.active_avatar_filters = vec![]; project_page.recently_updated_filter = false;
pp.recently_updated_filter = false; project_page.only_my_filter = false;
pp.only_my_filter = false;
} }
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model), Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model),
Msg::IssueDragStopped(_) => { Msg::IssueDragStopped(_) => {
model.project_page.dragged_issue_id = None; project_page.dragged_issue_id = None;
} }
Msg::ExchangePosition(issue_bellow_id) => { Msg::ExchangePosition(issue_bellow_id) => {
crate::ws::issue::exchange_position(issue_bellow_id, model) crate::ws::issue::exchange_position(issue_bellow_id, model)
@ -165,11 +164,14 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let project_page = &model.project_page; let project_page = match &model.page_content {
PageContent::Project(page_content) => page_content,
_ => return empty![],
};
let only_my = StyledButton::build() let only_my = StyledButton::build()
.empty() .empty()
.active(model.project_page.only_my_filter) .active(project_page.only_my_filter)
.text("Only My Issues") .text("Only My Issues")
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)) .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
.build() .build()
@ -205,7 +207,11 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
} }
fn avatars_filters(model: &Model) -> Node<Msg> { fn avatars_filters(model: &Model) -> Node<Msg> {
let active_avatar_filters = &model.project_page.active_avatar_filters; 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 let avatars: Vec<Node<Msg>> = model
.users .users
.iter() .iter()
@ -242,7 +248,11 @@ fn project_board_lists(model: &Model) -> Node<Msg> {
} }
fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg> { fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg> {
let ids = if model.project_page.recently_updated_filter { let project_page = match &model.page_content {
PageContent::Project(project_page) => project_page,
_ => return empty![],
};
let ids = if project_page.recently_updated_filter {
let mut v: Vec<(IssueId, NaiveDateTime)> = model let mut v: Vec<(IssueId, NaiveDateTime)> = model
.issues .issues
.iter() .iter()
@ -261,8 +271,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
.iter() .iter()
.filter(|issue| { .filter(|issue| {
issue_filter_status(issue, &status) issue_filter_status(issue, &status)
&& issue_filter_with_text(issue, model.project_page.text_filter.as_str()) && issue_filter_with_text(issue, project_page.text_filter.as_str())
&& issue_filter_with_only_my(issue, model.project_page.only_my_filter, &model.user) && issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
&& issue_filter_with_only_recent(issue, &ids) && issue_filter_with_only_recent(issue, &ids)
}) })
.map(|issue| project_issue(model, issue)) .map(|issue| project_issue(model, issue))

View File

@ -4,7 +4,6 @@ use crate::shared::styled_editor::StyledEditor;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelect;
use crate::shared::{inner_layout, ToNode}; use crate::shared::{inner_layout, ToNode};
use crate::{model, FieldId, Msg, ProjectSettingsFieldId}; use crate::{model, FieldId, Msg, ProjectSettingsFieldId};

View File

@ -61,14 +61,15 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
.into_node(); .into_node();
styled_tooltip::StyledTooltip { styled_tooltip::StyledTooltip {
visible: model.project_page.about_tooltip_visible, visible: model.about_tooltip_visible,
class_name: "aboutTooltipPopup".to_string(), class_name: "aboutTooltipPopup".to_string(),
children: div![ children: div![
ev(Ev::Click, |_| Msg::ToggleAboutTooltip), ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
attrs![At::Class => "feedbackDropdown"], attrs![At::Class => "feedbackDropdown"],
div![ div![
attrs![At::Class => "feedbackImageCont"], attrs![At::Class => "feedbackImageCont"],
img![attrs![At::Src => "/feedback.png", At::Class => "feedbackImage"]] img![attrs![At::Src => "/feedback.png"]],
class!["feedbackImage"],
], ],
div![ div![
attrs![At::Class => "feedbackParagraph"], attrs![At::Class => "feedbackParagraph"],

View File

@ -9,6 +9,7 @@ pub struct StyledInput {
id: FieldId, id: FieldId,
icon: Option<Icon>, icon: Option<Icon>,
valid: bool, valid: bool,
value: Option<String>,
} }
impl StyledInput { impl StyledInput {
@ -17,6 +18,7 @@ impl StyledInput {
id, id,
icon: None, icon: None,
valid: None, valid: None,
value: None,
} }
} }
} }
@ -26,6 +28,7 @@ pub struct StyledInputBuilder {
id: FieldId, id: FieldId,
icon: Option<Icon>, icon: Option<Icon>,
valid: Option<bool>, valid: Option<bool>,
value: Option<String>,
} }
impl StyledInputBuilder { impl StyledInputBuilder {
@ -39,11 +42,20 @@ impl StyledInputBuilder {
self self
} }
pub fn value<S>(mut self, v: S) -> Self
where
S: Into<String>,
{
self.value = Some(v.into());
self
}
pub fn build(self) -> StyledInput { pub fn build(self) -> StyledInput {
StyledInput { StyledInput {
id: self.id, id: self.id,
icon: self.icon, icon: self.icon,
valid: self.valid.unwrap_or_default(), valid: self.valid.unwrap_or_default(),
value: self.value,
} }
} }
} }
@ -55,7 +67,12 @@ impl ToNode for StyledInput {
} }
pub fn render(values: StyledInput) -> Node<Msg> { pub fn render(values: StyledInput) -> Node<Msg> {
let StyledInput { id, icon, valid } = values; let StyledInput {
id,
icon,
valid,
value,
} = values;
let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)]; let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)];
if !valid { if !valid {
@ -74,7 +91,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
let mut handlers = vec![]; let mut handlers = vec![];
handlers.push(input_ev(Ev::KeyUp, move |value| { handlers.push(input_ev(Ev::Change, move |value| {
Msg::InputChanged(id, value) Msg::InputChanged(id, value)
})); }));
@ -85,6 +102,12 @@ pub fn render(values: StyledInput) -> Node<Msg> {
ev.stop_propagation(); ev.stop_propagation();
Msg::NoOp Msg::NoOp
}), }),
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers], seed::input![
attrs![
At::Class => input_class_list.join(" "),
At::Value => value.unwrap_or_default(),
],
handlers
],
] ]
} }

View File

@ -1,21 +1,29 @@
use jirs_data::*; use jirs_data::*;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::Model; use crate::model::{Model, PageContent, ProjectPage};
pub fn drag_started(issue_id: IssueId, model: &mut Model) { pub fn drag_started(issue_id: IssueId, model: &mut Model) {
model.project_page.dragged_issue_id = Some(issue_id); 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, model); mark_dirty(issue_id, project_page);
} }
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) { pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
if model.project_page.dragged_issue_id == Some(issue_bellow_id) let project_page = match &mut model.page_content {
|| model.project_page.last_drag_exchange_id == Some(issue_bellow_id) 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; return;
} }
let dragged_id = match model.project_page.dragged_issue_id { let dragged_id = match project_page.dragged_issue_id {
Some(id) => id, Some(id) => id,
_ => return, _ => return,
}; };
@ -50,7 +58,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
for mut c in issues.into_iter() { for mut c in issues.into_iter() {
if c.status == below.status && c.list_position > below.list_position { if c.status == below.status && c.list_position > below.list_position {
c.list_position += 1; c.list_position += 1;
mark_dirty(c.id, model); mark_dirty(c.id, project_page);
} }
model.issues.push(c); model.issues.push(c);
} }
@ -59,20 +67,25 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
} }
std::mem::swap(&mut dragged.list_position, &mut below.list_position); std::mem::swap(&mut dragged.list_position, &mut below.list_position);
mark_dirty(dragged.id, model); mark_dirty(dragged.id, project_page);
mark_dirty(below.id, model); mark_dirty(below.id, project_page);
model.issues.push(below); model.issues.push(below);
model.issues.push(dragged); model.issues.push(dragged);
model model
.issues .issues
.sort_by(|a, b| a.list_position.cmp(&b.list_position)); .sort_by(|a, b| a.list_position.cmp(&b.list_position));
model.project_page.last_drag_exchange_id = Some(issue_bellow_id); project_page.last_drag_exchange_id = Some(issue_bellow_id);
} }
pub fn dropped(_status: IssueStatus, model: &mut Model) { 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() { for issue in model.issues.iter() {
if !model.project_page.dirty_issues.contains(&issue.id) { if !project_page.dirty_issues.contains(&issue.id) {
continue; continue;
} }
@ -91,14 +104,19 @@ pub fn dropped(_status: IssueStatus, model: &mut Model) {
reporter_id: issue.reporter_id, reporter_id: issue.reporter_id,
user_ids: issue.user_ids.clone(), user_ids: issue.user_ids.clone(),
}; };
model.project_page.dragged_issue_id = None; project_page.dragged_issue_id = None;
send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload)); send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload));
model.project_page.last_drag_exchange_id = None; project_page.last_drag_exchange_id = None;
} }
} }
pub fn change_status(status: IssueStatus, model: &mut Model) { pub fn change_status(status: IssueStatus, model: &mut Model) {
let issue_id = match model.project_page.dragged_issue_id.as_ref().cloned() { 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, Some(issue_id) => issue_id,
_ => return, _ => return,
}; };
@ -136,12 +154,12 @@ pub fn change_status(status: IssueStatus, model: &mut Model) {
issue.list_position = pos + 1; issue.list_position = pos + 1;
model.issues.push(issue); model.issues.push(issue);
mark_dirty(issue_id, model); mark_dirty(issue_id, project_page);
} }
#[inline] #[inline]
fn mark_dirty(id: IssueId, model: &mut Model) { fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
if !model.project_page.dirty_issues.contains(&id) { if !project_page.dirty_issues.contains(&id) {
model.project_page.dirty_issues.push(id); project_page.dirty_issues.push(id);
} }
} }