Add time tracking settings start handling
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 473 KiB |
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 605 KiB |
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 467 KiB |
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 604 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
@ -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;
|
||||||
|
@ -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;
|
||||||
|
43
jirs-client/js/css/styledCheckbox.css
Normal 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);
|
||||||
|
}
|
@ -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";
|
||||||
|
@ -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
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"],
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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 they’re 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();
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
232
jirs-client/src/shared/styled_checkbox.rs
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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()
|
||||||
|
@ -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![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 |
@ -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 |
@ -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)]
|
||||||
|