Handle hourly time tracking. Hide time tracking if is set to untracked

This commit is contained in:
Adrian Wozniak 2020-04-25 22:20:16 +02:00
parent 2d6d92f479
commit db1ebc266f
20 changed files with 348 additions and 204 deletions

View File

@ -75,7 +75,7 @@ impl std::fmt::Display for FieldId {
EditIssueModalSection::Issue(IssueFieldId::Estimate) => {
f.write_str("estimateIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::TimeSpend) => {
EditIssueModalSection::Issue(IssueFieldId::TimeSpent) => {
f.write_str("timeSpendIssueEditModal")
}
EditIssueModalSection::Issue(IssueFieldId::TimeRemaining) => {
@ -97,7 +97,7 @@ impl std::fmt::Display for FieldId {
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
IssueFieldId::Status => f.write_str("addIssueModal-status"),
IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"),
IssueFieldId::TimeSpend => f.write_str("addIssueModal-timeSpend"),
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
},
@ -144,9 +144,16 @@ pub enum UsersPageChange {
ResetForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ProjectPageChange {
ResetForm,
SubmitForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PageChanged {
Users(UsersPageChange),
ProjectSettings(ProjectPageChange),
}
#[derive(Clone, Debug, PartialEq)]
@ -186,7 +193,6 @@ pub enum Msg {
ProjectToggleOnlyMy,
ProjectToggleRecentlyUpdated,
ProjectClearFilters,
ProjectSaveChanges,
// dragging
IssueDragStarted(IssueId),

View File

@ -12,9 +12,8 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::ToNode;
use crate::shared::{ToChild, ToNode};
use crate::{FieldId, Msg};
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
@ -127,10 +126,10 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.options(
IssueType::ordered()
.iter()
.map(|t| t.to_select_child().name("type"))
.map(|t| t.to_child().name("type"))
.collect(),
)
.selected(vec![modal.issue_type.to_select_child().name("type")])
.selected(vec![modal.issue_type.to_child().name("type")])
.build()
.into_node();
let issue_type_field = StyledField::build()
@ -175,7 +174,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
model
.users
.iter()
.map(|u| u.to_select_child().name("reporter"))
.map(|u| u.to_child().name("reporter"))
.collect(),
)
.selected(
@ -184,7 +183,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.iter()
.filter_map(|user| {
if user.id == reporter_id {
Some(user.to_select_child().name("reporter"))
Some(user.to_child().name("reporter"))
} else {
None
}
@ -210,7 +209,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
model
.users
.iter()
.map(|u| u.to_select_child().name("assignees"))
.map(|u| u.to_child().name("assignees"))
.collect(),
)
.selected(
@ -219,7 +218,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.iter()
.filter_map(|user| {
if modal.user_ids.contains(&user.id) {
Some(user.to_select_child().name("assignees"))
Some(user.to_child().name("assignees"))
} else {
None
}
@ -245,10 +244,10 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.options(
IssuePriority::ordered()
.iter()
.map(|p| p.to_select_child().name("priority"))
.map(|p| p.to_child().name("priority"))
.collect(),
)
.selected(vec![modal.priority.to_select_child().name("priority")])
.selected(vec![modal.priority.to_child().name("priority")])
.build()
.into_node();
let issue_priority_field = StyledField::build()

View File

@ -11,10 +11,9 @@ use crate::shared::styled_field::StyledField;
use crate::shared::styled_icon::Icon;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::tracking_widget::tracking_link;
use crate::shared::ToNode;
use crate::shared::{ToChild, ToNode};
use crate::{EditIssueModalSection, FieldChange, FieldId, Msg};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -27,6 +26,9 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.reporter_state.update(msg, orders);
modal.assignees_state.update(msg, orders);
modal.priority_state.update(msg, orders);
modal.estimate.update(msg);
modal.time_spent.update(msg);
modal.time_remaining.update(msg);
match msg {
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
@ -136,25 +138,25 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)),
value,
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
_value,
) => {
modal.payload.time_spent = value.parse::<f64>().ok();
modal.payload.time_spent = modal.time_spent.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeSpend,
PayloadVariant::OptionF64(modal.payload.time_spent),
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
));
}
Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
value,
_value,
) => {
modal.payload.time_remaining = value.parse::<f64>().ok();
modal.payload.time_remaining = modal.time_remaining.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionF64(modal.payload.time_remaining),
PayloadVariant::OptionI32(modal.payload.time_remaining),
));
}
Msg::ModalChanged(FieldChange::TabChanged(
@ -182,26 +184,15 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value,
) => match value.parse::<f64>() {
Ok(n) if !value.is_empty() => {
modal.payload.estimate = Some(n);
_value,
) => {
modal.payload.estimate = modal.estimate.represent_f64_as_i32();
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionF64(modal.payload.estimate),
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
));
}
_ if value.is_empty() => {
modal.payload.estimate = None;
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionF64(modal.payload.estimate),
));
}
_ => {}
},
Msg::SaveComment => {
let msg = match modal.comment_form.id {
Some(id) => WsMsg::UpdateComment(UpdateCommentPayload {
@ -338,14 +329,14 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.options(
IssueType::ordered()
.into_iter()
.map(|t| t.to_select_child().name("type"))
.map(|t| t.to_child().name("type"))
.collect(),
)
.selected(vec![{
let id = modal.id;
let issue_type = &payload.issue_type;
issue_type
.to_select_child()
.to_child()
.name("type")
.text(format!("{} - {}", issue_type, id))
}])
@ -574,10 +565,10 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.options(
IssueStatus::ordered()
.into_iter()
.map(|opt| opt.to_select_child().name("status"))
.map(|opt| opt.to_child().name("status"))
.collect(),
)
.selected(vec![payload.status.to_select_child().name("status")])
.selected(vec![payload.status.to_child().name("status")])
.valid(true)
.build()
.into_node();
@ -599,7 +590,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
model
.users
.iter()
.map(|user| user.to_select_child().name("assignees"))
.map(|user| user.to_child().name("assignees"))
.collect(),
)
.selected(
@ -607,7 +598,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.users
.iter()
.filter(|user| payload.user_ids.contains(&user.id))
.map(|user| user.to_select_child().name("assignees"))
.map(|user| user.to_child().name("assignees"))
.collect(),
)
.build()
@ -629,7 +620,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
model
.users
.iter()
.map(|user| user.to_select_child().name("reporter"))
.map(|user| user.to_child().name("reporter"))
.collect(),
)
.selected(
@ -637,7 +628,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.users
.iter()
.filter(|user| payload.reporter_id == user.id)
.map(|user| user.to_select_child().name("reporter"))
.map(|user| user.to_child().name("reporter"))
.collect(),
)
.build()
@ -658,10 +649,10 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.options(
IssuePriority::ordered()
.into_iter()
.map(|p| p.to_select_child().name("priority"))
.map(|p| p.to_child().name("priority"))
.collect(),
)
.selected(vec![payload.priority.to_select_child().name("priority")])
.selected(vec![payload.priority.to_child().name("priority")])
.build()
.into_node();
let priority_field = StyledField::build()
@ -681,17 +672,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
IssueFieldId::Estimate,
)))
.valid(true)
.value(
payload
.estimate
.as_ref()
.map(|n| match time_tracking_type {
TimeTracking::Fibonacci => format!("{}", n),
TimeTracking::Hourly => format!("{:.1}", n),
_ => "".to_string(),
})
.unwrap_or_default(),
)
.value(modal.estimate.value.as_str())
.build()
.into_node();
let estimate_field = StyledField::build()

View File

@ -1,6 +1,6 @@
use seed::{prelude::*, *};
use jirs_data::WsMsg;
use jirs_data::{TimeTracking, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
@ -12,7 +12,7 @@ use crate::{model, FieldChange, FieldId, Msg};
mod add_issue;
mod confirm_delete_issue;
mod issue_details;
mod time_tracking;
pub mod time_tracking;
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg {
@ -96,12 +96,20 @@ pub fn view(model: &model::Model) -> Node<Msg> {
}
fn push_edit_modal(issue_id: i32, model: &mut Model) {
let time_tracking_type = model
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
let modal = {
let issue = match find_issue(model, issue_id) {
Some(issue) => issue,
_ => return,
};
ModalType::EditIssue(issue_id, Box::new(EditIssueModal::new(issue)))
ModalType::EditIssue(
issue_id,
Box::new(EditIssueModal::new(issue, time_tracking_type)),
)
};
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id));
model.modals.push(modal);

View File

@ -1,16 +1,27 @@
use seed::{prelude::*, *};
use jirs_data::{IssueFieldId, IssueId};
use jirs_data::{IssueFieldId, IssueId, TimeTracking};
use crate::model::{ModalType, Model};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_input::{StyledInput, StyledInputState};
use crate::shared::styled_modal::StyledModal;
use crate::shared::tracking_widget::tracking_widget;
use crate::shared::{find_issue, ToNode};
use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_select_child::*;
use crate::shared::tracking_widget::{fibonacci_values, tracking_widget};
use crate::shared::{find_issue, ToChild, ToNode};
use crate::{EditIssueModalSection, FieldId, Msg};
pub fn value_for_time_tracking(v: &Option<i32>, time_tracking_type: &TimeTracking) -> String {
match (time_tracking_type, v.as_ref()) {
(TimeTracking::Untracked, _) => "".to_string(),
(TimeTracking::Fibonacci, Some(n)) => n.to_string(),
(TimeTracking::Hourly, Some(n)) => format!("{:.1}", *n as f64 / 10.0f64),
_ => "".to_string(),
}
}
pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
let _issue = match find_issue(model, issue_id) {
Some(issue) => issue,
@ -21,49 +32,28 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
Some(ModalType::EditIssue(_, modal)) => modal,
_ => return empty![],
};
let time_tracking_type = model
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
let modal_title = div![class!["modalTitle"], "Time tracking"];
let tracking = tracking_widget(model, edit_issue_modal);
let time_spent = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::TimeSpend,
)))
.value(
edit_issue_modal
.payload
.time_spent
.as_ref()
.map(|n| n.to_string())
.unwrap_or_default(),
)
.valid(true)
.build()
.into_node();
let time_spent_field = StyledField::build()
.input(time_spent)
.label("Time spent")
.build()
.into_node();
let time_remaining = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::TimeRemaining,
)))
.value(
edit_issue_modal
.payload
.time_remaining
.as_ref()
.map(|n| n.to_string())
.unwrap_or_default(),
)
.valid(true)
.build()
.into_node();
let time_remaining_field = StyledField::build()
.input(time_remaining)
.label("Time remaining")
.build()
.into_node();
let time_spent_field = time_tracking_field(
time_tracking_type,
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
"Time spent",
&edit_issue_modal.time_spent,
);
let time_remaining_field = time_tracking_field(
time_tracking_type,
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
"Time remaining",
&edit_issue_modal.time_remaining,
);
let inputs = div![
class!["inputs"],
@ -89,3 +79,34 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
.build()
.into_node()
}
fn time_tracking_field(
time_tracking_type: TimeTracking,
field_id: FieldId,
label: &str,
state: &StyledInputState,
) -> Node<Msg> {
let input = match time_tracking_type {
TimeTracking::Untracked => empty![],
TimeTracking::Fibonacci => StyledSelect::build(field_id)
.selected(vec![(state.to_i32().unwrap_or_default() as u32).to_child()])
.options(
fibonacci_values()
.into_iter()
.map(|v| v.to_child())
.collect(),
)
.build()
.into_node(),
TimeTracking::Hourly => StyledInput::build(field_id)
.state(state)
.valid(true)
.build()
.into_node(),
};
StyledField::build()
.input(input)
.label(label)
.build()
.into_node()
}

View File

@ -5,8 +5,10 @@ use uuid::Uuid;
use jirs_data::*;
use crate::modal::time_tracking::value_for_time_tracking;
use crate::shared::styled_checkbox::StyledCheckboxState;
use crate::shared::styled_editor::Mode;
use crate::shared::styled_input::StyledInputState;
use crate::shared::styled_select::StyledSelectState;
use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL};
@ -37,6 +39,10 @@ pub struct EditIssueModal {
pub assignees_state: StyledSelectState,
pub priority_state: StyledSelectState,
pub estimate: StyledInputState,
pub time_spent: StyledInputState,
pub time_remaining: StyledInputState,
pub description_editor_mode: Mode,
// comments
@ -44,7 +50,7 @@ pub struct EditIssueModal {
}
impl EditIssueModal {
pub fn new(issue: &Issue) -> Self {
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self {
Self {
id: issue.id,
link_copied: false,
@ -78,6 +84,18 @@ impl EditIssueModal {
priority_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalSection::Issue(IssueFieldId::Priority),
)),
estimate: StyledInputState::new(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value_for_time_tracking(&issue.estimate, &time_tracking_type),
),
time_spent: StyledInputState::new(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)),
value_for_time_tracking(&issue.time_spent, &time_tracking_type),
),
time_remaining: StyledInputState::new(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
value_for_time_tracking(&issue.time_remaining, &time_tracking_type),
),
description_editor_mode: Mode::View,
comment_form: CommentForm {
id: None,
@ -96,9 +114,9 @@ pub struct AddIssueModal {
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: Option<i32>,
pub user_ids: Vec<i32>,
pub reporter_id: Option<i32>,
@ -212,6 +230,7 @@ impl ProjectSettingsPage {
url,
description,
category,
time_tracking,
..
} = project;
Self {
@ -221,6 +240,7 @@ impl ProjectSettingsPage {
url: Some(url.clone()),
description: Some(description.clone()),
category: Some(category.clone()),
time_tracking: Some(*time_tracking),
},
description_mode: EditorMode::View,
project_category_state: StyledSelectState::new(FieldId::ProjectSettings(
@ -228,7 +248,7 @@ impl ProjectSettingsPage {
)),
time_tracking: StyledCheckboxState::new(
FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
0,
(*time_tracking).into(),
),
}
}

View File

@ -1,6 +1,6 @@
use seed::{prelude::*, *};
use jirs_data::{ProjectCategory, TimeTracking, ToVec, WsMsg};
use jirs_data::{ProjectCategory, TimeTracking, ToVec, UpdateProjectPayload, WsMsg};
use crate::api::send_ws_msg;
use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
@ -10,11 +10,13 @@ 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, ToChild, ToNode};
use crate::FieldChange::TabChanged;
use crate::{model, FieldId, Msg, ProjectFieldId};
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
static TIME_TRACKING_FIBONACCI: &'static str = "Tracking employees time carries the risk of having them feel like they are being spied on. This is one of the most common fears that employees have when a time tracking system is implemented. No one likes to feel like theyre always being watched.";
static TIME_TRACKING_HOURLY: &'static str = "Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging.";
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
@ -47,7 +49,6 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
page.time_tracking.update(&msg);
match msg {
Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())),
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
page.payload.name = Some(text);
}
@ -70,6 +71,16 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
)) => {
page.description_mode = mode;
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm)) => {
send_ws_msg(WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
description: page.payload.description.clone(),
category: page.payload.category.clone(),
time_tracking: Some(page.time_tracking.value.into()),
}));
}
_ => (),
}
}
@ -142,7 +153,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.options(
ProjectCategory::ordered()
.into_iter()
.map(|c| c.to_select_child())
.map(|c| c.to_child())
.collect(),
)
.selected(vec![page
@ -151,7 +162,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.as_ref()
.cloned()
.unwrap_or_default()
.to_select_child()])
.to_child()])
.build()
.into_node();
let category_field = StyledField::build()
@ -174,23 +185,30 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let time_tracking_type: TimeTracking = page.time_tracking.value.into();
let time_tracking_field = StyledField::build()
.input(time_tracking)
.tip(if time_tracking_type == TimeTracking::Hourly {
"Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging."
} else {
""
.tip(match time_tracking_type {
TimeTracking::Fibonacci => TIME_TRACKING_FIBONACCI,
TimeTracking::Hourly => TIME_TRACKING_HOURLY,
_ => "",
})
.build()
.into_node();
let save_button = StyledButton::build()
.add_class("actionButton")
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectSaveChanges))
.on_click(mouse_ev(Ev::Click, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm))
}))
.text("Save changes")
.build()
.into_node();
let form = StyledForm::build()
.heading("Project Details")
.on_submit(ev(Ev::Submit, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm))
}))
.add_field(name_field)
.add_field(url_field)
.add_field(description_field)

View File

@ -4,6 +4,45 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::ToNode;
use crate::{FieldId, Msg};
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct StyledInputState {
id: FieldId,
pub value: String,
}
impl StyledInputState {
pub fn new<S>(id: FieldId, value: S) -> Self
where
S: Into<String>,
{
Self {
id,
value: value.into(),
}
}
pub fn to_i32(&self) -> Option<i32> {
self.value.parse::<i32>().ok()
}
pub fn to_f64(&self) -> Option<f64> {
self.value.parse::<f64>().ok()
}
pub fn represent_f64_as_i32(&self) -> Option<i32> {
self.to_f64().map(|f| (f * 10.0f64) as i32)
}
pub fn update(&mut self, msg: &Msg) {
match msg {
Msg::StrInputChanged(field_id, s) if field_id == &self.id => {
self.value = s.clone();
}
_ => (),
}
}
}
#[derive(Debug)]
pub struct StyledInput {
id: FieldId,
@ -59,6 +98,10 @@ impl StyledInputBuilder {
self
}
pub fn state(self, state: &StyledInputState) -> Self {
self.value(state.value.as_str())
}
pub fn add_input_class<S>(mut self, name: S) -> Self
where
S: Into<String>,

View File

@ -1,13 +1,9 @@
use seed::{prelude::*, *};
use crate::shared::styled_select::Variant;
use crate::shared::ToNode;
use crate::shared::{ToChild, ToNode};
use crate::Msg;
pub trait ToStyledSelectChild {
fn to_select_child(&self) -> StyledSelectChildBuilder;
}
pub enum DisplayType {
SelectOption,
SelectValue,
@ -180,8 +176,10 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
div![class![wrapper_class.as_str()], icon_node, label_node]
}
impl ToStyledSelectChild for jirs_data::User {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::User {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> Self::Builder {
let avatar = crate::shared::styled_avatar::StyledAvatar::build()
.avatar_url(self.avatar_url.as_ref().cloned().unwrap_or_default())
.size(20)
@ -195,8 +193,9 @@ impl ToStyledSelectChild for jirs_data::User {
}
}
impl ToStyledSelectChild for jirs_data::IssuePriority {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::IssuePriority {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> StyledSelectChildBuilder {
let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
.add_class(self.to_string())
.build()
@ -211,8 +210,10 @@ impl ToStyledSelectChild for jirs_data::IssuePriority {
}
}
impl ToStyledSelectChild for jirs_data::IssueStatus {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::IssueStatus {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> StyledSelectChildBuilder {
let text = self.to_label();
StyledSelectChild::build()
@ -222,8 +223,10 @@ impl ToStyledSelectChild for jirs_data::IssueStatus {
}
}
impl ToStyledSelectChild for jirs_data::IssueType {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::IssueType {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> StyledSelectChildBuilder {
let name = self.to_label().to_owned();
let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
@ -239,8 +242,10 @@ impl ToStyledSelectChild for jirs_data::IssueType {
}
}
impl ToStyledSelectChild for jirs_data::ProjectCategory {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::ProjectCategory {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> StyledSelectChildBuilder {
let name = self.to_string();
StyledSelectChild::build()
@ -250,8 +255,10 @@ impl ToStyledSelectChild for jirs_data::ProjectCategory {
}
}
impl ToStyledSelectChild for jirs_data::UserRole {
fn to_select_child(&self) -> StyledSelectChildBuilder {
impl ToChild for jirs_data::UserRole {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> StyledSelectChildBuilder {
let name = self.to_string();
StyledSelectChild::build()
@ -261,3 +268,16 @@ impl ToStyledSelectChild for jirs_data::UserRole {
.value(self.clone().into())
}
}
impl ToChild for u32 {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> Self::Builder {
let name = self.to_string();
StyledSelectChild::build()
.add_class(name.as_str())
.text(name)
.value(*self)
}
}

View File

@ -2,11 +2,17 @@ use seed::{prelude::*, *};
use jirs_data::{TimeTracking, UpdateIssuePayload};
use crate::modal::time_tracking::value_for_time_tracking;
use crate::model::{EditIssueModal, ModalType, Model};
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::ToNode;
use crate::Msg;
#[inline]
pub fn fibonacci_values() -> Vec<u32> {
vec![0, 1, 2, 3, 5, 8, 13, 21, 34, 55]
}
pub fn tracking_link(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal { id, .. } = modal;
@ -51,18 +57,18 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining);
let spent_text = match (time_spent, time_tracking_type) {
(Some(time), TimeTracking::Hourly) => format!("{}h logged", time),
(Some(time), TimeTracking::Fibonacci) => format!("{} point logged", time),
(Some(time), TimeTracking::Hourly) => format!(
"{}h logged",
value_for_time_tracking(&Some(*time), &time_tracking_type)
),
(Some(time), TimeTracking::Fibonacci) => format!(
"{} point logged",
value_for_time_tracking(&Some(*time), &time_tracking_type)
),
_ => "No time logged".to_string(),
};
let remaining_node: Node<Msg> = match (time_remaining, estimate, time_tracking_type) {
(Some(n), _, TimeTracking::Hourly) => div![format!("{}h remaining", n)],
(_, Some(n), TimeTracking::Hourly) => div![format!("{}h estimated", n)],
(Some(n), _, TimeTracking::Fibonacci) => div![format!("{} remaining", n)],
(_, Some(n), TimeTracking::Fibonacci) => div![format!("{} estimated", n)],
_ => empty![],
};
let remaining_node: Node<Msg> = remaining_node(time_remaining, estimate, time_tracking_type);
div![
class!["trackingWidget"],
@ -81,15 +87,39 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
]
}
#[inline]
fn remaining_node(
time_remaining: &Option<i32>,
estimate: &Option<i32>,
time_tracking_type: TimeTracking,
) -> Node<Msg> {
let text = match (time_remaining, estimate, time_tracking_type) {
(Some(n), _, TimeTracking::Hourly) => format!(
"{}h remaining",
value_for_time_tracking(&Some(*n), &time_tracking_type)
),
(_, Some(n), TimeTracking::Hourly) => format!(
"{}h estimated",
value_for_time_tracking(&Some(*n), &time_tracking_type)
),
(Some(n), _, TimeTracking::Fibonacci) => format!("{} remaining", n),
(_, Some(n), TimeTracking::Fibonacci) => format!("{} estimated", n),
_ => return empty![],
};
div![text]
}
#[inline]
fn calc_bar_width(
estimate: Option<f64>,
time_spent: Option<f64>,
time_remaining: Option<f64>,
estimate: Option<i32>,
time_spent: Option<i32>,
time_remaining: Option<i32>,
) -> f64 {
match (estimate, time_spent, time_remaining) {
(_, Some(spent), Some(remaining)) => ((spent / (spent + remaining)) * 100f64).min(100f64),
(Some(estimate), Some(spent), _) => ((spent / estimate) * 100f64).min(100f64),
(_, Some(spent), Some(remaining)) => {
((spent as f64 / (spent + remaining) as f64) * 100f64).min(100f64)
}
(Some(estimate), Some(spent), _) => ((spent / estimate) as f64 * 100f64).min(100f64),
(None, None, _) => 100f64,
(None, _, _) => 0f64,
_ => 0f64,

View File

@ -9,8 +9,7 @@ use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::*;
use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::{inner_layout, ToNode};
use crate::shared::{inner_layout, ToChild, ToNode};
use crate::validations::is_email;
use crate::{FieldId, Msg, PageChanged, UsersPageChange};
@ -144,11 +143,11 @@ pub fn view(model: &Model) -> Node<Msg> {
.valid(true)
.normal()
.with_state(&page.user_role_state)
.selected(vec![page.user_role.to_select_child()])
.selected(vec![page.user_role.to_child()])
.options(
UserRole::ordered()
.into_iter()
.map(|role| role.to_select_child())
.map(|role| role.to_child())
.collect(),
)
.build()

View File

@ -487,9 +487,9 @@ pub struct Issue {
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: UserId,
pub project_id: ProjectId,
pub created_at: NaiveDateTime,
@ -557,9 +557,9 @@ pub struct UpdateIssuePayload {
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub user_ids: Vec<UserId>,
@ -616,9 +616,9 @@ pub struct CreateIssuePayload {
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
pub reporter_id: UserId,
@ -631,12 +631,12 @@ pub struct UpdateProjectPayload {
pub url: Option<String>,
pub description: Option<String>,
pub category: Option<ProjectCategory>,
pub time_tracking: Option<TimeTracking>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PayloadVariant {
OptionI32(Option<i32>),
OptionF64(Option<f64>),
VecI32(Vec<i32>),
I32(i32),
String(String),
@ -696,7 +696,7 @@ pub enum IssueFieldId {
Reporter,
Priority,
Estimate,
TimeSpend,
TimeSpent,
TimeRemaining,
}

View File

@ -255,7 +255,7 @@ impl diesel::query_builder::QueryId for TimeTrackingType {
fn time_tracking_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<TimeTracking> {
match not_none!(bytes) {
b"untracked" => Ok(TimeTracking::Untracked),
b"fibnachi" => Ok(TimeTracking::Fibonacci),
b"fibonacci" => Ok(TimeTracking::Fibonacci),
b"hourly" => Ok(TimeTracking::Hourly),
_ => Ok(TimeTracking::Untracked),
}
@ -277,7 +277,7 @@ impl ToSql<TimeTrackingType, Pg> for TimeTracking {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> serialize::Result {
match *self {
TimeTracking::Untracked => out.write_all(b"untracked")?,
TimeTracking::Fibonacci => out.write_all(b"fibnacci")?,
TimeTracking::Fibonacci => out.write_all(b"fibonacci")?,
TimeTracking::Hourly => out.write_all(b"hourly")?,
}
Ok(IsNull::No)

View File

@ -1,7 +1,3 @@
alter TABLE issues alter COLUMN estimate SET DATA TYPE integer;
alter TABLE issues alter COLUMN time_spent SET DATA TYPE integer;
alter TABLE issues alter COLUMN time_remaining SET DATA TYPE integer;
alter TABLE projects drop COLUMN time_tracking;
drop TYPE IF EXISTS "TimeTrackingType";

View File

@ -4,8 +4,4 @@ CREATE TYPE "TimeTrackingType" AS ENUM (
'hourly'
);
ALTER TABLE issues ALTER COLUMN estimate SET DATA TYPE double precision;
ALTER TABLE issues ALTER COLUMN time_spent SET DATA TYPE double precision;
ALTER TABLE issues ALTER COLUMN time_remaining SET DATA TYPE double precision;
ALTER TABLE projects ADD COLUMN time_tracking "TimeTrackingType" NOT NULL DEFAULT 'untracked';

View File

@ -79,9 +79,9 @@ pub struct UpdateIssue {
pub list_position: Option<i32>,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: Option<i32>,
pub user_ids: Option<Vec<i32>>,
pub reporter_id: Option<i32>,
@ -209,9 +209,9 @@ pub struct CreateIssue {
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: i32,
pub reporter_id: i32,
pub user_ids: Vec<i32>,

View File

@ -26,9 +26,9 @@ pub struct Issue {
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: i32,
pub project_id: i32,
pub created_at: NaiveDateTime,
@ -69,9 +69,9 @@ pub struct CreateIssueForm {
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub reporter_id: i32,
pub project_id: i32,
}

View File

@ -211,22 +211,22 @@ table! {
description_text -> Nullable<Text>,
/// The `estimate` column of the `issues` table.
///
/// Its SQL type is `Nullable<Float8>`.
/// Its SQL type is `Nullable<Int4>`.
///
/// (Automatically generated by Diesel.)
estimate -> Nullable<Float8>,
estimate -> Nullable<Int4>,
/// The `time_spent` column of the `issues` table.
///
/// Its SQL type is `Nullable<Float8>`.
/// Its SQL type is `Nullable<Int4>`.
///
/// (Automatically generated by Diesel.)
time_spent -> Nullable<Float8>,
time_spent -> Nullable<Int4>,
/// The `time_remaining` column of the `issues` table.
///
/// Its SQL type is `Nullable<Float8>`.
/// Its SQL type is `Nullable<Int4>`.
///
/// (Automatically generated by Diesel.)
time_remaining -> Nullable<Float8>,
time_remaining -> Nullable<Int4>,
/// The `reporter_id` column of the `issues` table.
///
/// Its SQL type is `Int4`.

View File

@ -51,13 +51,13 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
(IssueFieldId::Priority, PayloadVariant::IssuePriority(p)) => {
msg.priority = Some(p);
}
(IssueFieldId::Estimate, PayloadVariant::OptionF64(o)) => {
(IssueFieldId::Estimate, PayloadVariant::OptionI32(o)) => {
msg.estimate = o;
}
(IssueFieldId::TimeSpend, PayloadVariant::OptionF64(o)) => {
(IssueFieldId::TimeSpent, PayloadVariant::OptionI32(o)) => {
msg.time_spent = o;
}
(IssueFieldId::TimeRemaining, PayloadVariant::OptionF64(o)) => {
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
msg.time_remaining = o;
}
_ => (),

View File

@ -35,10 +35,17 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
url: msg.url,
description: msg.description,
category: msg.category,
time_tracking: None,
time_tracking: msg.time_tracking,
})) {
Ok(Ok(project)) => project,
_ => return Ok(None),
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{:?}", e);
return Ok(None);
}
};
Ok(Some(WsMsg::ProjectLoaded(project)))
}