From 57adfac3a4e11e327868132c92a434c0b6f316c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Thu, 7 Jan 2021 23:34:36 +0100 Subject: [PATCH] Proc macros Fix update issue view. Refactor jirs-data. --- Cargo.lock | 15 + Cargo.toml | 3 + actors/database-actor/src/schema.patch | 2 +- actors/database-actor/src/schema.rs | 22 +- derive/derive_enum_iter/Cargo.toml | 16 + derive/derive_enum_iter/src/lib.rs | 117 ++++++++ derive/derive_enum_primitive/Cargo.toml | 16 + derive/derive_enum_primitive/src/lib.rs | 194 ++++++++++++ derive/derive_enum_sql/Cargo.toml | 16 + derive/derive_enum_sql/src/lib.rs | 155 ++++++++++ diesel.toml | 2 +- jirs-client/src/lib.rs | 2 +- jirs-client/src/modal/issues.rs | 2 +- jirs-client/src/modal/time_tracking.rs | 125 -------- jirs-client/src/modals/issues_create/view.rs | 37 +-- jirs-client/src/modals/issues_edit/update.rs | 5 +- .../src/modals/issues_edit/view/mod.rs | 43 +-- jirs-client/src/modals/time_tracking/view.rs | 2 +- jirs-client/src/pages/profile_page/view.rs | 73 +++-- .../src/pages/project_settings_page/view.rs | 13 +- jirs-client/src/pages/users_page/view.rs | 13 +- jirs-client/src/shared/mod.rs | 6 + jirs-client/src/shared/styled_select.rs | 115 ++++--- jirs-client/src/shared/styled_select_child.rs | 33 +- jirs-client/src/ws/mod.rs | 39 ++- shared/jirs-data/Cargo.toml | 12 +- shared/jirs-data/src/lib.rs | 275 +++-------------- shared/jirs-data/src/sql.rs | 284 ------------------ 28 files changed, 795 insertions(+), 842 deletions(-) create mode 100644 derive/derive_enum_iter/Cargo.toml create mode 100644 derive/derive_enum_iter/src/lib.rs create mode 100644 derive/derive_enum_primitive/Cargo.toml create mode 100644 derive/derive_enum_primitive/src/lib.rs create mode 100644 derive/derive_enum_sql/Cargo.toml create mode 100644 derive/derive_enum_sql/src/lib.rs delete mode 100644 jirs-client/src/modal/time_tracking.rs delete mode 100644 shared/jirs-data/src/sql.rs diff --git a/Cargo.lock b/Cargo.lock index 8a657ba5..04b4e64d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,6 +1055,18 @@ dependencies = [ "version_check 0.1.5", ] +[[package]] +name = "derive_enum_iter" +version = "0.1.0" + +[[package]] +name = "derive_enum_primitive" +version = "0.1.0" + +[[package]] +name = "derive_enum_sql" +version = "0.1.0" + [[package]] name = "derive_more" version = "0.99.11" @@ -1965,6 +1977,9 @@ version = "0.1.0" dependencies = [ "actix 0.10.0", "chrono", + "derive_enum_iter", + "derive_enum_primitive", + "derive_enum_sql", "diesel", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index fe73de2b..0b1608b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ members = [ "./jirs-css", "./shared/jirs-config", "./shared/jirs-data", + "./derive/derive_enum_iter", + "./derive/derive_enum_primitive", + "./derive/derive_enum_sql", "./actors/highlight-actor", "./actors/database-actor", "./actors/database-actor/database_actor-derive", diff --git a/actors/database-actor/src/schema.patch b/actors/database-actor/src/schema.patch index 28ff67f8..fa7e2685 100644 --- a/actors/database-actor/src/schema.patch +++ b/actors/database-actor/src/schema.patch @@ -7,7 +7,7 @@ index 00d1c0b..5b82ccf 100644 + table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `comments` table. /// diff --git a/actors/database-actor/src/schema.rs b/actors/database-actor/src/schema.rs index 1f1aea6f..0adc596f 100644 --- a/actors/database-actor/src/schema.rs +++ b/actors/database-actor/src/schema.rs @@ -2,7 +2,7 @@ table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `comments` table. /// @@ -49,7 +49,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `epics` table. /// @@ -108,7 +108,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `invitations` table. /// @@ -179,7 +179,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `issue_assignees` table. /// @@ -220,7 +220,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `issue_statuses` table. /// @@ -267,7 +267,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `issues` table. /// @@ -374,7 +374,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `messages` table. /// @@ -439,7 +439,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `projects` table. /// @@ -498,7 +498,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `tokens` table. /// @@ -551,7 +551,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `user_projects` table. /// @@ -610,7 +610,7 @@ table! { table! { use diesel::sql_types::*; - use jirs_data::sql::*; + use jirs_data::*; /// Representation of the `users` table. /// diff --git a/derive/derive_enum_iter/Cargo.toml b/derive/derive_enum_iter/Cargo.toml new file mode 100644 index 00000000..85bc90f5 --- /dev/null +++ b/derive/derive_enum_iter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "derive_enum_iter" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "derive_enum_iter" +path = "./src/lib.rs" +proc-macro = true + +[dependencies] diff --git a/derive/derive_enum_iter/src/lib.rs b/derive/derive_enum_iter/src/lib.rs new file mode 100644 index 00000000..eea53547 --- /dev/null +++ b/derive/derive_enum_iter/src/lib.rs @@ -0,0 +1,117 @@ +extern crate proc_macro; + +use proc_macro::{TokenStream, TokenTree}; + +#[proc_macro_derive(EnumIter)] +pub fn derive_enum_iter(item: TokenStream) -> TokenStream { + let mut it = item.into_iter().peekable(); + while let Some(token) = it.peek() { + if let TokenTree::Ident(_) = token { + break; + } else { + it.next(); + } + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "pub" { + panic!("Expect to find keyword pub but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword pub but nothing was found") + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "enum" { + panic!("Expect to find keyword struct but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword struct but nothing was found") + } + let name = it + .next() + .expect("Expect to struct name but nothing was found"); + + let mut variants = vec![]; + if let Some(TokenTree::Group(group)) = it.next() { + for token in group.stream() { + if let TokenTree::Ident(ident) = token { + variants.push(ident.to_string()) + } + } + } else { + panic!("Enum variants group expected"); + } + if variants.is_empty() { + panic!("Enum cannot be empty") + } + + let mut code = format!( + r#" +pub struct {name}Iter(Option<{name}>); + +impl std::iter::Iterator for {name}Iter {{ + type Item = {name}; + + fn count(self) -> usize {{ + {len} + }} + + fn last(self) -> Option {{ + Some({name}::{last}) + }} + + fn next(&mut self) -> Option {{ + let o = match self.0 {{ +"#, + name = name, + len = variants.len(), + last = variants.last().unwrap(), + ); + + let mut last_variant = ""; + for (idx, variant) in variants.iter().enumerate() { + match idx { + 0 => code.push_str( + format!( + "None => Some({name}::{variant}),\n", + variant = variant, + name = name + ) + .as_str(), + ), + _ if idx == variants.len() - 1 => code.push_str("_ => None,\n"), + _ => code.push_str( + format!( + "Some({name}::{last_variant}) => Some({name}::{variant}),\n", + last_variant = last_variant, + variant = variant, + name = name, + ) + .as_str(), + ), + } + last_variant = variant.as_str(); + } + + code.push_str( + format!( + r#" + }}; + self.0 = o; + o + }} +}} +impl std::iter::IntoIterator for {name} {{ + type Item = {name}; + type IntoIter = {name}Iter; + + fn into_iter(self) -> Self::IntoIter {{ + {name}Iter(None) + }} +}} +"#, + name = name + ) + .as_str(), + ); + code.parse().unwrap() +} diff --git a/derive/derive_enum_primitive/Cargo.toml b/derive/derive_enum_primitive/Cargo.toml new file mode 100644 index 00000000..850efeee --- /dev/null +++ b/derive/derive_enum_primitive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "derive_enum_primitive" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "derive_enum_primitive" +path = "./src/lib.rs" +proc-macro = true + +[dependencies] diff --git a/derive/derive_enum_primitive/src/lib.rs b/derive/derive_enum_primitive/src/lib.rs new file mode 100644 index 00000000..1fc7fdf8 --- /dev/null +++ b/derive/derive_enum_primitive/src/lib.rs @@ -0,0 +1,194 @@ +extern crate proc_macro; + +use proc_macro::{TokenStream, TokenTree}; + +fn into_str(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +#[cfg(feature = "frontend")] +impl {name} {{ + pub fn to_str(&self) -> &'static str {{ + match self {{ +"#, + name = name, + ); + + for variant in variants { + let lower = variant.to_lowercase(); + code.push_str( + format!( + " {name}::{variant} => \"{lower}\",\n", + variant = variant, + name = name, + lower = lower + ) + .as_str(), + ); + } + code.push_str(" }\n }\n}"); + code +} + +fn from_str(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +#[cfg(feature = "frontend")] +impl FromStr for {name} {{ + type Err = String; + fn from_str(s: &str) -> Result {{ + match s {{ +"#, + name = name, + ); + + for variant in variants { + let lower = variant.to_lowercase(); + code.push_str( + format!( + " \"{lower}\" => Ok({name}::{variant}),\n", + variant = variant, + name = name, + lower = lower + ) + .as_str(), + ); + } + + code.push_str( + format!( + " _ => Err(format!(\"Unknown {name} {{}}\", s)),", + name = name + ) + .as_str(), + ); + code.push_str(" }\n }\n}"); + code +} + +fn into_label(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +#[cfg(feature = "frontend")] +impl {name} {{ + pub fn to_label(&self) -> &'static str {{ + match self {{ +"#, + name = name, + ); + + for variant in variants { + code.push_str( + format!( + " {name}::{variant} => \"{variant}\",\n", + variant = variant, + name = name, + ) + .as_str(), + ); + } + code.push_str(" }\n }\n}"); + code +} + +fn into_u32(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +impl Into for {name} {{ + fn into(self) -> u32 {{ + match self {{ +"#, + name = name + ); + for (idx, variant) in variants.iter().enumerate() { + code.push_str( + format!( + " {name}::{variant} => {idx},\n", + variant = variant, + name = name, + idx = idx + ) + .as_str(), + ); + } + code.push_str(" }\n }\n}"); + code +} + +fn from_u32(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +impl Into<{name}> for u32 {{ + fn into(self) -> {name} {{ + match self {{ +"#, + name = name + ); + for (idx, variant) in variants.iter().enumerate() { + code.push_str( + format!( + " {idx} => {name}::{variant},\n", + variant = variant, + name = name, + idx = idx + ) + .as_str(), + ); + } + code.push_str(format!(" _ => {name}::default(),\n", name = name,).as_str()); + code.push_str(" }\n }\n}"); + code +} + +#[proc_macro_derive(EnumPrimitive)] +pub fn derive_enum_primitive(item: TokenStream) -> TokenStream { + let mut it = item.into_iter().peekable(); + while let Some(token) = it.peek() { + if let TokenTree::Ident(_) = token { + break; + } else { + it.next(); + } + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "pub" { + panic!("Expect to find keyword pub but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword pub but nothing was found") + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "enum" { + panic!("Expect to find keyword struct but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword struct but nothing was found") + } + let name = it + .next() + .expect("Expect to struct name but nothing was found") + .to_string(); + + let mut variants = vec![]; + if let Some(TokenTree::Group(group)) = it.next() { + for token in group.stream() { + if let TokenTree::Ident(ident) = token { + variants.push(ident.to_string()) + } + } + } else { + panic!("Enum variants group expected"); + } + if variants.is_empty() { + panic!("Enum cannot be empty") + } + + let mut code = String::new(); + + code.push_str(from_str(&name, &variants).as_str()); + code.push_str(into_str(&name, &variants).as_str()); + code.push_str(into_label(&name, &variants).as_str()); + code.push_str(into_u32(&name, &variants).as_str()); + code.push_str(from_u32(&name, &variants).as_str()); + + code.parse().unwrap() +} diff --git a/derive/derive_enum_sql/Cargo.toml b/derive/derive_enum_sql/Cargo.toml new file mode 100644 index 00000000..e4cb0428 --- /dev/null +++ b/derive/derive_enum_sql/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "derive_enum_sql" +version = "0.1.0" +authors = ["Adrian Wozniak "] +edition = "2018" +description = "JIRS (Simplified JIRA in Rust) shared data types" +repository = "https://gitlab.com/adrian.wozniak/jirs" +license = "MPL-2.0" +#license-file = "../LICENSE" + +[lib] +name = "derive_enum_sql" +path = "./src/lib.rs" +proc-macro = true + +[dependencies] diff --git a/derive/derive_enum_sql/src/lib.rs b/derive/derive_enum_sql/src/lib.rs new file mode 100644 index 00000000..16aa27c3 --- /dev/null +++ b/derive/derive_enum_sql/src/lib.rs @@ -0,0 +1,155 @@ +extern crate proc_macro; + +use proc_macro::{TokenStream, TokenTree}; + +fn to_lower_case(s: &str) -> String { + let mut lower = String::new(); + for (idx, c) in s.chars().enumerate() { + if idx > 0 && c.is_uppercase() { + lower.push('_'); + } + lower.push_str(c.to_lowercase().to_string().as_str()); + } + lower +} + +fn into_str(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +#[cfg(feature = "backend")] +impl diesel::serialize::ToSql<{name}Type, diesel::pg::Pg> for {name} {{ + fn to_sql(&self, out: &mut diesel::serialize::Output) -> diesel::serialize::Result {{ + match *self {{ +"#, + name = name, + ); + + for variant in variants { + let lower = to_lower_case(&variant); + code.push_str( + format!( + " {name}::{variant} => out.write_all(b\"{lower}\")?,\n", + variant = variant, + name = name, + lower = lower + ) + .as_str(), + ); + } + code.push_str(" };\n Ok(diesel::serialize::IsNull::No)\n }\n}"); + code +} + +fn from_str(name: &str, variants: &[String]) -> String { + let mut code = format!( + r#" +#[cfg(feature = "backend")] +impl {name} {{ + fn from_diesel_bytes(bytes: Option<&[u8]>) -> diesel::deserialize::Result<{name}> {{ + match diesel::not_none!(bytes) {{ +"#, + name = name, + ); + + for variant in variants { + let lower = to_lower_case(&variant); + code.push_str( + format!( + " b\"{lower}\" => Ok({name}::{variant}),\n", + variant = variant, + name = name, + lower = lower + ) + .as_str(), + ); + } + + code.push_str(format!(" _ => Ok({name}::default()),", name = name).as_str()); + code.push_str(" }\n }\n}"); + code.push_str( + format!( + r#" +#[cfg(feature = "backend")] +impl diesel::deserialize::FromSql<{name}Type, diesel::pg::Pg> for {name} {{ + fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result {{ + {name}::from_diesel_bytes(bytes) + }} +}} + +#[cfg(feature = "backend")] +impl diesel::deserialize::FromSql for {name} {{ + fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result {{ + {name}::from_diesel_bytes(bytes) + }} +}} + "#, + name = name + ) + .as_str(), + ); + code +} + +#[proc_macro_derive(EnumSql)] +pub fn derive_enum_sql(item: TokenStream) -> TokenStream { + let mut it = item.into_iter().peekable(); + while let Some(token) = it.peek() { + if let TokenTree::Ident(_) = token { + break; + } else { + it.next(); + } + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "pub" { + panic!("Expect to find keyword pub but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword pub but nothing was found") + } + if let Some(TokenTree::Ident(ident)) = it.next() { + if ident.to_string().as_str() != "enum" { + panic!("Expect to find keyword struct but was found {:?}", ident) + } + } else { + panic!("Expect to find keyword struct but nothing was found") + } + let name = it + .next() + .expect("Expect to struct name but nothing was found") + .to_string(); + + let mut variants = vec![]; + if let Some(TokenTree::Group(group)) = it.next() { + for token in group.stream() { + if let TokenTree::Ident(ident) = token { + variants.push(ident.to_string()) + } + } + } else { + panic!("Enum variants group expected"); + } + if variants.is_empty() { + panic!("Enum cannot be empty") + } + + let mut code = format!( + r#" +#[cfg(feature = "backend")] +#[derive(diesel::SqlType)] +#[postgres(type_name = "{name}Type")] +pub struct {name}Type; + +#[cfg(feature = "backend")] +impl diesel::query_builder::QueryId for {name}Type {{ + type QueryId = {name}; +}} + "#, + name = name + ); + + code.push_str(from_str(&name, &variants).as_str()); + code.push_str(into_str(&name, &variants).as_str()); + + code.parse().unwrap() +} diff --git a/diesel.toml b/diesel.toml index 5b6fce59..8c44ec59 100644 --- a/diesel.toml +++ b/diesel.toml @@ -3,6 +3,6 @@ [print_schema] file = "actors/database-actor/src/schema.rs" -import_types = ["diesel::sql_types::*", "jirs_data::sql::*"] +import_types = ["diesel::sql_types::*", "jirs_data::*"] with_docs = true patch_file = "./actors/database-actor/src/schema.patch" diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 3769e72e..a0cb73fa 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(or_patterns, type_ascription)] +#![feature(or_patterns, type_ascription, trait_alias)] use { crate::{ diff --git a/jirs-client/src/modal/issues.rs b/jirs-client/src/modal/issues.rs index 8db167b8..9fd1a7f2 100644 --- a/jirs-client/src/modal/issues.rs +++ b/jirs-client/src/modal/issues.rs @@ -23,7 +23,7 @@ where let input = StyledSelect::build() .name("epic") .selected(selected) - .options(model.epics.iter().map(|epic| epic.to_child()).collect()) + .options(model.epics.iter().map(|epic| epic.to_child())) .normal() .clearable() .text_filter(modal.epic_state().text_filter.as_str()) diff --git a/jirs-client/src/modal/time_tracking.rs b/jirs-client/src/modal/time_tracking.rs deleted file mode 100644 index cec5868a..00000000 --- a/jirs-client/src/modal/time_tracking.rs +++ /dev/null @@ -1,125 +0,0 @@ -use { - crate::{ - model::{ModalType, Model}, - shared::{ - find_issue, - styled_button::StyledButton, - styled_field::StyledField, - styled_input::{StyledInput, StyledInputState}, - styled_modal::StyledModal, - styled_select::{StyledSelect, StyledSelectState}, - tracking_widget::{fibonacci_values, tracking_widget}, - ToChild, ToNode, - }, - EditIssueModalSection, FieldId, Msg, - }, - jirs_data::{IssueFieldId, IssueId, TimeTracking}, - seed::{prelude::*, *}, -}; - -// use crate::shared::styled_select_child::*; - -pub fn value_for_time_tracking(v: &Option, time_tracking_type: &TimeTracking) -> String { - match (time_tracking_type, v.as_ref()) { - (TimeTracking::Untracked, _) => "".to_string(), - (TimeTracking::Fibonacci, Some(n)) => n.to_string(), - (TimeTracking::Hourly, Some(n)) => format!("{:.1}", *n as f64 / 10.0f64), - _ => "".to_string(), - } -} - -pub fn view(model: &Model, issue_id: IssueId) -> Node { - let _issue = match find_issue(model, issue_id) { - Some(issue) => issue, - _ => return empty![], - }; - - let edit_issue_modal = match model.modals.get(0) { - Some(ModalType::EditIssue(_, modal)) => modal, - _ => return empty![], - }; - let time_tracking_type = model - .project - .as_ref() - .map(|p| p.time_tracking) - .unwrap_or_else(|| TimeTracking::Untracked); - - let modal_title = div![C!["modalTitle"], "Time tracking"]; - - let tracking = tracking_widget(model, edit_issue_modal); - - let time_spent_field = time_tracking_field( - time_tracking_type, - FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeSpent)), - "Time spent", - &edit_issue_modal.time_spent, - &edit_issue_modal.time_spent_select, - ); - let time_remaining_field = time_tracking_field( - time_tracking_type, - FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::TimeRemaining)), - "Time remaining", - &edit_issue_modal.time_remaining, - &edit_issue_modal.time_remaining_select, - ); - - let inputs = div![ - C!["inputs"], - div![C!["inputContainer"], time_spent_field], - div![C!["inputContainer"], time_remaining_field] - ]; - - let close = StyledButton::build() - .text("Done") - .on_click(mouse_ev(Ev::Click, |_| Msg::ModalDropped)) - .build() - .into_node(); - - StyledModal::build() - .add_class("timeTrackingModal") - .children(vec![ - modal_title, - tracking, - inputs, - div![C!["actions"], close], - ]) - .width(400) - .build() - .into_node() -} - -#[inline] -pub fn time_tracking_field( - time_tracking_type: TimeTracking, - field_id: FieldId, - label: &str, - input_state: &StyledInputState, - select_state: &StyledSelectState, -) -> Node { - let fibonacci_values = fibonacci_values(); - let input = match time_tracking_type { - TimeTracking::Untracked => empty![], - TimeTracking::Fibonacci => StyledSelect::build() - .selected( - select_state - .values - .iter() - .map(|n| (*n).to_child()) - .collect(), - ) - .state(select_state) - .options(fibonacci_values.iter().map(|v| v.to_child()).collect()) - .build(field_id) - .into_node(), - TimeTracking::Hourly => StyledInput::build() - .state(input_state) - .valid(true) - .build(field_id) - .into_node(), - }; - StyledField::build() - .input(input) - .label(label) - .build() - .into_node() -} diff --git a/jirs-client/src/modals/issues_create/view.rs b/jirs-client/src/modals/issues_create/view.rs index 6c063888..499d3158 100644 --- a/jirs-client/src/modals/issues_create/view.rs +++ b/jirs-client/src/modals/issues_create/view.rs @@ -7,11 +7,11 @@ use { styled_button::StyledButton, styled_date_time_input::StyledDateTimeInput, styled_field::StyledField, styled_form::StyledForm, styled_input::StyledInput, styled_modal::StyledModal, styled_select::StyledSelect, - styled_textarea::StyledTextarea, ToChild, ToNode, + styled_textarea::StyledTextarea, IntoChild, ToChild, ToNode, }, FieldId, Msg, }, - jirs_data::{IssueFieldId, IssuePriority, ToVec}, + jirs_data::{IssueFieldId, IssuePriority}, seed::{prelude::*, *}, }; @@ -124,12 +124,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node { .text_filter(modal.type_state.text_filter.as_str()) .opened(modal.type_state.opened) .valid(true) - .options( - Type::ordered() - .iter() - .map(|t| t.to_child().name("type")) - .collect(), - ) + .options(Type::ordered().iter().map(|t| t.to_child().name("type"))) .selected(vec![Type::from( modal.type_state.values.get(0).cloned().unwrap_or_default(), ) @@ -182,13 +177,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { .normal() .text_filter(modal.reporter_state.text_filter.as_str()) .opened(modal.reporter_state.opened) - .options( - model - .users - .iter() - .map(|u| u.to_child().name("reporter")) - .collect(), - ) + .options(model.users.iter().map(|u| u.to_child().name("reporter"))) .selected( model .users @@ -219,13 +208,7 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { .multi() .text_filter(modal.assignees_state.text_filter.as_str()) .opened(modal.assignees_state.opened) - .options( - model - .users - .iter() - .map(|u| u.to_child().name("assignees")) - .collect(), - ) + .options(model.users.iter().map(|u| u.to_child().name("assignees"))) .selected( model .users @@ -251,19 +234,15 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { } fn issue_priority_field(modal: &AddIssueModal) -> Node { + let priorities = IssuePriority::default().into_iter(); let select_priority = StyledSelect::build() .name("priority") .normal() .text_filter(modal.priority_state.text_filter.as_str()) .opened(modal.priority_state.opened) .valid(true) - .options( - IssuePriority::ordered() - .iter() - .map(|p| p.to_child().name("priority")) - .collect(), - ) - .selected(vec![modal.priority.to_child().name("priority")]) + .options(priorities.map(|p| p.into_child().name("priority"))) + .selected(vec![modal.priority.into_child().name("priority")]) .build(FieldId::AddIssueModal(IssueFieldId::Priority)) .into_node(); StyledField::build() diff --git a/jirs-client/src/modals/issues_edit/update.rs b/jirs-client/src/modals/issues_edit/update.rs index a18bb11a..341471bb 100644 --- a/jirs-client/src/modals/issues_edit/update.rs +++ b/jirs-client/src/modals/issues_edit/update.rs @@ -21,6 +21,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleModified, Some(id)) => { if let Some(issue) = model.issues_by_id.get(id) { modal.payload = issue.clone().into(); + modal.description_state.initial_text = + issue.description_text.clone().unwrap_or_default(); } } Msg::StyledSelectChanged( @@ -139,7 +141,6 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), value, ) => { - // modal.payload.description = Some(value.clone()); modal.payload.description_text = Some(value.clone()); send_ws_msg( WsMsg::IssueUpdate( @@ -148,7 +149,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { PayloadVariant::String( modal .payload - .description + .description_text .as_ref() .cloned() .unwrap_or_default(), diff --git a/jirs-client/src/modals/issues_edit/view/mod.rs b/jirs-client/src/modals/issues_edit/view/mod.rs index 37637ece..f6fd0854 100644 --- a/jirs-client/src/modals/issues_edit/view/mod.rs +++ b/jirs-client/src/modals/issues_edit/view/mod.rs @@ -6,12 +6,13 @@ use { shared::{ styled_avatar::StyledAvatar, styled_button::StyledButton, styled_editor::StyledEditor, styled_field::StyledField, styled_icon::Icon, styled_input::StyledInput, - styled_select::StyledSelect, tracking_widget::tracking_link, ToChild, ToNode, + styled_select::StyledSelect, tracking_widget::tracking_link, IntoChild, ToChild, + ToNode, }, EditIssueModalSection, FieldChange, FieldId, Msg, }, comments::*, - jirs_data::{CommentFieldId, IssueFieldId, IssuePriority, IssueType, TimeTracking, ToVec}, + jirs_data::{CommentFieldId, IssueFieldId, IssuePriority, IssueType, TimeTracking}, seed::{prelude::*, *}, }; @@ -41,7 +42,14 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node { let issue_id = *id; let click_handler = mouse_ev(Ev::Click, move |_| { - let link = format!("http://localhost:7000/issues/{id}", id = issue_id); + let proto = seed::window().location().protocol().unwrap_or_default(); + let hostname = seed::window().location().hostname().unwrap_or_default(); + let link = format!( + "{proto}//{hostname}/issues/{id}", + proto = proto, + hostname = hostname, + id = issue_id + ); let el = match seed::html_document().create_element("textarea") { Ok(el) => el .dyn_ref::() @@ -95,7 +103,6 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node { .build() .into_node(); - let issue_types = IssueType::ordered(); let issue_type_select = StyledSelect::build() .dropdown_width(150) .name("type") @@ -103,16 +110,15 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node { .opened(top_type_state.opened) .valid(true) .options( - issue_types - .iter() - .map(|t| t.to_child().name("type")) - .collect(), + IssueType::default() + .into_iter() + .map(|t| t.into_child().name("type")), ) .selected(vec![{ let id = modal.id; let issue_type = &payload.issue_type; issue_type - .to_child() + .into_child() .name("type") .text_owned(format!("{} - {}", issue_type, id)) }]) @@ -244,8 +250,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { model .issue_statuses .iter() - .map(|opt| opt.to_child().name("status")) - .collect(), + .map(|opt| opt.to_child().name("status")), ) .selected( model @@ -276,8 +281,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { model .users .iter() - .map(|user| user.to_child().name("assignees")) - .collect(), + .map(|user| user.to_child().name("assignees")), ) .selected( model @@ -306,8 +310,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { model .users .iter() - .map(|user| user.to_child().name("reporter")) - .collect(), + .map(|user| user.to_child().name("reporter")), ) .selected( model @@ -327,19 +330,17 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .build() .into_node(); - let issue_priorities = IssuePriority::ordered(); let priority = StyledSelect::build() .name("priority") .opened(priority_state.opened) .empty() .text_filter(priority_state.text_filter.as_str()) .options( - issue_priorities - .iter() - .map(|p| p.to_child().name("priority")) - .collect(), + IssuePriority::default() + .into_iter() + .map(|p| p.into_child().name("priority")), ) - .selected(vec![payload.priority.to_child().name("priority")]) + .selected(vec![payload.priority.into_child().name("priority")]) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( IssueFieldId::Priority, ))) diff --git a/jirs-client/src/modals/time_tracking/view.rs b/jirs-client/src/modals/time_tracking/view.rs index cec5868a..9f782bb0 100644 --- a/jirs-client/src/modals/time_tracking/view.rs +++ b/jirs-client/src/modals/time_tracking/view.rs @@ -108,7 +108,7 @@ pub fn time_tracking_field( .collect(), ) .state(select_state) - .options(fibonacci_values.iter().map(|v| v.to_child()).collect()) + .options(fibonacci_values.iter().map(|v| v.to_child())) .build(field_id) .into_node(), TimeTracking::Hourly => StyledInput::build() diff --git a/jirs-client/src/pages/profile_page/view.rs b/jirs-client/src/pages/profile_page/view.rs index 63e6b4f9..993897d0 100644 --- a/jirs-client/src/pages/profile_page/view.rs +++ b/jirs-client/src/pages/profile_page/view.rs @@ -80,46 +80,41 @@ pub fn view(model: &Model) -> Node { } fn build_current_project(model: &Model, page: &ProfilePage) -> Node { - let inner = if model.projects.len() <= 1 { - let name = model - .project - .as_ref() - .map(|p| p.name.as_str()) - .unwrap_or_default(); - span![name] - } else { - let mut project_by_id = HashMap::new(); - for p in model.projects.iter() { - project_by_id.insert(p.id, p); - } - let mut joined_projects = HashMap::new(); - for p in model.user_projects.iter() { - joined_projects.insert(p.project_id, p); - } + let inner = + if model.projects.len() <= 1 { + let name = model + .project + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or_default(); + span![name] + } else { + let mut project_by_id = HashMap::new(); + for p in model.projects.iter() { + project_by_id.insert(p.id, p); + } + let mut joined_projects = HashMap::new(); + for p in model.user_projects.iter() { + joined_projects.insert(p.project_id, p); + } - StyledSelect::build() - .name("current_project") - .normal() - .options( - model - .projects - .iter() - .filter_map(|project| { - joined_projects.get(&project.id).map(|_| project.to_child()) - }) - .collect(), - ) - .selected( - page.current_project - .values - .iter() - .filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child())) - .collect(), - ) - .state(&page.current_project) - .build(FieldId::Profile(UsersFieldId::CurrentProject)) - .into_node() - }; + StyledSelect::build() + .name("current_project") + .normal() + .options(model.projects.iter().filter_map(|project| { + joined_projects.get(&project.id).map(|_| project.to_child()) + })) + .selected( + page.current_project + .values + .iter() + .filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child())) + .collect(), + ) + .state(&page.current_project) + .build(FieldId::Profile(UsersFieldId::CurrentProject)) + .into_node() + }; StyledField::build() .label("Current project") .input(div![C!["project-name"], inner]) diff --git a/jirs-client/src/pages/project_settings_page/view.rs b/jirs-client/src/pages/project_settings_page/view.rs index d1999694..e83bc464 100644 --- a/jirs-client/src/pages/project_settings_page/view.rs +++ b/jirs-client/src/pages/project_settings_page/view.rs @@ -14,11 +14,11 @@ use { styled_input::StyledInput, styled_select::StyledSelect, styled_textarea::StyledTextarea, - ToChild, ToNode, + IntoChild, ToChild, ToNode, }, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, }, - jirs_data::{IssueStatus, ProjectCategory, TimeTracking, ToVec}, + jirs_data::{IssueStatus, ProjectCategory, TimeTracking}, seed::{prelude::*, *}, std::collections::HashMap, }; @@ -168,20 +168,23 @@ fn description_field(page: &ProjectSettingsPage) -> Node { /// Build project category dropdown with styled field wrapper fn category_field(page: &ProjectSettingsPage) -> Node { - let project_categories = ProjectCategory::ordered(); let category = StyledSelect::build() .opened(page.project_category_state.opened) .text_filter(page.project_category_state.text_filter.as_str()) .valid(true) .normal() - .options(project_categories.iter().map(|c| c.to_child()).collect()) + .options( + ProjectCategory::default() + .into_iter() + .map(|c| c.into_child()), + ) .selected(vec![page .payload .category .as_ref() .cloned() .unwrap_or_default() - .to_child()]) + .into_child()]) .build(FieldId::ProjectSettings(ProjectFieldId::Category)) .into_node(); StyledField::build() diff --git a/jirs-client/src/pages/users_page/view.rs b/jirs-client/src/pages/users_page/view.rs index 547c193c..634aede3 100644 --- a/jirs-client/src/pages/users_page/view.rs +++ b/jirs-client/src/pages/users_page/view.rs @@ -4,12 +4,12 @@ use { shared::{ inner_layout, styled_button::StyledButton, styled_field::StyledField, styled_form::StyledForm, styled_input::StyledInput, styled_select::StyledSelect, - ToChild, ToNode, + IntoChild, ToNode, }, validations::is_email, FieldId, Msg, PageChanged, UsersPageChange, }, - jirs_data::{InvitationState, ToVec, UserRole, UsersFieldId}, + jirs_data::{InvitationState, UserRole, UsersFieldId}, seed::{prelude::*, *}, }; @@ -45,14 +45,17 @@ pub fn view(model: &Model) -> Node { .build() .into_node(); - let roles = UserRole::ordered(); let user_role = StyledSelect::build() .name("user_role") .valid(true) .normal() .state(&page.user_role_state) - .selected(vec![page.user_role.to_child()]) - .options(roles.iter().map(|role| role.to_child()).collect()) + .selected(vec![page.user_role.into_child()]) + .options( + UserRole::default() + .into_iter() + .map(|role| role.into_child()), + ) .build(FieldId::Users(UsersFieldId::UserRole)) .into_node(); let user_role_field = StyledField::build() diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index 574ac5c6..ec3fed04 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -36,6 +36,12 @@ pub trait ToChild<'l> { fn to_child<'m: 'l>(&'m self) -> Self::Builder; } +pub trait IntoChild<'l> { + type Builder: 'l; + + fn into_child(self) -> Self::Builder; +} + #[inline] pub fn go_to_board(orders: &mut impl Orders) { go_to("board", orders); diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs index 19d5b2ac..ec271e9a 100644 --- a/jirs-client/src/shared/styled_select.rs +++ b/jirs-client/src/shared/styled_select.rs @@ -1,9 +1,16 @@ -use seed::{prelude::*, *}; +use { + crate::{ + shared::{ + styled_icon::{Icon, StyledIcon}, + styled_select_child::*, + ToNode, + }, + FieldId, Msg, + }, + seed::{prelude::*, *}, +}; -use crate::shared::styled_icon::{Icon, StyledIcon}; -use crate::shared::styled_select_child::*; -use crate::shared::ToNode; -use crate::{FieldId, Msg}; +// pub trait ChildIter<'l> = Iterator>; #[derive(Clone, Debug, PartialEq)] pub enum StyledSelectChanged { @@ -105,28 +112,37 @@ impl StyledSelectState { } } -pub struct StyledSelect<'l> { +pub struct StyledSelect<'l, Options> +where + Options: Iterator>, +{ id: FieldId, variant: Variant, dropdown_width: Option, name: Option<&'l str>, valid: bool, is_multi: bool, - options: Vec>, + options: Option, selected: Vec>, text_filter: &'l str, opened: bool, clearable: bool, } -impl<'l> ToNode for StyledSelect<'l> { +impl<'l, Options> ToNode for StyledSelect<'l, Options> +where + Options: Iterator>, +{ fn into_node(self) -> Node { render(self) } } -impl<'l> StyledSelect<'l> { - pub fn build() -> StyledSelectBuilder<'l> { +impl<'l, Options> StyledSelect<'l, Options> +where + Options: Iterator>, +{ + pub fn build() -> StyledSelectBuilder<'l, Options> { StyledSelectBuilder { variant: None, dropdown_width: None, @@ -143,21 +159,27 @@ impl<'l> StyledSelect<'l> { } #[derive(Debug)] -pub struct StyledSelectBuilder<'l> { +pub struct StyledSelectBuilder<'l, Options> +where + Options: Iterator>, +{ variant: Option, dropdown_width: Option, name: Option<&'l str>, valid: Option, is_multi: Option, - options: Option>>, + options: Option, selected: Option>>, text_filter: Option<&'l str>, opened: Option, clearable: bool, } -impl<'l> StyledSelectBuilder<'l> { - pub fn build(self, id: FieldId) -> StyledSelect<'l> { +impl<'l, Options> StyledSelectBuilder<'l, Options> +where + Options: Iterator>, +{ + pub fn build(self, id: FieldId) -> StyledSelect<'l, Options> { StyledSelect { id, variant: self.variant.unwrap_or_default(), @@ -165,7 +187,7 @@ impl<'l> StyledSelectBuilder<'l> { name: self.name, valid: self.valid.unwrap_or(true), is_multi: self.is_multi.unwrap_or_default(), - options: self.options.unwrap_or_default(), + options: self.options, selected: self.selected.unwrap_or_default(), text_filter: self.text_filter.unwrap_or_default(), opened: self.opened.unwrap_or_default(), @@ -173,14 +195,6 @@ impl<'l> StyledSelectBuilder<'l> { } } - // pub fn try_state<'state: 'l>(self, state: Option<&'state StyledSelectState>) -> Self { - // if let Some(s) = state { - // self.state(s) - // } else { - // self - // } - // } - pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self { self.opened(state.opened) .text_filter(state.text_filter.as_str()) @@ -211,7 +225,7 @@ impl<'l> StyledSelectBuilder<'l> { self } - pub fn options(mut self, options: Vec>) -> Self { + pub fn options(mut self, options: Options) -> Self { self.options = Some(options); self } @@ -242,7 +256,10 @@ impl<'l> StyledSelectBuilder<'l> { } } -pub fn render(values: StyledSelect) -> Node { +pub fn render<'l, Options>(values: StyledSelect<'l, Options>) -> Node +where + Options: Iterator>, +{ let StyledSelect { id, variant, @@ -303,28 +320,34 @@ pub fn render(values: StyledSelect) -> Node { empty![] }; - let children: Vec> = options - .into_iter() - .filter(|o| !selected.contains(&o) && o.match_text(text_filter)) - .map(|child| { - let child = child.build(DisplayType::SelectOption); - let value = child.value(); - let node = child.into_node(); + let children: Vec> = if let Some(options) = options { + options + .filter(|o| !selected.contains(&o) && o.match_text(text_filter)) + .map(|child| { + let child = child.build(DisplayType::SelectOption); + let value = child.value(); + let node = child.into_node(); - let on_change = { - let field_id = id.clone(); - mouse_ev(Ev::Click, move |_| { - Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(Some(value))) - }) - }; - div![ - attrs![At::Class => "option"], - on_change, - on_handler.clone(), - node - ] - }) - .collect(); + let on_change = { + let field_id = id.clone(); + mouse_ev(Ev::Click, move |_| { + Msg::StyledSelectChanged( + field_id, + StyledSelectChanged::Changed(Some(value)), + ) + }) + }; + div![ + attrs![At::Class => "option"], + on_change, + on_handler.clone(), + node + ] + }) + .collect() + } else { + vec![] + }; let text_input = if opened { seed::input![ diff --git a/jirs-client/src/shared/styled_select_child.rs b/jirs-client/src/shared/styled_select_child.rs index bc32b8e8..7959c3e7 100644 --- a/jirs-client/src/shared/styled_select_child.rs +++ b/jirs-client/src/shared/styled_select_child.rs @@ -1,10 +1,12 @@ -use std::borrow::Cow; - -use seed::{prelude::*, *}; - -use crate::shared::styled_select::Variant; -use crate::shared::{ToChild, ToNode}; -use crate::Msg; +use { + crate::{ + shared::styled_select::Variant, + shared::{IntoChild, ToChild, ToNode}, + Msg, + }, + seed::{prelude::*, *}, + std::borrow::Cow, +}; pub enum DisplayType { SelectOption, @@ -186,9 +188,10 @@ impl<'l> ToChild<'l> for jirs_data::User { } } -impl<'l> ToChild<'l> for jirs_data::IssuePriority { +impl<'l> IntoChild<'l> for jirs_data::IssuePriority { type Builder = StyledSelectChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + + fn into_child(self) -> Self::Builder { let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into()) .add_class(self.to_str()) .build() @@ -214,10 +217,10 @@ impl<'l> ToChild<'l> for jirs_data::IssueStatus { } } -impl<'l> ToChild<'l> for jirs_data::IssueType { +impl<'l> IntoChild<'l> for jirs_data::IssueType { type Builder = StyledSelectChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + fn into_child(self) -> Self::Builder { let name = self.to_label(); let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into()) @@ -233,10 +236,10 @@ impl<'l> ToChild<'l> for jirs_data::IssueType { } } -impl<'l> ToChild<'l> for jirs_data::ProjectCategory { +impl<'l> IntoChild<'l> for jirs_data::ProjectCategory { type Builder = StyledSelectChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + fn into_child(self) -> Self::Builder { StyledSelectChild::build() .add_class(self.to_str()) .text(self.to_str()) @@ -244,10 +247,10 @@ impl<'l> ToChild<'l> for jirs_data::ProjectCategory { } } -impl<'l> ToChild<'l> for jirs_data::UserRole { +impl<'l> IntoChild<'l> for jirs_data::UserRole { type Builder = StyledSelectChildBuilder<'l>; - fn to_child<'m: 'l>(&'m self) -> Self::Builder { + fn into_child(self) -> Self::Builder { let name = self.to_str(); StyledSelectChild::build() diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index bd161e64..68d46da6 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -161,21 +161,6 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { )); } - // issues - WsMsg::ProjectIssuesLoaded(mut v) => { - v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); - model.issues = v; - model.issues_by_id.clear(); - for issue in model.issues.iter() { - model.issues_by_id.insert(issue.id, issue.clone()); - } - - orders.send_msg(Msg::ResourceChanged( - ResourceKind::Issue, - OperationKind::ListLoaded, - None, - )); - } // issue statuses WsMsg::IssueStatusesLoaded(v) => { model.issue_statuses = v; @@ -232,8 +217,24 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { )); } // issues + WsMsg::ProjectIssuesLoaded(mut v) => { + v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); + model.issues = v; + model.issues_by_id.clear(); + for issue in model.issues.iter() { + model.issues_by_id.insert(issue.id, issue.clone()); + } + + orders.send_msg(Msg::ResourceChanged( + ResourceKind::Issue, + OperationKind::ListLoaded, + None, + )); + } WsMsg::IssueUpdated(mut issue) => { let id = issue.id; + model.issues_by_id.remove(&id); + model.issues_by_id.insert(id, issue.clone()); if let Some(idx) = model.issues.iter().position(|i| i.id == issue.id) { std::mem::swap(&mut model.issues[idx], &mut issue); } @@ -245,16 +246,13 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { } WsMsg::IssueDeleted(id, _count) => { let mut old = vec![]; - std::mem::swap(&mut model.issue_statuses, &mut old); + std::mem::swap(&mut model.issues, &mut old); for is in old { if is.id == id { continue; } - model.issue_statuses.push(is); + model.issues.push(is); } - model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); orders.send_msg(Msg::ResourceChanged( ResourceKind::Issue, OperationKind::SingleRemoved, @@ -264,6 +262,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { // users WsMsg::ProjectUsersLoaded(v) => { model.users = v.clone(); + model.users_by_id.clear(); for user in v { model.users_by_id.insert(user.id, user.clone()); } diff --git a/shared/jirs-data/Cargo.toml b/shared/jirs-data/Cargo.toml index 8e183076..cf7e860a 100644 --- a/shared/jirs-data/Cargo.toml +++ b/shared/jirs-data/Cargo.toml @@ -13,7 +13,7 @@ name = "jirs_data" path = "./src/lib.rs" [features] -backend = ["diesel", "actix"] +backend = ["diesel", "actix", "derive_enum_sql"] frontend = [] [dependencies] @@ -30,3 +30,13 @@ optional = true optional = true version = "1.4.5" features = ["unstable", "postgres", "numeric", "extras", "uuidv07"] + +[dependencies.derive_enum_iter] +path = "../../derive/derive_enum_iter" + +[dependencies.derive_enum_primitive] +path = "../../derive/derive_enum_primitive" + +[dependencies.derive_enum_sql] +path = "../../derive/derive_enum_sql" +optional = true diff --git a/shared/jirs-data/src/lib.rs b/shared/jirs-data/src/lib.rs index aec013d0..df74308e 100644 --- a/shared/jirs-data/src/lib.rs +++ b/shared/jirs-data/src/lib.rs @@ -1,10 +1,10 @@ #[cfg(feature = "backend")] use diesel::*; -#[cfg(feature = "backend")] -pub use sql::*; use { chrono::NaiveDateTime, + derive_enum_iter::EnumIter, + derive_enum_primitive::EnumPrimitive, serde::{Deserialize, Serialize}, std::cmp::Ordering, std::str::FromStr, @@ -17,12 +17,7 @@ pub mod msg; mod payloads; #[cfg(feature = "backend")] -pub mod sql; - -pub trait ToVec { - type Item; - fn ordered() -> Vec; -} +use derive_enum_sql::EnumSql; pub type NumberOfDeleted = usize; pub type IssueId = i32; @@ -52,90 +47,34 @@ pub type Lang = String; pub type BindToken = Uuid; pub type InvitationToken = Uuid; -macro_rules! enum_to_u32 { - ($kind: ident, $fallback: ident, $($e: ident => $v: expr),+) => { - #[cfg(feature = "frontend")] - impl Into for $kind { - fn into(self) -> u32 { - match self { - $($kind :: $e => $v),+ - } - } - } - - #[cfg(feature = "frontend")] - impl Into<$kind> for u32 { - fn into(self) -> $kind { - match self { - $($v => $kind :: $e),+, - _else => $kind :: $fallback, - } - } - } - } -} - -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "IssueTypeType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive, +)] pub enum IssueType { Task, Bug, Story, } -impl ToVec for IssueType { - type Item = IssueType; - - fn ordered() -> Vec { - use IssueType::*; - vec![Task, Bug, Story] - } -} - impl Default for IssueType { fn default() -> Self { IssueType::Task } } -impl IssueType { - pub fn to_label(&self) -> &str { - use IssueType::*; - match self { - Task => "Task", - Bug => "Bug", - Story => "Story", - } - } -} - -enum_to_u32! { - IssueType, Task, - Task => 1, - Bug => 2, - Story => 3 -} - -impl IssueType { - pub fn to_str<'l>(&self) -> &'l str { - match self { - IssueType::Task => "task", - IssueType::Bug => "bug", - IssueType::Story => "story", - } - } -} - impl std::fmt::Display for IssueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "IssuePriorityType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive, +)] pub enum IssuePriority { Highest, High, @@ -144,71 +83,21 @@ pub enum IssuePriority { Lowest, } -impl ToVec for IssuePriority { - type Item = IssuePriority; - - fn ordered() -> Vec { - vec![ - IssuePriority::Highest, - IssuePriority::High, - IssuePriority::Medium, - IssuePriority::Low, - IssuePriority::Lowest, - ] - } -} - -impl FromStr for IssuePriority { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - "highest" => Ok(IssuePriority::Highest), - "high" => Ok(IssuePriority::High), - "medium" => Ok(IssuePriority::Medium), - "low" => Ok(IssuePriority::Low), - "lowest" => Ok(IssuePriority::Lowest), - _ => Err(format!("Unknown priority {}", s)), - } - } -} - impl Default for IssuePriority { fn default() -> Self { IssuePriority::Medium } } -impl IssuePriority { - pub fn to_str(&self) -> &'static str { - match self { - IssuePriority::Highest => "highest", - IssuePriority::High => "high", - IssuePriority::Medium => "medium", - IssuePriority::Low => "low", - IssuePriority::Lowest => "lowest", - } - } -} - impl std::fmt::Display for IssuePriority { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } -enum_to_u32!( - IssuePriority, Medium, - Highest => 5, - High => 4, - Medium => 3, - Low => 2, - Lowest => 1 -); - -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "UserRoleType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Hash)] +#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Hash, EnumIter, EnumPrimitive)] pub enum UserRole { User, Manager, @@ -230,122 +119,46 @@ impl PartialOrd for UserRole { } } -impl ToVec for UserRole { - type Item = UserRole; - - fn ordered() -> Vec { - vec![UserRole::User, UserRole::Manager, UserRole::Owner] - } -} - -impl FromStr for UserRole { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - "user" => Ok(UserRole::User), - "manager" => Ok(UserRole::Manager), - "owner" => Ok(UserRole::Owner), - _ => Err(format!("Unknown user role {}", s)), - } - } -} - impl Default for UserRole { fn default() -> Self { UserRole::User } } -impl UserRole { - pub fn to_str<'l>(&self) -> &'l str { - match self { - UserRole::User => "user", - UserRole::Manager => "manager", - UserRole::Owner => "owner", - } - } -} - impl std::fmt::Display for UserRole { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } -enum_to_u32!( - UserRole, User, - User => 0, - Manager => 1, - Owner => 2 -); - -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "ProjectCategoryType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive, +)] pub enum ProjectCategory { Software, Marketing, Business, } -impl ToVec for ProjectCategory { - type Item = ProjectCategory; - - fn ordered() -> Vec { - vec![ - ProjectCategory::Software, - ProjectCategory::Marketing, - ProjectCategory::Business, - ] - } -} - -impl FromStr for ProjectCategory { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().trim() { - "software" => Ok(ProjectCategory::Software), - "marketing" => Ok(ProjectCategory::Marketing), - "business" => Ok(ProjectCategory::Business), - _ => Err(format!("Unknown project category {}", s)), - } - } -} - impl Default for ProjectCategory { fn default() -> Self { ProjectCategory::Software } } -impl ProjectCategory { - pub fn to_str<'l>(&self) -> &'l str { - match self { - ProjectCategory::Software => "software", - ProjectCategory::Marketing => "marketing", - ProjectCategory::Business => "business", - } - } -} - impl std::fmt::Display for ProjectCategory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.to_str()) } } -enum_to_u32!( - ProjectCategory, Software, - Software => 0, - Marketing => 1, - Business => 2 -); - -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "InvitationStateType")] -#[derive(Clone, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive, +)] pub enum InvitationState { Sent, Accepted, @@ -360,29 +173,26 @@ impl Default for InvitationState { impl std::fmt::Display for InvitationState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InvitationState::Sent => f.write_str("sent"), - InvitationState::Accepted => f.write_str("accepted"), - InvitationState::Revoked => f.write_str("revoked"), - } + f.write_str(self.to_str()) } } -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "TimeTrackingType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumIter, EnumPrimitive, +)] pub enum TimeTracking { Untracked, Fibonacci, Hourly, } -enum_to_u32!( - TimeTracking, Untracked, - Untracked => 0, - Fibonacci => 1, - Hourly => 2 -); +impl Default for TimeTracking { + fn default() -> Self { + Self::Untracked + } +} #[derive(Clone, Serialize, Debug, PartialEq)] pub struct ErrorResponse { @@ -515,29 +325,26 @@ pub struct IssueAssignee { pub updated_at: NaiveDateTime, } -#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))] +#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression, EnumSql))] #[cfg_attr(feature = "backend", sql_type = "MessageTypeType")] -#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)] +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash, EnumPrimitive, +)] pub enum MessageType { ReceivedInvitation, AssignedToIssue, Mention, } -enum_to_u32!( - MessageType, Mention, - ReceivedInvitation => 0, - AssignedToIssue => 1, - Mention => 2 -); +impl Default for MessageType { + fn default() -> Self { + Self::Mention + } +} impl std::fmt::Display for MessageType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MessageType::ReceivedInvitation => f.write_str("ReceivedInvitation"), - MessageType::AssignedToIssue => f.write_str("AssignedToIssue"), - MessageType::Mention => f.write_str("Mention"), - } + f.write_str(self.to_label()) } } diff --git a/shared/jirs-data/src/sql.rs b/shared/jirs-data/src/sql.rs deleted file mode 100644 index 5aafaee0..00000000 --- a/shared/jirs-data/src/sql.rs +++ /dev/null @@ -1,284 +0,0 @@ -use { - crate::{ - InvitationState, IssuePriority, IssueType, MessageType, ProjectCategory, TimeTracking, - UserRole, - }, - diesel::{deserialize::*, pg::*, serialize::*, *}, - std::io::Write, -}; - -#[derive(SqlType)] -#[postgres(type_name = "IssuePriorityType")] -pub struct IssuePriorityType; - -impl ToSql for IssuePriority { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - IssuePriority::Highest => out.write_all(b"highest")?, - IssuePriority::High => out.write_all(b"high")?, - IssuePriority::Medium => out.write_all(b"medium")?, - IssuePriority::Low => out.write_all(b"low")?, - IssuePriority::Lowest => out.write_all(b"lowest")?, - } - Ok(IsNull::No) - } -} - -fn issue_priority_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"5" | b"highest" => Ok(IssuePriority::Highest), - b"4" | b"high" => Ok(IssuePriority::High), - b"3" | b"medium" => Ok(IssuePriority::Medium), - b"2" | b"low" => Ok(IssuePriority::Low), - b"1" | b"lowest" => Ok(IssuePriority::Lowest), - _ => Ok(IssuePriority::Lowest), - } -} - -impl FromSql for IssuePriority { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_priority_from_sql(bytes) - } -} - -impl FromSql for IssuePriority { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_priority_from_sql(bytes) - } -} - -#[derive(SqlType)] -#[postgres(type_name = "IssueTypeType")] -pub struct IssueTypeType; - -fn issue_type_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"task" => Ok(IssueType::Task), - b"bug" => Ok(IssueType::Bug), - b"story" => Ok(IssueType::Story), - _ => Ok(IssueType::Task), - } -} - -impl FromSql for IssueType { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_type_from_sql(bytes) - } -} - -impl FromSql for IssueType { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - issue_type_from_sql(bytes) - } -} - -impl ToSql for IssueType { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - IssueType::Task => out.write_all(b"task")?, - IssueType::Story => out.write_all(b"story")?, - IssueType::Bug => out.write_all(b"bug")?, - } - Ok(IsNull::No) - } -} - -#[derive(SqlType)] -#[postgres(type_name = "ProjectCategoryType")] -pub struct ProjectCategoryType; - -impl diesel::query_builder::QueryId for ProjectCategoryType { - type QueryId = ProjectCategory; -} - -fn project_category_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"software" => Ok(ProjectCategory::Software), - b"marketing" => Ok(ProjectCategory::Marketing), - b"business" => Ok(ProjectCategory::Business), - _ => Ok(ProjectCategory::Software), - } -} - -impl FromSql for ProjectCategory { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - project_category_from_sql(bytes) - } -} - -impl FromSql for ProjectCategory { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - project_category_from_sql(bytes) - } -} - -impl ToSql for ProjectCategory { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - ProjectCategory::Software => out.write_all(b"software")?, - ProjectCategory::Marketing => out.write_all(b"marketing")?, - ProjectCategory::Business => out.write_all(b"business")?, - } - Ok(IsNull::No) - } -} - -#[derive(SqlType)] -#[postgres(type_name = "UserRoleType")] -pub struct UserRoleType; - -impl diesel::query_builder::QueryId for UserRoleType { - type QueryId = UserRole; -} - -fn user_role_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"user" => Ok(UserRole::User), - b"manager" => Ok(UserRole::Manager), - b"owner" => Ok(UserRole::Owner), - _ => Ok(UserRole::User), - } -} - -impl FromSql for UserRole { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - user_role_from_sql(bytes) - } -} - -impl FromSql for UserRole { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - user_role_from_sql(bytes) - } -} - -impl ToSql for UserRole { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - UserRole::User => out.write_all(b"user")?, - UserRole::Manager => out.write_all(b"manager")?, - UserRole::Owner => out.write_all(b"owner")?, - } - Ok(IsNull::No) - } -} - -#[derive(SqlType)] -#[postgres(type_name = "InvitationStateType")] -pub struct InvitationStateType; - -impl diesel::query_builder::QueryId for InvitationStateType { - type QueryId = InvitationState; -} - -fn invitation_state_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"sent" => Ok(InvitationState::Sent), - b"accepted" => Ok(InvitationState::Accepted), - b"revoked" => Ok(InvitationState::Revoked), - _ => Ok(InvitationState::Sent), - } -} - -impl FromSql for InvitationState { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - invitation_state_from_sql(bytes) - } -} - -impl FromSql for InvitationState { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - invitation_state_from_sql(bytes) - } -} - -impl ToSql for InvitationState { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - InvitationState::Sent => out.write_all(b"sent")?, - InvitationState::Accepted => out.write_all(b"accepted")?, - InvitationState::Revoked => out.write_all(b"revoked")?, - } - 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"fibonacci" => 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"fibonacci")?, - TimeTracking::Hourly => out.write_all(b"hourly")?, - } - Ok(IsNull::No) - } -} - -#[derive(SqlType)] -#[postgres(type_name = "MessageTypeType")] -pub struct MessageTypeType; - -impl diesel::query_builder::QueryId for MessageTypeType { - type QueryId = MessageType; -} - -fn message_type_type_from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - match not_none!(bytes) { - b"received_invitation" => Ok(MessageType::ReceivedInvitation), - b"assigned_to_issue" => Ok(MessageType::AssignedToIssue), - b"mention" => Ok(MessageType::Mention), - _ => Ok(MessageType::Mention), - } -} - -impl FromSql for MessageType { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - message_type_type_from_sql(bytes) - } -} - -impl FromSql for MessageType { - fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result { - message_type_type_from_sql(bytes) - } -} - -impl ToSql for MessageType { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - match *self { - MessageType::ReceivedInvitation => out.write_all(b"received_invitation")?, - MessageType::AssignedToIssue => out.write_all(b"assigned_to_issue")?, - MessageType::Mention => out.write_all(b"mention")?, - } - Ok(IsNull::No) - } -}