Add time tracking settings

This commit is contained in:
Adrian Wozniak 2020-04-24 22:32:29 +02:00
parent c84a6ade8e
commit 6afa4dc6d1
14 changed files with 141 additions and 63 deletions

View File

@ -139,22 +139,22 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpend)),
value,
) => {
modal.payload.time_spent = value.parse::<i32>().ok();
modal.payload.time_spent = value.parse::<f64>().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::<i32>().ok();
modal.payload.time_remaining = value.parse::<f64>().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>) {
Msg::InputChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value,
) => match value.parse::<i32>() {
) => match value.parse::<f64>() {
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<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.estimate),
PayloadVariant::OptionF64(modal.payload.estimate),
));
}
_ => {}

View File

@ -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<AddIssueModal>),
EditIssue(IssueId, Box<EditIssueModal>),
@ -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<CommentId>,
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<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub project_id: Option<i32>,
pub user_ids: Vec<i32>,
pub reporter_id: Option<i32>,

View File

@ -73,15 +73,13 @@ pub fn tracking_widget(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
#[inline]
fn calc_bar_width(
estimate: Option<i32>,
time_spent: Option<i32>,
time_remaining: Option<i32>,
estimate: Option<f64>,
time_spent: Option<f64>,
time_remaining: Option<f64>,
) -> 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,

View File

@ -117,7 +117,6 @@ impl Application {
output_timestamp: SystemTime,
) -> Result<bool, String> {
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<Css, String> {
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();

View File

@ -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<String>,
@ -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<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub reporter_id: UserId,
pub project_id: ProjectId,
pub created_at: NaiveDateTime,
@ -517,7 +527,7 @@ pub struct Token {
pub bind_token: Option<Uuid>,
}
#[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<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub user_ids: Vec<UserId>,
@ -585,9 +595,9 @@ pub struct CreateIssuePayload {
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
pub reporter_id: UserId,
@ -605,6 +615,7 @@ pub struct UpdateProjectPayload {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PayloadVariant {
OptionI32(Option<i32>),
OptionF64(Option<f64>),
VecI32(Vec<i32>),
I32(i32),
String(String),

View File

@ -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<InvitationStateType, Pg> 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<TimeTracking> {
match not_none!(bytes) {
b"untracked" => Ok(TimeTracking::Untracked),
b"fibnachi" => Ok(TimeTracking::Fibonacci),
b"hourly" => Ok(TimeTracking::Hourly),
_ => Ok(TimeTracking::Untracked),
}
}
impl FromSql<TimeTrackingType, Pg> for TimeTracking {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<TimeTracking> {
time_tracking_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for TimeTracking {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<TimeTracking> {
time_tracking_from_sql(bytes)
}
}
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::Hourly => out.write_all(b"hourly")?,
}
Ok(IsNull::No)
}
}

View File

@ -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";

View File

@ -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';

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<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
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<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub project_id: i32,
pub reporter_id: i32,
pub user_ids: Vec<i32>,

View File

@ -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<String>,
pub description: Option<String>,
pub category: Option<ProjectCategory>,
pub time_tracking: Option<TimeTracking>,
}
impl Message for UpdateProject {
@ -68,9 +69,10 @@ impl Handler<UpdateProject> 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))

View File

@ -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<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
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<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub estimate: Option<f64>,
pub time_spent: Option<f64>,
pub time_remaining: Option<f64>,
pub reporter_id: i32,
pub project_id: i32,
}
@ -88,6 +90,7 @@ pub struct UpdateProjectForm {
pub url: Option<String>,
pub description: Option<String>,
pub category: Option<ProjectCategory>,
pub time_tracking: Option<TimeTracking>,
}
#[derive(Debug, Serialize, Deserialize, Insertable)]

View File

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

View File

@ -51,13 +51,13 @@ impl WsHandler<UpdateIssueHandler> 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;
}
_ => (),

View File

@ -35,6 +35,7 @@ impl WsHandler<UpdateProjectPayload> for WebSocketActor {
url: msg.url,
description: msg.description,
category: msg.category,
time_tracking: None,
})) {
Ok(Ok(project)) => project,
_ => return Ok(None),