Remove some builders

This commit is contained in:
Adrian Woźniak 2021-04-15 22:32:05 +02:00
parent 520d2400be
commit e6ef4d6b0e
51 changed files with 1940 additions and 2325 deletions

4
.env
View File

@ -1,7 +1,7 @@
DEBUG=true DEBUG=true
RUST_LOG=debug RUST_LOG=debug
JIRS_CLIENT_PORT=7000 JIRS_CLIENT_PORT=80
JIRS_CLIENT_BIND=0.0.0.0 JIRS_CLIENT_BIND=jirs.lvh.me
DATABASE_URL=postgres://postgres@localhost:5432/jirs DATABASE_URL=postgres://postgres@localhost:5432/jirs
JIRS_SERVER_PORT=5000 JIRS_SERVER_PORT=5000
JIRS_SERVER_BIND=0.0.0.0 JIRS_SERVER_BIND=0.0.0.0

1
.gitignore vendored
View File

@ -24,3 +24,4 @@ highlight/jirs-highlight/build
uploads uploads
config config
shared/jirs-config/target shared/jirs-config/target
jirs-client/src/location.rs

1
Cargo.lock generated
View File

@ -1988,6 +1988,7 @@ dependencies = [
"chrono", "chrono",
"derive_enum_iter", "derive_enum_iter",
"derive_enum_primitive", "derive_enum_primitive",
"dotenv",
"futures 0.1.31", "futures 0.1.31",
"jirs-data", "jirs-data",
"js-sys", "js-sys",

View File

@ -30,6 +30,8 @@ chrono = { version = "0.4", default-features = false, features = ["serde", "wasm
uuid = { version = "0.8.1", features = ["serde"] } uuid = { version = "0.8.1", features = ["serde"] }
futures = "^0.1.26" futures = "^0.1.26"
dotenv = { version = "*" }
[dependencies.wee_alloc] [dependencies.wee_alloc]
version = "*" version = "*"
features = ["static_array_backend"] features = ["static_array_backend"]

55
jirs-client/build.rs Normal file
View File

@ -0,0 +1,55 @@
#![feature(format_args_capture)]
fn main() {
if let Ok(contents) = std::fs::read_to_string("../.env") {
for line in contents.lines() {
if line.starts_with('#') {
continue;
}
let parts: Vec<&str> = line.split('=').collect();
match (parts.get(0), parts.get(1)) {
(Some(k), Some(v)) => std::env::set_var(k, v),
_ => continue,
}
}
}
let addr = std::env::var("JIRS_SERVER_BIND").unwrap_or("0.0.0.0".to_string());
let addr = if addr.as_str() == "0.0.0.0" || addr.as_str() == "localhost" {
"localhost"
} else {
addr.as_str()
}
.to_string();
let port = std::env::var("JIRS_SERVER_PORT").unwrap_or("80".to_string());
let port = match port.as_str() {
"80" | "8080" | "443" => "".to_string(),
_ => format!(":{}", port),
};
let addr = format!("{}{}", addr, port);
std::fs::write(
"./src/location.rs",
format!(
"
pub fn host_url() -> &'static str {{
if cfg!(debug_assertions) {{
\"http://{addr}\"
}} else {{
\"https://{addr}\"
}}
}}
pub fn ws_url() -> &'static str {{
if cfg!(debug_assertions) {{
\"ws://{addr}/ws/\"
}} else {{
\"wss://{addr}/ws/\"
}}
}}
",
addr = addr
)
.trim(),
)
.unwrap();
}

View File

@ -1,14 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
RSASS_PATH=$(command -v rsass) RSASS_PATH=$(command -v rsass)
if [[ "${RSASS_PATH}" == "" ]]; if [[ "${RSASS_PATH}" == "" ]]; then
then
cargo install rsass --features=commandline cargo install rsass --features=commandline
fi fi
WASM_PACK_PATH=$(command -v wasm-pack) WASM_PACK_PATH=$(command -v wasm-pack)
if [[ "${WASM_PACK_PATH}" == "" ]]; if [[ "${WASM_PACK_PATH}" == "" ]]; then
then
cargo install wasm-pack cargo install wasm-pack
fi fi
@ -23,6 +21,7 @@ cd ${CLIENT_ROOT}
. .env . .env
cargo watch \ cargo watch \
-i ./jirs-client/src/location.rs \
-s ${CLIENT_ROOT}/scripts/run-wasm-pack.sh \ -s ${CLIENT_ROOT}/scripts/run-wasm-pack.sh \
-w ${CLIENT_ROOT}/src \ -w ${CLIENT_ROOT}/src \
-w ${CLIENT_ROOT}/Cargo.toml \ -w ${CLIENT_ROOT}/Cargo.toml \

View File

@ -13,13 +13,10 @@ wasm-pack --verbose build --mode ${MODE} ${BUILD_TYPE} --out-name jirs --out-dir
cd ${CLIENT_ROOT} cd ${CLIENT_ROOT}
rm -Rf ${CLIENT_ROOT}/build/styles.css rm -Rf ${CLIENT_ROOT}/build/styles.css
rsass -t Expanded ${PROJECT_ROOT}/jirs-client/js/styles.css > ${CLIENT_ROOT}/tmp/styles.css rsass -t Expanded ${PROJECT_ROOT}/jirs-client/js/styles.css >${CLIENT_ROOT}/tmp/styles.css
cp -r ${CLIENT_ROOT}/static/* ${CLIENT_ROOT}/tmp cp -r ${CLIENT_ROOT}/static/* ${CLIENT_ROOT}/tmp
cat ${CLIENT_ROOT}/static/index.js | cat ${CLIENT_ROOT}/static/index.js &>${CLIENT_ROOT}/tmp/index.js
sed -e "s/process.env.JIRS_SERVER_BIND/'$JIRS_SERVER_BIND'/g" |
sed -e "s/process.env.JIRS_SERVER_PORT/'$JIRS_SERVER_PORT'/g" &>${CLIENT_ROOT}/tmp/index.js
cp ${CLIENT_ROOT}/build/*.{js,wasm} ${CLIENT_ROOT}/tmp/ cp ${CLIENT_ROOT}/build/*.{js,wasm} ${CLIENT_ROOT}/tmp/
cp ${CLIENT_ROOT}/js/template.html ${CLIENT_ROOT}/tmp/index.html cp ${CLIENT_ROOT}/js/template.html ${CLIENT_ROOT}/tmp/index.html

View File

@ -3,13 +3,14 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
#[derive(Debug)]
pub struct StyledAvatar<'l> { pub struct StyledAvatar<'l> {
avatar_url: Option<&'l str>, pub avatar_url: Option<&'l str>,
size: u32, pub size: u32,
name: &'l str, pub name: &'l str,
on_click: Option<EventHandler<Msg>>, pub on_click: Option<EventHandler<Msg>>,
class_list: Vec<&'l str>, pub class_list: &'l str,
user_index: usize, pub user_index: usize,
} }
impl<'l> Default for StyledAvatar<'l> { impl<'l> Default for StyledAvatar<'l> {
@ -19,21 +20,7 @@ impl<'l> Default for StyledAvatar<'l> {
size: 32, size: 32,
name: "", name: "",
on_click: None, on_click: None,
class_list: vec![], class_list: "",
user_index: 0,
}
}
}
impl<'l> StyledAvatar<'l> {
#[inline(always)]
pub fn build() -> StyledAvatarBuilder<'l> {
StyledAvatarBuilder {
avatar_url: None,
size: None,
name: "",
on_click: None,
class_list: vec![],
user_index: 0, user_index: 0,
} }
} }
@ -46,67 +33,6 @@ impl<'l> ToNode for StyledAvatar<'l> {
} }
} }
pub struct StyledAvatarBuilder<'l> {
avatar_url: Option<&'l str>,
size: Option<u32>,
name: &'l str,
on_click: Option<EventHandler<Msg>>,
class_list: Vec<&'l str>,
user_index: usize,
}
impl<'l> StyledAvatarBuilder<'l> {
#[inline(always)]
pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
if !avatar_url.is_empty() {
self.avatar_url = Some(avatar_url);
}
self
}
#[inline(always)]
pub fn size(mut self, size: u32) -> Self {
self.size = Some(size);
self
}
#[inline(always)]
pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
self.name = name;
self
}
#[inline(always)]
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
self.on_click = Some(on_click);
self
}
#[inline(always)]
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
self.class_list.push(name);
self
}
#[inline(always)]
pub fn user_index(mut self, user_index: usize) -> Self {
self.user_index = user_index;
self
}
#[inline(always)]
pub fn build(self) -> StyledAvatar<'l> {
StyledAvatar {
avatar_url: self.avatar_url,
size: self.size.unwrap_or(32),
name: self.name,
on_click: self.on_click,
class_list: self.class_list,
user_index: self.user_index,
}
}
}
pub fn render(values: StyledAvatar) -> Node<Msg> { pub fn render(values: StyledAvatar) -> Node<Msg> {
let StyledAvatar { let StyledAvatar {
avatar_url, avatar_url,
@ -120,10 +46,6 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
let index = user_index % 8; let index = user_index % 8;
let shared_style = format!("width: {size}px; height: {size}px", size = size); let shared_style = format!("width: {size}px; height: {size}px", size = size);
let class_list: Attrs = {
let s: String = class_list.join(" ");
C![s.as_str()]
};
let letter = name let letter = name
.chars() .chars()
.rev() .rev()
@ -138,16 +60,14 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
url = url url = url
); );
div![ div![
C!["styledAvatar image"], C!["styledAvatar image", class_list],
class_list,
attrs![At::Style => style, At::Title => name], attrs![At::Style => style, At::Title => name],
on_click on_click
] ]
} }
_ => { _ => {
div![ div![
C!["styledAvatar letter"], C!["styledAvatar letter", class_list],
class_list,
attrs![ attrs![
At::Class => format!("avatarColor{}", index + 1), At::Class => format!("avatarColor{}", index + 1),
At::Style => shared_style, At::Style => shared_style,

View File

@ -4,7 +4,7 @@ use {
}; };
#[allow(dead_code)] #[allow(dead_code)]
enum Variant { pub enum ButtonVariant {
Primary, Primary,
Success, Success,
Danger, Danger,
@ -12,154 +12,35 @@ enum Variant {
Empty, Empty,
} }
impl Variant { impl ButtonVariant {
fn to_str(&self) -> &'static str { fn to_str(&self) -> &'static str {
match self { match self {
Variant::Primary => "primary", ButtonVariant::Primary => "primary",
Variant::Success => "success", ButtonVariant::Success => "success",
Variant::Danger => "danger", ButtonVariant::Danger => "danger",
Variant::Secondary => "secondary", ButtonVariant::Secondary => "secondary",
Variant::Empty => "empty", ButtonVariant::Empty => "empty",
} }
} }
} }
impl ToString for Variant { impl std::fmt::Display for ButtonVariant {
fn to_string(&self) -> String { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.to_str().to_string() f.write_str(self.to_str())
}
}
#[derive(Default)]
pub struct StyledButtonBuilder<'l> {
variant: Option<Variant>,
disabled: Option<bool>,
active: Option<bool>,
text: Option<&'l str>,
icon: Option<Node<Msg>>,
on_click: Option<EventHandler<Msg>>,
children: Option<Vec<Node<Msg>>>,
class_list: Vec<&'l str>,
button_type: Option<&'l str>,
button_id: Option<ButtonId>,
}
impl<'l> StyledButtonBuilder<'l> {
#[inline(always)]
fn variant(mut self, value: Variant) -> Self {
self.variant = Some(value);
self
}
#[inline(always)]
pub fn primary(self) -> Self {
self.variant(Variant::Primary)
}
#[inline(always)]
pub fn success(self) -> Self {
self.variant(Variant::Success)
}
#[inline(always)]
pub fn danger(self) -> Self {
self.variant(Variant::Danger)
}
#[inline(always)]
pub fn secondary(self) -> Self {
self.variant(Variant::Secondary)
}
#[inline(always)]
pub fn empty(self) -> Self {
self.variant(Variant::Empty)
}
// pub fn button_id(mut self, button_id: ButtonId) -> Self {
// self.button_id = Some(button_id);
// self
// }
#[inline(always)]
pub fn disabled(mut self, value: bool) -> Self {
self.disabled = Some(value);
self
}
#[inline(always)]
pub fn active(mut self, value: bool) -> Self {
self.active = Some(value);
self
}
#[inline(always)]
pub fn text(mut self, value: &'l str) -> Self {
self.text = Some(value);
self
}
#[inline(always)]
pub fn icon<I>(mut self, value: I) -> Self
where
I: ToNode,
{
self.icon = Some(value.into_node());
self
}
#[inline(always)]
pub fn on_click(mut self, value: EventHandler<Msg>) -> Self {
self.on_click = Some(value);
self
}
#[inline(always)]
pub fn children(mut self, value: Vec<Node<Msg>>) -> Self {
self.children = Some(value);
self
}
#[inline(always)]
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
#[inline(always)]
pub fn set_type_reset(mut self) -> Self {
self.button_type = Some("reset");
self
}
#[inline(always)]
pub fn build(self) -> StyledButton<'l> {
StyledButton {
variant: self.variant.unwrap_or(Variant::Primary),
disabled: self.disabled.unwrap_or(false),
active: self.active.unwrap_or(false),
text: self.text,
icon: self.icon,
on_click: self.on_click,
children: self.children.unwrap_or_default(),
class_list: self.class_list,
button_type: self.button_type.unwrap_or("submit"),
button_id: self.button_id,
}
} }
} }
pub struct StyledButton<'l> { pub struct StyledButton<'l> {
variant: Variant, pub variant: ButtonVariant,
disabled: bool, pub disabled: bool,
active: bool, pub active: bool,
text: Option<&'l str>, pub text: Option<&'l str>,
icon: Option<Node<Msg>>, pub icon: Option<Node<Msg>>,
on_click: Option<EventHandler<Msg>>, pub on_click: Option<EventHandler<Msg>>,
children: Vec<Node<Msg>>, pub children: Vec<Node<Msg>>,
class_list: Vec<&'l str>, pub class_list: &'l str,
button_type: &'l str, pub button_type: &'l str,
button_id: Option<ButtonId>, pub button_id: Option<ButtonId>,
} }
impl<'l> StyledButton<'l> { impl<'l> StyledButton<'l> {
@ -168,24 +49,34 @@ impl<'l> StyledButton<'l> {
I: ToNode, I: ToNode,
{ {
Self { Self {
variant: Variant::Secondary, variant: ButtonVariant::Secondary,
disabled: false, disabled: false,
active: false, active: false,
text: Some(text), text: Some(text),
icon: Some(icon.into_node()), icon: Some(icon.into_node()),
on_click: None, on_click: None,
children: vec![], children: vec![],
class_list: vec![], class_list: "",
button_type: "", button_type: "submit",
button_id: None, button_id: None,
} }
} }
} }
impl<'l> StyledButton<'l> { impl<'l> Default for StyledButton<'l> {
#[inline(always)] fn default() -> Self {
pub fn build() -> StyledButtonBuilder<'l> { Self {
StyledButtonBuilder::default() variant: ButtonVariant::Primary,
disabled: false,
active: false,
text: None,
icon: None,
on_click: None,
children: vec![],
class_list: "",
button_type: "",
button_id: None,
}
} }
} }
@ -206,20 +97,22 @@ pub fn render(values: StyledButton) -> Node<Msg> {
icon, icon,
on_click, on_click,
children, children,
mut class_list, class_list,
button_type, button_type,
button_id, button_id,
} = values; } = values;
class_list.push(variant.to_str()); let class_list = format!(
if children.is_empty() && text.is_none() { "{} {} {} {} {}",
class_list.push("iconOnly"); class_list,
} variant,
if active { if children.is_empty() && text.is_none() {
class_list.push("isActive"); "iconOnly"
} } else {
if icon.is_some() { ""
class_list.push("withIcon"); },
} if active { "isActive" } else { "" },
if icon.is_some() { "withIcon" } else { "" }
);
let handler = match on_click { let handler = match on_click {
Some(h) if !disabled => vec![h], Some(h) if !disabled => vec![h],
_ => vec![], _ => vec![],
@ -232,25 +125,13 @@ pub fn render(values: StyledButton) -> Node<Msg> {
span![C!["text"], text.unwrap_or_default(), children] span![C!["text"], text.unwrap_or_default(), children]
}; };
let class_list: Attrs = {
let class_list: String = class_list.join(" ");
C![class_list.as_str()]
};
let button_id = button_id.map(|id| id.to_str()).unwrap_or_default(); let button_id = button_id.map(|id| id.to_str()).unwrap_or_default();
seed::button![ seed::button![
C!["styledButton"], C!["styledButton", class_list],
class_list, attrs![At::Id => button_id, At::Type => button_type],
attrs![ IF![disabled => attrs![At::Disabled => true]],
At::Id => button_id,
At::Type => button_type,
],
handler, handler,
if disabled {
vec![attrs![At::Disabled => true]]
} else {
vec![]
},
icon_node, icon_node,
content, content,
] ]

View File

@ -7,6 +7,8 @@ use {
seed::{prelude::*, EventHandler, *}, seed::{prelude::*, EventHandler, *},
}; };
use crate::components::styled_button::ButtonVariant;
const TITLE: &str = "Warning"; const TITLE: &str = "Warning";
const MESSAGE: &str = "Are you sure you want to continue with this action?"; const MESSAGE: &str = "Are you sure you want to continue with this action?";
const CONFIRM_TEXT: &str = "Confirm"; const CONFIRM_TEXT: &str = "Confirm";
@ -14,11 +16,23 @@ const CANCEL_TEXT: &str = "Cancel";
#[derive(Debug)] #[derive(Debug)]
pub struct StyledConfirmModal<'l> { pub struct StyledConfirmModal<'l> {
title: &'l str, pub title: &'l str,
message: &'l str, pub message: &'l str,
confirm_text: &'l str, pub confirm_text: &'l str,
cancel_text: &'l str, pub cancel_text: &'l str,
on_confirm: Option<EventHandler<Msg>>, pub on_confirm: Option<EventHandler<Msg>>,
}
impl<'l> Default for StyledConfirmModal<'l> {
fn default() -> Self {
Self {
title: TITLE,
message: MESSAGE,
confirm_text: CONFIRM_TEXT,
cancel_text: CANCEL_TEXT,
on_confirm: None,
}
}
} }
impl<'l> StyledConfirmModal<'l> { impl<'l> StyledConfirmModal<'l> {
@ -70,10 +84,10 @@ impl<'l> StyledConfirmModalBuilder<'l> {
pub fn build(self) -> StyledConfirmModal<'l> { pub fn build(self) -> StyledConfirmModal<'l> {
StyledConfirmModal { StyledConfirmModal {
title: self.title.unwrap_or_else(|| TITLE), title: self.title.unwrap_or(TITLE),
message: self.message.unwrap_or_else(|| MESSAGE), message: self.message.unwrap_or(MESSAGE),
confirm_text: self.confirm_text.unwrap_or_else(|| CONFIRM_TEXT), confirm_text: self.confirm_text.unwrap_or(CONFIRM_TEXT),
cancel_text: self.cancel_text.unwrap_or_else(|| CANCEL_TEXT), cancel_text: self.cancel_text.unwrap_or(CANCEL_TEXT),
on_confirm: self.on_confirm, on_confirm: self.on_confirm,
} }
} }
@ -88,32 +102,47 @@ pub fn render(values: StyledConfirmModal) -> Node<Msg> {
on_confirm, on_confirm,
} = values; } = values;
let title = if title.is_empty() { TITLE } else { title };
let message = if message.is_empty() { MESSAGE } else { message };
let confirm_text = if confirm_text.is_empty() {
CONFIRM_TEXT
} else {
confirm_text
};
let cancel_text = if cancel_text.is_empty() {
CANCEL_TEXT
} else {
cancel_text
};
let message_node = match message { let message_node = match message {
_ if message.is_empty() => empty![], _ if message.is_empty() => empty![],
_ => p![attrs![At::Class => "message"], message], _ => p![attrs![At::Class => "message"], message],
}; };
let confirm_button = match on_confirm { let confirm_button = StyledButton {
Some(handler) => StyledButton::build() text: Some(confirm_text),
.text(confirm_text) on_click: on_confirm,
.on_click(handler) ..Default::default()
.build() }
.into_node(), .into_node();
_ => StyledButton::build().text(confirm_text).build().into_node(), let cancel_button = StyledButton {
}; text: Some(cancel_text),
let cancel_button = StyledButton::build() variant: ButtonVariant::Secondary,
.text(cancel_text) on_click: Some(mouse_ev(Ev::Click, |_| Msg::ModalDropped)),
.secondary() ..Default::default()
.on_click(mouse_ev(Ev::Click, |_| Msg::ModalDropped)) }
.build() .into_node();
.into_node();
StyledModal::build() StyledModal {
.width(600) width: Some(600),
.child(div![C!["title"], title]) children: vec![
.child(message_node) div![C!["title"], title],
.child(div![C!["actions"], confirm_button, cancel_button]) message_node,
.add_class("confirmModal") div![C!["actions"], confirm_button, cancel_button],
.build() ],
.into_node() class_list: "confirmModal",
..Default::default()
}
.into_node()
} }

View File

@ -12,6 +12,9 @@ use {
std::ops::RangeInclusive, std::ops::RangeInclusive,
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_tooltip::TooltipVariant;
#[derive(Debug)] #[derive(Debug)]
pub enum StyledDateTimeChanged { pub enum StyledDateTimeChanged {
MonthChanged(Option<NaiveDateTime>), MonthChanged(Option<NaiveDateTime>),
@ -175,18 +178,19 @@ fn render(values: StyledDateTimeInput) -> Node<Msg> {
let date = last_day_of_prev_month let date = last_day_of_prev_month
.with_day0(timestamp.day0()) .with_day0(timestamp.day0())
.unwrap_or_else(|| last_day_of_prev_month); .unwrap_or(last_day_of_prev_month);
Msg::StyledDateTimeInputChanged( Msg::StyledDateTimeInputChanged(
field_id, field_id,
StyledDateTimeChanged::MonthChanged(Some(date)), StyledDateTimeChanged::MonthChanged(Some(date)),
) )
}); });
StyledButton::build() StyledButton {
.on_click(on_click_left) on_click: Some(on_click_left),
.icon(Icon::DoubleLeft) icon: Some(Icon::DoubleLeft.into_node()),
.empty() variant: ButtonVariant::Empty,
.build() ..Default::default()
.into_node() }
.into_node()
}; };
let right_action = { let right_action = {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -201,42 +205,46 @@ fn render(values: StyledDateTimeInput) -> Node<Msg> {
- Duration::days(1); - Duration::days(1);
let date = first_day_of_next_month let date = first_day_of_next_month
.with_day0(timestamp.day0()) .with_day0(timestamp.day0())
.unwrap_or_else(|| last_day_of_next_month); .unwrap_or(last_day_of_next_month);
Msg::StyledDateTimeInputChanged( Msg::StyledDateTimeInputChanged(
field_id, field_id,
StyledDateTimeChanged::MonthChanged(Some(date)), StyledDateTimeChanged::MonthChanged(Some(date)),
) )
}); });
StyledButton::build() StyledButton {
.on_click(on_click_right) on_click: Some(on_click_right),
.icon(Icon::DoubleRight) icon: Some(Icon::DoubleRight.into_node()),
.empty() variant: ButtonVariant::Empty,
.build() ..Default::default()
.into_node() }
.into_node()
}; };
let header_text = current.format("%B %Y").to_string(); let header_text = current.format("%B %Y").to_string();
let tooltip = StyledTooltip::build() let tooltip = StyledTooltip {
.visible(values.popup_visible) visible: values.popup_visible,
.date_time_picker() class_list: "",
.add_child(h2![left_action, span![header_text], right_action]) children: vec![
.add_child(div![ h2![left_action, span![header_text], right_action],
C!["calendar"],
div![ div![
C!["weekHeader week"], C!["calendar"],
div![C!["day"], format!("{}", Weekday::Mon).as_str()], div![
div![C!["day"], format!("{}", Weekday::Tue).as_str()], C!["weekHeader week"],
div![C!["day"], format!("{}", Weekday::Wed).as_str()], div![C!["day"], format!("{}", Weekday::Mon).as_str()],
div![C!["day"], format!("{}", Weekday::Thu).as_str()], div![C!["day"], format!("{}", Weekday::Tue).as_str()],
div![C!["day"], format!("{}", Weekday::Fri).as_str()], div![C!["day"], format!("{}", Weekday::Wed).as_str()],
div![C!["day"], format!("{}", Weekday::Sat).as_str()], div![C!["day"], format!("{}", Weekday::Thu).as_str()],
div![C!["day"], format!("{}", Weekday::Sun).as_str()], div![C!["day"], format!("{}", Weekday::Fri).as_str()],
div![C!["day"], format!("{}", Weekday::Sat).as_str()],
div![C!["day"], format!("{}", Weekday::Sun).as_str()],
],
weeks
], ],
weeks ],
]) variant: TooltipVariant::DateTimeBuilder,
.build() }
.into_node(); .into_node();
let input = { let input = {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -255,12 +263,13 @@ fn render(values: StyledDateTimeInput) -> Node<Msg> {
.date() .date()
.format("%d/%m/%Y") .format("%d/%m/%Y")
.to_string(); .to_string();
StyledButton::build() StyledButton {
.on_click(on_focus) on_click: Some(on_focus),
.text(text.as_str()) text: Some(text.as_str()),
.empty() variant: ButtonVariant::Empty,
.build() ..Default::default()
.into_node() }
.into_node()
}; };
div![ div![

View File

@ -27,93 +27,29 @@ impl StyledEditorState {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledEditor { pub struct StyledEditor<'l> {
id: FieldId, pub id: Option<FieldId>,
initial_text: String, pub initial_text: &'l str,
text: String, pub text: &'l str,
html: String, pub html: &'l str,
mode: Mode, pub mode: Mode,
update_event: Ev, pub update_event: Ev,
} }
impl StyledEditor { impl<'l> Default for StyledEditor<'l> {
#[inline] fn default() -> Self {
pub fn build(id: FieldId) -> StyledEditorBuilder { Self {
StyledEditorBuilder { id: None,
id, initial_text: "",
initial_text: "".to_string(), text: "",
text: "".to_string(), html: "",
html: "".to_string(), mode: Mode::Editor,
mode: Mode::View, update_event: Ev::Cached,
update_event: None,
} }
} }
} }
#[derive(Debug)] impl<'l> ToNode for StyledEditor<'l> {
pub struct StyledEditorBuilder {
id: FieldId,
initial_text: String,
text: String,
html: String,
mode: Mode,
update_event: Option<Ev>,
}
impl StyledEditorBuilder {
#[inline]
pub fn text<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.text = text.into();
self
}
#[inline]
pub fn initial_text<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.initial_text = text.into();
self
}
#[inline]
pub fn html<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.html = text.into();
self
}
#[inline]
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
#[inline]
pub fn build(self) -> StyledEditor {
StyledEditor {
id: self.id,
initial_text: self.initial_text,
text: self.text,
html: self.html,
mode: self.mode,
update_event: self.update_event.unwrap_or(Ev::KeyUp),
}
}
#[inline]
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
}
impl ToNode for StyledEditor {
#[inline] #[inline]
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
@ -131,6 +67,7 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
update_event, update_event,
} = values; } = values;
let id = id.expect("Styled Editor requires ID");
let on_editor_clicked = click_handler(id.clone(), Mode::Editor); let on_editor_clicked = click_handler(id.clone(), Mode::Editor);
let on_view_clicked = click_handler(id.clone(), Mode::View); let on_view_clicked = click_handler(id.clone(), Mode::View);
@ -138,40 +75,31 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
let view_id = format!("view-{}", id); let view_id = format!("view-{}", id);
let name = format!("styled-editor-{}", id); let name = format!("styled-editor-{}", id);
let text_area = StyledTextarea::build(id) let text_area = StyledTextarea {
.height(40) id: Some(id),
.update_on(update_event) height: 40,
// .disable_auto_resize() max_height: 0,
.value(initial_text.as_str()) value: initial_text,
.build() class_list: "",
.into_node(); update_event,
placeholder: "",
disable_auto_resize: false,
}
.into_node();
let (editor_radio_node, view_radio_node, parsed_node) = match mode { let (editor_radio_node, view_radio_node) = (
Mode::Editor => ( seed::input![
seed::input![ id![editor_id.as_str()],
id![editor_id.as_str()], C!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true], attrs![At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
], ],
seed::input![ seed::input![
id![view_id.as_str()], id![view_id.as_str()],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";], C!["viewRadio"],
], attrs![ At::Type => "radio"; At::Name => name.as_str();],
vec![], IF![mode == Mode::View => attrs![At::Checked => true]]
), ],
Mode::View => ( );
seed::input![
id![editor_id.as_str()],
C!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str();],
],
seed::input![
id![view_id.as_str()],
C!["viewRadio"],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
],
Node::from_html(None, html.as_str()),
),
};
div![ div![
C!["styledEditor"], C!["styledEditor"],
@ -198,7 +126,11 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
editor_radio_node, editor_radio_node,
text_area, text_area,
view_radio_node, view_radio_node,
div![C!["view"], parsed_node], div![
C!["view"],
IF![mode == Mode::Editor => empty![]],
IF![mode == Mode::View => raw![html]],
],
] ]
} }

View File

@ -5,10 +5,21 @@ use {
#[derive(Debug)] #[derive(Debug)]
pub struct StyledField<'l> { pub struct StyledField<'l> {
label: &'l str, pub label: &'l str,
tip: Option<&'l str>, pub tip: Option<&'l str>,
input: Node<Msg>, pub input: Node<Msg>,
class_list: Vec<&'l str>, pub class_list: &'l str,
}
impl<'l> Default for StyledField<'l> {
fn default() -> Self {
Self {
label: "",
tip: None,
input: Node::Empty,
class_list: "",
}
}
} }
impl<'l> StyledField<'l> { impl<'l> StyledField<'l> {
@ -28,7 +39,7 @@ pub struct StyledFieldBuilder<'l> {
label: Option<&'l str>, label: Option<&'l str>,
tip: Option<&'l str>, tip: Option<&'l str>,
input: Option<Node<Msg>>, input: Option<Node<Msg>>,
class_list: Vec<&'l str>, class_list: &'l str,
} }
impl<'l> StyledFieldBuilder<'l> { impl<'l> StyledFieldBuilder<'l> {
@ -47,8 +58,8 @@ impl<'l> StyledFieldBuilder<'l> {
self self
} }
pub fn add_class(mut self, name: &'l str) -> Self { pub fn class_list(mut self, name: &'l str) -> Self {
self.class_list.push(name); self.class_list = name;
self self
} }
@ -72,8 +83,8 @@ pub fn render(values: StyledField) -> Node<Msg> {
let tip_node = tip.map(|s| div![C!["styledTip"], s]).unwrap_or(empty![]); let tip_node = tip.map(|s| div![C!["styledTip"], s]).unwrap_or(empty![]);
div![ div![
attrs![At::Class => class_list.join(" "), At::Class => "styledField"], C!["styledField", class_list],
seed::label![attrs![At::Class => "styledLabel"], label], seed::label![C!["styledLabel"], label],
input, input,
tip_node, tip_node,
] ]

View File

@ -5,9 +5,9 @@ use {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledForm<'l> { pub struct StyledForm<'l> {
heading: &'l str, pub heading: &'l str,
fields: Vec<Node<Msg>>, pub fields: Vec<Node<Msg>>,
on_submit: Option<EventHandler<Msg>>, pub on_submit: Option<EventHandler<Msg>>,
} }
impl<'l> StyledForm<'l> { impl<'l> StyledForm<'l> {

View File

@ -107,10 +107,6 @@ impl Icon {
} }
} }
pub fn into_styled_builder<'l>(self) -> StyledIconBuilder<'l> {
StyledIcon::build(self)
}
pub fn to_str<'l>(&self) -> &'l str { pub fn to_str<'l>(&self) -> &'l str {
match self { match self {
Icon::Bug => "bug", Icon::Bug => "bug",
@ -233,9 +229,12 @@ impl From<IssuePriority> for Icon {
} }
} }
impl<'l> Into<StyledIcon<'l>> for Icon { impl<'l> From<Icon> for StyledIcon<'l> {
fn into(self) -> StyledIcon<'l> { fn from(icon: Icon) -> StyledIcon<'l> {
StyledIcon::build(self).build() StyledIcon {
icon,
..Default::default()
}
} }
} }
@ -247,12 +246,12 @@ impl ToNode for Icon {
} }
pub struct StyledIconBuilder<'l> { pub struct StyledIconBuilder<'l> {
icon: Icon, pub icon: Icon,
size: Option<i32>, pub size: Option<i32>,
class_list: Vec<Cow<'l, str>>, pub class_list: &'l str,
style_list: Vec<Cow<'l, str>>, pub style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>, pub color: Option<Cow<'l, str>>,
on_click: Option<EventHandler<Msg>>, pub on_click: Option<EventHandler<Msg>>,
} }
impl<'l> StyledIconBuilder<'l> { impl<'l> StyledIconBuilder<'l> {
@ -261,8 +260,8 @@ impl<'l> StyledIconBuilder<'l> {
self self
} }
pub fn add_class(mut self, name: &'l str) -> Self { pub fn class_list(mut self, name: &'l str) -> Self {
self.class_list.push(Cow::Borrowed(name)); self.class_list = name;
self self
} }
@ -275,34 +274,23 @@ impl<'l> StyledIconBuilder<'l> {
self.on_click = Some(on_click); self.on_click = Some(on_click);
self self
} }
pub fn build(self) -> StyledIcon<'l> {
StyledIcon {
icon: self.icon,
size: self.size,
color: self.color,
class_list: self.class_list,
style_list: self.style_list,
on_click: self.on_click,
}
}
} }
pub struct StyledIcon<'l> { pub struct StyledIcon<'l> {
icon: Icon, pub icon: Icon,
size: Option<i32>, pub size: Option<i32>,
class_list: Vec<Cow<'l, str>>, pub class_list: &'l str,
style_list: Vec<Cow<'l, str>>, pub style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>, pub color: Option<&'l str>,
on_click: Option<EventHandler<Msg>>, pub on_click: Option<EventHandler<Msg>>,
} }
impl<'l> StyledIcon<'l> { impl<'l> Default for StyledIcon<'l> {
pub fn build(icon: Icon) -> StyledIconBuilder<'l> { fn default() -> Self {
StyledIconBuilder { Self {
icon, icon: Icon::Stopwatch,
size: None, size: None,
class_list: vec![], class_list: "",
style_list: vec![], style_list: vec![],
color: None, color: None,
on_click: None, on_click: None,
@ -335,25 +323,12 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
let color = format!("color: {}", s); let color = format!("color: {}", s);
attrs![At::Style => color] attrs![At::Style => color]
}), }),
color.map(|s| { color.map(|s| attrs![At::Style => format!("color: var(--{})", s)]),
let s = match s {
Cow::Owned(s) => format!("color: var(--{})", s.as_str()),
Cow::Borrowed(s) => format!("color: var(--{})", s),
};
attrs![At::Style => s]
}),
] ]
.into_iter() .into_iter()
.filter_map(|o| o) .flatten()
.collect(); .collect();
let class_list: Vec<seed::Attrs> = class_list
.into_iter()
.map(|s| match s {
Cow::Borrowed(s) => C![s],
Cow::Owned(s) => C![s.as_str()],
})
.collect();
let style_list = style_list.into_iter().fold("".to_string(), |mut mem, s| { let style_list = style_list.into_iter().fold("".to_string(), |mut mem, s| {
match s { match s {
Cow::Borrowed(s) => { Cow::Borrowed(s) => {
@ -368,9 +343,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
}); });
i![ i![
C!["styledIcon"], C!["styledIcon", class_list, icon.to_str()],
class_list,
C![icon.to_str()],
styles, styles,
attrs![ At::Style => style_list ], attrs![ At::Style => style_list ],
on_click, on_click,

View File

@ -31,19 +31,9 @@ impl StyledImageInputState {
} }
pub struct StyledImageInput<'l> { pub struct StyledImageInput<'l> {
id: FieldId, pub id: FieldId,
class_list: Vec<&'l str>, pub class_list: &'l str,
url: Option<String>, pub url: Option<&'l str>,
}
impl<'l> StyledImageInput<'l> {
pub fn build(field_id: FieldId) -> StyledInputInputBuilder<'l> {
StyledInputInputBuilder {
id: field_id,
class_list: vec![],
url: None,
}
}
} }
impl<'l> ToNode for StyledImageInput<'l> { impl<'l> ToNode for StyledImageInput<'l> {
@ -52,32 +42,6 @@ impl<'l> ToNode for StyledImageInput<'l> {
} }
} }
pub struct StyledInputInputBuilder<'l> {
id: FieldId,
class_list: Vec<&'l str>,
url: Option<String>,
}
impl<'l> StyledInputInputBuilder<'l> {
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
pub fn state(mut self, state: &StyledImageInputState) -> Self {
self.url = state.url.as_ref().cloned();
self
}
pub fn build(self) -> StyledImageInput<'l> {
StyledImageInput {
id: self.id,
class_list: self.class_list,
url: self.url,
}
}
}
fn render(values: StyledImageInput) -> Node<Msg> { fn render(values: StyledImageInput) -> Node<Msg> {
let StyledImageInput { let StyledImageInput {
id, id,
@ -104,8 +68,7 @@ fn render(values: StyledImageInput) -> Node<Msg> {
let input_id = id.to_string(); let input_id = id.to_string();
div![ div![
C!["styledImageInput"], C!["styledImageInput", class_list],
attrs![At::Class => class_list.join(" ")],
label![ label![
C!["label"], C!["label"],
attrs![At::For => input_id], attrs![At::For => input_id],

View File

@ -8,22 +8,22 @@ use {
}; };
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum Variant { pub enum InputVariant {
Normal, Normal,
Primary, Primary,
} }
impl Variant { impl InputVariant {
#[inline] #[inline]
pub fn to_str<'l>(&self) -> &'l str { pub fn to_str<'l>(&self) -> &'l str {
match self { match self {
Variant::Normal => "normal", InputVariant::Normal => "normal",
Variant::Primary => "primary", InputVariant::Primary => "primary",
} }
} }
} }
impl ToString for Variant { impl ToString for InputVariant {
#[inline] #[inline]
fn to_string(&self) -> String { fn to_string(&self) -> String {
self.to_str().to_string() self.to_str().to_string()
@ -96,150 +96,70 @@ impl StyledInputState {
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.value.clear(); self.value.clear();
} }
pub fn is_valid(&self) -> bool {
match (
self.touched,
self.value.as_str(),
self.min.as_ref(),
self.max.as_ref(),
) {
(false, ..) => true,
(_, s, None, None) => !s.is_empty(),
(_, s, Some(min), None) => s.len() >= *min,
(_, s, None, Some(max)) => s.len() <= *max,
(_, s, Some(min), Some(max)) => s.len() >= *min && s.len() <= *max,
}
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledInput<'l, 'm: 'l> { pub struct StyledInput<'l, 'm: 'l> {
id: FieldId, pub id: Option<FieldId>,
icon: Option<Icon>, pub icon: Option<Icon>,
valid: bool, pub valid: bool,
value: Option<&'m str>, pub value: &'m str,
input_type: Option<&'l str>, pub input_type: Option<&'l str>,
input_class_list: Vec<&'l str>, pub input_class_list: &'l str,
wrapper_class_list: Vec<&'l str>, pub wrapper_class_list: &'l str,
variant: Variant, pub variant: InputVariant,
auto_focus: bool, pub auto_focus: bool,
input_handlers: Vec<EventHandler<Msg>>, pub input_handlers: Vec<EventHandler<Msg>>,
}
impl<'l, 'm: 'l> Default for StyledInput<'l, 'm> {
fn default() -> Self {
Self {
id: None,
icon: None,
valid: false,
value: "",
input_type: None,
input_class_list: "",
wrapper_class_list: "",
variant: InputVariant::Normal,
auto_focus: false,
input_handlers: vec![],
}
}
} }
impl<'l, 'm: 'l> StyledInput<'l, 'm> { impl<'l, 'm: 'l> StyledInput<'l, 'm> {
#[inline] #[inline]
pub fn new_with_id_and_value_and_valid(id: FieldId, value: &'m str, valid: bool) -> Self { pub fn new_with_id_and_value_and_valid(id: FieldId, value: &'m str, valid: bool) -> Self {
Self { Self {
id, id: Some(id),
icon: None, icon: None,
valid, valid,
value: Some(value), value,
input_type: None, input_type: None,
input_class_list: vec![], input_class_list: "",
wrapper_class_list: vec![], wrapper_class_list: "",
variant: Variant::Normal, variant: InputVariant::Normal,
auto_focus: false, auto_focus: false,
input_handlers: vec![], input_handlers: vec![],
} }
} }
#[inline]
pub fn build() -> StyledInputBuilder<'l, 'm> {
StyledInputBuilder {
icon: None,
valid: None,
value: None,
input_type: None,
input_class_list: vec![],
wrapper_class_list: vec![],
variant: Variant::Normal,
auto_focus: false,
input_handlers: vec![],
}
}
}
#[derive(Debug)]
pub struct StyledInputBuilder<'l, 'm: 'l> {
icon: Option<Icon>,
valid: Option<bool>,
value: Option<&'m str>,
input_type: Option<&'l str>,
input_class_list: Vec<&'l str>,
wrapper_class_list: Vec<&'l str>,
variant: Variant,
auto_focus: bool,
input_handlers: Vec<EventHandler<Msg>>,
}
impl<'l, 'm: 'l> StyledInputBuilder<'l, 'm> {
#[inline]
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
#[inline]
pub fn valid(mut self, valid: bool) -> Self {
self.valid = Some(valid);
self
}
#[inline]
pub fn value(mut self, v: &'m str) -> Self {
self.value = Some(v);
self
}
#[inline]
pub fn state(self, state: &'m StyledInputState) -> Self {
self.value(&state.value.as_str()).valid(
match (
state.touched,
state.value.as_str(),
state.min.as_ref(),
state.max.as_ref(),
) {
(false, ..) => true,
(_, s, None, None) => !s.is_empty(),
(_, s, Some(min), None) => s.len() >= *min,
(_, s, None, Some(max)) => s.len() <= *max,
(_, s, Some(min), Some(max)) => s.len() >= *min && s.len() <= *max,
},
)
}
#[inline]
pub fn add_input_class(mut self, name: &'l str) -> Self {
self.input_class_list.push(name);
self
}
#[inline]
pub fn add_wrapper_class(mut self, name: &'l str) -> Self {
self.wrapper_class_list.push(name);
self
}
#[inline]
pub fn primary(mut self) -> Self {
self.variant = Variant::Primary;
self
}
#[inline]
pub fn auto_focus(mut self) -> Self {
self.auto_focus = true;
self
}
#[inline]
pub fn on_input_ev(mut self, handler: EventHandler<Msg>) -> Self {
self.input_handlers.push(handler);
self
}
#[inline]
pub fn build(self, id: FieldId) -> StyledInput<'l, 'm> {
StyledInput {
id,
icon: self.icon,
valid: self.valid.unwrap_or_default(),
value: self.value,
input_type: self.input_type,
input_class_list: self.input_class_list,
wrapper_class_list: self.wrapper_class_list,
variant: self.variant,
auto_focus: self.auto_focus,
input_handlers: self.input_handlers,
}
}
} }
impl<'l, 'm: 'l> ToNode for StyledInput<'l, 'm> { impl<'l, 'm: 'l> ToNode for StyledInput<'l, 'm> {
@ -262,9 +182,10 @@ pub fn render(values: StyledInput) -> Node<Msg> {
auto_focus, auto_focus,
input_handlers, input_handlers,
} = values; } = values;
let id = id.expect("Input id is required");
let icon_node = icon let icon_node = icon
.map(|icon| StyledIcon::build(icon).build().into_node()) .map(|icon| StyledIcon::from(icon).into_node())
.unwrap_or(Node::Empty); .unwrap_or(Node::Empty);
let on_change = { let on_change = {
@ -277,28 +198,27 @@ pub fn render(values: StyledInput) -> Node<Msg> {
}; };
div![ div![
C!["styledInput"], C![
C![variant.to_str()], "styledInput",
if !valid { Some(C!["invalid"]) } else { None }, format!("{}", id),
attrs!( variant.to_str(),
"class" => format!("{} {}", id, wrapper_class_list.join(" ")), wrapper_class_list
), ],
IF![!valid => C!["invalid"]],
icon_node, icon_node,
seed::input![ seed::input![
C!["inputElement"], C![
icon.as_ref().map(|_| C!["withIcon"]), "inputElement",
C![variant.to_str()], variant.to_str(),
input_class_list,
icon.as_ref().map(|_| "withIcon").unwrap_or_default()
],
attrs![ attrs![
"id" => format!("{}", id), "id" => format!("{}", id),
At::Class => input_class_list.join(" "), "value" => value,
"value" => value.unwrap_or_default(),
"type" => input_type.unwrap_or("text"), "type" => input_type.unwrap_or("text"),
], ],
if auto_focus { IF![auto_focus => attrs![At::AutoFocus => true]],
vec![attrs![At::AutoFocus => true]]
} else {
vec![]
},
on_change, on_change,
input_handlers, input_handlers,
], ],

View File

@ -5,21 +5,21 @@ use {
}; };
pub struct StyledLink<'l> { pub struct StyledLink<'l> {
children: Vec<Node<Msg>>, pub children: Vec<Node<Msg>>,
class_list: Vec<&'l str>, pub class_list: &'l str,
href: &'l str, pub href: &'l str,
} }
impl<'l> StyledLink<'l> { impl<'l> StyledLink<'l> {
pub fn build() -> StyledLinkBuilder<'l> { // pub fn build() -> StyledLinkBuilder<'l> {
StyledLinkBuilder::default() // StyledLinkBuilder::default()
} // }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledLinkBuilder<'l> { pub struct StyledLinkBuilder<'l> {
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<&'l str>, class_list: &'l str,
href: &'l str, href: &'l str,
} }
@ -29,13 +29,8 @@ impl<'l> StyledLinkBuilder<'l> {
self self
} }
pub fn with_icon(self) -> Self { pub fn class_list(mut self, name: &'l str) -> Self {
self.add_child(crate::components::styled_icon::Icon::Link.into_node()) self.class_list = name;
.add_class("withIcon")
}
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self self
} }
@ -86,11 +81,8 @@ pub fn render(values: StyledLink) -> Node<Msg> {
}; };
a![ a![
C!["styledLink"], C!["styledLink", class_list],
attrs![ attrs![ At::Href => href, ],
At::Class => class_list.join(" "),
At::Href => href,
],
on_click, on_click,
children, children,
] ]

View File

@ -9,44 +9,56 @@ use {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
pub enum Variant { pub enum ModalVariant {
Center, Center,
Aside, Aside,
} }
impl Variant { impl ModalVariant {
pub fn to_class_name(&self) -> &str { pub fn to_class_name(&self) -> &str {
match self { match self {
Variant::Center => "center", ModalVariant::Center => "center",
Variant::Aside => "aside", ModalVariant::Aside => "aside",
} }
} }
pub fn to_icon_class_name(&self) -> &str { pub fn to_icon_class_name(&self) -> &str {
match self { match self {
Variant::Center => "modalVariantCenter", ModalVariant::Center => "modalVariantCenter",
Variant::Aside => "modalVariantAside", ModalVariant::Aside => "modalVariantAside",
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledModal<'l> { pub struct StyledModal<'l> {
variant: Variant, pub variant: ModalVariant,
width: Option<usize>, pub width: Option<usize>,
with_icon: bool, pub with_icon: bool,
children: Vec<Node<Msg>>, pub children: Vec<Node<Msg>>,
class_list: Vec<&'l str>, pub class_list: &'l str,
}
impl<'l> Default for StyledModal<'l> {
fn default() -> Self {
Self {
variant: ModalVariant::Center,
width: None,
with_icon: false,
children: vec![],
class_list: "",
}
}
} }
impl<'l> StyledModal<'l> { impl<'l> StyledModal<'l> {
pub fn centered_with_width_and_body(width: usize, children: Vec<Node<Msg>>) -> Self { pub fn centered_with_width_and_body(width: usize, children: Vec<Node<Msg>>) -> Self {
Self { Self {
variant: Variant::Center, variant: ModalVariant::Center,
width: Some(width), width: Some(width),
with_icon: false, with_icon: false,
children, children,
class_list: vec![], class_list: "",
} }
} }
} }
@ -57,72 +69,6 @@ impl<'l> ToNode for StyledModal<'l> {
} }
} }
impl<'l> StyledModal<'l> {
pub fn build() -> StyledModalBuilder<'l> {
Default::default()
}
}
#[derive(Default)]
pub struct StyledModalBuilder<'l> {
variant: Option<Variant>,
width: Option<usize>,
with_icon: Option<bool>,
children: Option<Vec<Node<Msg>>>,
class_list: Vec<&'l str>,
}
impl<'l> StyledModalBuilder<'l> {
#[inline]
pub fn variant(mut self, variant: Variant) -> Self {
self.variant = Some(variant);
self
}
#[inline]
pub fn center(self) -> Self {
self.variant(Variant::Center)
}
#[inline]
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
#[inline]
pub fn child(mut self, child: Node<Msg>) -> Self {
self.children.get_or_insert(vec![]).push(child);
self
}
#[inline]
pub fn children<ChildIter>(mut self, children: ChildIter) -> Self
where
ChildIter: Iterator<Item = Node<Msg>>,
{
self.children.get_or_insert(vec![]).extend(children);
self
}
#[inline]
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
#[inline]
pub fn build(self) -> StyledModal<'l> {
StyledModal {
variant: self.variant.unwrap_or(Variant::Center),
width: self.width,
with_icon: self.with_icon.unwrap_or_default(),
children: self.children.unwrap_or_default(),
class_list: self.class_list,
}
}
}
#[inline] #[inline]
pub fn render(values: StyledModal) -> Node<Msg> { pub fn render(values: StyledModal) -> Node<Msg> {
let StyledModal { let StyledModal {
@ -130,14 +76,16 @@ pub fn render(values: StyledModal) -> Node<Msg> {
width, width,
with_icon, with_icon,
children, children,
mut class_list, class_list,
} = values; } = values;
let icon = if with_icon { let icon = if with_icon {
StyledIcon::build(Icon::Close) StyledIcon {
.add_class(variant.to_icon_class_name()) icon: Icon::Close,
.build() class_list: variant.to_icon_class_name(),
.into_node() ..Default::default()
}
.into_node()
} else { } else {
empty![] empty![]
}; };
@ -154,8 +102,6 @@ pub fn render(values: StyledModal) -> Node<Msg> {
}); });
let clickable_class = format!("clickableOverlay {}", variant.to_class_name()); let clickable_class = format!("clickableOverlay {}", variant.to_class_name());
class_list.push("styledModal");
class_list.push(variant.to_class_name());
let styled_modal_style = match width { let styled_modal_style = match width {
Some(0) => "".to_string(), Some(0) => "".to_string(),
Some(n) => format!("max-width: {width}px", width = n), Some(n) => format!("max-width: {width}px", width = n),
@ -164,10 +110,11 @@ pub fn render(values: StyledModal) -> Node<Msg> {
div![ div![
C!["modal"], C!["modal"],
div![ div![
attrs![At::Class => clickable_class], C![clickable_class],
close_handler, close_handler,
div![ div![
attrs![At::Class => class_list.join(" "), At::Style => styled_modal_style], C![class_list, "styledModal", variant.to_class_name()],
attrs![At::Style => styled_modal_style],
body_handler, body_handler,
icon, icon,
children children

View File

@ -19,27 +19,27 @@ pub enum StyledSelectChanged {
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
pub enum Variant { pub enum SelectVariant {
Empty, Empty,
Normal, Normal,
} }
impl Default for Variant { impl Default for SelectVariant {
fn default() -> Self { fn default() -> Self {
Variant::Empty SelectVariant::Empty
} }
} }
impl Variant { impl SelectVariant {
pub fn to_str<'l>(&self) -> &'l str { pub fn to_str<'l>(&self) -> &'l str {
match self { match self {
Variant::Empty => "empty", SelectVariant::Empty => "empty",
Variant::Normal => "normal", SelectVariant::Normal => "normal",
} }
} }
} }
impl std::fmt::Display for Variant { impl std::fmt::Display for SelectVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str()) f.write_str(self.to_str())
} }
@ -114,17 +114,38 @@ pub struct StyledSelect<'l, Options>
where where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>, Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{ {
id: FieldId, pub id: FieldId,
variant: Variant, pub variant: SelectVariant,
dropdown_width: Option<usize>, pub dropdown_width: Option<usize>,
name: Option<&'l str>, pub name: &'l str,
valid: bool, pub valid: bool,
is_multi: bool, pub is_multi: bool,
options: Option<Options>, pub options: Option<Options>,
selected: Vec<StyledSelectChildBuilder<'l>>, pub selected: Vec<StyledSelectChildBuilder<'l>>,
text_filter: &'l str, pub text_filter: &'l str,
opened: bool, pub opened: bool,
clearable: bool, pub clearable: bool,
}
impl<'l, Options> Default for StyledSelect<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
fn default() -> Self {
Self {
id: FieldId::TextFilterBoard,
variant: Default::default(),
dropdown_width: None,
name: "",
valid: true,
is_multi: false,
options: None,
selected: vec![],
text_filter: "",
opened: false,
clearable: false,
}
}
} }
impl<'l, Options> ToNode for StyledSelect<'l, Options> impl<'l, Options> ToNode for StyledSelect<'l, Options>
@ -136,124 +157,6 @@ where
} }
} }
impl<'l, Options> StyledSelect<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
pub fn build() -> StyledSelectBuilder<'l, Options> {
StyledSelectBuilder {
variant: None,
dropdown_width: None,
name: None,
valid: None,
is_multi: None,
options: None,
selected: None,
text_filter: None,
opened: None,
clearable: false,
}
}
}
#[derive(Debug)]
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<Options>,
selected: Option<Vec<StyledSelectChildBuilder<'l>>>,
text_filter: Option<&'l str>,
opened: Option<bool>,
clearable: bool,
}
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(),
dropdown_width: self.dropdown_width,
name: self.name,
valid: self.valid.unwrap_or(true),
is_multi: self.is_multi.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(),
clearable: self.clearable,
}
}
pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self {
self.opened(state.opened)
.text_filter(state.text_filter.as_str())
}
pub fn dropdown_width(mut self, dropdown_width: usize) -> Self {
self.dropdown_width = Some(dropdown_width);
self
}
pub fn name(mut self, name: &'l str) -> Self {
self.name = Some(name);
self
}
pub fn text_filter(mut self, text_filter: &'l str) -> Self {
self.text_filter = Some(text_filter);
self
}
pub fn opened(mut self, opened: bool) -> Self {
self.opened = Some(opened);
self
}
pub fn valid(mut self, valid: bool) -> Self {
self.valid = Some(valid);
self
}
pub fn options(mut self, options: Options) -> Self {
self.options = Some(options);
self
}
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder<'l>>) -> Self {
self.selected = Some(selected);
self
}
pub fn normal(mut self) -> Self {
self.variant = Some(Variant::Normal);
self
}
pub fn empty(mut self) -> Self {
self.variant = Some(Variant::Empty);
self
}
pub fn multi(mut self) -> Self {
self.is_multi = Some(true);
self
}
pub fn clearable(mut self) -> Self {
self.clearable = true;
self
}
}
pub fn render<'l, Options>(values: StyledSelect<'l, Options>) -> Node<Msg> pub fn render<'l, Options>(values: StyledSelect<'l, Options>) -> Node<Msg>
where where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>, Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
@ -286,14 +189,10 @@ where
}) })
}; };
let dropdown_style = dropdown_width let dropdown_style = dropdown_width.map_or_else(
.map(|n| format!("width: {}px;", n)) || "width: 100%;".to_string(),
.unwrap_or_else(|| "width: 100%;".to_string()); |n| format!("width: {}px;", n),
);
let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)];
if !valid {
select_class.push("invalid".to_string());
}
let action_icon = if clearable && !selected.is_empty() { let action_icon = if clearable && !selected.is_empty() {
let on_click = { let on_click = {
@ -304,16 +203,20 @@ where
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None)) Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None))
}) })
}; };
StyledIcon::build(Icon::Close) StyledIcon {
.add_class("chevronIcon") icon: Icon::Close,
.on_click(on_click) class_list: "chevronIcon",
.build() on_click: Some(on_click),
.into_node() ..Default::default()
} else if (selected.is_empty() || !is_multi) && variant != Variant::Empty { }
StyledIcon::build(Icon::ChevronDown) .into_node()
.add_class("chevronIcon") } else if (selected.is_empty() || !is_multi) && variant != SelectVariant::Empty {
.build() StyledIcon {
.into_node() icon: Icon::ChevronDown,
class_list: "chevronIcon",
..Default::default()
}
.into_node()
} else { } else {
empty![] empty![]
}; };
@ -335,12 +238,7 @@ where
) )
}) })
}; };
div![ div![C!["option"], on_change, on_handler.clone(), node]
attrs![At::Class => "option"],
on_change,
on_handler.clone(),
node
]
}) })
.collect() .collect()
} else { } else {
@ -349,9 +247,9 @@ where
let text_input = if opened { let text_input = if opened {
seed::input![ seed::input![
C!["dropDownInput"],
attrs![ attrs![
At::Name => name.unwrap_or_default(), At::Name => name,
At::Class => "dropDownInput",
At::Type => "text" At::Type => "text"
At::Placeholder => "Search" At::Placeholder => "Search"
At::AutoFocus => "true", At::AutoFocus => "true",
@ -364,24 +262,24 @@ where
let option_list = match (opened, children.is_empty()) { let option_list = match (opened, children.is_empty()) {
(false, _) => empty![], (false, _) => empty![],
(_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"], (_, true) => seed::div![C!["noOptions"], "No results"],
_ => seed::div![attrs![ At::Class => "options" ], children], _ => seed::div![C!["options"], children],
}; };
let value: Vec<Node<Msg>> = if is_multi { let value: Vec<Node<Msg>> = if is_multi {
let add_icon = StyledIcon::build(Icon::Plus).build().into_node(); let add_icon = StyledIcon::from(Icon::Plus).into_node();
let mut children: Vec<Node<Msg>> = selected let mut children: Vec<Node<Msg>> = selected
.into_iter() .into_iter()
.map(|m| into_multi_value(m, id.clone())) .map(|m| into_multi_value(m, id.clone()))
.collect(); .collect();
if !children.is_empty() { if !children.is_empty() {
children.push(div![attrs![At::Class => "addMore"], add_icon, "Add more"]); children.push(div![C!["addMore"], add_icon, "Add more"]);
} else { } else {
children.push(div![attrs![At::Class => "placeholder"], "Select"]); children.push(div![C!["placeholder"], "Select"]);
} }
vec![div![attrs![At::Class => "valueMulti"], children]] vec![div![C!["valueMulti"], children]]
} else { } else {
selected selected
.into_iter() .into_iter()
@ -390,13 +288,14 @@ where
}; };
seed::div![ seed::div![
attrs![At::Class => select_class.join(" "), At::Style => dropdown_style.as_str()], C!["styledSelect", variant.to_str(), IF![!valid => "invalid"]],
attrs![At::Style => dropdown_style.as_str()],
keyboard_ev(Ev::KeyUp, |ev| { keyboard_ev(Ev::KeyUp, |ev| {
ev.stop_propagation(); ev.stop_propagation();
None as Option<Msg> None as Option<Msg>
}), }),
div![ div![
attrs![At::Class => format!("valueContainer {}", variant)], C!["valueContainer", variant.to_str()],
on_handler, on_handler,
value, value,
action_icon, action_icon,
@ -416,21 +315,25 @@ fn render_value(mut content: Node<Msg>) -> Node<Msg> {
} }
fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node<Msg> { fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node<Msg> {
let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node(); let close_icon = StyledIcon {
icon: Icon::Close,
size: Some(14),
..Default::default()
}
.into_node();
let child = opt.build(DisplayType::SelectValue); let child = opt.build(DisplayType::SelectValue);
let value = child.value(); let value = child.value();
let mut opt = child.into_node(); let mut opt = child.into_node();
opt.add_class("value"); opt.add_class("value").add_child(close_icon);
opt.add_child(close_icon);
let handler = { let handler = {
let field_id = id.clone(); let field_id = id;
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value)) Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value))
}) })
}; };
div![attrs![At::Class => "valueMultiItem"], opt, handler] div![C!["valueMultiItem"], opt, handler]
} }

View File

@ -1,40 +1,45 @@
use { use {
crate::{ crate::{
components::styled_select::Variant, components::styled_select::SelectVariant,
shared::{IntoChild, ToChild, ToNode}, shared::{IntoChild, ToChild, ToNode},
Msg, Msg,
}, },
seed::{prelude::*, *}, seed::{prelude::*, *},
std::borrow::Cow,
}; };
use crate::components::styled_avatar::StyledAvatar;
use crate::components::styled_icon::StyledIcon;
pub enum DisplayType { pub enum DisplayType {
SelectOption, SelectOption,
SelectValue, SelectValue,
} }
pub struct StyledSelectChild<'l> { pub struct StyledSelectChild<'l> {
name: Option<&'l str>, pub name: Option<&'l str>,
icon: Option<Node<Msg>>, pub icon: Option<Node<Msg>>,
text: Option<std::borrow::Cow<'l, str>>, pub text: Option<&'l str>,
display_type: DisplayType, pub display_type: DisplayType,
value: u32, pub value: u32,
class_list: Vec<std::borrow::Cow<'l, str>>, pub class_list: &'l str,
variant: Variant, pub variant: SelectVariant,
} }
impl<'l> StyledSelectChild<'l> { impl<'l> Default for StyledSelectChild<'l> {
pub fn build() -> StyledSelectChildBuilder<'l> { fn default() -> Self {
StyledSelectChildBuilder { Self {
name: None,
icon: None, icon: None,
text: None, text: None,
name: None, display_type: DisplayType::SelectOption,
value: 0, value: 0,
class_list: vec![], class_list: "",
variant: Default::default(), variant: Default::default(),
} }
} }
}
impl<'l> StyledSelectChild<'l> {
#[inline] #[inline]
pub fn value(&self) -> u32 { pub fn value(&self) -> u32 {
self.value self.value
@ -49,12 +54,25 @@ impl<'l> ToNode for StyledSelectChild<'l> {
#[derive(Debug)] #[derive(Debug)]
pub struct StyledSelectChildBuilder<'l> { pub struct StyledSelectChildBuilder<'l> {
icon: Option<Node<Msg>>, pub icon: Option<Node<Msg>>,
text: Option<std::borrow::Cow<'l, str>>, pub text: Option<&'l str>,
name: Option<&'l str>, pub name: Option<&'l str>,
value: u32, pub value: u32,
class_list: Vec<std::borrow::Cow<'l, str>>, pub class_list: &'l str,
variant: Variant, pub variant: SelectVariant,
}
impl<'l> Default for StyledSelectChildBuilder<'l> {
fn default() -> Self {
Self {
icon: None,
text: None,
name: None,
value: 0,
class_list: "",
variant: Default::default(),
}
}
} }
impl<'l> PartialEq for StyledSelectChildBuilder<'l> { impl<'l> PartialEq for StyledSelectChildBuilder<'l> {
@ -70,12 +88,7 @@ impl<'l> StyledSelectChildBuilder<'l> {
} }
pub fn text<'m: 'l>(mut self, text: &'m str) -> Self { pub fn text<'m: 'l>(mut self, text: &'m str) -> Self {
self.text = Some(std::borrow::Cow::Borrowed(text)); self.text = Some(text);
self
}
pub fn text_owned(mut self, text: String) -> Self {
self.text = Some(std::borrow::Cow::Owned(text));
self self
} }
@ -96,8 +109,8 @@ impl<'l> StyledSelectChildBuilder<'l> {
.unwrap_or(true) .unwrap_or(true)
} }
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self { pub fn class_list<'m: 'l>(mut self, name: &'m str) -> Self {
self.class_list.push(Cow::Borrowed(name)); self.class_list = name;
self self
} }
@ -125,26 +138,6 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
variant, variant,
} = values; } = values;
let label_class = match display_type {
DisplayType::SelectOption => vec![
"optionLabel",
variant.to_str(),
name.as_deref().unwrap_or_default(),
],
DisplayType::SelectValue => vec![
"selectItemLabel",
variant.to_str(),
name.as_deref().unwrap_or_default(),
],
}
.join(" ");
let wrapper_class = match display_type {
DisplayType::SelectOption => vec!["optionItem", name.as_deref().unwrap_or_default()],
DisplayType::SelectValue => vec!["selectItem", name.as_deref().unwrap_or_default()],
}
.join(" ");
let icon_node = match icon { let icon_node = match icon {
Some(icon) => icon, Some(icon) => icon,
_ => empty![], _ => empty![],
@ -152,20 +145,32 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
let label_node = match text { let label_node = match text {
Some(text) => div![ Some(text) => div![
attrs![ C![
At::Class => name.as_deref().map(|s| format!("{}Label", s)).unwrap_or_default(), variant.to_str(),
At::Class => class_list.join(" "), name.as_deref()
.map(|s| format!("{}Label", s))
.unwrap_or_default(),
match display_type {
DisplayType::SelectOption => "optionLabel",
DisplayType::SelectValue => "selectItemLabel",
},
class_list
], ],
C![label_class.as_str()],
text text
], ],
_ => empty![], _ => empty![],
}; };
div![ div![
C![variant.to_str()], C![
C![wrapper_class.as_str()], variant.to_str(),
attrs![At::Class => class_list.join(" ")], name.as_deref().unwrap_or_default(),
match display_type {
DisplayType::SelectOption => "optionItem",
DisplayType::SelectValue => "selectItem",
},
class_list
],
icon_node, icon_node,
label_node label_node
] ]
@ -175,16 +180,19 @@ impl<'l> ToChild<'l> for jirs_data::User {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let avatar = crate::components::styled_avatar::StyledAvatar::build() let avatar = StyledAvatar {
.avatar_url(self.avatar_url.as_deref().unwrap_or_default()) size: 20,
.size(20) name: &self.name,
.name(self.name.as_str()) avatar_url: self.avatar_url.as_deref(),
.build() ..StyledAvatar::default()
.into_node(); }
StyledSelectChild::build() .into_node();
.value(self.id as u32) StyledSelectChildBuilder {
.icon(avatar) value: self.id as u32,
.text(self.name.as_str()) icon: Some(avatar),
text: Some(self.name.as_str()),
..Default::default()
}
} }
} }
@ -192,17 +200,20 @@ impl<'l> IntoChild<'l> for jirs_data::IssuePriority {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn into_child(self) -> Self::Builder { fn into_child(self) -> Self::Builder {
let icon = crate::components::styled_icon::StyledIcon::build(self.clone().into()) let icon = StyledIcon {
.add_class(self.to_str()) icon: self.clone().into(),
.build() class_list: self.to_str(),
.into_node(); ..Default::default()
let text = self.to_str(); }
.into_node();
StyledSelectChild::build() StyledSelectChildBuilder {
.icon(icon) icon: Some(icon),
.value(self.clone().into()) text: Some(self.to_str()),
.text(text) class_list: self.to_str(),
.add_class(self.to_str()) value: self.into(),
..Default::default()
}
} }
} }
@ -210,10 +221,12 @@ impl<'l> ToChild<'l> for jirs_data::IssueStatus {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChildBuilder {
.value(self.id as u32) value: self.id as u32,
.add_class(self.name.as_str()) class_list: self.name.as_str(),
.text(self.name.as_str()) text: Some(self.name.as_str()),
..Default::default()
}
} }
} }
@ -223,16 +236,20 @@ impl<'l> IntoChild<'l> for jirs_data::IssueType {
fn into_child(self) -> Self::Builder { fn into_child(self) -> Self::Builder {
let name = self.to_label(); let name = self.to_label();
let type_icon = crate::components::styled_icon::StyledIcon::build(self.clone().into()) let type_icon = StyledIcon {
.add_class(name) icon: self.clone().into(),
.build() class_list: name,
.into_node(); ..Default::default()
}
.into_node();
StyledSelectChild::build() StyledSelectChildBuilder {
.add_class(name) class_list: name,
.text(name) text: Some(name),
.icon(type_icon) icon: Some(type_icon),
.value(self.clone().into()) value: self.into(),
..Default::default()
}
} }
} }
@ -240,10 +257,12 @@ impl<'l> IntoChild<'l> for jirs_data::ProjectCategory {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn into_child(self) -> Self::Builder { fn into_child(self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChildBuilder {
.add_class(self.to_str()) class_list: self.to_str(),
.text(self.to_str()) text: Some(self.to_str()),
.value(self.clone().into()) value: self.into(),
..Default::default()
}
} }
} }
@ -253,32 +272,35 @@ impl<'l> IntoChild<'l> for jirs_data::UserRole {
fn into_child(self) -> Self::Builder { fn into_child(self) -> Self::Builder {
let name = self.to_str(); let name = self.to_str();
StyledSelectChild::build() StyledSelectChildBuilder {
.add_class(name) text: Some(name),
.add_class("capitalize") value: self.into(),
.text(name) class_list: name,
.value(self.clone().into()) ..Default::default()
}
} }
} }
macro_rules! id_name_builder {
() => {
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChildBuilder {
text: Some(self.name.as_str()),
value: self.id as u32,
..Default::default()
}
}
};
}
impl<'l> ToChild<'l> for jirs_data::Project { impl<'l> ToChild<'l> for jirs_data::Project {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
id_name_builder!();
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build()
.text(self.name.as_str())
.value(self.id as u32)
}
} }
impl<'l> ToChild<'l> for jirs_data::Epic { impl<'l> ToChild<'l> for jirs_data::Epic {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
id_name_builder!();
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build()
.text(self.name.as_str())
.value(self.id as u32)
}
} }
impl<'l> ToChild<'l> for u32 { impl<'l> ToChild<'l> for u32 {
@ -287,10 +309,12 @@ impl<'l> ToChild<'l> for u32 {
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let name = stringify!(self); let name = stringify!(self);
StyledSelectChild::build() StyledSelectChildBuilder {
.add_class(name) class_list: name,
.text(name) text: Some(name),
.value(*self) value: *self,
..Default::default()
}
} }
} }
@ -301,8 +325,10 @@ impl<'l> ToChild<'l> for (Label, Value) {
type Builder = StyledSelectChildBuilder<'l>; type Builder = StyledSelectChildBuilder<'l>;
fn to_child<'m: 'l>(&'m self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChildBuilder {
.text(self.0.as_str()) text: Some(self.0.as_str()),
.value(self.1) value: self.1,
..Default::default()
}
} }
} }

View File

@ -5,103 +5,34 @@ use {
#[derive(Debug)] #[derive(Debug)]
pub struct StyledTextarea<'l> { pub struct StyledTextarea<'l> {
id: FieldId, pub id: Option<FieldId>,
height: usize, pub height: usize,
max_height: usize, pub max_height: usize,
value: &'l str, pub value: &'l str,
class_list: Vec<&'l str>, pub class_list: &'l str,
update_event: Ev, pub update_event: Ev,
placeholder: Option<&'l str>, pub placeholder: &'l str,
disable_auto_resize: bool, pub disable_auto_resize: bool,
} }
impl<'l> ToNode for StyledTextarea<'l> { impl<'l> Default for StyledTextarea<'l> {
fn into_node(self) -> Node<Msg> { fn default() -> Self {
render(self) Self {
} id: None,
} height: 0,
max_height: 0,
impl<'l> StyledTextarea<'l> {
pub fn build(field_id: FieldId) -> StyledTextareaBuilder<'l> {
StyledTextareaBuilder {
id: field_id,
height: None,
max_height: None,
on_change: None,
value: "", value: "",
class_list: vec![], class_list: "",
update_event: None, update_event: Ev::Cached,
placeholder: None, placeholder: "",
disable_auto_resize: false, disable_auto_resize: false,
} }
} }
} }
#[derive(Debug)] impl<'l> ToNode for StyledTextarea<'l> {
pub struct StyledTextareaBuilder<'l> { fn into_node(self) -> Node<Msg> {
id: FieldId, render(self)
height: Option<usize>,
max_height: Option<usize>,
on_change: Option<EventHandler<Msg>>,
value: &'l str,
class_list: Vec<&'l str>,
update_event: Option<Ev>,
placeholder: Option<&'l str>,
disable_auto_resize: bool,
}
impl<'l> StyledTextareaBuilder<'l> {
#[inline]
pub fn height(mut self, height: usize) -> Self {
self.height = Some(height);
self
}
#[inline]
pub fn max_height(mut self, height: usize) -> Self {
self.max_height = Some(height);
self
}
#[inline]
pub fn value(mut self, value: &'l str) -> Self {
self.value = value;
self
}
#[inline]
pub fn add_class(mut self, value: &'l str) -> Self {
self.class_list.push(value);
self
}
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
pub fn placeholder(mut self, placeholder: &'l str) -> Self {
self.placeholder = Some(placeholder);
self
}
pub fn disable_auto_resize(mut self) -> Self {
self.disable_auto_resize = true;
self
}
#[inline]
pub fn build(self) -> StyledTextarea<'l> {
StyledTextarea {
id: self.id,
value: self.value,
height: self.height.unwrap_or(110),
class_list: self.class_list,
max_height: self.max_height.unwrap_or_default(),
update_event: self.update_event.unwrap_or(Ev::KeyUp),
placeholder: self.placeholder,
disable_auto_resize: self.disable_auto_resize,
}
} }
} }
@ -124,11 +55,12 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
height, height,
max_height, max_height,
value, value,
mut class_list, class_list,
update_event, update_event,
placeholder, placeholder,
disable_auto_resize, disable_auto_resize,
} = values; } = values;
let id = id.expect("Text area requires FieldId");
let mut style_list = vec![]; let mut style_list = vec![];
let min_height = get_min_height(value, height as f64, disable_auto_resize); let min_height = get_min_height(value, height as f64, disable_auto_resize);
@ -141,15 +73,14 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
} }
if disable_auto_resize { if disable_auto_resize {
style_list.push("resize: none".to_string());
style_list.push(format!( style_list.push(format!(
"height: {h}px; max-height: {h}px; min-height: {h}px", "resize: none; height: {h}px; max-height: {h}px; min-height: {h}px",
h = max_height h = max_height
)); ));
} }
let handler_disable_auto_resize = disable_auto_resize; let handler_disable_auto_resize = disable_auto_resize;
let resize_handler = ev(Ev::KeyUp, move |event| { let resize_handler = ev(Ev::Change, move |event| {
event.stop_propagation(); event.stop_propagation();
if handler_disable_auto_resize { if handler_disable_auto_resize {
return None as Option<Msg>; return None as Option<Msg>;
@ -167,39 +98,41 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
}); });
let handler_disable_auto_resize = disable_auto_resize; let handler_disable_auto_resize = disable_auto_resize;
let text_input_handler = ev(update_event, move |event| { let text_input_handler = {
event.stop_propagation(); let id = id.clone();
ev(update_event, move |event| {
event.stop_propagation();
let value = event let value = event
.target() .target()
.map(|target| seed::to_textarea(&target).value()) .map(|target| seed::to_textarea(&target).value())
.unwrap_or_default(); .unwrap_or_default();
if handler_disable_auto_resize && value.contains('\n') { if handler_disable_auto_resize && value.contains('\n') {
event.prevent_default(); event.prevent_default();
} }
Some(Msg::StrInputChanged( Some(Msg::StrInputChanged(
id, id,
if handler_disable_auto_resize { if handler_disable_auto_resize {
value.trim().to_string() value.trim().to_string()
} else { } else {
value value
}, },
)) ))
}); })
};
class_list.push("textAreaInput");
div![ div![
id![format!("styledTextArea-{}", id)],
C!["styledTextArea"], C!["styledTextArea"],
div![C!["textAreaHeading"]], div![C!["textAreaHeading"]],
textarea![ textarea![
C![class_list, "textAreaInput"],
attrs![ attrs![
At::Class => class_list.join(" ");
At::AutoFocus => "true"; At::AutoFocus => "true";
At::Style => style_list.join(";"); At::Style => style_list.join(";");
At::Placeholder => placeholder.unwrap_or_default(); At::Placeholder => placeholder;
At::Rows => if disable_auto_resize { "5" } else { "auto" } At::Rows => if disable_auto_resize { "5" } else { "auto" }
], ],
value, value,

View File

@ -4,7 +4,7 @@ use {
}; };
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum Variant { pub enum TooltipVariant {
About, About,
Messages, Messages,
TableBuilder, TableBuilder,
@ -12,35 +12,35 @@ pub enum Variant {
DateTimeBuilder, DateTimeBuilder,
} }
impl Default for Variant { impl Default for TooltipVariant {
fn default() -> Self { fn default() -> Self {
Variant::Messages TooltipVariant::Messages
} }
} }
impl Variant { impl TooltipVariant {
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
Variant::About => "about", TooltipVariant::About => "about",
Variant::Messages => "messages", TooltipVariant::Messages => "messages",
Variant::TableBuilder => "tableTooltip", TooltipVariant::TableBuilder => "tableTooltip",
Variant::CodeBuilder => "codeTooltip", TooltipVariant::CodeBuilder => "codeTooltip",
Variant::DateTimeBuilder => "dateTimeTooltip", TooltipVariant::DateTimeBuilder => "dateTimeTooltip",
} }
} }
} }
impl std::fmt::Display for Variant { impl std::fmt::Display for TooltipVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str()) f.write_str(self.to_str())
} }
} }
pub struct StyledTooltip<'l> { pub struct StyledTooltip<'l> {
visible: bool, pub visible: bool,
class_list: Vec<&'l str>, pub class_list: &'l str,
children: Vec<Node<Msg>>, pub children: Vec<Node<Msg>>,
variant: Variant, pub variant: TooltipVariant,
} }
impl<'l> ToNode for StyledTooltip<'l> { impl<'l> ToNode for StyledTooltip<'l> {
@ -49,71 +49,6 @@ impl<'l> ToNode for StyledTooltip<'l> {
} }
} }
impl<'l> StyledTooltip<'l> {
pub fn build() -> StyledTooltipBuilder<'l> {
StyledTooltipBuilder::default()
}
}
#[derive(Default)]
pub struct StyledTooltipBuilder<'l> {
visible: bool,
class_list: Vec<&'l str>,
children: Vec<Node<Msg>>,
variant: Variant,
}
impl<'l> StyledTooltipBuilder<'l> {
pub fn visible(mut self, b: bool) -> Self {
self.visible = b;
self
}
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
pub fn add_child(mut self, child: Node<Msg>) -> Self {
self.children.push(child);
self
}
pub fn about_tooltip(mut self) -> Self {
self.variant = Variant::About;
self
}
pub fn messages_tooltip(mut self) -> Self {
self.variant = Variant::Messages;
self
}
// pub fn table_tooltip(mut self) -> Self {
// self.variant = Variant::TableBuilder;
// self
// }
//
// pub fn code_tooltip(mut self) -> Self {
// self.variant = Variant::CodeBuilder;
// self
// }
pub fn date_time_picker(mut self) -> Self {
self.variant = Variant::DateTimeBuilder;
self
}
pub fn build(self) -> StyledTooltip<'l> {
StyledTooltip {
visible: self.visible,
class_list: self.class_list,
children: self.children,
variant: self.variant,
}
}
}
pub fn render(values: StyledTooltip) -> Node<Msg> { pub fn render(values: StyledTooltip) -> Node<Msg> {
let StyledTooltip { let StyledTooltip {
visible, visible,
@ -122,10 +57,7 @@ pub fn render(values: StyledTooltip) -> Node<Msg> {
variant, variant,
} = values; } = values;
if visible { if visible {
div![ div![C!["styledTooltip", class_list, variant.to_str()], children]
attrs![At::Class => format!("styledTooltip {} {}", class_list.join(" "), variant)],
children
]
} else { } else {
empty!() empty!()
} }

View File

@ -6,7 +6,7 @@ use {
styled_date_time_input::StyledDateTimeChanged, styled_date_time_input::StyledDateTimeChanged,
styled_select::StyledSelectChanged, styled_select::StyledSelectChanged,
styled_tooltip, styled_tooltip,
styled_tooltip::{Variant as StyledTooltip, Variant}, styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant},
}, },
model::{ModalType, Model, Page}, model::{ModalType, Model, Page},
shared::{go_to_board, go_to_login}, shared::{go_to_board, go_to_login},
@ -24,6 +24,7 @@ mod changes;
mod components; mod components;
mod fields; mod fields;
mod images; mod images;
mod location;
mod modals; mod modals;
mod model; mod model;
mod pages; mod pages;
@ -217,15 +218,15 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
model.page = *page; model.page = *page;
} }
Msg::ToggleTooltip(variant) => match variant { Msg::ToggleTooltip(variant) => match variant {
styled_tooltip::Variant::About => { styled_tooltip::TooltipVariant::About => {
model.about_tooltip_visible = !model.about_tooltip_visible; model.about_tooltip_visible = !model.about_tooltip_visible;
} }
styled_tooltip::Variant::Messages => { styled_tooltip::TooltipVariant::Messages => {
model.messages_tooltip_visible = !model.messages_tooltip_visible; model.messages_tooltip_visible = !model.messages_tooltip_visible;
} }
styled_tooltip::Variant::CodeBuilder => {} styled_tooltip::TooltipVariant::CodeBuilder => {}
Variant::TableBuilder => {} TooltipVariant::TableBuilder => {}
Variant::DateTimeBuilder => {} TooltipVariant::DateTimeBuilder => {}
}, },
_ => (), _ => (),
} }
@ -305,16 +306,8 @@ fn resolve_page(url: Url) -> Option<Page> {
Some(page) Some(page)
} }
pub static mut HOST_URL: String = String::new();
pub static mut WS_URL: String = String::new();
#[wasm_bindgen] #[wasm_bindgen]
pub fn render(host_url: String, ws_url: String) { pub fn render() {
unsafe {
HOST_URL = host_url;
WS_URL = ws_url;
}
let app = seed::App::start("app", init, update, view); let app = seed::App::start("app", init, update, view);
{ {
@ -348,13 +341,10 @@ pub fn render(host_url: String, ws_url: String) {
} }
fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model { fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
let host_url = unsafe { HOST_URL.clone() }; let mut model = Model::new(
let ws_url = unsafe { WS_URL.clone() }; location::host_url().to_string(),
let mut model = Model::new(host_url, ws_url); location::ws_url().to_string(),
unsafe { );
HOST_URL = "".to_string();
WS_URL = "".to_string();
}
model.page = resolve_page(url).unwrap_or(Page::Project); model.page = resolve_page(url).unwrap_or(Page::Project);
open_socket(&mut model, orders); open_socket(&mut model, orders);

View File

@ -6,11 +6,12 @@ use {
pub fn view(_model: &model::Model, modal: &super::Model) -> Node<Msg> { pub fn view(_model: &model::Model, modal: &super::Model) -> Node<Msg> {
let comment_id: CommentId = modal.comment_id; let comment_id: CommentId = modal.comment_id;
StyledConfirmModal::build() StyledConfirmModal {
.title("Are you sure you want to delete this comment?") title: "Are you sure you want to delete this comment?",
.message("Once you delete, it's gone for good.") message: "Once you delete, it's gone for good.",
.confirm_text("Delete comment") confirm_text: "Delete comment",
.on_confirm(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))) on_confirm: Some(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))),
.build() ..Default::default()
.into_node() }
.into_node()
} }

View File

@ -6,11 +6,11 @@ use {
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let text = format!("{:#?}", model); let text = format!("{:#?}", model);
let code = pre![text]; let code = pre![text];
StyledModal::build() StyledModal {
.width(1200) width: Some(1200),
.add_class("debugModal") class_list: "debugModal",
.center() children: vec![code],
.children(vec![code].into_iter()) ..Default::default()
.build() }
.into_node() .into_node()
} }

View File

@ -9,36 +9,40 @@ use {
seed::prelude::Node, seed::prelude::Node,
}; };
use crate::components::styled_select::SelectVariant;
pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>> pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>>
where where
Modal: IssueModal, Modal: IssueModal,
{ {
if model.epics.is_empty() { if model.epics.is_empty() {
None return None;
} else {
let selected = modal
.epic_id_value()
.and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId))
.map(|epic| vec![epic.to_child()])
.unwrap_or_default();
let input = StyledSelect::build()
.name("epic")
.selected(selected)
.options(model.epics.iter().map(|epic| epic.to_child()))
.normal()
.clearable()
.text_filter(modal.epic_state().text_filter.as_str())
.opened(modal.epic_state().opened)
.valid(true)
.build(field_id)
.into_node();
Some(
StyledField::build()
.label("Epic")
.tip("Feature group")
.input(input)
.build()
.into_node(),
)
} }
let selected = modal
.epic_id_value()
.and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId))
.map(|epic| vec![epic.to_child()])
.unwrap_or_default();
let input = StyledSelect {
id: field_id,
name: "epic",
selected,
options: Some(model.epics.iter().map(|epic| epic.to_child())),
variant: SelectVariant::Normal,
clearable: true,
text_filter: modal.epic_state().text_filter.as_str(),
opened: modal.epic_state().opened,
valid: true,
..Default::default()
}
.into_node();
Some(
StyledField {
label: "Epic",
tip: Some("Feature group"),
input,
..Default::default()
}
.into_node(),
)
} }

View File

@ -11,25 +11,26 @@ use {
pub fn view(model: &model::Model, modal: &Model) -> Node<Msg> { pub fn view(model: &model::Model, modal: &Model) -> Node<Msg> {
if modal.related_issues.is_empty() { if modal.related_issues.is_empty() {
StyledConfirmModal::build() StyledConfirmModal {
.title("Delete empty epic") title: "Delete empty epic",
.cancel_text("Cancel") confirm_text: "Delete epic",
.confirm_text("Delete epic") cancel_text: "Cancel",
.on_confirm(mouse_ev("click", move |ev| { on_confirm: Some(mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::DeleteEpic Msg::DeleteEpic
})) })),
.build() ..Default::default()
.into_node() }
.into_node()
} else { } else {
StyledModal::build() StyledModal {
.add_class("deleteEpic") children: vec![warning(model, modal)],
.center() width: Some(600),
.width(600) class_list: "deleteEpic",
.child(warning(model, modal)) ..Default::default()
.build() }
.into_node() .into_node()
} }
} }
@ -39,7 +40,7 @@ fn warning(model: &model::Model, modal: &Model) -> Node<Msg> {
.iter() .iter()
.flat_map(|id| model.issues_by_id.get(id)) .flat_map(|id| model.issues_by_id.get(id))
.map(|issue| { .map(|issue| {
let link = StyledIcon::build(Icon::Link).build().into_node(); let link = StyledIcon::from(Icon::Link).into_node();
li![div![ li![div![
C!["relatedIssue"], C!["relatedIssue"],
a![ a![
@ -51,16 +52,17 @@ fn warning(model: &model::Model, modal: &Model) -> Node<Msg> {
}) })
.collect(); .collect();
let close = StyledButton::build() let close = StyledButton {
.text("Close") text: Some("Close"),
.on_click(mouse_ev("click", move |ev| { on_click: Some(mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::ModalDropped Msg::ModalDropped
})) })),
.secondary() variant: ButtonVariant::Secondary,
.build() ..Default::default()
.into_node(); }
.into_node();
section![ section![
h3![C!["header"], "Cannot delete epic"], h3![C!["header"], "Cannot delete epic"],

View File

@ -33,30 +33,34 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
} else { } else {
transform_into_unavailable(modal) transform_into_unavailable(modal)
}; };
let close = StyledButton::build() let close = StyledButton {
.on_click(mouse_ev("click", |ev| { on_click: Some(mouse_ev("click", |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::ModalDropped Msg::ModalDropped
})) })),
.empty() variant: ButtonVariant::Empty,
.icon(Icon::Close) icon: Some(Icon::Close.into_node()),
.build() ..Default::default()
.into_node(); }
StyledModal::build() .into_node();
.center() StyledModal {
.width(600) width: Some(600),
.add_class("editEpic") class_list: "editEpic",
.child(div![C!["header"], h1!["Edit epic"], close]) children: vec![
.child( div![C!["header"], h1!["Edit epic"], close],
StyledInput::build() StyledInput {
.state(&modal.name) value: modal.name.value.as_str(),
.build(FieldId::EditEpic(EpicFieldId::Name)) valid: modal.name.is_valid(),
.into_node(), id: Some(FieldId::EditEpic(EpicFieldId::Name)),
) ..Default::default()
.child(transform) }
.build() .into_node(),
.into_node() transform,
],
..Default::default()
}
.into_node()
} }
fn transform_into_available(modal: &super::Model) -> Node<Msg> { fn transform_into_available(modal: &super::Model) -> Node<Msg> {
@ -69,15 +73,16 @@ fn transform_into_available(modal: &super::Model) -> Node<Msg> {
.state(&modal.transform_into) .state(&modal.transform_into)
.build(FieldId::EditEpic(EpicFieldId::TransformInto)) .build(FieldId::EditEpic(EpicFieldId::TransformInto))
.into_node(); .into_node();
let execute = StyledButton::build() let execute = StyledButton {
.on_click(mouse_ev("click", |ev| { on_click: Some(mouse_ev("click", |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::TransformEpic Msg::TransformEpic
})) })),
.text("Transform") text: Some("Transform"),
.build() ..Default::default()
.into_node(); }
.into_node();
div![C!["transform available"], div![types], div![execute]] div![C!["transform available"], div![types], div![execute]]
} }

View File

@ -5,14 +5,14 @@ use {
}; };
pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node<Msg> { pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node<Msg> {
StyledConfirmModal::build() StyledConfirmModal {
.title("Delete column") title: "Delete column",
.cancel_text("No") message: "Are you sure you want to delete column?",
.confirm_text("Yes") confirm_text: "Yes",
.on_confirm(mouse_ev(Ev::Click, move |_| { cancel_text: "No",
on_confirm: Some(mouse_ev(Ev::Click, move |_| {
Msg::DeleteIssueStatus(issue_status_id) Msg::DeleteIssueStatus(issue_status_id)
})) })),
.message("Are you sure you want to delete column?") }
.build() .into_node()
.into_node()
} }

View File

@ -67,25 +67,26 @@ impl<'l> IntoChild<'l> for Type {
let type_icon = { let type_icon = {
use crate::components::styled_icon::*; use crate::components::styled_icon::*;
let icon = { StyledIcon {
match self { icon: match self {
Type::Task => Icon::Task, Type::Task => Icon::Task,
Type::Bug => Icon::Bug, Type::Bug => Icon::Bug,
Type::Story => Icon::Story, Type::Story => Icon::Story,
Type::Epic => Icon::Epic, Type::Epic => Icon::Epic,
} },
}; class_list: name,
crate::components::styled_icon::StyledIcon::build(icon) ..Default::default()
.add_class(name) }
.build() .into_node()
.into_node()
}; };
StyledSelectChild::build() StyledSelectChildBuilder {
.add_class(name) class_list: name,
.text(name) text: Some(name),
.icon(type_icon) icon: Some(type_icon),
.value(value) value,
..Default::default()
}
} }
} }

View File

@ -18,6 +18,9 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_select::SelectVariant;
pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let issue_type = modal let issue_type = modal
.type_state .type_state
@ -80,55 +83,54 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
}; };
let submit = { let submit = {
StyledButton::build() StyledButton {
.primary() variant: ButtonVariant::Primary,
.text(issue_type.submit_label()) text: Some(issue_type.submit_label()),
.add_class("action") class_list: "action submit actionButton",
.add_class("submit") on_click: Some(mouse_ev(Ev::Click, move |ev| {
.add_class("actionButton")
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Some(issue_type.submit_action()) Some(issue_type.submit_action())
})) })),
.build() ..Default::default()
.into_node() }
.into_node()
}; };
let cancel = StyledButton::build() let cancel = StyledButton {
.empty() variant: ButtonVariant::Empty,
.add_class("action") class_list: "action cancel actionButton",
.add_class("cancel") text: Some("Cancel"),
.add_class("actionButton") on_click: Some(mouse_ev(Ev::Click, |ev| {
.text("Cancel")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Some(Msg::ModalDropped) Some(Msg::ModalDropped)
})) })),
.build() ..Default::default()
.into_node(); }
.into_node();
let actions = div![attrs![At::Class => "actions"], submit, cancel]; let actions = div![attrs![At::Class => "actions"], submit, cancel];
let form = form.add_field(actions).build().into_node(); let form = form.add_field(actions).build().into_node();
StyledModal::build() StyledModal {
.add_class("addIssue") class_list: "addIssue",
.width(0) width: Some(0),
.variant(crate::components::styled_modal::Variant::Center) children: vec![form],
.child(form) ..Default::default()
.build() }
.into_node() .into_node()
} }
fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> { fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build() let select_type = StyledSelect {
.name("type") id: FieldId::AddIssueModal(IssueFieldId::Type),
.normal() name: "type",
.text_filter(modal.type_state.text_filter.as_str()) variant: SelectVariant::Normal,
.opened(modal.type_state.opened) text_filter: modal.type_state.text_filter.as_str(),
.valid(true) opened: modal.type_state.opened,
.options(Type::Task.into_iter().map(|t| t.into_child().name("type"))) valid: true,
.selected(vec![{ options: Some(Type::Task.into_iter().map(|t| t.into_child().name("type"))),
selected: vec![{
let v: Type = modal let v: Type = modal
.type_state .type_state
.values .values
@ -139,43 +141,52 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
v v
} }
.into_child() .into_child()
.name("type")]) .name("type")],
.build(FieldId::AddIssueModal(IssueFieldId::Type)) ..Default::default()
.into_node(); }
StyledField::build() .into_node();
.label("Issue Type") StyledField {
.tip("Start typing to get a list of possible matches.") label: "Issue Type",
.input(select_type) tip: Some("Start typing to get a list of possible matches."),
.build() input: select_type,
.into_node() ..Default::default()
}
.into_node()
} }
#[inline] #[inline]
fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> { fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> {
let short_summary = StyledInput::build() let short_summary = StyledInput {
.state(&modal.title_state) value: modal.title_state.value.as_str(),
.build(FieldId::AddIssueModal(IssueFieldId::Title)) valid: modal.title_state.is_valid(),
.into_node(); id: Some(FieldId::AddIssueModal(IssueFieldId::Title)),
StyledField::build() ..Default::default()
.label("Short Summary") }
.tip("Concisely summarize the issue in one or two sentences.") .into_node();
.input(short_summary) StyledField {
.build() label: "Short Summary",
.into_node() tip: Some("Concisely summarize the issue in one or two sentences."),
input: short_summary,
..Default::default()
}
.into_node()
} }
fn description_field() -> Node<Msg> { fn description_field() -> Node<Msg> {
let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description)) let description = StyledTextarea {
.height(110) id: Some(FieldId::AddIssueModal(IssueFieldId::Description)),
.add_class("textarea") height: 110,
.build() class_list: "textarea",
.into_node(); ..Default::default()
StyledField::build() }
.label("Description") .into_node();
.tip("Describe the issue in as much detail as you'd like.") StyledField {
.input(description) label: "Description",
.build() tip: Some("Describe the issue in as much detail as you'd like."),
.into_node() input: description,
..Default::default()
}
.into_node()
} }
fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> { fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
@ -183,58 +194,60 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.reporter_id .reporter_id
.or_else(|| model.user.as_ref().map(|u| u.id)) .or_else(|| model.user.as_ref().map(|u| u.id))
.unwrap_or_default(); .unwrap_or_default();
let reporter = StyledSelect::build() let reporter = StyledSelect {
.normal() id: FieldId::AddIssueModal(IssueFieldId::Reporter),
.text_filter(modal.reporter_state.text_filter.as_str()) variant: SelectVariant::Normal,
.opened(modal.reporter_state.opened) text_filter: modal.reporter_state.text_filter.as_str(),
.options(model.users.iter().map(|u| u.to_child().name("reporter"))) opened: modal.reporter_state.opened,
.selected( options: Some(model.users.iter().map(|u| u.to_child().name("reporter"))),
model selected: model
.users .users
.iter() .iter()
.filter_map(|user| { .filter_map(|user| {
if user.id == reporter_id { if user.id == reporter_id {
Some(user.to_child().name("reporter")) Some(user.to_child().name("reporter"))
} else { } else {
None None
} }
}) })
.collect(), .collect(),
)
.valid(true) valid: true,
.build(FieldId::AddIssueModal(IssueFieldId::Reporter)) ..Default::default()
.into_node(); }
StyledField::build() .into_node();
.input(reporter) StyledField {
.label("Reporter") input: reporter,
.tip("") label: "Reporter",
.build() ..Default::default()
.into_node() }
.into_node()
} }
fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> { fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let assignees = StyledSelect::build() let assignees = StyledSelect {
.normal() id: FieldId::AddIssueModal(IssueFieldId::Assignees),
.multi() variant: SelectVariant::Normal,
.text_filter(modal.assignees_state.text_filter.as_str()) is_multi: true,
.opened(modal.assignees_state.opened) text_filter: modal.assignees_state.text_filter.as_str(),
.options(model.users.iter().map(|u| u.to_child().name("assignees"))) opened: modal.assignees_state.opened,
.selected( options: Some(model.users.iter().map(|u| u.to_child().name("assignees"))),
model selected: model
.users .users
.iter() .iter()
.filter_map(|user| { .filter_map(|user| {
if modal.user_ids.contains(&user.id) { if modal.user_ids.contains(&user.id) {
Some(user.to_child().name("assignees")) Some(user.to_child().name("assignees"))
} else { } else {
None None
} }
}) })
.collect(), .collect(),
)
.valid(true) valid: true,
.build(FieldId::AddIssueModal(IssueFieldId::Assignees)) ..Default::default()
.into_node(); }
.into_node();
StyledField::build() StyledField::build()
.input(assignees) .input(assignees)
.label("Assignees") .label("Assignees")
@ -245,33 +258,40 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> { fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
let priorities = IssuePriority::default().into_iter(); let priorities = IssuePriority::default().into_iter();
let select_priority = StyledSelect::build() let select_priority = StyledSelect {
.name("priority") id: FieldId::AddIssueModal(IssueFieldId::Priority),
.normal() name: "priority",
.text_filter(modal.priority_state.text_filter.as_str()) variant: SelectVariant::Normal,
.opened(modal.priority_state.opened) text_filter: modal.priority_state.text_filter.as_str(),
.valid(true) opened: modal.priority_state.opened,
.options(priorities.map(|p| p.into_child().name("priority"))) valid: true,
.selected(vec![modal.priority.into_child().name("priority")]) options: Some(priorities.map(|p| p.into_child().name("priority"))),
.build(FieldId::AddIssueModal(IssueFieldId::Priority)) selected: vec![modal.priority.into_child().name("priority")],
.into_node(); ..Default::default()
StyledField::build() }
.label("Issue Type") .into_node();
.tip("Priority in relation to other issues.") StyledField {
.input(select_priority) label: "Issue Type",
.build() tip: Some("Priority in relation to other issues."),
.into_node() input: select_priority,
..Default::default()
}
.into_node()
} }
fn name_field(modal: &AddIssueModal) -> Node<Msg> { fn name_field(modal: &AddIssueModal) -> Node<Msg> {
let name = StyledInput::build() let name = StyledInput {
.state(&modal.title_state) value: modal.title_state.value.as_str(),
.build(FieldId::AddIssueModal(IssueFieldId::Title)) valid: modal.title_state.is_valid(),
.into_node(); id: Some(FieldId::AddIssueModal(IssueFieldId::Title)),
StyledField::build() ..Default::default()
.label("Epic name") }
.tip("Describe upcoming feature.") .into_node();
.input(name) StyledField {
.build() label: "Epic name",
.into_node() tip: Some("Describe upcoming feature."),
input: name,
..Default::default()
}
.into_node()
} }

View File

@ -9,14 +9,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return Node::Empty, _ => return Node::Empty,
}; };
let handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id)); StyledConfirmModal {
title: "Are you sure you want to delete this issue?",
StyledConfirmModal::build() message: "Once you delete, it's gone for good.",
.title("Are you sure you want to delete this issue?") confirm_text: "Delete issue",
.message("Once you delete, it's gone for good.") cancel_text: "Cancel",
.confirm_text("Delete issue") on_confirm: Some(mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id))),
.cancel_text("Cancel") }
.on_confirm(handle_issue_delete) .into_node()
.build()
.into_node()
} }

View File

@ -17,6 +17,11 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_icon::StyledIcon;
use crate::components::styled_select::SelectVariant;
use crate::components::styled_select_child::StyledSelectChildBuilder;
mod comments; mod comments;
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
@ -88,53 +93,87 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
Msg::ModalOpened(ModalType::DeleteIssueConfirm(Some(issue_id))) Msg::ModalOpened(ModalType::DeleteIssueConfirm(Some(issue_id)))
}); });
let copy_button = StyledButton::build() let copy_button = StyledButton {
.empty() variant: ButtonVariant::Empty,
.icon(Icon::Link) icon: Some(Icon::Link.into_node()),
.on_click(click_handler) on_click: Some(click_handler),
.children(vec![span![if *link_copied { children: vec![span![if *link_copied {
"Link Copied" "Link Copied"
} else { } else {
"Copy link" "Copy link"
}]]) }]],
.build() ..Default::default()
.into_node(); }
let delete_button = StyledButton::build() .into_node();
.empty() let delete_button = StyledButton {
.icon(Icon::Trash.into_styled_builder().size(19).build()) variant: ButtonVariant::Empty,
.on_click(delete_confirmation_handler) icon: Some(
.build() StyledIcon {
.into_node(); icon: Icon::Trash,
let close_button = StyledButton::build() size: Some(19),
.empty() ..Default::default()
.icon(Icon::Close.into_styled_builder().size(24).build()) }
.on_click(close_handler) .into_node(),
.build() ),
.into_node(); on_click: Some(delete_confirmation_handler),
..Default::default()
}
.into_node();
let close_button = StyledButton {
variant: ButtonVariant::Empty,
icon: Some(
StyledIcon {
icon: Icon::Close,
size: Some(24),
..Default::default()
}
.into_node(),
),
on_click: Some(close_handler),
..Default::default()
}
.into_node();
let issue_type_select = StyledSelect::build() let issue_type_select = {
.dropdown_width(150) let id = modal.id;
.name("type") let issue_type = &payload.issue_type;
.text_filter(top_type_state.text_filter.as_str()) let text = format!("{} - {}", issue_type, id);
.opened(top_type_state.opened)
.valid(true) StyledSelect {
.options( id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
IssueType::default() name: "type",
.into_iter() text_filter: top_type_state.text_filter.as_str(),
.map(|t| t.into_child().name("type")), dropdown_width: Some(150),
) valid: true,
.selected(vec![{ opened: top_type_state.opened,
let id = modal.id; options: Some(
let issue_type = &payload.issue_type; IssueType::default()
issue_type .into_iter()
.into_child() .map(|t| t.into_child().name("type")),
.name("type") ),
.text_owned(format!("{} - {}", issue_type, id)) selected: vec![{
}]) let name = payload.issue_type.to_label();
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Type, let type_icon = StyledIcon {
))) icon: payload.issue_type.clone().into(),
.into_node(); class_list: name,
..Default::default()
}
.into_node();
StyledSelectChildBuilder {
class_list: name,
text: Some(&text),
icon: Some(type_icon),
value: payload.issue_type.into(),
name: Some("type"),
..Default::default()
}
}],
..Default::default()
}
.into_node()
};
div![ div![
C!["topActions"], C!["topActions"],
@ -156,41 +195,40 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let title = StyledInput::build() let title = StyledInput {
.add_input_class("issueSummary") input_class_list: "issueSummary",
.add_wrapper_class("issueSummary") wrapper_class_list: "issueSummary textarea",
.add_wrapper_class("textarea") value: modal.title_state.value.as_str(),
.state(&modal.title_state) valid: modal.title_state.is_valid(),
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( id: Some(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Title, IssueFieldId::Title,
))) ))),
.into_node(); ..Default::default()
}
.into_node();
let description = { let description = {
StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( StyledEditor {
IssueFieldId::Description, id: Some(FieldId::EditIssueModal(EditIssueModalSection::Issue(
))) IssueFieldId::Description,
.initial_text(description_state.initial_text.as_str()) ))),
.html(payload.description.as_ref().cloned().unwrap_or_default()) initial_text: description_state.initial_text.as_str(),
.mode(description_state.mode.clone()) text: description_state.initial_text.as_str(),
.update_on(Ev::Change) html: payload.description.as_deref().unwrap_or_default(),
.build() mode: description_state.mode.clone(),
update_event: Ev::Change,
}
.into_node() .into_node()
}; };
let description_field = StyledField::build().input(description).build().into_node(); let description_field = StyledField::build().input(description).build().into_node();
let user_avatar = StyledAvatar::build() let user_avatar = StyledAvatar {
.add_class("userAvatar") avatar_url: model.user.as_ref().and_then(|u| u.avatar_url.as_deref()),
.size(32) size: 32,
.avatar_url( class_list: "userAvatar",
model ..StyledAvatar::default()
.user }
.as_ref() .into_node();
.and_then(|u| u.avatar_url.as_deref())
.unwrap_or_default(),
)
.build()
.into_node();
let create_comment = if comment_form.creating && comment_form.id.is_none() { let create_comment = if comment_form.creating && comment_form.id.is_none() {
build_comment_form(comment_form) build_comment_form(comment_form)
@ -206,11 +244,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
vec![div![C!["fakeTextArea"], "Add a comment...", handler]] vec![div![C!["fakeTextArea"], "Add a comment...", handler]]
}; };
let comments: Vec<Node<Msg>> = model let comments = model.comments.iter().flat_map(|c| comment(model, modal, c));
.comments
.iter()
.flat_map(|c| comment(model, modal, c))
.collect();
div![ div![
C!["left"], C!["left"],
@ -249,110 +283,105 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let status = StyledSelect::build() let status = StyledSelect {
.name("status") id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
.opened(status_state.opened) name: "status",
.normal() opened: status_state.opened,
.text_filter(status_state.text_filter.as_str()) variant: SelectVariant::Normal,
.options( text_filter: status_state.text_filter.as_str(),
options: Some(
model model
.issue_statuses .issue_statuses
.iter() .iter()
.map(|opt| opt.to_child().name("status")), .map(|opt| opt.to_child().name("status")),
) ),
.selected( selected: model
model .issue_statuses
.issue_statuses .iter()
.iter() .filter(|is| is.id == payload.issue_status_id)
.filter(|is| is.id == payload.issue_status_id) .map(|is| is.to_child().name("status"))
.map(|is| is.to_child().name("status")) .collect(),
.collect(),
) valid: true,
.valid(true) ..Default::default()
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( }
IssueFieldId::IssueStatusId, .into_node();
)))
.into_node();
let status_field = StyledField::build() let status_field = StyledField::build()
.input(status) .input(status)
.label("Status") .label("Status")
.build() .build()
.into_node(); .into_node();
let assignees = StyledSelect::build() let assignees = StyledSelect {
.name("assignees") id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
.opened(assignees_state.opened) name: "assignees",
.empty() variant: SelectVariant::Empty,
.multi() is_multi: true,
.text_filter(assignees_state.text_filter.as_str()) opened: assignees_state.opened,
.options( text_filter: assignees_state.text_filter.as_str(),
options: Some(
model model
.users .users
.iter() .iter()
.map(|user| user.to_child().name("assignees")), .map(|user| user.to_child().name("assignees")),
) ),
.selected( selected: model
model .users
.users .iter()
.iter() .filter(|user| payload.user_ids.contains(&user.id))
.filter(|user| payload.user_ids.contains(&user.id)) .map(|user| user.to_child().name("assignees"))
.map(|user| user.to_child().name("assignees")) .collect(),
.collect(), ..Default::default()
) }
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( .into_node();
IssueFieldId::Assignees,
)))
.into_node();
let assignees_field = StyledField::build() let assignees_field = StyledField::build()
.input(assignees) .input(assignees)
.label("Assignees") .label("Assignees")
.build() .build()
.into_node(); .into_node();
let reporter = StyledSelect::build() let reporter = StyledSelect {
.name("reporter") id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
.opened(reporter_state.opened) name: "reporter",
.empty() opened: reporter_state.opened,
.text_filter(reporter_state.text_filter.as_str()) variant: SelectVariant::Empty,
.options( text_filter: reporter_state.text_filter.as_str(),
options: Some(
model model
.users .users
.iter() .iter()
.map(|user| user.to_child().name("reporter")), .map(|user| user.to_child().name("reporter")),
) ),
.selected( selected: model
model .users
.users .iter()
.iter() .filter(|user| payload.reporter_id == user.id)
.filter(|user| payload.reporter_id == user.id) .map(|user| user.to_child().name("reporter"))
.map(|user| user.to_child().name("reporter")) .collect(),
.collect(), ..Default::default()
) }
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( .into_node();
IssueFieldId::Reporter,
)))
.into_node();
let reporter_field = StyledField::build() let reporter_field = StyledField::build()
.input(reporter) .input(reporter)
.label("Reporter") .label("Reporter")
.build() .build()
.into_node(); .into_node();
let priority = StyledSelect::build() let priority = StyledSelect {
.name("priority") id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
.opened(priority_state.opened) name: "priority",
.empty() variant: SelectVariant::Empty,
.text_filter(priority_state.text_filter.as_str()) opened: priority_state.opened,
.options( text_filter: priority_state.text_filter.as_str(),
options: Some(
IssuePriority::default() IssuePriority::default()
.into_iter() .into_iter()
.map(|p| p.into_child().name("priority")), .map(|p| p.into_child().name("priority")),
) ),
.selected(vec![payload.priority.into_child().name("priority")]) selected: vec![payload.priority.into_child().name("priority")],
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( ..Default::default()
IssueFieldId::Priority, }
))) .into_node();
.into_node();
let priority_field = StyledField::build() let priority_field = StyledField::build()
.input(priority) .input(priority)
.label("Priority") .label("Priority")

View File

@ -13,6 +13,8 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> { pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
let submit_comment_form = mouse_ev(Ev::Click, move |ev| { let submit_comment_form = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
@ -26,26 +28,30 @@ pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
)) ))
}); });
let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment( let text_area = StyledTextarea {
CommentFieldId::Body, id: Some(FieldId::EditIssueModal(EditIssueModalSection::Comment(
))) CommentFieldId::Body,
.value(form.body.as_str()) ))),
.placeholder("Add a comment...") value: form.body.as_str(),
.build() placeholder: "Add a comment...",
..Default::default()
}
.into_node(); .into_node();
let submit = StyledButton::build() let submit = StyledButton {
.primary() variant: ButtonVariant::Primary,
.on_click(submit_comment_form) on_click: Some(submit_comment_form),
.text("Save") text: Some("Save"),
.build() ..Default::default()
.into_node(); }
let cancel = StyledButton::build() .into_node();
.empty() let cancel = StyledButton {
.on_click(close_comment_form) variant: ButtonVariant::Empty,
.text("Cancel") on_click: Some(close_comment_form),
.build() text: Some("Cancel"),
.into_node(); ..Default::default()
}
.into_node();
vec![text_area, div![C!["actions"], submit, cancel]] vec![text_area, div![C!["actions"], submit, cancel]]
} }
@ -55,12 +61,13 @@ pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Opti
let user = model.users_by_id.get(&comment.user_id)?; let user = model.users_by_id.get(&comment.user_id)?;
let avatar = StyledAvatar::build() let avatar = StyledAvatar {
.size(32) avatar_url: user.avatar_url.as_deref(),
.avatar_url(user.avatar_url.as_deref()?) size: 32,
.add_class("userAvatar") class_list: "userAvatar",
.build() ..StyledAvatar::default()
.into_node(); }
.into_node();
let buttons = if model.user.as_ref().map(|u| u.id) == Some(comment.user_id) { let buttons = if model.user.as_ref().map(|u| u.id) == Some(comment.user_id) {
let comment_id = comment.id; let comment_id = comment.id;
@ -68,26 +75,28 @@ pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Opti
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalOpened(ModalType::DeleteCommentConfirm(Some(comment_id))) Msg::ModalOpened(ModalType::DeleteCommentConfirm(Some(comment_id)))
}); });
let edit_button = StyledButton::build() let edit_button = StyledButton {
.add_class("editButton") class_list: "editButton",
.on_click(mouse_ev(Ev::Click, move |_| { on_click: Some(mouse_ev(Ev::Click, move |_| {
Msg::ModalChanged(FieldChange::EditComment( Msg::ModalChanged(FieldChange::EditComment(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
comment_id, comment_id,
)) ))
})) })),
.text("Edit") text: Some("Edit"),
.empty() variant: ButtonVariant::Empty,
.build() ..Default::default()
.into_node(); }
.into_node();
let cancel_button = StyledButton::build() let cancel_button = StyledButton {
.add_class("deleteButton") class_list: "deleteButton",
.on_click(delete_comment_handler) on_click: Some(delete_comment_handler),
.text("Delete") text: Some("Delete"),
.empty() variant: ButtonVariant::Empty,
.build() ..Default::default()
.into_node(); }
.into_node();
vec![edit_button, cancel_button] vec![edit_button, cancel_button]
} else { } else {

View File

@ -69,18 +69,20 @@ pub fn view(model: &Model, modal: &super::Model) -> Node<Msg> {
div![C!["inputContainer"], time_remaining_field] div![C!["inputContainer"], time_remaining_field]
]; ];
let close = StyledButton::build() let close = StyledButton {
.text("Done") text: Some("Done"),
.on_click(mouse_ev(Ev::Click, |_| Msg::ModalDropped)) on_click: Some(mouse_ev(Ev::Click, |_| Msg::ModalDropped)),
.build() ..Default::default()
.into_node(); }
.into_node();
StyledModal::build() StyledModal {
.add_class("timeTrackingModal") class_list: "timeTrackingModal",
.children(vec![modal_title, tracking, inputs, div![C!["actions"], close]].into_iter()) children: vec![modal_title, tracking, inputs, div![C!["actions"], close]],
.width(400) width: Some(400),
.build() ..Default::default()
.into_node() }
.into_node()
} }
#[inline] #[inline]
@ -94,27 +96,32 @@ pub fn time_tracking_field(
let fibonacci_values = fibonacci_values(); let fibonacci_values = fibonacci_values();
let input = match time_tracking_type { let input = match time_tracking_type {
TimeTracking::Untracked => empty![], TimeTracking::Untracked => empty![],
TimeTracking::Fibonacci => StyledSelect::build() TimeTracking::Fibonacci => StyledSelect {
.selected( id: field_id,
select_state selected: select_state
.values .values
.iter() .iter()
.map(|n| (*n).to_child()) .map(|n| (*n).to_child())
.collect(), .collect(),
)
.state(select_state) text_filter: select_state.text_filter.as_str(),
.options(fibonacci_values.iter().map(|v| v.to_child())) opened: select_state.opened,
.build(field_id) options: Some(fibonacci_values.iter().map(|v| v.to_child())),
.into_node(), ..Default::default()
TimeTracking::Hourly => StyledInput::build() }
.state(input_state) .into_node(),
.valid(true) TimeTracking::Hourly => StyledInput {
.build(field_id) valid: input_state.is_valid(),
.into_node(), value: input_state.value.as_str(),
id: Some(field_id),
..Default::default()
}
.into_node(),
}; };
StyledField::build() StyledField {
.input(input) input,
.label(label) label,
.build() ..Default::default()
.into_node() }
.into_node()
} }

View File

@ -142,6 +142,12 @@ macro_rules! match_page {
_ => return, _ => return,
} }
}; };
($model: ident, $ty: ident; Empty) => {
match &$model.page_content {
PageContent::$ty(page) => page,
_ => return Node::Empty,
}
};
} }
#[macro_export] #[macro_export]
macro_rules! match_page_mut { macro_rules! match_page_mut {

View File

@ -4,6 +4,7 @@ use {
styled_button::StyledButton, styled_field::StyledField, styled_form::StyledForm, styled_button::StyledButton, styled_field::StyledField, styled_form::StyledForm,
styled_input::StyledInput, styled_input::StyledInput,
}, },
match_page,
model::{Model, PageContent}, model::{Model, PageContent},
pages::invite_page::InvitePage, pages::invite_page::InvitePage,
shared::{outer_layout, ToNode}, shared::{outer_layout, ToNode},
@ -14,11 +15,10 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let page = match &model.page_content { let page = match_page!(model, Invite; Empty);
PageContent::Invite(page) => page,
_ => return empty![],
};
let token_field = token_field(page); let token_field = token_field(page);
let submit_field = submit(page); let submit_field = submit(page);
@ -43,24 +43,32 @@ pub fn view(model: &Model) -> Node<Msg> {
} }
fn submit(_page: &InvitePage) -> Node<Msg> { fn submit(_page: &InvitePage) -> Node<Msg> {
let submit = StyledButton::build() let submit = StyledButton {
.text("Accept") text: Some("Accept"),
.primary() variant: ButtonVariant::Primary,
.build() ..Default::default()
.into_node(); }
StyledField::build().input(submit).build().into_node() .into_node();
StyledField {
input: submit,
..Default::default()
}
.into_node()
} }
fn token_field(page: &InvitePage) -> Node<Msg> { fn token_field(page: &InvitePage) -> Node<Msg> {
let token = StyledInput::build() let input = StyledInput {
.valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none()) valid: !page.token_touched || is_token(page.token.as_str()) && page.error.is_none(),
.value(page.token.as_str()) id: Some(FieldId::Invite(InviteFieldId::Token)),
.build(FieldId::Invite(InviteFieldId::Token)) value: page.token.as_str(),
.into_node(); ..Default::default()
}
.into_node();
StyledField::build() StyledField {
.input(token) input,
.label("Your invite token") label: "Your invite token",
.build() ..Default::default()
.into_node() }
.into_node()
} }

View File

@ -15,53 +15,65 @@ use {
std::collections::HashMap, std::collections::HashMap,
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_input::InputVariant;
use crate::components::styled_select::SelectVariant;
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::Profile(profile_page) => profile_page, PageContent::Profile(profile_page) => profile_page,
_ => return empty![], _ => return empty![],
}; };
let avatar = StyledImageInput::build(FieldId::Profile(UsersFieldId::Avatar)) let avatar = StyledImageInput {
.add_class("avatar") id: FieldId::Profile(UsersFieldId::Avatar),
.state(&page.avatar) class_list: "avatar",
.build() url: page.avatar.url.as_deref(),
.into_node(); }
.into_node();
let username = StyledInput::build() let username = StyledInput {
.state(&page.name) id: Some(FieldId::Profile(UsersFieldId::Username)),
.valid(true) valid: page.name.is_valid(),
.primary() value: page.name.value.as_str(),
.build(FieldId::Profile(UsersFieldId::Username)) variant: InputVariant::Primary,
.into_node(); ..Default::default()
let username_field = StyledField::build() }
.label("Username") .into_node();
.input(username) let username_field = StyledField {
.build() label: "Username",
.into_node(); input: username,
..Default::default()
}
.into_node();
let email = StyledInput::build() let email = StyledInput {
.state(&page.email) id: Some(FieldId::Profile(UsersFieldId::Email)),
.valid(true) valid: page.email.is_valid(),
.primary() value: page.email.value.as_str(),
.build(FieldId::Profile(UsersFieldId::Username)) variant: InputVariant::Primary,
.into_node(); ..Default::default()
let email_field = StyledField::build() }
.label("E-Mail") .into_node();
.input(email) let email_field = StyledField {
.build() label: "E-Mail",
.into_node(); input: email,
..Default::default()
}
.into_node();
let current_project = build_current_project(model, page); let current_project = build_current_project(model, page);
let submit = StyledButton::build() let submit = StyledButton {
.primary() variant: ButtonVariant::Primary,
.text("Save") text: Some("Save"),
.on_click(mouse_ev(Ev::Click, |ev| { on_click: Some(mouse_ev(Ev::Click, |ev| {
ev.prevent_default(); ev.prevent_default();
Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm)) Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm))
})) })),
.build() ..Default::default()
.into_node(); }
.into_node();
let submit_field = StyledField::build().input(submit).build().into_node(); let submit_field = StyledField::build().input(submit).build().into_node();
let content = StyledForm::build() let content = StyledForm::build()
@ -81,44 +93,47 @@ pub fn view(model: &Model) -> Node<Msg> {
} }
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> { fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
let inner = let inner = if model.projects.len() <= 1 {
if model.projects.len() <= 1 { let name = model
let name = model .project
.project .as_ref()
.as_ref() .map(|p| p.name.as_str())
.map(|p| p.name.as_str()) .unwrap_or_default();
.unwrap_or_default(); span![name]
span![name] } else {
} else { let mut project_by_id = HashMap::new();
let mut project_by_id = HashMap::new(); for p in model.projects.iter() {
for p in model.projects.iter() { project_by_id.insert(p.id, p);
project_by_id.insert(p.id, p); }
} let mut joined_projects = HashMap::new();
let mut joined_projects = HashMap::new(); for p in model.user_projects.iter() {
for p in model.user_projects.iter() { joined_projects.insert(p.project_id, p);
joined_projects.insert(p.project_id, p); }
}
StyledSelect::build() StyledSelect {
.name("current_project") id: FieldId::Profile(UsersFieldId::CurrentProject),
.normal() name: "current_project",
.options(model.projects.iter().filter_map(|project| { valid: true,
joined_projects.get(&project.id).map(|_| project.to_child()) opened: page.current_project.opened,
})) text_filter: page.current_project.text_filter.as_str(),
.selected( variant: SelectVariant::Normal,
page.current_project options: Some(model.projects.iter().filter_map(|project| {
.values joined_projects.get(&project.id).map(|_| project.to_child())
.iter() })),
.filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child())) selected: page
.collect(), .current_project
) .values
.state(&page.current_project) .iter()
.build(FieldId::Profile(UsersFieldId::CurrentProject)) .filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child()))
.into_node() .collect(),
}; ..Default::default()
StyledField::build() }
.label("Current project")
.input(div![C!["project-name"], inner])
.build()
.into_node() .into_node()
};
StyledField {
label: "Current project",
input: div![C!["project-name"], inner],
..Default::default()
}
.into_node()
} }

View File

@ -9,6 +9,8 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
pub fn project_board_lists(model: &Model) -> Node<Msg> { pub fn project_board_lists(model: &Model) -> Node<Msg> {
let project_page = match &model.page_content { let project_page = match &model.page_content {
PageContent::Project(project_page) => project_page, PageContent::Project(project_page) => project_page,
@ -35,10 +37,10 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
let epic_name = match per_epic.epic_ref.as_ref() { let epic_name = match per_epic.epic_ref.as_ref() {
Some((id, name)) => { Some((id, name)) => {
let id = *id; let id = *id;
let edit_button = StyledButton::build() let edit_button = StyledButton {
.empty() variant: ButtonVariant::Empty,
.icon(Icon::EditAlt) icon: Some(Icon::EditAlt.into_node()),
.on_click(mouse_ev("click", move |ev| { on_click: Some(mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
seed::Url::new() seed::Url::new()
@ -46,13 +48,14 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
.add_path_part(id.to_string()) .add_path_part(id.to_string())
.go_and_push(); .go_and_push();
Msg::ChangePage(Page::EditEpic(id)) Msg::ChangePage(Page::EditEpic(id))
})) })),
.build() ..Default::default()
.into_node(); }
let delete_button = StyledButton::build() .into_node();
.empty() let delete_button = StyledButton {
.icon(Icon::DeleteAlt) variant: ButtonVariant::Empty,
.on_click(mouse_ev("click", move |ev| { icon: Some(Icon::DeleteAlt.into_node()),
on_click: Some(mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
seed::Url::new() seed::Url::new()
@ -60,9 +63,10 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
.add_path_part(id.to_string()) .add_path_part(id.to_string())
.go_and_push(); .go_and_push();
Msg::ChangePage(Page::DeleteEpic(id)) Msg::ChangePage(Page::DeleteEpic(id))
})) })),
.build() ..Default::default()
.into_node(); }
.into_node();
div![ div![
C!["epicHeader"], C!["epicHeader"],
@ -130,26 +134,31 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
.iter() .iter()
.filter_map(|id| model.users_by_id.get(id)) .filter_map(|id| model.users_by_id.get(id))
.map(|user| { .map(|user| {
StyledAvatar::build() StyledAvatar {
.size(24) avatar_url: user.avatar_url.as_deref(),
.name(user.name.as_str()) size: 24,
.avatar_url(user.avatar_url.as_deref().unwrap_or_default()) name: &user.name,
.user_index(0) ..StyledAvatar::default()
.build() }
.into_node() .into_node()
}) })
.collect(); .collect();
let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into()) let issue_type_icon = StyledIcon {
.with_color(issue.issue_type.to_str()) icon: issue.issue_type.into(),
.build() class_list: issue.issue_type.to_str(),
.into_node(); color: Some(issue.issue_type.to_str()),
..Default::default()
}
.into_node();
let priority_icon = StyledIcon::build(issue.priority.into()) let priority_icon = StyledIcon {
.add_class(issue.priority.to_str()) icon: issue.priority.into(),
.with_color(issue.priority.to_str()) class_list: issue.priority.to_str(),
.build() color: Some(issue.priority.to_str()),
.into_node(); ..Default::default()
}
.into_node();
let issue_id = issue.id; let issue_id = issue.id;
let drag_started = drag_ev(Ev::DragStart, move |ev| { let drag_started = drag_ev(Ev::DragStart, move |ev| {

View File

@ -14,29 +14,33 @@ pub fn project_board_filters(model: &Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let search_input = StyledInput::build() let search_input = StyledInput {
.icon(Icon::Search) value: project_page.text_filter.as_str(),
.valid(true) valid: true,
.value(project_page.text_filter.as_str()) id: Some(FieldId::TextFilterBoard),
.build(FieldId::TextFilterBoard) icon: Some(Icon::Search),
.into_node(); ..Default::default()
}
.into_node();
let only_my = StyledButton::build() let only_my = StyledButton {
.empty() variant: ButtonVariant::Empty,
.active(project_page.only_my_filter) active: project_page.only_my_filter,
.text("Only My Issues") text: Some("Only My Issues"),
.add_class("filterChild") class_list: "filterChild",
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)) on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)),
.build() ..Default::default()
.into_node(); }
.into_node();
let recently_updated = StyledButton::build() let recently_updated = StyledButton {
.empty() variant: ButtonVariant::Empty,
.text("Recently Updated") text: Some("Recently Updated"),
.add_class("filterChild") class_list: "filterChild",
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)) on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)),
.build() ..Default::default()
.into_node(); }
.into_node();
let clear_all = if project_page.only_my_filter let clear_all = if project_page.only_my_filter
|| project_page.recently_updated_filter || project_page.recently_updated_filter
@ -75,17 +79,18 @@ pub fn avatars_filters(model: &Model) -> Node<Msg> {
.map(|(idx, user)| { .map(|(idx, user)| {
let user_id = user.id; let user_id = user.id;
let active = active_avatar_filters.contains(&user_id); let active = active_avatar_filters.contains(&user_id);
let styled_avatar = StyledAvatar::build() let styled_avatar = StyledAvatar {
.avatar_url(user.avatar_url.as_deref().unwrap_or_default()) avatar_url: user.avatar_url.as_deref(),
.on_click(mouse_ev(Ev::Click, move |_| { name: &user.name,
on_click: Some(mouse_ev(Ev::Click, move |_| {
Msg::ProjectAvatarFilterChanged(user_id, active) Msg::ProjectAvatarFilterChanged(user_id, active)
})) })),
.name(user.name.as_str()) user_index: idx,
.user_index(idx) ..StyledAvatar::default()
.build() }
.into_node(); .into_node();
div![ div![
if active { Some(C!["isActive"]) } else { None }, IF![active => C!["isActive"]],
C!["avatarIsActiveBorder"], C!["avatarIsActiveBorder"],
styled_avatar styled_avatar
] ]

View File

@ -21,6 +21,10 @@ use {
std::collections::HashMap, std::collections::HashMap,
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_input::InputVariant;
use crate::components::styled_select::SelectVariant;
// use crate::shared::styled_rte::StyledRte; // use crate::shared::styled_rte::StyledRte;
static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt"); static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
@ -60,29 +64,31 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking))
.into_node(); .into_node();
let time_tracking_type: TimeTracking = page.time_tracking.value.into(); let time_tracking_type: TimeTracking = page.time_tracking.value.into();
let time_tracking_field = StyledField::build() let time_tracking_field = StyledField {
.input(time_tracking) input: time_tracking,
.tip(match time_tracking_type { tip: Some(match time_tracking_type {
TimeTracking::Fibonacci => TIME_TRACKING_FIBONACCI, TimeTracking::Fibonacci => TIME_TRACKING_FIBONACCI,
TimeTracking::Hourly => TIME_TRACKING_HOURLY, TimeTracking::Hourly => TIME_TRACKING_HOURLY,
_ => "", _ => "",
}) }),
.build() ..Default::default()
.into_node(); }
.into_node();
let columns_field = columns_section(model, page); let columns_field = columns_section(model, page);
let save_button = StyledButton::build() let save_button = StyledButton {
.add_class("actionButton") class_list: "actionButton",
.on_click(mouse_ev(Ev::Click, |ev| { on_click: Some(mouse_ev(Ev::Click, |ev| {
ev.prevent_default(); ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings( Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitProjectSettingsForm, ProjectPageChange::SubmitProjectSettingsForm,
)) ))
})) })),
.text("Save changes") text: Some("Save changes"),
.build() ..Default::default()
.into_node(); }
.into_node();
let form = StyledForm::build() let form = StyledForm::build()
.heading("Project Details") .heading("Project Details")
@ -110,13 +116,15 @@ pub fn view(model: &model::Model) -> Node<Msg> {
/// Build project name input with styled field wrapper /// Build project name input with styled field wrapper
fn name_field(page: &ProjectSettingsPage) -> Node<Msg> { fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name)) let name = StyledTextarea {
.value(page.payload.name.as_deref().unwrap_or_default()) id: Some(FieldId::ProjectSettings(ProjectFieldId::Name)),
.height(39) value: page.payload.name.as_deref().unwrap_or_default(),
.max_height(39) height: 39,
.disable_auto_resize() max_height: 39,
.build() disable_auto_resize: true,
.into_node(); ..Default::default()
}
.into_node();
StyledField::build() StyledField::build()
.label("Name") .label("Name")
.input(name) .input(name)
@ -127,13 +135,15 @@ fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
/// Build project url input with styled field wrapper /// Build project url input with styled field wrapper
fn url_field(page: &ProjectSettingsPage) -> Node<Msg> { fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url)) let url = StyledTextarea {
.height(39) id: Some(FieldId::ProjectSettings(ProjectFieldId::Url)),
.max_height(39) height: 39,
.disable_auto_resize() max_height: 39,
.value(page.payload.url.as_deref().unwrap_or_default()) disable_auto_resize: true,
.build() value: page.payload.url.as_deref().unwrap_or_default(),
.into_node(); ..Default::default()
}
.into_node();
StyledField::build() StyledField::build()
.label("Url") .label("Url")
.input(url) .input(url)
@ -144,52 +154,53 @@ fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
/// Build project description text area with styled field wrapper /// Build project description text area with styled field wrapper
fn description_field(page: &ProjectSettingsPage) -> Node<Msg> { fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description)) let description = StyledEditor {
.text( id: Some(FieldId::ProjectSettings(ProjectFieldId::Description)),
page.payload initial_text: page.payload.description.as_deref().unwrap_or_default(),
.description text: page.payload.description.as_deref().unwrap_or_default(),
.as_ref() html: page.payload.description.as_deref().unwrap_or_default(),
.cloned() mode: page.description_mode.clone(),
.unwrap_or_default(), update_event: Ev::Change,
) }
.update_on(Ev::Change) .into_node();
.mode(page.description_mode.clone()) StyledField {
.build() label: "Description",
.into_node(); tip: Some("Describe the project in as much detail as you'd like."),
StyledField::build() input: description,
.input(description) ..Default::default()
.label("Description") }
.tip("Describe the project in as much detail as you'd like.") .into_node()
.build()
.into_node()
} }
/// Build project category dropdown with styled field wrapper /// Build project category dropdown with styled field wrapper
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> { fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
let category = StyledSelect::build() let category = StyledSelect {
.opened(page.project_category_state.opened) id: FieldId::ProjectSettings(ProjectFieldId::Category),
.text_filter(page.project_category_state.text_filter.as_str()) opened: page.project_category_state.opened,
.valid(true) text_filter: page.project_category_state.text_filter.as_str(),
.normal() valid: true,
.options( variant: SelectVariant::Normal,
options: Some(
ProjectCategory::default() ProjectCategory::default()
.into_iter() .into_iter()
.map(|c| c.into_child()), .map(|c| c.into_child()),
) ),
.selected(vec![page selected: vec![page
.payload .payload
.category .category
.as_ref() .as_ref()
.cloned() .cloned()
.unwrap_or_default() .unwrap_or_default()
.into_child()]) .into_child()],
.build(FieldId::ProjectSettings(ProjectFieldId::Category)) ..Default::default()
.into_node(); }
StyledField::build() .into_node();
.label("Project Category") StyledField {
.input(category) input: category,
.build() label: "Project Category",
.into_node() ..Default::default()
}
.into_node()
} }
/// Build draggable columns preview with option to remove and add new columns /// Build draggable columns preview with option to remove and add new columns
@ -216,13 +227,13 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
add_column(page, column_style.as_str()) add_column(page, column_style.as_str())
] ]
]; ];
StyledField::build() StyledField {
.add_class("columnsField") label: "Columns",
.input(columns_section) tip: Some("Double-click on name to change it."),
.label("Columns") input: columns_section,
.tip("Double-click on name to change it.") class_list: "columnsField",
.build() }
.into_node() .into_node()
} }
#[inline] #[inline]
@ -246,20 +257,23 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
))) )))
}); });
let input = StyledInput::build() let input = StyledInput {
.state(&page.name) value: page.name.value.as_str(),
.primary() valid: page.name.is_valid(),
.auto_focus() auto_focus: true,
.on_input_ev(blur) variant: InputVariant::Primary,
.build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)),
.into_node(); input_handlers: vec![blur],
..Default::default()
}
.into_node();
div![ div![
C!["columnPreview"], C!["columnPreview"],
div![C!["columnName"], form![on_submit, input]] div![C!["columnName"], form![on_submit, input]]
] ]
} else { } else {
let add_column = StyledIcon::build(Icon::Plus).build().into_node(); let add_column = StyledIcon::from(Icon::Plus).into_node();
div![ div![
C!["columnPreview"], C!["columnPreview"],
attrs![At::Style => column_style], attrs![At::Style => column_style],
@ -282,13 +296,16 @@ fn column_preview(
ProjectPageChange::EditIssueStatusName(None), ProjectPageChange::EditIssueStatusName(None),
)) ))
}); });
let input = StyledInput::build() let input = StyledInput {
.state(&page.name) value: page.name.value.as_str(),
.primary() valid: page.name.is_valid(),
.auto_focus() variant: InputVariant::Primary,
.on_input_ev(blur) auto_focus: true,
.build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) input_handlers: vec![blur],
.into_node(); id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)),
..Default::default()
}
.into_node();
div![C!["columnPreview"], div![C!["columnName"], input]] div![C!["columnPreview"], div![C!["columnName"], input]]
} else { } else {
@ -337,13 +354,14 @@ fn show_column_preview(
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalOpened(ModalType::DeleteIssueStatusModal(Some(id))) Msg::ModalOpened(ModalType::DeleteIssueStatusModal(Some(id)))
}); });
let delete = StyledButton::build() let delete = StyledButton {
.primary() variant: ButtonVariant::Primary,
.add_class("removeColumn") class_list: "removeColumn",
.icon(Icon::Trash) icon: Some(Icon::Trash.into_node()),
.on_click(on_delete) on_click: Some(on_delete),
.build() ..Default::default()
.into_node(); }
.into_node();
div![C!["removeColumn"], delete] div![C!["removeColumn"], delete]
} else { } else {
div![ div![

View File

@ -12,6 +12,8 @@ use {
std::collections::HashMap, std::collections::HashMap,
}; };
use crate::components::styled_icon::Icon;
const SVG_MARGIN_X: u32 = 10; const SVG_MARGIN_X: u32 = 10;
const SVG_DRAWABLE_HEIGHT: u32 = 300; const SVG_DRAWABLE_HEIGHT: u32 = 300;
const SVG_HEIGHT: u32 = SVG_DRAWABLE_HEIGHT + 30; const SVG_HEIGHT: u32 = SVG_DRAWABLE_HEIGHT + 30;
@ -200,27 +202,26 @@ fn issue_list(page: &ReportsPage, project_name: &str, this_month_updated: &[&Iss
} = issue; } = issue;
let day = date.format("%Y-%m-%d").to_string(); let day = date.format("%Y-%m-%d").to_string();
let type_icon = StyledIcon::build(issue_type.clone().into()) let type_icon = StyledIcon::from(Icon::from(issue_type.clone()))
.build()
.into_node(); .into_node();
let priority_icon = StyledIcon::build(priority.clone().into()) let priority_icon = StyledIcon::from(Icon::from(priority.clone()))
.build()
.into_node(); .into_node();
let desc = Node::from_html(None, let desc = Node::from_html(None,
description description
.as_deref() .as_deref()
.unwrap_or_default() .unwrap_or_default(),
); );
let link = StyledLink::build() let link = StyledLink {
.with_icon() children: vec![
.text(format!("{}-{}", project_name, id).as_str()) Icon::Link.into_node(),
.href(format!("/issues/{}", id).as_str()) span![format!("{}-{}", project_name, id).as_str()]
.build() ],
.into_node(); class_list: "withIcon",
href: format!("/issues/{}", id).as_str(),
}.into_node();
li![ li![
C!["issue"], C!["issue", selection_state.to_str()],
C![selection_state.to_str()],
div![C!["number"], link], div![C!["number"], link],
div![C!["type"], type_icon], div![C!["type"], type_icon],
IF!( selection_state != SelectionState::NotSelected => div![C!["priority"], priority_icon]), IF!( selection_state != SelectionState::NotSelected => div![C!["priority"], priority_icon]),

View File

@ -16,28 +16,34 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::SignIn(page) => page, PageContent::SignIn(page) => page,
_ => return empty![], _ => return empty![],
}; };
let username = StyledInput::build() let username = StyledInput {
.value(page.username.as_str()) value: page.username.as_str(),
.valid(is_valid_username(page.username_touched, &page.username)) valid: is_valid_username(page.username_touched, &page.username),
.build(FieldId::SignIn(SignInFieldId::Username)) id: Some(FieldId::SignIn(SignInFieldId::Username)),
.into_node(); ..Default::default()
}
.into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
.input(username) .input(username)
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build() let email = StyledInput {
.value(page.email.as_str()) value: page.email.as_str(),
.valid(is_valid_email(page.email_touched, page.email.as_str())) valid: is_valid_email(page.email_touched, page.email.as_str()),
.build(FieldId::SignIn(SignInFieldId::Email)) id: Some(FieldId::SignIn(SignInFieldId::Email)),
.into_node(); ..Default::default()
}
.into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")
.input(email) .input(email)
@ -45,33 +51,39 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node(); .into_node();
let submit = if page.login_success { let submit = if page.login_success {
StyledButton::build() StyledButton {
.success() variant: ButtonVariant::Success,
.text("✓ Please check your mail") text: Some("✓ Please check your mail"),
..Default::default()
}
} else { } else {
StyledButton::build() StyledButton {
.primary() variant: ButtonVariant::Primary,
.text("Sign In") text: Some("Sign In"),
.on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest)) on_click: Some(mouse_ev(Ev::Click, |_| Msg::SignInRequest)),
..Default::default()
}
}
.into_node();
let register_link = StyledLink {
children: vec![span!["Register"]],
class_list: "signUpLink",
href: "/register",
}
.into_node();
let submit_field = StyledField {
input: div![C!["twoRow"], submit, register_link],
..Default::default()
} }
.build()
.into_node(); .into_node();
let register_link = StyledLink::build()
.text("Register")
.href("/register")
.add_class("signUpLink")
.build()
.into_node();
let submit_field = StyledField::build()
.input(div![C!["twoRow"], submit, register_link,])
.build()
.into_node();
let help_icon = StyledIcon::build(Icon::Help) let help_icon = StyledIcon {
.add_class("noPasswordHelp") icon: Icon::Help,
.size(22) class_list: "noPasswordHelp",
.build() size: Some(22),
.into_node(); ..Default::default()
}
.into_node();
let no_pass_section = div![ let no_pass_section = div![
C!["noPasswordSection"], C!["noPasswordSection"],
@ -80,19 +92,16 @@ pub fn view(model: &model::Model) -> Node<Msg> {
span!["Why I don't see password?"] span!["Why I don't see password?"]
]; ];
let sign_in_form = StyledForm::build() let sign_in_form = StyledForm {
.heading("Sign In to your account") heading: "Sign In to your account",
.on_submit(ev(Ev::Submit, |ev| { fields: vec![username_field, email_field, submit_field, no_pass_section],
on_submit: Some(ev(Ev::Submit, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::SignInRequest Msg::SignInRequest
})) })),
.add_field(username_field) }
.add_field(email_field) .into_node();
.add_field(submit_field)
.add_field(no_pass_section)
.build()
.into_node();
let token = StyledInput::new_with_id_and_value_and_valid( let token = StyledInput::new_with_id_and_value_and_valid(
FieldId::SignIn(SignInFieldId::Token), FieldId::SignIn(SignInFieldId::Token),
@ -105,12 +114,13 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.input(token) .input(token)
.build() .build()
.into_node(); .into_node();
let submit_token = StyledButton::build() let submit_token = StyledButton {
.primary() variant: ButtonVariant::Primary,
.text("Authorize") text: Some("Authorize"),
.on_click(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)) on_click: Some(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)),
.build() ..Default::default()
.into_node(); }
.into_node();
let submit_token_field = StyledField::build().input(submit_token).build().into_node(); let submit_token_field = StyledField::build().input(submit_token).build().into_node();
let bind_token_form = StyledForm::build() let bind_token_form = StyledForm::build()

View File

@ -8,6 +8,7 @@ use {
styled_input::StyledInput, styled_input::StyledInput,
styled_link::StyledLink, styled_link::StyledLink,
}, },
match_page,
model::{self, PageContent}, model::{self, PageContent},
shared::{outer_layout, ToNode}, shared::{outer_layout, ToNode},
validations::is_email, validations::is_email,
@ -17,64 +18,75 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match_page!(model, SignUp; Empty);
PageContent::SignUp(page) => page,
_ => return empty![],
};
let username = StyledInput::build() let username = StyledInput {
.value(page.username.as_str()) value: page.username.as_str(),
.valid(!page.username_touched || page.username.len() > 1) valid: !page.username_touched || page.username.len() > 1,
.build(FieldId::SignUp(SignUpFieldId::Username)) id: Some(FieldId::SignUp(SignUpFieldId::Username)),
.into_node(); ..Default::default()
let username_field = StyledField::build() }
.label("Username") .into_node();
.input(username) let username_field = StyledField {
.build() label: "Username",
.into_node(); input: username,
..Default::default()
let email = StyledInput::build()
.value(page.email.as_str())
.valid(!page.email_touched || is_email(page.email.as_str()))
.build(FieldId::SignUp(SignUpFieldId::Email))
.into_node();
let email_field = StyledField::build()
.label("E-Mail")
.input(email)
.build()
.into_node();
let submit = if page.sign_up_success {
StyledButton::build()
.success()
.text("✓ Please check your mail")
} else {
StyledButton::build()
.primary()
.text("Register")
.on_click(mouse_ev(Ev::Click, |_| Msg::SignUpRequest))
} }
.build()
.into_node(); .into_node();
let sign_in_link = StyledLink::build() let email = StyledInput {
.text("Sign In") value: page.email.as_str(),
.href("/login") valid: !page.email_touched || is_email(page.email.as_str()),
.add_class("signInLink") id: Some(FieldId::SignUp(SignUpFieldId::Email)),
.build() ..Default::default()
.into_node(); }
.into_node();
let email_field = StyledField {
label: "E-Mail",
input: email,
..Default::default()
}
.into_node();
let submit_field = StyledField::build() let submit = if page.sign_up_success {
.input(div![C!["twoRow"], submit, sign_in_link,]) StyledButton {
.build() variant: ButtonVariant::Success,
.into_node(); text: Some("✓ Please check your mail"),
..Default::default()
}
} else {
StyledButton {
variant: ButtonVariant::Primary,
text: Some("Register"),
on_click: Some(mouse_ev(Ev::Click, |_| Msg::SignUpRequest)),
..Default::default()
}
}
.into_node();
let help_icon = StyledIcon::build(Icon::Help) let sign_in_link = StyledLink {
.add_class("noPasswordHelp") children: vec![span!["Sign In"]],
.size(22) class_list: "signInLink",
.build() href: "/login",
.into_node(); }
.into_node();
let submit_field = StyledField {
input: div![C!["twoRow"], submit, sign_in_link],
..Default::default()
}
.into_node();
let help_icon = StyledIcon {
icon: Icon::Help,
class_list: "noPasswordHelp",
size: Some(22),
..Default::default()
}
.into_node();
let no_pass_section = div![ let no_pass_section = div![
C!["noPasswordSection"], C!["noPasswordSection"],

View File

@ -7,13 +7,17 @@ use {
FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged, FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged,
}, },
jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg}, jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg},
seed::prelude::Orders, seed::{log, prelude::Orders},
}; };
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
log!(model);
if model.user.is_none() {
return;
}
if let Msg::ChangePage(Page::Users) = msg { if let Msg::ChangePage(Page::Users) = msg {
build_page_content(model); build_page_content(model);
// return;
} }
let page = match &mut model.page_content { let page = match &mut model.page_content {

View File

@ -13,76 +13,89 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
pub fn view(model: &Model) -> Node<Msg> { use crate::components::styled_button::ButtonVariant;
if model.user.is_none() { use crate::components::styled_input::InputVariant;
return empty![]; use crate::components::styled_select::SelectVariant;
}
pub fn view(model: &Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
PageContent::Users(page) => page, PageContent::Users(page) => page,
_ => return empty![], _ => return empty![],
}; };
let name = StyledInput::build() let name = StyledInput {
.valid(!page.name_touched || page.name.len() >= 3) valid: !page.name_touched || page.name.len() >= 3,
.value(page.name.as_str()) value: page.name.as_str(),
.build(FieldId::Users(UsersFieldId::Username)) variant: InputVariant::Normal,
.into_node(); id: Some(FieldId::Users(UsersFieldId::Username)),
let name_field = StyledField::build() ..Default::default()
.input(name) }
.label("Name") .into_node();
.build() let name_field = StyledField {
.into_node(); label: "Name",
input: name,
..Default::default()
}
.into_node();
let email = StyledInput::build() let email = StyledInput {
.valid(!page.email_touched || is_email(page.email.as_str())) id: Some(FieldId::Users(UsersFieldId::Email)),
.value(page.email.as_str()) valid: !page.email_touched || is_email(page.email.as_str()),
.build(FieldId::Users(UsersFieldId::Email)) value: page.email.as_str(),
.into_node(); variant: InputVariant::Normal,
let email_field = StyledField::build() ..Default::default()
.input(email) }
.label("E-Mail") .into_node();
.build() let email_field = StyledField {
.into_node(); input: email,
label: "E-Mail",
..Default::default()
}
.into_node();
let user_role = StyledSelect::build() let user_role = StyledSelect {
.name("user_role") id: FieldId::Users(UsersFieldId::UserRole),
.valid(true) name: "user_role",
.normal() valid: true,
.state(&page.user_role_state) variant: SelectVariant::Normal,
.selected(vec![page.user_role.into_child()]) text_filter: page.user_role_state.text_filter.as_str(),
.options( opened: page.user_role_state.opened,
selected: vec![page.user_role.into_child()],
options: Some(
UserRole::default() UserRole::default()
.into_iter() .into_iter()
.map(|role| role.into_child()), .map(|role| role.into_child()),
) ),
.build(FieldId::Users(UsersFieldId::UserRole)) ..Default::default()
.into_node(); }
.into_node();
let user_role_field = StyledField::build() let user_role_field = StyledField::build()
.input(user_role) .input(user_role)
.label("Role") .label("Role")
.build() .build()
.into_node(); .into_node();
let submit = StyledButton::build() let submit = StyledButton {
.add_class("submitUserInvite") text: Some("Invite user"),
.active(page.form_state != InvitationFormState::Sent) variant: ButtonVariant::Primary,
.primary() class_list: "submitUserInvite",
.text("Invite user") active: page.form_state != InvitationFormState::Sent,
.build() ..Default::default()
.into_node(); }
.into_node();
let submit_supplement = match page.form_state { let submit_supplement = match page.form_state {
InvitationFormState::Succeed => StyledButton::build() InvitationFormState::Succeed => StyledButton {
.add_class("resetUserInvite") variant: ButtonVariant::Empty,
.active(true) class_list: "resetUserInvite",
.empty() active: true,
.set_type_reset() on_click: Some(mouse_ev(Ev::Click, |_| {
.on_click(mouse_ev(Ev::Click, |_| {
Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm)) Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm))
})) })),
.text("Reset") text: Some("Reset"),
.build() button_type: "reset",
.into_node(), ..Default::default()
}
.into_node(),
InvitationFormState::Failed => div![C!["error"], "There was an error"], InvitationFormState::Failed => div![C!["error"], "There was an error"],
_ => empty![], _ => empty![],
}; };
@ -109,13 +122,14 @@ pub fn view(model: &Model) -> Node<Msg> {
.iter() .iter()
.map(|user| { .map(|user| {
let user_id = user.id; let user_id = user.id;
let remove = StyledButton::build() let remove = StyledButton {
.text("Remove") text: Some("Remove"),
.on_click(mouse_ev(Ev::Click, move |_| { on_click: Some(mouse_ev(Ev::Click, move |_| {
Msg::InvitedUserRemove(user_id) Msg::InvitedUserRemove(user_id)
})) })),
.build() ..Default::default()
.into_node(); }
.into_node();
let role = page let role = page
.invitations .invitations
.iter() .iter()
@ -144,15 +158,21 @@ pub fn view(model: &Model) -> Node<Msg> {
.iter() .iter()
.map(|invitation| { .map(|invitation| {
let id = invitation.id; let id = invitation.id;
let revoke = StyledButton::build() let revoke = StyledButton {
.text("Revoke") disabled: invitation.state == InvitationState::Revoked,
.disabled(invitation.state == InvitationState::Revoked) text: Some("Revoke"),
.on_click(mouse_ev(Ev::Click, move |_| Msg::InviteRevokeRequest(id))) on_click: Some(mouse_ev(Ev::Click, move |_| Msg::InviteRevokeRequest(id))),
.build() ..Default::default()
.into_node(); }
.into_node();
// let revoke = StyledButton::build()
// .text("Revoke")
// .disabled(invitation.state == InvitationState::Revoked)
// .on_click(mouse_ev(Ev::Click, move |_| Msg::InviteRevokeRequest(id)))
// .build()
// .into_node();
li![ li![
C!["invitation"], C!["invitation", format!("{}", invitation.state)],
attrs![At::Class => format!("{}", invitation.state)],
span![invitation.name.as_str()], span![invitation.name.as_str()],
span![invitation.email.as_str()], span![invitation.email.as_str()],
span![format!("{}", invitation.state)], span![format!("{}", invitation.state)],

View File

@ -98,7 +98,7 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>)
None None
}; };
let active_flag = page.filter(|p| *p == model.page).map(|_| C!["active"]); let active_flag = page.filter(|p| *p == model.page).map(|_| C!["active"]);
let icon_node = StyledIcon::build(icon).build().into_node(); let icon_node = StyledIcon::from(icon).into_node();
let on_click = page.map(|p| { let on_click = page.map(|p| {
mouse_ev("click", move |ev| { mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
@ -111,10 +111,9 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>)
}); });
li![ li![
C!["linkItem"], C!["linkItem", icon.to_str()],
active_flag, active_flag,
allow_flag, allow_flag,
C![icon.to_str()],
a![ a![
attrs![At::Href => path], attrs![At::Href => path],
on_click, on_click,

View File

@ -15,6 +15,9 @@ use {
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
use crate::components::styled_button::ButtonVariant;
use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant};
trait IntoNavItemIcon { trait IntoNavItemIcon {
fn into_nav_item_icon(self) -> Node<Msg>; fn into_nav_item_icon(self) -> Node<Msg>;
} }
@ -27,7 +30,12 @@ impl IntoNavItemIcon for Node<Msg> {
impl IntoNavItemIcon for Icon { impl IntoNavItemIcon for Icon {
fn into_nav_item_icon(self) -> Node<Msg> { fn into_nav_item_icon(self) -> Node<Msg> {
StyledIcon::build(self).size(21).build().into_node() StyledIcon {
icon: self,
size: Some(21),
..Default::default()
}
.into_node()
} }
} }
@ -47,16 +55,22 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
]; ];
let user_icon = match model.user.as_ref() { let user_icon = match model.user.as_ref() {
Some(user) => i![ Some(user) => {
C!["styledIcon"], let avatar = StyledAvatar {
StyledAvatar::build() avatar_url: user.avatar_url.as_deref(),
.size(27) size: 27,
.name(user.name.as_str()) name: &user.name,
.avatar_url(user.avatar_url.as_deref().unwrap_or_default()) ..StyledAvatar::default()
.build() }
.into_node() .into_node();
], i![C!["styledIcon"], avatar]
_ => StyledIcon::build(Icon::User).size(21).build().into_node(), }
_ => StyledIcon {
icon: Icon::User,
size: Some(21),
..Default::default()
}
.into_node(),
}; };
let messages = if model.messages.is_empty() { let messages = if model.messages.is_empty() {
@ -68,7 +82,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
None, None,
Some(mouse_ev(Ev::Click, |ev| { Some(mouse_ev(Ev::Click, |ev| {
ev.prevent_default(); ev.prevent_default();
Msg::ToggleTooltip(styled_tooltip::Variant::Messages) Msg::ToggleTooltip(styled_tooltip::TooltipVariant::Messages)
})), })),
) )
}; };
@ -144,14 +158,14 @@ where
pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> { pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> {
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| { let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::About)) Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About))
}); });
div![C!["aboutTooltip"], on_click, children] div![C!["aboutTooltip"], on_click, children]
} }
fn messages_tooltip_popup(model: &Model) -> Node<Msg> { fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| { let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::Messages)) Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::Messages))
}); });
let mut messages: Vec<Node<Msg>> = vec![]; let mut messages: Vec<Node<Msg>> = vec![];
for (idx, message) in model.messages.iter().enumerate() { for (idx, message) in model.messages.iter().enumerate() {
@ -163,13 +177,13 @@ fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
}; };
} }
let body = div![on_click, C!["messagesList"], messages]; let body = div![on_click, C!["messagesList"], messages];
styled_tooltip::StyledTooltip::build() styled_tooltip::StyledTooltip {
.add_class("messagesPopup") visible: model.messages_tooltip_visible,
.visible(model.messages_tooltip_visible) class_list: "messagesPopup",
.messages_tooltip() children: vec![body],
.add_child(body) variant: TooltipVariant::Messages,
.build() }
.into_node() .into_node()
} }
fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> { fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
@ -186,7 +200,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with('#') { let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with('#') {
empty![] empty![]
} else { } else {
let link_icon = StyledIcon::build(Icon::Link).build().into_node(); let link_icon = StyledIcon::from(Icon::Link).into_node();
div![ div![
C!["hyperlink"], C!["hyperlink"],
a![ a![
@ -199,16 +213,17 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
}; };
let message_description = parse_description(model, description.as_str()); let message_description = parse_description(model, description.as_str());
let close_button = StyledButton::build() let close_button = StyledButton {
.icon(Icon::Close) variant: ButtonVariant::Empty,
.empty() icon: Some(Icon::Close.into_node()),
.on_click(mouse_ev(Ev::Click, move |ev| { on_click: Some(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Some(Msg::MessageSeen(message_id)) Some(Msg::MessageSeen(message_id))
})) })),
.build() ..Default::default()
.into_node(); }
.into_node();
let top = div![ let top = div![
C!["top"], C!["top"],
div![C!["summary"], summary], div![C!["summary"], summary],
@ -218,30 +233,32 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let node = match message_type { let node = match message_type {
MessageType::ReceivedInvitation => { MessageType::ReceivedInvitation => {
let token: InvitationToken = hyper_link.trim_start_matches('#').parse().ok()?; let token: InvitationToken = hyper_link.trim_start_matches('#').parse().ok()?;
let accept = StyledButton::build() let accept = StyledButton {
.primary() variant: ButtonVariant::Primary,
.text("Accept") active: true,
.active(true) text: Some("Accept"),
.icon(Icon::Check) icon: Some(Icon::Check.into_node()),
.on_click(mouse_ev(Ev::Click, move |ev| { on_click: Some(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Some(Msg::MessageInvitationApproved(token)) Some(Msg::MessageInvitationApproved(token))
})) })),
.build() ..Default::default()
.into_node(); }
let reject = StyledButton::build() .into_node();
.danger() let reject = StyledButton {
.text("Dismiss") variant: ButtonVariant::Danger,
.icon(Icon::Close) active: true,
.on_click(mouse_ev(Ev::Click, move |ev| { text: Some("Dismiss"),
icon: Some(Icon::Close.into_node()),
on_click: Some(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Some(Msg::MessageInvitationDismiss(token)) Some(Msg::MessageInvitationDismiss(token))
})) })),
.active(true) ..Default::default()
.build() }
.into_node(); .into_node();
div![ div![
C!["message"], C!["message"],
attrs![At::Class => format!("{}", message_type)], attrs![At::Class => format!("{}", message_type)],
@ -267,28 +284,29 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
} }
fn about_tooltip_popup(model: &Model) -> Node<Msg> { fn about_tooltip_popup(model: &Model) -> Node<Msg> {
let visit_website = StyledButton::build() let visit_website = StyledButton {
.text("Visit Website") variant: ButtonVariant::Primary,
.primary() text: Some("Visit Website"),
.build() ..Default::default()
.into_node(); }
let github_repo = StyledButton::build() .into_node();
.text("Github Repo") let github_repo = StyledButton {
.secondary() variant: ButtonVariant::Secondary,
.icon(Icon::Github) text: Some("Github Repo"),
.build() icon: Some(Icon::Github.into_node()),
.into_node(); ..Default::default()
}
.into_node();
let on_click = mouse_ev(Ev::Click, |_| { let on_click = mouse_ev(Ev::Click, |_| {
Msg::ToggleTooltip(styled_tooltip::Variant::About) Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About)
}); });
let body = div![ let body = div![
on_click, on_click,
C!["feedbackDropdown"], C!["feedbackDropdown"],
div![ div![
C!["feedbackImageCont"], C!["feedbackImageCont feedbackImage"],
img![attrs![At::Src => "/feedback.png"]], img![attrs![At::Src => "/feedback.png"]],
C!["feedbackImage"],
], ],
div![ div![
C!["feedbackParagraph"], C!["feedbackParagraph"],
@ -321,13 +339,13 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
] ]
]; ];
styled_tooltip::StyledTooltip::build() StyledTooltip {
.visible(model.about_tooltip_visible) visible: model.about_tooltip_visible,
.about_tooltip() class_list: "aboutTooltipPopup",
.add_class("aboutTooltipPopup") children: vec![body],
.add_child(body) variant: TooltipVariant::About,
.build() }
.into_node() .into_node()
} }
fn parse_description(model: &Model, desc: &str) -> Node<Msg> { fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
@ -342,12 +360,13 @@ fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
.find(|(_, user)| user.email == email) .find(|(_, user)| user.email == email)
}) })
.map(|(index, user)| { .map(|(index, user)| {
let avatar = StyledAvatar::build() let avatar = StyledAvatar {
.avatar_url(user.avatar_url.as_deref().unwrap_or_default()) avatar_url: user.avatar_url.as_deref(),
.user_index(index) size: 16,
.size(16) user_index: index,
.build() ..StyledAvatar::default()
.into_node(); }
.into_node();
span![C!["mention"], avatar, user.name.as_str()] span![C!["mention"], avatar, user.name.as_str()]
}) })
.unwrap_or_else(|| span![word]); .unwrap_or_else(|| span![word]);

View File

@ -47,11 +47,13 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let icon = StyledIcon::build(Icon::Stopwatch) let icon = StyledIcon {
.add_class("watchIcon") icon: Icon::Stopwatch,
.size(32) class_list: "watchIcon",
.build() size: Some(32),
.into_node(); ..Default::default()
}
.into_node();
let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining); let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining);
let spent_text = match (time_spent, time_tracking_type) { let spent_text = match (time_spent, time_tracking_type) {

View File

@ -1,12 +1,7 @@
const getWsHostName = () => process.env.JIRS_SERVER_BIND === "0.0.0.0" ? 'localhost' : process.env.JIRS_SERVER_BIND;
const getProtocol = () => window.location.protocol.replace(/^http/, 'ws');
const wsUrl = () => `${getProtocol()}//${getWsHostName()}:${process.env.JIRS_SERVER_PORT}/ws/`;
import("/jirs.js").then(async module => { import("/jirs.js").then(async module => {
// window.module = module; // window.module = module;
await module.default(); await module.default();
const host_url = `${ location.protocol }//${ process.env.JIRS_SERVER_BIND }:${ process.env.JIRS_SERVER_PORT }`; module.render();
module.render(host_url, wsUrl());
document.querySelector('main').className = ''; document.querySelector('main').className = '';
const spinner = document.querySelector('.spinner'); const spinner = document.querySelector('.spinner');
spinner && spinner.remove(); spinner && spinner.remove();