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) => {
model.page = page.clone();
}
Msg::ToggleAboutTooltip => {
model.about_tooltip_visible = !model.about_tooltip_visible;
}
_ => (),
}
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) => {
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 => {
let msg = match modal.comment_form.id {
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))
.valid(true)
.value(
payload
.estimate
.as_ref()
.map(|n| n.to_string())
.clone()
.unwrap_or_default(),
)
.build()
.into_node();
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>) {
match msg {
Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) => {
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
seed::push_route(vec!["board"]);
orders.send_msg(Msg::ChangePage(Page::Project));
}

View File

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

View File

@ -4,7 +4,7 @@ use seed::{prelude::*, *};
use jirs_data::*;
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_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon};
@ -14,6 +14,11 @@ 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>) {
let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page,
_ => return,
};
match msg {
Msg::ChangePage(Page::Project)
| 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);
}
Msg::ToggleAboutTooltip => {
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
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) => {
model.project_page.text_filter = text;
project_page.text_filter = text;
}
Msg::ProjectAvatarFilterChanged(user_id, active) => match active {
true => {
model.project_page.active_avatar_filters = model
.project_page
project_page.active_avatar_filters = project_page
.active_avatar_filters
.iter()
.filter(|id| **id != user_id)
@ -77,25 +78,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
.collect();
}
false => {
model.project_page.active_avatar_filters.push(user_id);
project_page.active_avatar_filters.push(user_id);
}
},
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 => {
model.project_page.recently_updated_filter =
!model.project_page.recently_updated_filter;
project_page.recently_updated_filter = !project_page.recently_updated_filter;
}
Msg::ProjectClearFilters => {
let pp = &mut model.project_page;
pp.active_avatar_filters = vec![];
pp.recently_updated_filter = false;
pp.only_my_filter = false;
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(_) => {
model.project_page.dragged_issue_id = None;
project_page.dragged_issue_id = None;
}
Msg::ExchangePosition(issue_bellow_id) => {
crate::ws::issue::exchange_position(issue_bellow_id, model)
@ -165,11 +164,14 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
.build()
.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()
.empty()
.active(model.project_page.only_my_filter)
.active(project_page.only_my_filter)
.text("Only My Issues")
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
.build()
@ -205,7 +207,11 @@ fn project_board_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
.users
.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> {
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
.issues
.iter()
@ -261,8 +271,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
.iter()
.filter(|issue| {
issue_filter_status(issue, &status)
&& issue_filter_with_text(issue, model.project_page.text_filter.as_str())
&& issue_filter_with_only_my(issue, model.project_page.only_my_filter, &model.user)
&& 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)
})
.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_form::StyledForm;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelect;
use crate::shared::{inner_layout, ToNode};
use crate::{model, FieldId, Msg, ProjectSettingsFieldId};

View File

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

View File

@ -9,6 +9,7 @@ pub struct StyledInput {
id: FieldId,
icon: Option<Icon>,
valid: bool,
value: Option<String>,
}
impl StyledInput {
@ -17,6 +18,7 @@ impl StyledInput {
id,
icon: None,
valid: None,
value: None,
}
}
}
@ -26,6 +28,7 @@ pub struct StyledInputBuilder {
id: FieldId,
icon: Option<Icon>,
valid: Option<bool>,
value: Option<String>,
}
impl StyledInputBuilder {
@ -39,11 +42,20 @@ impl StyledInputBuilder {
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 {
StyledInput {
id: self.id,
icon: self.icon,
valid: self.valid.unwrap_or_default(),
value: self.value,
}
}
}
@ -55,7 +67,12 @@ impl ToNode for StyledInput {
}
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)];
if !valid {
@ -74,7 +91,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
let mut handlers = vec![];
handlers.push(input_ev(Ev::KeyUp, move |value| {
handlers.push(input_ev(Ev::Change, move |value| {
Msg::InputChanged(id, value)
}));
@ -85,6 +102,12 @@ pub fn render(values: StyledInput) -> Node<Msg> {
ev.stop_propagation();
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 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) {
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) {
if model.project_page.dragged_issue_id == Some(issue_bellow_id)
|| model.project_page.last_drag_exchange_id == Some(issue_bellow_id)
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 model.project_page.dragged_issue_id {
let dragged_id = match project_page.dragged_issue_id {
Some(id) => id,
_ => return,
};
@ -50,7 +58,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
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, model);
mark_dirty(c.id, project_page);
}
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);
mark_dirty(dragged.id, model);
mark_dirty(below.id, model);
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));
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) {
let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page,
_ => return,
};
for issue in model.issues.iter() {
if !model.project_page.dirty_issues.contains(&issue.id) {
if !project_page.dirty_issues.contains(&issue.id) {
continue;
}
@ -91,14 +104,19 @@ pub fn dropped(_status: IssueStatus, model: &mut Model) {
reporter_id: issue.reporter_id,
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));
model.project_page.last_drag_exchange_id = None;
project_page.last_drag_exchange_id = None;
}
}
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,
_ => return,
};
@ -136,12 +154,12 @@ pub fn change_status(status: IssueStatus, model: &mut Model) {
issue.list_position = pos + 1;
model.issues.push(issue);
mark_dirty(issue_id, model);
mark_dirty(issue_id, project_page);
}
#[inline]
fn mark_dirty(id: IssueId, model: &mut Model) {
if !model.project_page.dirty_issues.contains(&id) {
model.project_page.dirty_issues.push(id);
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
if !project_page.dirty_issues.contains(&id) {
project_page.dirty_issues.push(id);
}
}