Add time tracking settings start handling

This commit is contained in:
Adrian Wozniak 2020-04-25 16:12:06 +02:00
parent 6afa4dc6d1
commit 2d6d92f479
29 changed files with 46050 additions and 37079 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 473 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 433 KiB

After

Width:  |  Height:  |  Size: 605 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 467 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 604 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -91,7 +91,8 @@ aside#navbar-left > .bottom {
width: 100%; width: 100%;
} }
aside#navbar-left > .bottom > .aboutTooltip {} aside#navbar-left > .bottom > .aboutTooltip {
}
aside#navbar-left:hover .item > .itemText { aside#navbar-left:hover .item > .itemText {
right: 0; right: 0;

View File

@ -63,19 +63,24 @@ select::-ms-value {
[role="button"], button, input, select, textarea { [role="button"], button, input, select, textarea {
outline: none; outline: none;
} }
[role="button"]:disabled, button:disabled, input:disabled, select:disabled, textarea:disabled { [role="button"]:disabled, button:disabled, input:disabled, select:disabled, textarea:disabled {
opacity: 1; opacity: 1;
} }
[role="button"], button, input, textarea { [role="button"], button, input, textarea {
appearance: none; appearance: none;
} }
select:-moz-focusring { select:-moz-focusring {
color: transparent; color: transparent;
text-shadow: 0 0 0 #000; text-shadow: 0 0 0 #000;
} }
select::-ms-expand { select::-ms-expand {
display: none; display: none;
} }
select option { select option {
color: var(--textDark) color: var(--textDark)
} }
@ -88,14 +93,17 @@ p a::-webkit-input-placeholder {
color: var(--textLight); color: var(--textLight);
opacity: 1 !important; opacity: 1 !important;
} }
p a:-moz-placeholder { p a:-moz-placeholder {
color: var(--textLight); color: var(--textLight);
opacity: 1 !important; opacity: 1 !important;
} }
p a::-moz-placeholder { p a::-moz-placeholder {
color: var(--textLight); color: var(--textLight);
opacity: 1 !important; opacity: 1 !important;
} }
p a:-ms-input-placeholder { p a:-ms-input-placeholder {
color: var(--textLight); color: var(--textLight);
opacity: 1 !important; opacity: 1 !important;

View File

@ -0,0 +1,43 @@
.styledCheckbox {
display: flex;
justify-content: space-between;
}
.styledCheckbox > .styledCheckboxChild {
display: block;
border: 1px solid var(--borderLight);
font-family: var(--font-medium);
line-height: 2;
white-space: nowrap;
cursor: pointer;
font-size: 14.5px;
padding: 0 12px;
}
.styledCheckbox > .styledCheckboxChild.selected,
.styledCheckbox > .styledCheckboxChild:focus {
border-color: var(--borderInputFocus);
}
.styledCheckbox > .styledCheckboxChild.selected {
color: var(--borderInputFocus);
}
.styledCheckbox > .styledCheckboxChild > input[type=radio] {
display: none;
}
.styledCheckbox > .styledCheckboxChild.untracking.selected {
color: var(--success);
border-color: var(--success);
}
.styledCheckbox > .styledCheckboxChild.fibonacci.selected {
color: var(--warning);
border-color: var(--warning);
}
.styledCheckbox > .styledCheckboxChild.hourly.selected {
color: var(--danger);
border-color: var(--danger);
}

View File

@ -45,19 +45,22 @@
--issue-background-done: var(--success); --issue-background-done: var(--success);
} }
:root /* sizes */ { :root /* sizes */
{
--appNavBarLeftWidth: 64px; --appNavBarLeftWidth: 64px;
--secondarySideBarWidth: 230px; --secondarySideBarWidth: 230px;
--minViewportWidth: 1000px; --minViewportWidth: 1000px;
} }
:root /* z-index */ { :root /* z-index */
{
--modal: 20; --modal: 20;
--dropdown: 11; --dropdown: 11;
--navLeft: 10; --navLeft: 10;
} }
:root /* font */ { :root /* font */
{
--font-regular: "CircularStdBook"; --font-regular: "CircularStdBook";
--font-medium: "CircularStdMedium"; --font-medium: "CircularStdMedium";
--font-bold: "CircularStdBold"; --font-bold: "CircularStdBold";

View File

@ -25,6 +25,7 @@
@import "./css/project.css"; @import "./css/project.css";
@import "./css/projectSettings.css"; @import "./css/projectSettings.css";
@import "./css/timeTracking.css"; @import "./css/timeTracking.css";
@import "./css/styledCheckbox.css";
@import "./css/login.css"; @import "./css/login.css";
@import "./css/register.css"; @import "./css/register.css";
@import "./css/users.css"; @import "./css/users.css";

1894
jirs-client/pkg/index.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ pub fn update(msg: Msg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
_ => return, _ => return,
}; };
if let Msg::InputChanged(FieldId::Invite(InviteFieldId::Token), text) = msg { if let Msg::StrInputChanged(FieldId::Invite(InviteFieldId::Token), text) = msg {
page.token_touched = true; page.token_touched = true;
page.token = text; page.token = text;
} }

View File

@ -108,6 +108,7 @@ impl std::fmt::Display for FieldId {
ProjectFieldId::Url => f.write_str("projectSettings-url"), ProjectFieldId::Url => f.write_str("projectSettings-url"),
ProjectFieldId::Description => f.write_str("projectSettings-description"), ProjectFieldId::Description => f.write_str("projectSettings-description"),
ProjectFieldId::Category => f.write_str("projectSettings-category"), ProjectFieldId::Category => f.write_str("projectSettings-category"),
ProjectFieldId::TimeTracking => f.write_str("projectSettings-timeTracking"),
}, },
FieldId::SignIn(sub) => match sub { FieldId::SignIn(sub) => match sub {
SignInFieldId::Email => f.write_str("login-email"), SignInFieldId::Email => f.write_str("login-email"),
@ -197,7 +198,8 @@ pub enum Msg {
UnlockDragOver, UnlockDragOver,
// inputs // inputs
InputChanged(FieldId, String), StrInputChanged(FieldId, String),
U32InputChanged(FieldId, u32),
// issues // issues
AddIssue, AddIssue,

View File

@ -58,10 +58,10 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
orders.skip().send_msg(Msg::ModalDropped); orders.skip().send_msg(Msg::ModalDropped);
} }
Msg::InputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => { Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => {
modal.description = Some(value.clone()); modal.description = Some(value.clone());
} }
Msg::InputChanged(FieldId::AddIssueModal(IssueFieldId::Title), value) => { Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Title), value) => {
modal.title = value.clone(); modal.title = value.clone();
} }

View File

@ -105,7 +105,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::IssuePriority(modal.payload.priority), PayloadVariant::IssuePriority(modal.payload.priority),
)); ));
} }
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
value, value,
) => { ) => {
@ -116,7 +116,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::String(modal.payload.title.clone()), PayloadVariant::String(modal.payload.title.clone()),
)); ));
} }
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
value, value,
) => { ) => {
@ -135,7 +135,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
), ),
)); ));
} }
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)),
value, value,
) => { ) => {
@ -146,7 +146,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionF64(modal.payload.time_spent), PayloadVariant::OptionF64(modal.payload.time_spent),
)); ));
} }
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)),
value, value,
) => { ) => {
@ -174,13 +174,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
} }
// comments // comments
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
text, text,
) => { ) => {
modal.comment_form.body = text.clone(); modal.comment_form.body = text.clone();
} }
Msg::InputChanged( Msg::StrInputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value, value,
) => match value.parse::<f64>() { ) => match value.parse::<f64>() {
@ -670,6 +670,13 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let time_tracking_type = model
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
let (estimate_field, tracking_field) = if time_tracking_type != TimeTracking::Untracked {
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Estimate, IssueFieldId::Estimate,
))) )))
@ -678,7 +685,11 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
payload payload
.estimate .estimate
.as_ref() .as_ref()
.map(|n| n.to_string()) .map(|n| match time_tracking_type {
TimeTracking::Fibonacci => format!("{}", n),
TimeTracking::Hourly => format!("{:.1}", n),
_ => "".to_string(),
})
.unwrap_or_default(), .unwrap_or_default(),
) )
.build() .build()
@ -696,6 +707,10 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.input(tracking) .input(tracking)
.build() .build()
.into_node(); .into_node();
(estimate_field, tracking_field)
} else {
(empty![], empty![])
};
div![ div![
attrs![At::Class => "right"], attrs![At::Class => "right"],

View File

@ -5,6 +5,7 @@ use uuid::Uuid;
use jirs_data::*; use jirs_data::*;
use crate::shared::styled_checkbox::StyledCheckboxState;
use crate::shared::styled_editor::Mode; use crate::shared::styled_editor::Mode;
use crate::shared::styled_select::StyledSelectState; use crate::shared::styled_select::StyledSelectState;
use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL}; use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL};
@ -199,6 +200,7 @@ pub struct ProjectSettingsPage {
pub payload: UpdateProjectPayload, pub payload: UpdateProjectPayload,
pub project_category_state: StyledSelectState, pub project_category_state: StyledSelectState,
pub description_mode: crate::shared::styled_editor::Mode, pub description_mode: crate::shared::styled_editor::Mode,
pub time_tracking: StyledCheckboxState,
} }
impl ProjectSettingsPage { impl ProjectSettingsPage {
@ -224,6 +226,10 @@ impl ProjectSettingsPage {
project_category_state: StyledSelectState::new(FieldId::ProjectSettings( project_category_state: StyledSelectState::new(FieldId::ProjectSettings(
ProjectFieldId::Category, ProjectFieldId::Category,
)), )),
time_tracking: StyledCheckboxState::new(
FieldId::ProjectSettings(ProjectFieldId::TimeTracking),
0,
),
} }
} }
} }

View File

@ -78,7 +78,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
m.top_type_state.text_filter = text; m.top_type_state.text_filter = text;
} }
} }
Msg::InputChanged(FieldId::TextFilterBoard, text) => { Msg::StrInputChanged(FieldId::TextFilterBoard, text) => {
project_page.text_filter = text; project_page.text_filter = text;
} }
Msg::ProjectAvatarFilterChanged(user_id, active) => { Msg::ProjectAvatarFilterChanged(user_id, active) => {

View File

@ -1,17 +1,18 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{ProjectCategory, ToVec, WsMsg}; use jirs_data::{ProjectCategory, TimeTracking, ToVec, WsMsg};
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{Model, Page, PageContent, ProjectSettingsPage}; use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_checkbox::StyledCheckbox;
use crate::shared::styled_editor::StyledEditor; 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_select::{StyledSelect, StyledSelectChange}; use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_select_child::ToStyledSelectChild; use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{inner_layout, ToNode}; use crate::shared::{inner_layout, ToChild, ToNode};
use crate::FieldChange::TabChanged; use crate::FieldChange::TabChanged;
use crate::{model, FieldId, Msg, ProjectFieldId}; use crate::{model, FieldId, Msg, ProjectFieldId};
@ -43,15 +44,17 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
_ => return, _ => return,
}; };
page.project_category_state.update(&msg, orders); page.project_category_state.update(&msg, orders);
page.time_tracking.update(&msg);
match msg { match msg {
Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())), Msg::ProjectSaveChanges => send_ws_msg(WsMsg::ProjectUpdateRequest(page.payload.clone())),
Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => { Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
page.payload.name = Some(text); page.payload.name = Some(text);
} }
Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Url), text) => { Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Url), text) => {
page.payload.url = Some(text); page.payload.url = Some(text);
} }
Msg::InputChanged(FieldId::ProjectSettings(ProjectFieldId::Description), text) => { Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Description), text) => {
page.payload.description = Some(text); page.payload.description = Some(text);
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
@ -157,6 +160,28 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let time_tracking =
StyledCheckbox::build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking))
.options(vec![
TimeTracking::Untracked.to_child(),
TimeTracking::Fibonacci.to_child(),
TimeTracking::Hourly.to_child(),
])
.state(&page.time_tracking)
.add_class("timeTracking")
.build()
.into_node();
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 {
""
})
.build()
.into_node();
let save_button = StyledButton::build() let save_button = StyledButton::build()
.add_class("actionButton") .add_class("actionButton")
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectSaveChanges)) .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectSaveChanges))
@ -170,6 +195,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.add_field(url_field) .add_field(url_field)
.add_field(description_field) .add_field(description_field)
.add_field(category_field) .add_field(category_field)
.add_field(time_tracking_field)
.add_field(save_button) .add_field(save_button)
.build() .build()
.into_node(); .into_node();

View File

@ -10,6 +10,7 @@ pub mod aside;
pub mod navbar_left; pub mod navbar_left;
pub mod styled_avatar; pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;
pub mod styled_checkbox;
pub mod styled_confirm_modal; pub mod styled_confirm_modal;
pub mod styled_editor; pub mod styled_editor;
pub mod styled_field; pub mod styled_field;
@ -24,6 +25,12 @@ pub mod styled_textarea;
pub mod styled_tooltip; pub mod styled_tooltip;
pub mod tracking_widget; pub mod tracking_widget;
pub trait ToChild {
type Builder;
fn to_child(&self) -> Self::Builder;
}
pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> { pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> {
model.issues.iter().find(|issue| issue.id == issue_id) model.issues.iter().find(|issue| issue.id == issue_id)
} }

View File

@ -0,0 +1,232 @@
use seed::{prelude::*, *};
use jirs_data::TimeTracking;
use crate::shared::{ToChild, ToNode};
use crate::{FieldId, Msg};
#[derive(Debug)]
pub struct StyledCheckboxState {
pub field_id: FieldId,
pub value: u32,
}
impl StyledCheckboxState {
pub fn new(field_id: FieldId, value: u32) -> Self {
Self { field_id, value }
}
pub fn update(&mut self, msg: &Msg) {
if let Msg::U32InputChanged(field_id, value) = msg {
if field_id != &self.field_id {
return;
}
self.value = *value;
}
}
}
#[derive(Debug)]
pub struct ChildBuilder {
field_id: Option<FieldId>,
name: String,
label: String,
value: u32,
selected: bool,
class_list: Vec<String>,
}
impl Default for ChildBuilder {
fn default() -> Self {
Self {
field_id: None,
name: "".to_string(),
label: "".to_string(),
value: 0,
selected: false,
class_list: vec![],
}
}
}
impl ChildBuilder {
pub fn value(mut self, value: u32) -> Self {
self.value = value;
self
}
pub fn name<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.name = name.into();
self
}
pub fn label<S>(mut self, label: S) -> Self
where
S: Into<String>,
{
self.label = label.into();
self
}
pub fn with_id(mut self, id: FieldId) -> Self {
self.field_id = Some(id);
self
}
pub fn try_select(mut self, value: u32) -> Self {
self.selected = self.value == value;
self
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
}
impl ToNode for ChildBuilder {
fn into_node(self) -> Node<Msg> {
let ChildBuilder {
field_id,
name,
label,
value,
selected,
mut class_list,
} = self;
let id = field_id.as_ref().map(|f| f.to_string()).unwrap_or_default();
let handler = if let Some(field_id) = field_id {
mouse_ev(Ev::Click, move |_| Msg::U32InputChanged(field_id, value))
} else {
ev(Ev::FullScreenError, move |_| Msg::NoOp)
};
class_list.push("styledCheckboxChild".to_string());
class_list.push(if selected { "selected" } else { "" }.to_string());
let input_attrs = if selected {
attrs![At::Type => "radio", At::Name => name.as_str(), At::Checked => selected, At::Id => format!("{}-{}", id, name)]
} else {
attrs![At::Type => "radio", At::Name => name.as_str(), At::Id => format!("{}-{}", id, name)]
};
div![
attrs![At::Class => class_list.join(" ")],
handler,
label![attrs![At::For => format!("{}-{}", id, name)], label],
input![input_attrs],
]
}
}
#[derive(Debug)]
pub struct StyledCheckbox {
id: FieldId,
options: Vec<ChildBuilder>,
selected: u32,
class_list: Vec<String>,
}
impl ToNode for StyledCheckbox {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
impl StyledCheckbox {
pub fn build(field_id: FieldId) -> StyledCheckboxBuilder {
StyledCheckboxBuilder {
id: field_id,
options: vec![],
selected: 0,
class_list: vec![],
}
}
}
pub struct StyledCheckboxBuilder {
id: FieldId,
options: Vec<ChildBuilder>,
selected: u32,
class_list: Vec<String>,
}
impl StyledCheckboxBuilder {
pub fn state(mut self, state: &StyledCheckboxState) -> Self {
self.selected = state.value;
self
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn options(mut self, options: Vec<ChildBuilder>) -> Self {
self.options = options;
self
}
pub fn build(self) -> StyledCheckbox {
StyledCheckbox {
id: self.id,
options: self.options,
selected: self.selected,
class_list: self.class_list,
}
}
}
fn render(values: StyledCheckbox) -> Node<Msg> {
let StyledCheckbox {
id,
options,
selected,
class_list,
} = values;
let opt: Vec<Node<Msg>> = options
.into_iter()
.map(|child| child.with_id(id.clone()).try_select(selected).into_node())
.collect();
div![
class!["styledCheckbox"],
attrs![At::Class => class_list.join(" ")],
opt,
]
}
impl ToChild for TimeTracking {
type Builder = ChildBuilder;
fn to_child(&self) -> Self::Builder {
Self::Builder::default()
.label(match self {
TimeTracking::Untracked => "No tracking",
TimeTracking::Fibonacci => "Fibonacci (Bad mode)",
TimeTracking::Hourly => "Evil Mode (Hourly)",
})
.name(match self {
TimeTracking::Untracked => "untracked",
TimeTracking::Fibonacci => "fibonacci",
TimeTracking::Hourly => "hourly",
})
.value((*self).into())
.add_class(match self {
TimeTracking::Untracked => "untracked",
TimeTracking::Fibonacci => "fibonacci",
TimeTracking::Hourly => "hourly",
})
}
}

View File

@ -126,7 +126,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
let target = event.target().unwrap(); let target = event.target().unwrap();
let input = seed::to_input(&target); let input = seed::to_input(&target);
let value = input.value(); let value = input.value();
Msg::InputChanged(field_id, value) Msg::StrInputChanged(field_id, value)
}); });
let key_handler = ev(Ev::KeyUp, move |event| { let key_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation(); event.stop_propagation();

View File

@ -187,7 +187,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
event.prevent_default(); event.prevent_default();
} }
Msg::InputChanged( Msg::StrInputChanged(
id, id,
if handler_disable_auto_resize { if handler_disable_auto_resize {
value.trim().to_string() value.trim().to_string()

View File

@ -1,6 +1,6 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::UpdateIssuePayload; use jirs_data::{TimeTracking, UpdateIssuePayload};
use crate::model::{EditIssueModal, ModalType, Model}; use crate::model::{EditIssueModal, ModalType, Model};
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
@ -23,7 +23,15 @@ pub fn tracking_link(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
] ]
} }
pub fn tracking_widget(_model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let time_tracking_type = model
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
if time_tracking_type == TimeTracking::Untracked {
return empty![];
}
let EditIssueModal { let EditIssueModal {
payload: payload:
UpdateIssuePayload { UpdateIssuePayload {
@ -42,15 +50,17 @@ pub fn tracking_widget(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.into_node(); .into_node();
let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining); let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining);
let spent_text = if let Some(time) = time_spent { let spent_text = match (time_spent, time_tracking_type) {
format!("{}h logged", time) (Some(time), TimeTracking::Hourly) => format!("{}h logged", time),
} else { (Some(time), TimeTracking::Fibonacci) => format!("{} point logged", time),
"No time logged".to_string() _ => "No time logged".to_string(),
}; };
let remaining_node: Node<Msg> = match (time_remaining, estimate) { let remaining_node: Node<Msg> = match (time_remaining, estimate, time_tracking_type) {
(Some(n), _) => div![format!("{}h remaining", n)], (Some(n), _, TimeTracking::Hourly) => div![format!("{}h remaining", n)],
(_, Some(n)) => div![format!("{}h estimated", 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![], _ => empty![],
}; };

View File

@ -33,15 +33,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
}; };
match msg { match msg {
Msg::InputChanged(FieldId::SignIn(SignInFieldId::Username), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Username), value) => {
page.username = value; page.username = value;
page.username_touched = true; page.username_touched = true;
} }
Msg::InputChanged(FieldId::SignIn(SignInFieldId::Email), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Email), value) => {
page.email = value; page.email = value;
page.email_touched = true; page.email_touched = true;
} }
Msg::InputChanged(FieldId::SignIn(SignInFieldId::Token), value) => { Msg::StrInputChanged(FieldId::SignIn(SignInFieldId::Token), value) => {
page.token = value; page.token = value;
page.token_touched = true; page.token_touched = true;
} }

View File

@ -30,11 +30,11 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
}; };
match msg { match msg {
Msg::InputChanged(FieldId::SignUp(SignUpFieldId::Username), value) => { Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Username), value) => {
page.username = value; page.username = value;
page.username_touched = true; page.username_touched = true;
} }
Msg::InputChanged(FieldId::SignUp(SignUpFieldId::Email), value) => { Msg::StrInputChanged(FieldId::SignUp(SignUpFieldId::Email), value) => {
page.email = value; page.email = value;
page.email_touched = true; page.email_touched = true;
} }

View File

@ -55,11 +55,11 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => { ) => {
page.user_role = role.into(); page.user_role = role.into();
} }
Msg::InputChanged(FieldId::Users(UsersFieldId::Username), name) => { Msg::StrInputChanged(FieldId::Users(UsersFieldId::Username), name) => {
page.name = name; page.name = name;
page.name_touched = true; page.name_touched = true;
} }
Msg::InputChanged(FieldId::Users(UsersFieldId::Email), email) => { Msg::StrInputChanged(FieldId::Users(UsersFieldId::Email), email) => {
page.email = email; page.email = email;
page.email_touched = true; page.email_touched = true;
} }

View File

@ -1,6 +1,6 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 75.76 75.76" viewBox="0 0 75.76 75.76"
width="28" width="28"
> >

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -2,17 +2,16 @@
viewBox="0 0 128 128" viewBox="0 0 128 128"
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="40" width="40"
height="40" height="40"
> >
<defs> <defs>
<rect id="path-1" x="0" y="0" width="128" height="128" fill="#FF5630" /> <rect id="path-1" x="0" y="0" width="128" height="128" fill="#FF5630"/>
</defs> </defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="project_avatar_settings"> <g id="project_avatar_settings">
<g> <g>
<rect id="path-1" x="0" y="0" width="128" height="128" fill="#FF5630" /> <rect id="path-1" x="0" y="0" width="128" height="128" fill="#FF5630"/>
<g id="Settings" fill-rule="nonzero"> <g id="Settings" fill-rule="nonzero">
<g transform="translate(20.000000, 17.000000)"> <g transform="translate(20.000000, 17.000000)">
<path <path

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -431,13 +431,34 @@ impl std::fmt::Display for InvitationState {
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] #[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "TimeTrackingType")] #[cfg_attr(feature = "backend", sql_type = "TimeTrackingType")]
#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
pub enum TimeTracking { pub enum TimeTracking {
Untracked, Untracked,
Fibonacci, Fibonacci,
Hourly, Hourly,
} }
impl Into<u32> for TimeTracking {
fn into(self) -> u32 {
match self {
TimeTracking::Untracked => 0,
TimeTracking::Fibonacci => 1,
TimeTracking::Hourly => 2,
}
}
}
impl Into<TimeTracking> for u32 {
fn into(self) -> TimeTracking {
match self {
0 => TimeTracking::Untracked,
1 => TimeTracking::Fibonacci,
2 => TimeTracking::Hourly,
_ => TimeTracking::Untracked,
}
}
}
#[derive(Clone, Serialize, Debug, PartialEq)] #[derive(Clone, Serialize, Debug, PartialEq)]
pub struct ErrorResponse { pub struct ErrorResponse {
pub errors: Vec<String>, pub errors: Vec<String>,
@ -631,6 +652,7 @@ pub enum ProjectFieldId {
Url, Url,
Description, Description,
Category, Category,
TimeTracking,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]