Proc macros

Fix update issue view. Refactor jirs-data.
This commit is contained in:
Adrian Woźniak 2021-01-07 23:34:36 +01:00
parent c96a5021f0
commit 57adfac3a4
28 changed files with 795 additions and 842 deletions

15
Cargo.lock generated
View File

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

View File

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

View File

@ -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.
///

View File

@ -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.
///

View File

@ -0,0 +1,16 @@
[package]
name = "derive_enum_iter"
version = "0.1.0"
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
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]

View File

@ -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<Self::Item> {{
Some({name}::{last})
}}
fn next(&mut self) -> Option<Self::Item> {{
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()
}

View File

@ -0,0 +1,16 @@
[package]
name = "derive_enum_primitive"
version = "0.1.0"
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
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]

View File

@ -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<Self, Self::Err> {{
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<u32> 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()
}

View File

@ -0,0 +1,16 @@
[package]
name = "derive_enum_sql"
version = "0.1.0"
authors = ["Adrian Wozniak <adrian.wozniak@ita-prog.pl>"]
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]

View File

@ -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<W: std::io::Write>(&self, out: &mut diesel::serialize::Output<W, diesel::pg::Pg>) -> 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<Self> {{
{name}::from_diesel_bytes(bytes)
}}
}}
#[cfg(feature = "backend")]
impl diesel::deserialize::FromSql<diesel::sql_types::Text, diesel::pg::Pg> for {name} {{
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {{
{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()
}

View File

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

View File

@ -1,4 +1,4 @@
#![feature(or_patterns, type_ascription)]
#![feature(or_patterns, type_ascription, trait_alias)]
use {
crate::{

View File

@ -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())

View File

@ -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<i32>, 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<Msg> {
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<Msg> {
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()
}

View File

@ -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<Msg> {
.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<Msg> {
.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<Msg> {
.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<Msg> {
}
fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
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()

View File

@ -21,6 +21,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
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<Msg>) {
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<Msg>) {
PayloadVariant::String(
modal
.payload
.description
.description_text
.as_ref()
.cloned()
.unwrap_or_default(),

View File

@ -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<Msg> {
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::<web_sys::HtmlTextAreaElement>()
@ -95,7 +103,6 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.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<Msg> {
.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<Msg> {
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<Msg> {
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<Msg> {
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<Msg> {
.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,
)))

View File

@ -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()

View File

@ -80,7 +80,8 @@ pub fn view(model: &Model) -> Node<Msg> {
}
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
let inner = if model.projects.len() <= 1 {
let inner =
if model.projects.len() <= 1 {
let name = model
.project
.as_ref()
@ -100,15 +101,9 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
StyledSelect::build()
.name("current_project")
.normal()
.options(
model
.projects
.iter()
.filter_map(|project| {
.options(model.projects.iter().filter_map(|project| {
joined_projects.get(&project.id).map(|_| project.to_child())
})
.collect(),
)
}))
.selected(
page.current_project
.values

View File

@ -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<Msg> {
/// Build project category dropdown with styled field wrapper
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
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()

View File

@ -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<Msg> {
.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()

View File

@ -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<Msg>) {
go_to("board", orders);

View File

@ -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<Item = StyledSelectChildBuilder<'l>>;
#[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<Item = StyledSelectChildBuilder<'l>>,
{
id: FieldId,
variant: Variant,
dropdown_width: Option<usize>,
name: Option<&'l str>,
valid: bool,
is_multi: bool,
options: Vec<StyledSelectChildBuilder<'l>>,
options: Option<Options>,
selected: Vec<StyledSelectChildBuilder<'l>>,
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<Item = StyledSelectChildBuilder<'l>>,
{
fn into_node(self) -> Node<Msg> {
render(self)
}
}
impl<'l> StyledSelect<'l> {
pub fn build() -> StyledSelectBuilder<'l> {
impl<'l, Options> StyledSelect<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
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<Item = StyledSelectChildBuilder<'l>>,
{
variant: Option<Variant>,
dropdown_width: Option<usize>,
name: Option<&'l str>,
valid: Option<bool>,
is_multi: Option<bool>,
options: Option<Vec<StyledSelectChildBuilder<'l>>>,
options: Option<Options>,
selected: Option<Vec<StyledSelectChildBuilder<'l>>>,
text_filter: Option<&'l str>,
opened: Option<bool>,
clearable: bool,
}
impl<'l> StyledSelectBuilder<'l> {
pub fn build(self, id: FieldId) -> StyledSelect<'l> {
impl<'l, Options> StyledSelectBuilder<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
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<StyledSelectChildBuilder<'l>>) -> 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<Msg> {
pub fn render<'l, Options>(values: StyledSelect<'l, Options>) -> Node<Msg>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
let StyledSelect {
id,
variant,
@ -303,8 +320,8 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
empty![]
};
let children: Vec<Node<Msg>> = options
.into_iter()
let children: Vec<Node<Msg>> = if let Some(options) = options {
options
.filter(|o| !selected.contains(&o) && o.match_text(text_filter))
.map(|child| {
let child = child.build(DisplayType::SelectOption);
@ -314,7 +331,10 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
let on_change = {
let field_id = id.clone();
mouse_ev(Ev::Click, move |_| {
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(Some(value)))
Msg::StyledSelectChanged(
field_id,
StyledSelectChanged::Changed(Some(value)),
)
})
};
div![
@ -324,7 +344,10 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
node
]
})
.collect();
.collect()
} else {
vec![]
};
let text_input = if opened {
seed::input![

View File

@ -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()

View File

@ -161,21 +161,6 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
));
}
// 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<Msg>) {
));
}
// 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<Msg>) {
}
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<Msg>) {
// 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());
}

View File

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

View File

@ -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<Self::Item>;
}
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<u32> 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<Self> {
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<Self> {
vec![
IssuePriority::Highest,
IssuePriority::High,
IssuePriority::Medium,
IssuePriority::Low,
IssuePriority::Lowest,
]
}
}
impl FromStr for IssuePriority {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self::Item> {
vec![UserRole::User, UserRole::Manager, UserRole::Owner]
}
}
impl FromStr for UserRole {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<Self> {
vec![
ProjectCategory::Software,
ProjectCategory::Marketing,
ProjectCategory::Business,
]
}
}
impl FromStr for ProjectCategory {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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())
}
}

View File

@ -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<IssuePriorityType, Pg> for IssuePriority {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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<IssuePriority> {
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<IssuePriorityType, Pg> for IssuePriority {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_priority_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for IssuePriority {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_priority_from_sql(bytes)
}
}
#[derive(SqlType)]
#[postgres(type_name = "IssueTypeType")]
pub struct IssueTypeType;
fn issue_type_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<IssueType> {
match not_none!(bytes) {
b"task" => Ok(IssueType::Task),
b"bug" => Ok(IssueType::Bug),
b"story" => Ok(IssueType::Story),
_ => Ok(IssueType::Task),
}
}
impl FromSql<IssueTypeType, Pg> for IssueType {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_type_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for IssueType {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
issue_type_from_sql(bytes)
}
}
impl ToSql<IssueTypeType, Pg> for IssueType {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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<ProjectCategory> {
match not_none!(bytes) {
b"software" => Ok(ProjectCategory::Software),
b"marketing" => Ok(ProjectCategory::Marketing),
b"business" => Ok(ProjectCategory::Business),
_ => Ok(ProjectCategory::Software),
}
}
impl FromSql<ProjectCategoryType, Pg> for ProjectCategory {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
project_category_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for ProjectCategory {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
project_category_from_sql(bytes)
}
}
impl ToSql<ProjectCategoryType, Pg> for ProjectCategory {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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<UserRole> {
match not_none!(bytes) {
b"user" => Ok(UserRole::User),
b"manager" => Ok(UserRole::Manager),
b"owner" => Ok(UserRole::Owner),
_ => Ok(UserRole::User),
}
}
impl FromSql<UserRoleType, Pg> for UserRole {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
user_role_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for UserRole {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
user_role_from_sql(bytes)
}
}
impl ToSql<UserRoleType, Pg> for UserRole {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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<InvitationState> {
match not_none!(bytes) {
b"sent" => Ok(InvitationState::Sent),
b"accepted" => Ok(InvitationState::Accepted),
b"revoked" => Ok(InvitationState::Revoked),
_ => Ok(InvitationState::Sent),
}
}
impl FromSql<InvitationStateType, Pg> for InvitationState {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
invitation_state_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for InvitationState {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<Self> {
invitation_state_from_sql(bytes)
}
}
impl ToSql<InvitationStateType, Pg> for InvitationState {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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<TimeTracking> {
match not_none!(bytes) {
b"untracked" => Ok(TimeTracking::Untracked),
b"fibonacci" => 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"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<MessageType> {
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<MessageTypeType, Pg> for MessageType {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<MessageType> {
message_type_type_from_sql(bytes)
}
}
impl FromSql<sql_types::Text, Pg> for MessageType {
fn from_sql(bytes: Option<&[u8]>) -> deserialize::Result<MessageType> {
message_type_type_from_sql(bytes)
}
}
impl ToSql<MessageTypeType, Pg> for MessageType {
fn to_sql<W: Write>(&self, out: &mut Output<W, Pg>) -> 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)
}
}