From 6afa4dc6d16ceca75ac2809b628a9c335b685ec6 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Fri, 24 Apr 2020 22:32:29 +0200 Subject: [PATCH] Add time tracking settings --- jirs-client/src/modal/issue_details.rs | 14 +++--- jirs-client/src/model.rs | 14 +++--- jirs-client/src/shared/tracking_widget.rs | 12 +++-- jirs-css/src/main.rs | 11 ++--- jirs-data/src/lib.rs | 31 ++++++++----- jirs-data/src/sql.rs | 44 ++++++++++++++++++- .../2020-04-24-163323_add_settings/down.sql | 7 +++ .../2020-04-24-163323_add_settings/up.sql | 11 +++++ jirs-server/src/db/issues.rs | 12 ++--- jirs-server/src/db/projects.rs | 6 ++- jirs-server/src/models.rs | 17 ++++--- jirs-server/src/schema.rs | 18 +++++--- jirs-server/src/ws/issues.rs | 6 +-- jirs-server/src/ws/projects.rs | 1 + 14 files changed, 141 insertions(+), 63 deletions(-) create mode 100644 jirs-server/migrations/2020-04-24-163323_add_settings/down.sql create mode 100644 jirs-server/migrations/2020-04-24-163323_add_settings/up.sql diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issue_details.rs index 8b5a7f19..155f95d7 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issue_details.rs @@ -139,22 +139,22 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)), value, ) => { - modal.payload.time_spent = value.parse::().ok(); + modal.payload.time_spent = value.parse::().ok(); send_ws_msg(WsMsg::IssueUpdateRequest( modal.id, IssueFieldId::TimeSpend, - PayloadVariant::OptionI32(modal.payload.time_spent), + PayloadVariant::OptionF64(modal.payload.time_spent), )); } Msg::InputChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)), value, ) => { - modal.payload.time_remaining = value.parse::().ok(); + modal.payload.time_remaining = value.parse::().ok(); send_ws_msg(WsMsg::IssueUpdateRequest( modal.id, IssueFieldId::TimeRemaining, - PayloadVariant::OptionI32(modal.payload.time_remaining), + PayloadVariant::OptionF64(modal.payload.time_remaining), )); } Msg::ModalChanged(FieldChange::TabChanged( @@ -183,13 +183,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { Msg::InputChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)), value, - ) => match value.parse::() { + ) => match value.parse::() { Ok(n) if !value.is_empty() => { modal.payload.estimate = Some(n); send_ws_msg(WsMsg::IssueUpdateRequest( modal.id, IssueFieldId::TimeRemaining, - PayloadVariant::OptionI32(modal.payload.estimate), + PayloadVariant::OptionF64(modal.payload.estimate), )); } _ if value.is_empty() => { @@ -197,7 +197,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { send_ws_msg(WsMsg::IssueUpdateRequest( modal.id, IssueFieldId::TimeRemaining, - PayloadVariant::OptionI32(modal.payload.estimate), + PayloadVariant::OptionF64(modal.payload.estimate), )); } _ => {} diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 5708c673..8b7017e0 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -9,7 +9,7 @@ use crate::shared::styled_editor::Mode; use crate::shared::styled_select::StyledSelectState; use crate::{EditIssueModalSection, FieldId, ProjectFieldId, HOST_URL}; -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub enum ModalType { AddIssue(Box), EditIssue(IssueId, Box), @@ -18,14 +18,14 @@ pub enum ModalType { TimeTracking(IssueId), } -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct CommentForm { pub id: Option, pub body: String, pub creating: bool, } -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct EditIssueModal { pub id: i32, pub link_copied: bool, @@ -87,7 +87,7 @@ impl EditIssueModal { } } -#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +#[derive(Clone, Debug, PartialOrd, PartialEq)] pub struct AddIssueModal { pub title: String, pub issue_type: IssueType, @@ -95,9 +95,9 @@ pub struct AddIssueModal { pub priority: IssuePriority, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub project_id: Option, pub user_ids: Vec, pub reporter_id: Option, diff --git a/jirs-client/src/shared/tracking_widget.rs b/jirs-client/src/shared/tracking_widget.rs index 908f0ae5..46844647 100644 --- a/jirs-client/src/shared/tracking_widget.rs +++ b/jirs-client/src/shared/tracking_widget.rs @@ -73,15 +73,13 @@ pub fn tracking_widget(_model: &Model, modal: &EditIssueModal) -> Node { #[inline] fn calc_bar_width( - estimate: Option, - time_spent: Option, - time_remaining: Option, + estimate: Option, + time_spent: Option, + time_remaining: Option, ) -> f64 { match (estimate, time_spent, time_remaining) { - (_, Some(spent), Some(remaining)) => { - ((spent as f64 / (spent as f64 + remaining as f64)) * 100f64).min(100f64) - } - (Some(estimate), Some(spent), _) => ((spent as f64 / estimate as f64) * 100f64).min(100f64), + (_, Some(spent), Some(remaining)) => ((spent / (spent + remaining)) * 100f64).min(100f64), + (Some(estimate), Some(spent), _) => ((spent / estimate) * 100f64).min(100f64), (None, None, _) => 100f64, (None, _, _) => 0f64, _ => 0f64, diff --git a/jirs-css/src/main.rs b/jirs-css/src/main.rs index 37729d19..e6409013 100644 --- a/jirs-css/src/main.rs +++ b/jirs-css/src/main.rs @@ -117,7 +117,6 @@ impl Application { output_timestamp: SystemTime, ) -> Result { let input_dir = input - .clone() .parent() .ok_or_else(|| format!("Not a valid path {:?}", input))?; @@ -143,7 +142,6 @@ impl Application { fn parse_file(&mut self, input: &Path) -> Result { let file_path = input.display().to_string(); let input_dir = input - .clone() .parent() .ok_or_else(|| format!("Not a valid path {:?}", input))?; @@ -179,7 +177,6 @@ impl Application { .replace(";", "") .to_string(); let child = input_dir - .clone() .join(imported.as_str()) .canonicalize() .map_err(|e| format!("{}", e))?; @@ -292,11 +289,11 @@ fn main() -> Result<(), String> { let output_timestamp = matches .value_of("output") - .ok_or(std::io::Error::from_raw_os_error(0)) - .and_then(|path| File::open(path)) + .ok_or_else(|| std::io::Error::from_raw_os_error(0)) + .and_then(File::open) .and_then(|file| file.metadata()) .and_then(|meta| meta.modified()) - .unwrap_or_else(|_| SystemTime::UNIX_EPOCH.clone()); + .unwrap_or_else(|_| SystemTime::UNIX_EPOCH); if app.check_timestamps(root, output_timestamp)? { return Ok(()); @@ -304,7 +301,7 @@ fn main() -> Result<(), String> { let (tx, rx) = channel(); app.pipe(tx.clone()); - let mut watcher = watcher(tx.clone(), Duration::from_secs(1)).unwrap(); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); app.parse()?; app.print(); diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 8dfe4854..820c60bc 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -429,6 +429,15 @@ impl std::fmt::Display for InvitationState { } } +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", sql_type = "TimeTrackingType")] +#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +pub enum TimeTracking { + Untracked, + Fibonacci, + Hourly, +} + #[derive(Clone, Serialize, Debug, PartialEq)] pub struct ErrorResponse { pub errors: Vec, @@ -444,6 +453,7 @@ pub struct Project { pub category: ProjectCategory, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, + pub time_tracking: TimeTracking, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] @@ -456,9 +466,9 @@ pub struct Issue { pub list_position: i32, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub reporter_id: UserId, pub project_id: ProjectId, pub created_at: NaiveDateTime, @@ -517,7 +527,7 @@ pub struct Token { pub bind_token: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd, Hash)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] pub struct UpdateIssuePayload { pub title: String, pub issue_type: IssueType, @@ -526,9 +536,9 @@ pub struct UpdateIssuePayload { pub list_position: i32, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub project_id: ProjectId, pub reporter_id: UserId, pub user_ids: Vec, @@ -585,9 +595,9 @@ pub struct CreateIssuePayload { pub priority: IssuePriority, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub project_id: ProjectId, pub user_ids: Vec, pub reporter_id: UserId, @@ -605,6 +615,7 @@ pub struct UpdateProjectPayload { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub enum PayloadVariant { OptionI32(Option), + OptionF64(Option), VecI32(Vec), I32(i32), String(String), diff --git a/jirs-data/src/sql.rs b/jirs-data/src/sql.rs index 6ac367eb..d134e947 100644 --- a/jirs-data/src/sql.rs +++ b/jirs-data/src/sql.rs @@ -2,7 +2,9 @@ use std::io::Write; use diesel::{deserialize::*, pg::*, serialize::*, *}; -use crate::{InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, UserRole}; +use crate::{ + InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking, UserRole, +}; #[derive(SqlType)] #[postgres(type_name = "IssuePriorityType")] @@ -241,3 +243,43 @@ impl ToSql for InvitationState { Ok(IsNull::No) } } + +#[derive(SqlType)] +#[postgres(type_name = "TimeTrackingType")] +pub struct TimeTrackingType; + +impl diesel::query_builder::QueryId for TimeTrackingType { + type QueryId = TimeTracking; +} + +fn time_tracking_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { + match not_none!(bytes) { + b"untracked" => Ok(TimeTracking::Untracked), + b"fibnachi" => Ok(TimeTracking::Fibonacci), + b"hourly" => Ok(TimeTracking::Hourly), + _ => Ok(TimeTracking::Untracked), + } +} + +impl FromSql for TimeTracking { + fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { + time_tracking_from_sql(bytes) + } +} + +impl FromSql for TimeTracking { + fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { + time_tracking_from_sql(bytes) + } +} + +impl ToSql for TimeTracking { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + match *self { + TimeTracking::Untracked => out.write_all(b"untracked")?, + TimeTracking::Fibonacci => out.write_all(b"fibnacci")?, + TimeTracking::Hourly => out.write_all(b"hourly")?, + } + Ok(IsNull::No) + } +} diff --git a/jirs-server/migrations/2020-04-24-163323_add_settings/down.sql b/jirs-server/migrations/2020-04-24-163323_add_settings/down.sql new file mode 100644 index 00000000..d119c577 --- /dev/null +++ b/jirs-server/migrations/2020-04-24-163323_add_settings/down.sql @@ -0,0 +1,7 @@ +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"; diff --git a/jirs-server/migrations/2020-04-24-163323_add_settings/up.sql b/jirs-server/migrations/2020-04-24-163323_add_settings/up.sql new file mode 100644 index 00000000..d2e4fb9a --- /dev/null +++ b/jirs-server/migrations/2020-04-24-163323_add_settings/up.sql @@ -0,0 +1,11 @@ +CREATE TYPE "TimeTrackingType" AS ENUM ( + 'untracked', + 'fibonacci', + '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'; diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs index ea7a1fd7..8a740f77 100644 --- a/jirs-server/src/db/issues.rs +++ b/jirs-server/src/db/issues.rs @@ -79,9 +79,9 @@ pub struct UpdateIssue { pub list_position: Option, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub project_id: Option, pub user_ids: Option>, pub reporter_id: Option, @@ -209,9 +209,9 @@ pub struct CreateIssue { pub priority: IssuePriority, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub project_id: i32, pub reporter_id: i32, pub user_ids: Vec, diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs index 7de2f5a5..78bea0ef 100644 --- a/jirs-server/src/db/projects.rs +++ b/jirs-server/src/db/projects.rs @@ -2,7 +2,7 @@ use actix::{Handler, Message}; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use jirs_data::{Project, ProjectCategory}; +use jirs_data::{Project, ProjectCategory, TimeTracking}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; @@ -46,6 +46,7 @@ pub struct UpdateProject { pub url: Option, pub description: Option, pub category: Option, + pub time_tracking: Option, } impl Message for UpdateProject { @@ -68,9 +69,10 @@ impl Handler for DbExecutor { msg.url.map(|v| url.eq(v)), msg.description.map(|v| description.eq(v)), msg.category.map(|v| category.eq(v)), + msg.time_tracking.map(|v| time_tracking.eq(v)), )) .execute(conn) - .map_err(|_| ServiceErrors::DatabaseConnectionLost)?; + .map_err(|e| ServiceErrors::DatabaseQueryFailed(format!("{}", e)))?; projects .filter(id.eq(msg.project_id)) diff --git a/jirs-server/src/models.rs b/jirs-server/src/models.rs index c4d6c22f..57af1ed2 100644 --- a/jirs-server/src/models.rs +++ b/jirs-server/src/models.rs @@ -2,7 +2,9 @@ use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use jirs_data::{InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory}; +use jirs_data::{ + InvitationState, IssuePriority, IssueStatus, IssueType, ProjectCategory, TimeTracking, +}; use crate::schema::*; @@ -24,9 +26,9 @@ pub struct Issue { pub list_position: i32, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub reporter_id: i32, pub project_id: i32, pub created_at: NaiveDateTime, @@ -67,9 +69,9 @@ pub struct CreateIssueForm { pub list_position: i32, pub description: Option, pub description_text: Option, - pub estimate: Option, - pub time_spent: Option, - pub time_remaining: Option, + pub estimate: Option, + pub time_spent: Option, + pub time_remaining: Option, pub reporter_id: i32, pub project_id: i32, } @@ -88,6 +90,7 @@ pub struct UpdateProjectForm { pub url: Option, pub description: Option, pub category: Option, + pub time_tracking: Option, } #[derive(Debug, Serialize, Deserialize, Insertable)] diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index 965c674c..144ff6f5 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -211,22 +211,22 @@ table! { description_text -> Nullable, /// The `estimate` column of the `issues` table. /// - /// Its SQL type is `Nullable`. + /// Its SQL type is `Nullable`. /// /// (Automatically generated by Diesel.) - estimate -> Nullable, + estimate -> Nullable, /// The `time_spent` column of the `issues` table. /// - /// Its SQL type is `Nullable`. + /// Its SQL type is `Nullable`. /// /// (Automatically generated by Diesel.) - time_spent -> Nullable, + time_spent -> Nullable, /// The `time_remaining` column of the `issues` table. /// - /// Its SQL type is `Nullable`. + /// Its SQL type is `Nullable`. /// /// (Automatically generated by Diesel.) - time_remaining -> Nullable, + time_remaining -> Nullable, /// The `reporter_id` column of the `issues` table. /// /// Its SQL type is `Int4`. @@ -304,6 +304,12 @@ table! { /// /// (Automatically generated by Diesel.) updated_at -> Timestamp, + /// The `time_tracking` column of the `projects` table. + /// + /// Its SQL type is `TimeTrackingType`. + /// + /// (Automatically generated by Diesel.) + time_tracking -> TimeTrackingType, } } diff --git a/jirs-server/src/ws/issues.rs b/jirs-server/src/ws/issues.rs index 3d33b711..cfd6bda5 100644 --- a/jirs-server/src/ws/issues.rs +++ b/jirs-server/src/ws/issues.rs @@ -51,13 +51,13 @@ impl WsHandler for WebSocketActor { (IssueFieldId::Priority, PayloadVariant::IssuePriority(p)) => { msg.priority = Some(p); } - (IssueFieldId::Estimate, PayloadVariant::OptionI32(o)) => { + (IssueFieldId::Estimate, PayloadVariant::OptionF64(o)) => { msg.estimate = o; } - (IssueFieldId::TimeSpend, PayloadVariant::OptionI32(o)) => { + (IssueFieldId::TimeSpend, PayloadVariant::OptionF64(o)) => { msg.time_spent = o; } - (IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => { + (IssueFieldId::TimeRemaining, PayloadVariant::OptionF64(o)) => { msg.time_remaining = o; } _ => (), diff --git a/jirs-server/src/ws/projects.rs b/jirs-server/src/ws/projects.rs index 8d6dc342..679fcee0 100644 --- a/jirs-server/src/ws/projects.rs +++ b/jirs-server/src/ws/projects.rs @@ -35,6 +35,7 @@ impl WsHandler for WebSocketActor { url: msg.url, description: msg.description, category: msg.category, + time_tracking: None, })) { Ok(Ok(project)) => project, _ => return Ok(None),