From e6ef4d6b0e83d4f1bff0e2e9dcfbe27252be8d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Thu, 15 Apr 2021 22:32:05 +0200 Subject: [PATCH] Remove some builders --- .env | 4 +- .gitignore | 1 + Cargo.lock | 1 + jirs-client/Cargo.toml | 2 + jirs-client/build.rs | 55 +++ jirs-client/scripts/dev.sh | 7 +- jirs-client/scripts/run-wasm-pack.sh | 7 +- jirs-client/src/components/styled_avatar.rs | 100 +----- jirs-client/src/components/styled_button.rs | 225 +++--------- .../src/components/styled_confirm_modal.rs | 91 +++-- .../src/components/styled_date_time_input.rs | 85 ++--- jirs-client/src/components/styled_editor.rs | 162 +++------ jirs-client/src/components/styled_field.rs | 29 +- jirs-client/src/components/styled_form.rs | 6 +- jirs-client/src/components/styled_icon.rs | 83 ++--- .../src/components/styled_image_input.rs | 45 +-- jirs-client/src/components/styled_input.rs | 218 ++++-------- jirs-client/src/components/styled_link.rs | 30 +- jirs-client/src/components/styled_modal.rs | 123 ++----- jirs-client/src/components/styled_select.rs | 253 +++++--------- .../src/components/styled_select_child.rs | 258 +++++++------- jirs-client/src/components/styled_textarea.rs | 163 +++------ jirs-client/src/components/styled_tooltip.rs | 98 +----- jirs-client/src/lib.rs | 34 +- .../src/modals/comments_delete/view.rs | 15 +- jirs-client/src/modals/debug/view.rs | 14 +- jirs-client/src/modals/epic_field.rs | 56 +-- jirs-client/src/modals/epics_delete/view.rs | 48 +-- jirs-client/src/modals/epics_edit/view.rs | 59 ++-- .../src/modals/issue_statuses_delete/view.rs | 18 +- jirs-client/src/modals/issues_create/model.rs | 27 +- jirs-client/src/modals/issues_create/view.rs | 300 ++++++++-------- jirs-client/src/modals/issues_delete/view.rs | 18 +- jirs-client/src/modals/issues_edit/view.rs | 323 ++++++++++-------- .../src/modals/issues_edit/view/comments.rs | 87 ++--- jirs-client/src/modals/time_tracking/view.rs | 73 ++-- jirs-client/src/model.rs | 6 + jirs-client/src/pages/invite_page/view.rs | 48 +-- jirs-client/src/pages/profile_page/view.rs | 159 +++++---- .../src/pages/project_page/view/board.rs | 69 ++-- .../src/pages/project_page/view/filters.rs | 65 ++-- .../src/pages/project_settings_page/view.rs | 198 ++++++----- jirs-client/src/pages/reports_page/view.rs | 31 +- jirs-client/src/pages/sign_in_page/view.rs | 108 +++--- jirs-client/src/pages/sign_up_page/view.rs | 114 ++++--- jirs-client/src/pages/users_page/update.rs | 8 +- jirs-client/src/pages/users_page/view.rs | 150 ++++---- jirs-client/src/shared/aside.rs | 5 +- jirs-client/src/shared/navbar_left.rs | 167 +++++---- jirs-client/src/shared/tracking_widget.rs | 12 +- jirs-client/static/index.js | 7 +- 51 files changed, 1940 insertions(+), 2325 deletions(-) create mode 100644 jirs-client/build.rs diff --git a/.env b/.env index 2a22425c..b70b90bf 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ DEBUG=true RUST_LOG=debug -JIRS_CLIENT_PORT=7000 -JIRS_CLIENT_BIND=0.0.0.0 +JIRS_CLIENT_PORT=80 +JIRS_CLIENT_BIND=jirs.lvh.me DATABASE_URL=postgres://postgres@localhost:5432/jirs JIRS_SERVER_PORT=5000 JIRS_SERVER_BIND=0.0.0.0 diff --git a/.gitignore b/.gitignore index b1b442e3..8e72fa33 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ highlight/jirs-highlight/build uploads config shared/jirs-config/target +jirs-client/src/location.rs diff --git a/Cargo.lock b/Cargo.lock index 908b96f0..df78445c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1988,6 +1988,7 @@ dependencies = [ "chrono", "derive_enum_iter", "derive_enum_primitive", + "dotenv", "futures 0.1.31", "jirs-data", "js-sys", diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index da210f5d..f1053f87 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -30,6 +30,8 @@ chrono = { version = "0.4", default-features = false, features = ["serde", "wasm uuid = { version = "0.8.1", features = ["serde"] } futures = "^0.1.26" +dotenv = { version = "*" } + [dependencies.wee_alloc] version = "*" features = ["static_array_backend"] diff --git a/jirs-client/build.rs b/jirs-client/build.rs new file mode 100644 index 00000000..715dfd19 --- /dev/null +++ b/jirs-client/build.rs @@ -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(); +} diff --git a/jirs-client/scripts/dev.sh b/jirs-client/scripts/dev.sh index 793757f2..d34568fd 100755 --- a/jirs-client/scripts/dev.sh +++ b/jirs-client/scripts/dev.sh @@ -1,14 +1,12 @@ #!/usr/bin/env bash RSASS_PATH=$(command -v rsass) -if [[ "${RSASS_PATH}" == "" ]]; -then +if [[ "${RSASS_PATH}" == "" ]]; then cargo install rsass --features=commandline fi WASM_PACK_PATH=$(command -v wasm-pack) -if [[ "${WASM_PACK_PATH}" == "" ]]; -then +if [[ "${WASM_PACK_PATH}" == "" ]]; then cargo install wasm-pack fi @@ -23,6 +21,7 @@ cd ${CLIENT_ROOT} . .env cargo watch \ + -i ./jirs-client/src/location.rs \ -s ${CLIENT_ROOT}/scripts/run-wasm-pack.sh \ -w ${CLIENT_ROOT}/src \ -w ${CLIENT_ROOT}/Cargo.toml \ diff --git a/jirs-client/scripts/run-wasm-pack.sh b/jirs-client/scripts/run-wasm-pack.sh index 0d082442..b4083109 100755 --- a/jirs-client/scripts/run-wasm-pack.sh +++ b/jirs-client/scripts/run-wasm-pack.sh @@ -13,13 +13,10 @@ wasm-pack --verbose build --mode ${MODE} ${BUILD_TYPE} --out-name jirs --out-dir cd ${CLIENT_ROOT} 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 -cat ${CLIENT_ROOT}/static/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 - +cat ${CLIENT_ROOT}/static/index.js &>${CLIENT_ROOT}/tmp/index.js cp ${CLIENT_ROOT}/build/*.{js,wasm} ${CLIENT_ROOT}/tmp/ cp ${CLIENT_ROOT}/js/template.html ${CLIENT_ROOT}/tmp/index.html diff --git a/jirs-client/src/components/styled_avatar.rs b/jirs-client/src/components/styled_avatar.rs index 0063d8c1..3a45d3c1 100644 --- a/jirs-client/src/components/styled_avatar.rs +++ b/jirs-client/src/components/styled_avatar.rs @@ -3,13 +3,14 @@ use { seed::{prelude::*, *}, }; +#[derive(Debug)] pub struct StyledAvatar<'l> { - avatar_url: Option<&'l str>, - size: u32, - name: &'l str, - on_click: Option>, - class_list: Vec<&'l str>, - user_index: usize, + pub avatar_url: Option<&'l str>, + pub size: u32, + pub name: &'l str, + pub on_click: Option>, + pub class_list: &'l str, + pub user_index: usize, } impl<'l> Default for StyledAvatar<'l> { @@ -19,21 +20,7 @@ impl<'l> Default for StyledAvatar<'l> { size: 32, name: "", on_click: None, - class_list: vec![], - 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![], + class_list: "", user_index: 0, } } @@ -46,67 +33,6 @@ impl<'l> ToNode for StyledAvatar<'l> { } } -pub struct StyledAvatarBuilder<'l> { - avatar_url: Option<&'l str>, - size: Option, - name: &'l str, - on_click: Option>, - 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) -> 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 { let StyledAvatar { avatar_url, @@ -120,10 +46,6 @@ pub fn render(values: StyledAvatar) -> Node { let index = user_index % 8; 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 .chars() .rev() @@ -138,16 +60,14 @@ pub fn render(values: StyledAvatar) -> Node { url = url ); div![ - C!["styledAvatar image"], - class_list, + C!["styledAvatar image", class_list], attrs![At::Style => style, At::Title => name], on_click ] } _ => { div![ - C!["styledAvatar letter"], - class_list, + C!["styledAvatar letter", class_list], attrs![ At::Class => format!("avatarColor{}", index + 1), At::Style => shared_style, diff --git a/jirs-client/src/components/styled_button.rs b/jirs-client/src/components/styled_button.rs index 4d2f91b2..18b0f965 100644 --- a/jirs-client/src/components/styled_button.rs +++ b/jirs-client/src/components/styled_button.rs @@ -4,7 +4,7 @@ use { }; #[allow(dead_code)] -enum Variant { +pub enum ButtonVariant { Primary, Success, Danger, @@ -12,154 +12,35 @@ enum Variant { Empty, } -impl Variant { +impl ButtonVariant { fn to_str(&self) -> &'static str { match self { - Variant::Primary => "primary", - Variant::Success => "success", - Variant::Danger => "danger", - Variant::Secondary => "secondary", - Variant::Empty => "empty", + ButtonVariant::Primary => "primary", + ButtonVariant::Success => "success", + ButtonVariant::Danger => "danger", + ButtonVariant::Secondary => "secondary", + ButtonVariant::Empty => "empty", } } } -impl ToString for Variant { - fn to_string(&self) -> String { - self.to_str().to_string() - } -} - -#[derive(Default)] -pub struct StyledButtonBuilder<'l> { - variant: Option, - disabled: Option, - active: Option, - text: Option<&'l str>, - icon: Option>, - on_click: Option>, - children: Option>>, - class_list: Vec<&'l str>, - button_type: Option<&'l str>, - button_id: Option, -} - -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(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) -> Self { - self.on_click = Some(value); - self - } - - #[inline(always)] - pub fn children(mut self, value: Vec>) -> 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, - } +impl std::fmt::Display for ButtonVariant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.to_str()) } } pub struct StyledButton<'l> { - variant: Variant, - disabled: bool, - active: bool, - text: Option<&'l str>, - icon: Option>, - on_click: Option>, - children: Vec>, - class_list: Vec<&'l str>, - button_type: &'l str, - button_id: Option, + pub variant: ButtonVariant, + pub disabled: bool, + pub active: bool, + pub text: Option<&'l str>, + pub icon: Option>, + pub on_click: Option>, + pub children: Vec>, + pub class_list: &'l str, + pub button_type: &'l str, + pub button_id: Option, } impl<'l> StyledButton<'l> { @@ -168,24 +49,34 @@ impl<'l> StyledButton<'l> { I: ToNode, { Self { - variant: Variant::Secondary, + variant: ButtonVariant::Secondary, disabled: false, active: false, text: Some(text), icon: Some(icon.into_node()), on_click: None, children: vec![], - class_list: vec![], - button_type: "", + class_list: "", + button_type: "submit", button_id: None, } } } -impl<'l> StyledButton<'l> { - #[inline(always)] - pub fn build() -> StyledButtonBuilder<'l> { - StyledButtonBuilder::default() +impl<'l> Default for StyledButton<'l> { + fn default() -> Self { + Self { + 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 { icon, on_click, children, - mut class_list, + class_list, button_type, button_id, } = values; - class_list.push(variant.to_str()); - if children.is_empty() && text.is_none() { - class_list.push("iconOnly"); - } - if active { - class_list.push("isActive"); - } - if icon.is_some() { - class_list.push("withIcon"); - } + let class_list = format!( + "{} {} {} {} {}", + class_list, + variant, + if children.is_empty() && text.is_none() { + "iconOnly" + } else { + "" + }, + if active { "isActive" } else { "" }, + if icon.is_some() { "withIcon" } else { "" } + ); let handler = match on_click { Some(h) if !disabled => vec![h], _ => vec![], @@ -232,25 +125,13 @@ pub fn render(values: StyledButton) -> Node { 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(); seed::button![ - C!["styledButton"], - class_list, - attrs![ - At::Id => button_id, - At::Type => button_type, - ], + C!["styledButton", class_list], + attrs![At::Id => button_id, At::Type => button_type], + IF![disabled => attrs![At::Disabled => true]], handler, - if disabled { - vec![attrs![At::Disabled => true]] - } else { - vec![] - }, icon_node, content, ] diff --git a/jirs-client/src/components/styled_confirm_modal.rs b/jirs-client/src/components/styled_confirm_modal.rs index bc8685d3..122d1db8 100644 --- a/jirs-client/src/components/styled_confirm_modal.rs +++ b/jirs-client/src/components/styled_confirm_modal.rs @@ -7,6 +7,8 @@ use { seed::{prelude::*, EventHandler, *}, }; +use crate::components::styled_button::ButtonVariant; + const TITLE: &str = "Warning"; const MESSAGE: &str = "Are you sure you want to continue with this action?"; const CONFIRM_TEXT: &str = "Confirm"; @@ -14,11 +16,23 @@ const CANCEL_TEXT: &str = "Cancel"; #[derive(Debug)] pub struct StyledConfirmModal<'l> { - title: &'l str, - message: &'l str, - confirm_text: &'l str, - cancel_text: &'l str, - on_confirm: Option>, + pub title: &'l str, + pub message: &'l str, + pub confirm_text: &'l str, + pub cancel_text: &'l str, + pub on_confirm: Option>, +} + +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> { @@ -70,10 +84,10 @@ impl<'l> StyledConfirmModalBuilder<'l> { pub fn build(self) -> StyledConfirmModal<'l> { StyledConfirmModal { - title: self.title.unwrap_or_else(|| TITLE), - message: self.message.unwrap_or_else(|| MESSAGE), - confirm_text: self.confirm_text.unwrap_or_else(|| CONFIRM_TEXT), - cancel_text: self.cancel_text.unwrap_or_else(|| CANCEL_TEXT), + title: self.title.unwrap_or(TITLE), + message: self.message.unwrap_or(MESSAGE), + confirm_text: self.confirm_text.unwrap_or(CONFIRM_TEXT), + cancel_text: self.cancel_text.unwrap_or(CANCEL_TEXT), on_confirm: self.on_confirm, } } @@ -88,32 +102,47 @@ pub fn render(values: StyledConfirmModal) -> Node { on_confirm, } = 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 { _ if message.is_empty() => empty![], _ => p![attrs![At::Class => "message"], message], }; - let confirm_button = match on_confirm { - Some(handler) => StyledButton::build() - .text(confirm_text) - .on_click(handler) - .build() - .into_node(), - _ => StyledButton::build().text(confirm_text).build().into_node(), - }; - let cancel_button = StyledButton::build() - .text(cancel_text) - .secondary() - .on_click(mouse_ev(Ev::Click, |_| Msg::ModalDropped)) - .build() - .into_node(); + let confirm_button = StyledButton { + text: Some(confirm_text), + on_click: on_confirm, + ..Default::default() + } + .into_node(); + let cancel_button = StyledButton { + text: Some(cancel_text), + variant: ButtonVariant::Secondary, + on_click: Some(mouse_ev(Ev::Click, |_| Msg::ModalDropped)), + ..Default::default() + } + .into_node(); - StyledModal::build() - .width(600) - .child(div![C!["title"], title]) - .child(message_node) - .child(div![C!["actions"], confirm_button, cancel_button]) - .add_class("confirmModal") - .build() - .into_node() + StyledModal { + width: Some(600), + children: vec![ + div![C!["title"], title], + message_node, + div![C!["actions"], confirm_button, cancel_button], + ], + class_list: "confirmModal", + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/components/styled_date_time_input.rs b/jirs-client/src/components/styled_date_time_input.rs index 56f6c28d..33584746 100644 --- a/jirs-client/src/components/styled_date_time_input.rs +++ b/jirs-client/src/components/styled_date_time_input.rs @@ -12,6 +12,9 @@ use { std::ops::RangeInclusive, }; +use crate::components::styled_button::ButtonVariant; +use crate::components::styled_tooltip::TooltipVariant; + #[derive(Debug)] pub enum StyledDateTimeChanged { MonthChanged(Option), @@ -175,18 +178,19 @@ fn render(values: StyledDateTimeInput) -> Node { let date = last_day_of_prev_month .with_day0(timestamp.day0()) - .unwrap_or_else(|| last_day_of_prev_month); + .unwrap_or(last_day_of_prev_month); Msg::StyledDateTimeInputChanged( field_id, StyledDateTimeChanged::MonthChanged(Some(date)), ) }); - StyledButton::build() - .on_click(on_click_left) - .icon(Icon::DoubleLeft) - .empty() - .build() - .into_node() + StyledButton { + on_click: Some(on_click_left), + icon: Some(Icon::DoubleLeft.into_node()), + variant: ButtonVariant::Empty, + ..Default::default() + } + .into_node() }; let right_action = { let field_id = values.field_id.clone(); @@ -201,42 +205,46 @@ fn render(values: StyledDateTimeInput) -> Node { - Duration::days(1); let date = first_day_of_next_month .with_day0(timestamp.day0()) - .unwrap_or_else(|| last_day_of_next_month); + .unwrap_or(last_day_of_next_month); Msg::StyledDateTimeInputChanged( field_id, StyledDateTimeChanged::MonthChanged(Some(date)), ) }); - StyledButton::build() - .on_click(on_click_right) - .icon(Icon::DoubleRight) - .empty() - .build() - .into_node() + StyledButton { + on_click: Some(on_click_right), + icon: Some(Icon::DoubleRight.into_node()), + variant: ButtonVariant::Empty, + ..Default::default() + } + .into_node() }; let header_text = current.format("%B %Y").to_string(); - let tooltip = StyledTooltip::build() - .visible(values.popup_visible) - .date_time_picker() - .add_child(h2![left_action, span![header_text], right_action]) - .add_child(div![ - C!["calendar"], + let tooltip = StyledTooltip { + visible: values.popup_visible, + class_list: "", + children: vec![ + h2![left_action, span![header_text], right_action], div![ - C!["weekHeader week"], - div![C!["day"], format!("{}", Weekday::Mon).as_str()], - div![C!["day"], format!("{}", Weekday::Tue).as_str()], - div![C!["day"], format!("{}", Weekday::Wed).as_str()], - div![C!["day"], format!("{}", Weekday::Thu).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()], + C!["calendar"], + div![ + C!["weekHeader week"], + div![C!["day"], format!("{}", Weekday::Mon).as_str()], + div![C!["day"], format!("{}", Weekday::Tue).as_str()], + div![C!["day"], format!("{}", Weekday::Wed).as_str()], + div![C!["day"], format!("{}", Weekday::Thu).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 - ]) - .build() - .into_node(); + ], + variant: TooltipVariant::DateTimeBuilder, + } + .into_node(); let input = { let field_id = values.field_id.clone(); @@ -255,12 +263,13 @@ fn render(values: StyledDateTimeInput) -> Node { .date() .format("%d/%m/%Y") .to_string(); - StyledButton::build() - .on_click(on_focus) - .text(text.as_str()) - .empty() - .build() - .into_node() + StyledButton { + on_click: Some(on_focus), + text: Some(text.as_str()), + variant: ButtonVariant::Empty, + ..Default::default() + } + .into_node() }; div![ diff --git a/jirs-client/src/components/styled_editor.rs b/jirs-client/src/components/styled_editor.rs index f335cb51..268b8c9f 100644 --- a/jirs-client/src/components/styled_editor.rs +++ b/jirs-client/src/components/styled_editor.rs @@ -27,93 +27,29 @@ impl StyledEditorState { } #[derive(Debug, Clone)] -pub struct StyledEditor { - id: FieldId, - initial_text: String, - text: String, - html: String, - mode: Mode, - update_event: Ev, +pub struct StyledEditor<'l> { + pub id: Option, + pub initial_text: &'l str, + pub text: &'l str, + pub html: &'l str, + pub mode: Mode, + pub update_event: Ev, } -impl StyledEditor { - #[inline] - pub fn build(id: FieldId) -> StyledEditorBuilder { - StyledEditorBuilder { - id, - initial_text: "".to_string(), - text: "".to_string(), - html: "".to_string(), - mode: Mode::View, - update_event: None, +impl<'l> Default for StyledEditor<'l> { + fn default() -> Self { + Self { + id: None, + initial_text: "", + text: "", + html: "", + mode: Mode::Editor, + update_event: Ev::Cached, } } } -#[derive(Debug)] -pub struct StyledEditorBuilder { - id: FieldId, - initial_text: String, - text: String, - html: String, - mode: Mode, - update_event: Option, -} - -impl StyledEditorBuilder { - #[inline] - pub fn text(mut self, text: S) -> Self - where - S: Into, - { - self.text = text.into(); - self - } - - #[inline] - pub fn initial_text(mut self, text: S) -> Self - where - S: Into, - { - self.initial_text = text.into(); - self - } - - #[inline] - pub fn html(mut self, text: S) -> Self - where - S: Into, - { - 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 { +impl<'l> ToNode for StyledEditor<'l> { #[inline] fn into_node(self) -> Node { render(self) @@ -131,6 +67,7 @@ pub fn render(values: StyledEditor) -> Node { update_event, } = values; + let id = id.expect("Styled Editor requires ID"); let on_editor_clicked = click_handler(id.clone(), Mode::Editor); let on_view_clicked = click_handler(id.clone(), Mode::View); @@ -138,40 +75,31 @@ pub fn render(values: StyledEditor) -> Node { let view_id = format!("view-{}", id); let name = format!("styled-editor-{}", id); - let text_area = StyledTextarea::build(id) - .height(40) - .update_on(update_event) - // .disable_auto_resize() - .value(initial_text.as_str()) - .build() - .into_node(); + let text_area = StyledTextarea { + id: Some(id), + height: 40, + max_height: 0, + value: initial_text, + class_list: "", + update_event, + placeholder: "", + disable_auto_resize: false, + } + .into_node(); - let (editor_radio_node, view_radio_node, parsed_node) = match mode { - Mode::Editor => ( - seed::input![ - id![editor_id.as_str()], - attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true], - ], - seed::input![ - id![view_id.as_str()], - attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";], - ], - vec![], - ), - 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()), - ), - }; + let (editor_radio_node, view_radio_node) = ( + seed::input![ + id![editor_id.as_str()], + C!["editorRadio"], + attrs![At::Type => "radio"; At::Name => name.as_str(); At::Checked => true], + ], + seed::input![ + id![view_id.as_str()], + C!["viewRadio"], + attrs![ At::Type => "radio"; At::Name => name.as_str();], + IF![mode == Mode::View => attrs![At::Checked => true]] + ], + ); div![ C!["styledEditor"], @@ -198,7 +126,11 @@ pub fn render(values: StyledEditor) -> Node { editor_radio_node, text_area, view_radio_node, - div![C!["view"], parsed_node], + div![ + C!["view"], + IF![mode == Mode::Editor => empty![]], + IF![mode == Mode::View => raw![html]], + ], ] } diff --git a/jirs-client/src/components/styled_field.rs b/jirs-client/src/components/styled_field.rs index fec010f9..3a22dd27 100644 --- a/jirs-client/src/components/styled_field.rs +++ b/jirs-client/src/components/styled_field.rs @@ -5,10 +5,21 @@ use { #[derive(Debug)] pub struct StyledField<'l> { - label: &'l str, - tip: Option<&'l str>, - input: Node, - class_list: Vec<&'l str>, + pub label: &'l str, + pub tip: Option<&'l str>, + pub input: Node, + 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> { @@ -28,7 +39,7 @@ pub struct StyledFieldBuilder<'l> { label: Option<&'l str>, tip: Option<&'l str>, input: Option>, - class_list: Vec<&'l str>, + class_list: &'l str, } impl<'l> StyledFieldBuilder<'l> { @@ -47,8 +58,8 @@ impl<'l> StyledFieldBuilder<'l> { self } - pub fn add_class(mut self, name: &'l str) -> Self { - self.class_list.push(name); + pub fn class_list(mut self, name: &'l str) -> Self { + self.class_list = name; self } @@ -72,8 +83,8 @@ pub fn render(values: StyledField) -> Node { let tip_node = tip.map(|s| div![C!["styledTip"], s]).unwrap_or(empty![]); div![ - attrs![At::Class => class_list.join(" "), At::Class => "styledField"], - seed::label![attrs![At::Class => "styledLabel"], label], + C!["styledField", class_list], + seed::label![C!["styledLabel"], label], input, tip_node, ] diff --git a/jirs-client/src/components/styled_form.rs b/jirs-client/src/components/styled_form.rs index c6f06ca0..fd367b00 100644 --- a/jirs-client/src/components/styled_form.rs +++ b/jirs-client/src/components/styled_form.rs @@ -5,9 +5,9 @@ use { #[derive(Debug, Clone)] pub struct StyledForm<'l> { - heading: &'l str, - fields: Vec>, - on_submit: Option>, + pub heading: &'l str, + pub fields: Vec>, + pub on_submit: Option>, } impl<'l> StyledForm<'l> { diff --git a/jirs-client/src/components/styled_icon.rs b/jirs-client/src/components/styled_icon.rs index c9ffa29a..f54d2459 100644 --- a/jirs-client/src/components/styled_icon.rs +++ b/jirs-client/src/components/styled_icon.rs @@ -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 { match self { Icon::Bug => "bug", @@ -233,9 +229,12 @@ impl From for Icon { } } -impl<'l> Into> for Icon { - fn into(self) -> StyledIcon<'l> { - StyledIcon::build(self).build() +impl<'l> From for StyledIcon<'l> { + fn from(icon: Icon) -> StyledIcon<'l> { + StyledIcon { + icon, + ..Default::default() + } } } @@ -247,12 +246,12 @@ impl ToNode for Icon { } pub struct StyledIconBuilder<'l> { - icon: Icon, - size: Option, - class_list: Vec>, - style_list: Vec>, - color: Option>, - on_click: Option>, + pub icon: Icon, + pub size: Option, + pub class_list: &'l str, + pub style_list: Vec>, + pub color: Option>, + pub on_click: Option>, } impl<'l> StyledIconBuilder<'l> { @@ -261,8 +260,8 @@ impl<'l> StyledIconBuilder<'l> { self } - pub fn add_class(mut self, name: &'l str) -> Self { - self.class_list.push(Cow::Borrowed(name)); + pub fn class_list(mut self, name: &'l str) -> Self { + self.class_list = name; self } @@ -275,34 +274,23 @@ impl<'l> StyledIconBuilder<'l> { self.on_click = Some(on_click); 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> { - icon: Icon, - size: Option, - class_list: Vec>, - style_list: Vec>, - color: Option>, - on_click: Option>, + pub icon: Icon, + pub size: Option, + pub class_list: &'l str, + pub style_list: Vec>, + pub color: Option<&'l str>, + pub on_click: Option>, } -impl<'l> StyledIcon<'l> { - pub fn build(icon: Icon) -> StyledIconBuilder<'l> { - StyledIconBuilder { - icon, +impl<'l> Default for StyledIcon<'l> { + fn default() -> Self { + Self { + icon: Icon::Stopwatch, size: None, - class_list: vec![], + class_list: "", style_list: vec![], color: None, on_click: None, @@ -335,25 +323,12 @@ pub fn render(values: StyledIcon) -> Node { let color = format!("color: {}", s); attrs![At::Style => color] }), - color.map(|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] - }), + color.map(|s| attrs![At::Style => format!("color: var(--{})", s)]), ] .into_iter() - .filter_map(|o| o) + .flatten() .collect(); - let class_list: Vec = 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| { match s { Cow::Borrowed(s) => { @@ -368,9 +343,7 @@ pub fn render(values: StyledIcon) -> Node { }); i![ - C!["styledIcon"], - class_list, - C![icon.to_str()], + C!["styledIcon", class_list, icon.to_str()], styles, attrs![ At::Style => style_list ], on_click, diff --git a/jirs-client/src/components/styled_image_input.rs b/jirs-client/src/components/styled_image_input.rs index 877f7baa..be114ef7 100644 --- a/jirs-client/src/components/styled_image_input.rs +++ b/jirs-client/src/components/styled_image_input.rs @@ -31,19 +31,9 @@ impl StyledImageInputState { } pub struct StyledImageInput<'l> { - id: FieldId, - class_list: Vec<&'l str>, - url: Option, -} - -impl<'l> StyledImageInput<'l> { - pub fn build(field_id: FieldId) -> StyledInputInputBuilder<'l> { - StyledInputInputBuilder { - id: field_id, - class_list: vec![], - url: None, - } - } + pub id: FieldId, + pub class_list: &'l str, + pub url: Option<&'l str>, } 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, -} - -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 { let StyledImageInput { id, @@ -104,8 +68,7 @@ fn render(values: StyledImageInput) -> Node { let input_id = id.to_string(); div![ - C!["styledImageInput"], - attrs![At::Class => class_list.join(" ")], + C!["styledImageInput", class_list], label![ C!["label"], attrs![At::For => input_id], diff --git a/jirs-client/src/components/styled_input.rs b/jirs-client/src/components/styled_input.rs index e0ad4ff2..8083beb4 100644 --- a/jirs-client/src/components/styled_input.rs +++ b/jirs-client/src/components/styled_input.rs @@ -8,22 +8,22 @@ use { }; #[derive(Clone, Debug, PartialOrd, PartialEq)] -pub enum Variant { +pub enum InputVariant { Normal, Primary, } -impl Variant { +impl InputVariant { #[inline] pub fn to_str<'l>(&self) -> &'l str { match self { - Variant::Normal => "normal", - Variant::Primary => "primary", + InputVariant::Normal => "normal", + InputVariant::Primary => "primary", } } } -impl ToString for Variant { +impl ToString for InputVariant { #[inline] fn to_string(&self) -> String { self.to_str().to_string() @@ -96,150 +96,70 @@ impl StyledInputState { pub fn reset(&mut self) { 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)] pub struct StyledInput<'l, 'm: 'l> { - id: FieldId, - icon: Option, - valid: 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>, + pub id: Option, + pub icon: Option, + pub valid: bool, + pub value: &'m str, + pub input_type: Option<&'l str>, + pub input_class_list: &'l str, + pub wrapper_class_list: &'l str, + pub variant: InputVariant, + pub auto_focus: bool, + pub input_handlers: Vec>, +} + +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> { #[inline] pub fn new_with_id_and_value_and_valid(id: FieldId, value: &'m str, valid: bool) -> Self { Self { - id, + id: Some(id), icon: None, valid, - value: Some(value), + value, input_type: None, - input_class_list: vec![], - wrapper_class_list: vec![], - variant: Variant::Normal, + input_class_list: "", + wrapper_class_list: "", + variant: InputVariant::Normal, auto_focus: false, 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, - valid: Option, - 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>, -} - -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) -> 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> { @@ -262,9 +182,10 @@ pub fn render(values: StyledInput) -> Node { auto_focus, input_handlers, } = values; + let id = id.expect("Input id is required"); let icon_node = icon - .map(|icon| StyledIcon::build(icon).build().into_node()) + .map(|icon| StyledIcon::from(icon).into_node()) .unwrap_or(Node::Empty); let on_change = { @@ -277,28 +198,27 @@ pub fn render(values: StyledInput) -> Node { }; div![ - C!["styledInput"], - C![variant.to_str()], - if !valid { Some(C!["invalid"]) } else { None }, - attrs!( - "class" => format!("{} {}", id, wrapper_class_list.join(" ")), - ), + C![ + "styledInput", + format!("{}", id), + variant.to_str(), + wrapper_class_list + ], + IF![!valid => C!["invalid"]], icon_node, seed::input![ - C!["inputElement"], - icon.as_ref().map(|_| C!["withIcon"]), - C![variant.to_str()], + C![ + "inputElement", + variant.to_str(), + input_class_list, + icon.as_ref().map(|_| "withIcon").unwrap_or_default() + ], attrs![ "id" => format!("{}", id), - At::Class => input_class_list.join(" "), - "value" => value.unwrap_or_default(), + "value" => value, "type" => input_type.unwrap_or("text"), ], - if auto_focus { - vec![attrs![At::AutoFocus => true]] - } else { - vec![] - }, + IF![auto_focus => attrs![At::AutoFocus => true]], on_change, input_handlers, ], diff --git a/jirs-client/src/components/styled_link.rs b/jirs-client/src/components/styled_link.rs index 4e2ad67b..fd061255 100644 --- a/jirs-client/src/components/styled_link.rs +++ b/jirs-client/src/components/styled_link.rs @@ -5,21 +5,21 @@ use { }; pub struct StyledLink<'l> { - children: Vec>, - class_list: Vec<&'l str>, - href: &'l str, + pub children: Vec>, + pub class_list: &'l str, + pub href: &'l str, } impl<'l> StyledLink<'l> { - pub fn build() -> StyledLinkBuilder<'l> { - StyledLinkBuilder::default() - } + // pub fn build() -> StyledLinkBuilder<'l> { + // StyledLinkBuilder::default() + // } } #[derive(Default)] pub struct StyledLinkBuilder<'l> { children: Vec>, - class_list: Vec<&'l str>, + class_list: &'l str, href: &'l str, } @@ -29,13 +29,8 @@ impl<'l> StyledLinkBuilder<'l> { self } - pub fn with_icon(self) -> Self { - self.add_child(crate::components::styled_icon::Icon::Link.into_node()) - .add_class("withIcon") - } - - pub fn add_class(mut self, name: &'l str) -> Self { - self.class_list.push(name); + pub fn class_list(mut self, name: &'l str) -> Self { + self.class_list = name; self } @@ -86,11 +81,8 @@ pub fn render(values: StyledLink) -> Node { }; a![ - C!["styledLink"], - attrs![ - At::Class => class_list.join(" "), - At::Href => href, - ], + C!["styledLink", class_list], + attrs![ At::Href => href, ], on_click, children, ] diff --git a/jirs-client/src/components/styled_modal.rs b/jirs-client/src/components/styled_modal.rs index 62613e05..f28d93e2 100644 --- a/jirs-client/src/components/styled_modal.rs +++ b/jirs-client/src/components/styled_modal.rs @@ -9,44 +9,56 @@ use { #[allow(dead_code)] #[derive(Debug, Copy, Clone, PartialOrd, PartialEq)] -pub enum Variant { +pub enum ModalVariant { Center, Aside, } -impl Variant { +impl ModalVariant { pub fn to_class_name(&self) -> &str { match self { - Variant::Center => "center", - Variant::Aside => "aside", + ModalVariant::Center => "center", + ModalVariant::Aside => "aside", } } pub fn to_icon_class_name(&self) -> &str { match self { - Variant::Center => "modalVariantCenter", - Variant::Aside => "modalVariantAside", + ModalVariant::Center => "modalVariantCenter", + ModalVariant::Aside => "modalVariantAside", } } } #[derive(Debug)] pub struct StyledModal<'l> { - variant: Variant, - width: Option, - with_icon: bool, - children: Vec>, - class_list: Vec<&'l str>, + pub variant: ModalVariant, + pub width: Option, + pub with_icon: bool, + pub children: Vec>, + 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> { pub fn centered_with_width_and_body(width: usize, children: Vec>) -> Self { Self { - variant: Variant::Center, + variant: ModalVariant::Center, width: Some(width), with_icon: false, 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, - width: Option, - with_icon: Option, - children: Option>>, - 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) -> Self { - self.children.get_or_insert(vec![]).push(child); - self - } - - #[inline] - pub fn children(mut self, children: ChildIter) -> Self - where - ChildIter: Iterator>, - { - 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] pub fn render(values: StyledModal) -> Node { let StyledModal { @@ -130,14 +76,16 @@ pub fn render(values: StyledModal) -> Node { width, with_icon, children, - mut class_list, + class_list, } = values; let icon = if with_icon { - StyledIcon::build(Icon::Close) - .add_class(variant.to_icon_class_name()) - .build() - .into_node() + StyledIcon { + icon: Icon::Close, + class_list: variant.to_icon_class_name(), + ..Default::default() + } + .into_node() } else { empty![] }; @@ -154,8 +102,6 @@ pub fn render(values: StyledModal) -> Node { }); 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 { Some(0) => "".to_string(), Some(n) => format!("max-width: {width}px", width = n), @@ -164,10 +110,11 @@ pub fn render(values: StyledModal) -> Node { div![ C!["modal"], div![ - attrs![At::Class => clickable_class], + C![clickable_class], close_handler, 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, icon, children diff --git a/jirs-client/src/components/styled_select.rs b/jirs-client/src/components/styled_select.rs index 72f4fa16..0d17f31a 100644 --- a/jirs-client/src/components/styled_select.rs +++ b/jirs-client/src/components/styled_select.rs @@ -19,27 +19,27 @@ pub enum StyledSelectChanged { } #[derive(Copy, Clone, Debug, PartialEq)] -pub enum Variant { +pub enum SelectVariant { Empty, Normal, } -impl Default for Variant { +impl Default for SelectVariant { fn default() -> Self { - Variant::Empty + SelectVariant::Empty } } -impl Variant { +impl SelectVariant { pub fn to_str<'l>(&self) -> &'l str { match self { - Variant::Empty => "empty", - Variant::Normal => "normal", + SelectVariant::Empty => "empty", + 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 { f.write_str(self.to_str()) } @@ -114,17 +114,38 @@ pub struct StyledSelect<'l, Options> where Options: Iterator>, { - id: FieldId, - variant: Variant, - dropdown_width: Option, - name: Option<&'l str>, - valid: bool, - is_multi: bool, - options: Option, - selected: Vec>, - text_filter: &'l str, - opened: bool, - clearable: bool, + pub id: FieldId, + pub variant: SelectVariant, + pub dropdown_width: Option, + pub name: &'l str, + pub valid: bool, + pub is_multi: bool, + pub options: Option, + pub selected: Vec>, + pub text_filter: &'l str, + pub opened: bool, + pub clearable: bool, +} + +impl<'l, Options> Default for StyledSelect<'l, Options> +where + Options: Iterator>, +{ + 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> @@ -136,124 +157,6 @@ where } } -impl<'l, Options> StyledSelect<'l, Options> -where - Options: Iterator>, -{ - 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>, -{ - variant: Option, - dropdown_width: Option, - name: Option<&'l str>, - valid: Option, - is_multi: Option, - options: Option, - selected: Option>>, - text_filter: Option<&'l str>, - opened: Option, - clearable: bool, -} - -impl<'l, Options> StyledSelectBuilder<'l, Options> -where - Options: Iterator>, -{ - pub fn build(self, id: FieldId) -> StyledSelect<'l, Options> { - StyledSelect { - id, - variant: self.variant.unwrap_or_default(), - 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>) -> 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 where Options: Iterator>, @@ -286,14 +189,10 @@ where }) }; - let dropdown_style = dropdown_width - .map(|n| format!("width: {}px;", n)) - .unwrap_or_else(|| "width: 100%;".to_string()); - - let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)]; - if !valid { - select_class.push("invalid".to_string()); - } + let dropdown_style = dropdown_width.map_or_else( + || "width: 100%;".to_string(), + |n| format!("width: {}px;", n), + ); let action_icon = if clearable && !selected.is_empty() { let on_click = { @@ -304,16 +203,20 @@ where Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None)) }) }; - StyledIcon::build(Icon::Close) - .add_class("chevronIcon") - .on_click(on_click) - .build() - .into_node() - } else if (selected.is_empty() || !is_multi) && variant != Variant::Empty { - StyledIcon::build(Icon::ChevronDown) - .add_class("chevronIcon") - .build() - .into_node() + StyledIcon { + icon: Icon::Close, + class_list: "chevronIcon", + on_click: Some(on_click), + ..Default::default() + } + .into_node() + } else if (selected.is_empty() || !is_multi) && variant != SelectVariant::Empty { + StyledIcon { + icon: Icon::ChevronDown, + class_list: "chevronIcon", + ..Default::default() + } + .into_node() } else { empty![] }; @@ -335,12 +238,7 @@ where ) }) }; - div![ - attrs![At::Class => "option"], - on_change, - on_handler.clone(), - node - ] + div![C!["option"], on_change, on_handler.clone(), node] }) .collect() } else { @@ -349,9 +247,9 @@ where let text_input = if opened { seed::input![ + C!["dropDownInput"], attrs![ - At::Name => name.unwrap_or_default(), - At::Class => "dropDownInput", + At::Name => name, At::Type => "text" At::Placeholder => "Search" At::AutoFocus => "true", @@ -364,24 +262,24 @@ where let option_list = match (opened, children.is_empty()) { (false, _) => empty![], - (_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"], - _ => seed::div![attrs![ At::Class => "options" ], children], + (_, true) => seed::div![C!["noOptions"], "No results"], + _ => seed::div![C!["options"], children], }; let value: Vec> = 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> = selected .into_iter() .map(|m| into_multi_value(m, id.clone())) .collect(); 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 { - 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 { selected .into_iter() @@ -390,13 +288,14 @@ where }; 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| { ev.stop_propagation(); None as Option }), div![ - attrs![At::Class => format!("valueContainer {}", variant)], + C!["valueContainer", variant.to_str()], on_handler, value, action_icon, @@ -416,21 +315,25 @@ fn render_value(mut content: Node) -> Node { } fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node { - 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 value = child.value(); let mut opt = child.into_node(); - opt.add_class("value"); - opt.add_child(close_icon); + opt.add_class("value").add_child(close_icon); let handler = { - let field_id = id.clone(); + let field_id = id; mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value)) }) }; - div![attrs![At::Class => "valueMultiItem"], opt, handler] + div![C!["valueMultiItem"], opt, handler] } diff --git a/jirs-client/src/components/styled_select_child.rs b/jirs-client/src/components/styled_select_child.rs index 78b627cc..c62adc43 100644 --- a/jirs-client/src/components/styled_select_child.rs +++ b/jirs-client/src/components/styled_select_child.rs @@ -1,40 +1,45 @@ use { crate::{ - components::styled_select::Variant, + components::styled_select::SelectVariant, shared::{IntoChild, ToChild, ToNode}, Msg, }, seed::{prelude::*, *}, - std::borrow::Cow, }; +use crate::components::styled_avatar::StyledAvatar; +use crate::components::styled_icon::StyledIcon; + pub enum DisplayType { SelectOption, SelectValue, } pub struct StyledSelectChild<'l> { - name: Option<&'l str>, - icon: Option>, - text: Option>, - display_type: DisplayType, - value: u32, - class_list: Vec>, - variant: Variant, + pub name: Option<&'l str>, + pub icon: Option>, + pub text: Option<&'l str>, + pub display_type: DisplayType, + pub value: u32, + pub class_list: &'l str, + pub variant: SelectVariant, } -impl<'l> StyledSelectChild<'l> { - pub fn build() -> StyledSelectChildBuilder<'l> { - StyledSelectChildBuilder { +impl<'l> Default for StyledSelectChild<'l> { + fn default() -> Self { + Self { + name: None, icon: None, text: None, - name: None, + display_type: DisplayType::SelectOption, value: 0, - class_list: vec![], + class_list: "", variant: Default::default(), } } +} +impl<'l> StyledSelectChild<'l> { #[inline] pub fn value(&self) -> u32 { self.value @@ -49,12 +54,25 @@ impl<'l> ToNode for StyledSelectChild<'l> { #[derive(Debug)] pub struct StyledSelectChildBuilder<'l> { - icon: Option>, - text: Option>, - name: Option<&'l str>, - value: u32, - class_list: Vec>, - variant: Variant, + pub icon: Option>, + pub text: Option<&'l str>, + pub name: Option<&'l str>, + pub value: u32, + pub class_list: &'l str, + 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> { @@ -70,12 +88,7 @@ impl<'l> StyledSelectChildBuilder<'l> { } pub fn text<'m: 'l>(mut self, text: &'m str) -> Self { - self.text = Some(std::borrow::Cow::Borrowed(text)); - self - } - - pub fn text_owned(mut self, text: String) -> Self { - self.text = Some(std::borrow::Cow::Owned(text)); + self.text = Some(text); self } @@ -96,8 +109,8 @@ impl<'l> StyledSelectChildBuilder<'l> { .unwrap_or(true) } - pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self { - self.class_list.push(Cow::Borrowed(name)); + pub fn class_list<'m: 'l>(mut self, name: &'m str) -> Self { + self.class_list = name; self } @@ -125,26 +138,6 @@ pub fn render(values: StyledSelectChild) -> Node { variant, } = 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 { Some(icon) => icon, _ => empty![], @@ -152,20 +145,32 @@ pub fn render(values: StyledSelectChild) -> Node { let label_node = match text { Some(text) => div![ - attrs![ - At::Class => name.as_deref().map(|s| format!("{}Label", s)).unwrap_or_default(), - At::Class => class_list.join(" "), + C![ + variant.to_str(), + 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 ], _ => empty![], }; div![ - C![variant.to_str()], - C![wrapper_class.as_str()], - attrs![At::Class => class_list.join(" ")], + C![ + variant.to_str(), + name.as_deref().unwrap_or_default(), + match display_type { + DisplayType::SelectOption => "optionItem", + DisplayType::SelectValue => "selectItem", + }, + class_list + ], icon_node, label_node ] @@ -175,16 +180,19 @@ impl<'l> ToChild<'l> for jirs_data::User { type Builder = StyledSelectChildBuilder<'l>; fn to_child<'m: 'l>(&'m self) -> Self::Builder { - let avatar = crate::components::styled_avatar::StyledAvatar::build() - .avatar_url(self.avatar_url.as_deref().unwrap_or_default()) - .size(20) - .name(self.name.as_str()) - .build() - .into_node(); - StyledSelectChild::build() - .value(self.id as u32) - .icon(avatar) - .text(self.name.as_str()) + let avatar = StyledAvatar { + size: 20, + name: &self.name, + avatar_url: self.avatar_url.as_deref(), + ..StyledAvatar::default() + } + .into_node(); + StyledSelectChildBuilder { + value: self.id as u32, + 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>; fn into_child(self) -> Self::Builder { - let icon = crate::components::styled_icon::StyledIcon::build(self.clone().into()) - .add_class(self.to_str()) - .build() - .into_node(); - let text = self.to_str(); + let icon = StyledIcon { + icon: self.clone().into(), + class_list: self.to_str(), + ..Default::default() + } + .into_node(); - StyledSelectChild::build() - .icon(icon) - .value(self.clone().into()) - .text(text) - .add_class(self.to_str()) + StyledSelectChildBuilder { + icon: Some(icon), + text: Some(self.to_str()), + class_list: self.to_str(), + value: self.into(), + ..Default::default() + } } } @@ -210,10 +221,12 @@ impl<'l> ToChild<'l> for jirs_data::IssueStatus { type Builder = StyledSelectChildBuilder<'l>; fn to_child<'m: 'l>(&'m self) -> Self::Builder { - StyledSelectChild::build() - .value(self.id as u32) - .add_class(self.name.as_str()) - .text(self.name.as_str()) + StyledSelectChildBuilder { + value: self.id as u32, + class_list: 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 { let name = self.to_label(); - let type_icon = crate::components::styled_icon::StyledIcon::build(self.clone().into()) - .add_class(name) - .build() - .into_node(); + let type_icon = StyledIcon { + icon: self.clone().into(), + class_list: name, + ..Default::default() + } + .into_node(); - StyledSelectChild::build() - .add_class(name) - .text(name) - .icon(type_icon) - .value(self.clone().into()) + StyledSelectChildBuilder { + class_list: name, + text: Some(name), + icon: Some(type_icon), + value: self.into(), + ..Default::default() + } } } @@ -240,10 +257,12 @@ impl<'l> IntoChild<'l> for jirs_data::ProjectCategory { type Builder = StyledSelectChildBuilder<'l>; fn into_child(self) -> Self::Builder { - StyledSelectChild::build() - .add_class(self.to_str()) - .text(self.to_str()) - .value(self.clone().into()) + StyledSelectChildBuilder { + class_list: self.to_str(), + text: Some(self.to_str()), + value: self.into(), + ..Default::default() + } } } @@ -253,32 +272,35 @@ impl<'l> IntoChild<'l> for jirs_data::UserRole { fn into_child(self) -> Self::Builder { let name = self.to_str(); - StyledSelectChild::build() - .add_class(name) - .add_class("capitalize") - .text(name) - .value(self.clone().into()) + StyledSelectChildBuilder { + text: Some(name), + value: self.into(), + class_list: name, + ..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 { type Builder = StyledSelectChildBuilder<'l>; - - fn to_child<'m: 'l>(&'m self) -> Self::Builder { - StyledSelectChild::build() - .text(self.name.as_str()) - .value(self.id as u32) - } + id_name_builder!(); } impl<'l> ToChild<'l> for jirs_data::Epic { type Builder = StyledSelectChildBuilder<'l>; - - fn to_child<'m: 'l>(&'m self) -> Self::Builder { - StyledSelectChild::build() - .text(self.name.as_str()) - .value(self.id as u32) - } + id_name_builder!(); } 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 { let name = stringify!(self); - StyledSelectChild::build() - .add_class(name) - .text(name) - .value(*self) + StyledSelectChildBuilder { + class_list: name, + text: Some(name), + value: *self, + ..Default::default() + } } } @@ -301,8 +325,10 @@ impl<'l> ToChild<'l> for (Label, Value) { type Builder = StyledSelectChildBuilder<'l>; fn to_child<'m: 'l>(&'m self) -> Self::Builder { - StyledSelectChild::build() - .text(self.0.as_str()) - .value(self.1) + StyledSelectChildBuilder { + text: Some(self.0.as_str()), + value: self.1, + ..Default::default() + } } } diff --git a/jirs-client/src/components/styled_textarea.rs b/jirs-client/src/components/styled_textarea.rs index 31dcebb4..f3c0b9ab 100644 --- a/jirs-client/src/components/styled_textarea.rs +++ b/jirs-client/src/components/styled_textarea.rs @@ -5,103 +5,34 @@ use { #[derive(Debug)] pub struct StyledTextarea<'l> { - id: FieldId, - height: usize, - max_height: usize, - value: &'l str, - class_list: Vec<&'l str>, - update_event: Ev, - placeholder: Option<&'l str>, - disable_auto_resize: bool, + pub id: Option, + pub height: usize, + pub max_height: usize, + pub value: &'l str, + pub class_list: &'l str, + pub update_event: Ev, + pub placeholder: &'l str, + pub disable_auto_resize: bool, } -impl<'l> ToNode for StyledTextarea<'l> { - fn into_node(self) -> Node { - render(self) - } -} - -impl<'l> StyledTextarea<'l> { - pub fn build(field_id: FieldId) -> StyledTextareaBuilder<'l> { - StyledTextareaBuilder { - id: field_id, - height: None, - max_height: None, - on_change: None, +impl<'l> Default for StyledTextarea<'l> { + fn default() -> Self { + Self { + id: None, + height: 0, + max_height: 0, value: "", - class_list: vec![], - update_event: None, - placeholder: None, + class_list: "", + update_event: Ev::Cached, + placeholder: "", disable_auto_resize: false, } } } -#[derive(Debug)] -pub struct StyledTextareaBuilder<'l> { - id: FieldId, - height: Option, - max_height: Option, - on_change: Option>, - value: &'l str, - class_list: Vec<&'l str>, - update_event: Option, - 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, - } +impl<'l> ToNode for StyledTextarea<'l> { + fn into_node(self) -> Node { + render(self) } } @@ -124,11 +55,12 @@ pub fn render(values: StyledTextarea) -> Node { height, max_height, value, - mut class_list, + class_list, update_event, placeholder, disable_auto_resize, } = values; + let id = id.expect("Text area requires FieldId"); let mut style_list = vec![]; let min_height = get_min_height(value, height as f64, disable_auto_resize); @@ -141,15 +73,14 @@ pub fn render(values: StyledTextarea) -> Node { } if disable_auto_resize { - style_list.push("resize: none".to_string()); 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 )); } 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(); if handler_disable_auto_resize { return None as Option; @@ -167,39 +98,41 @@ pub fn render(values: StyledTextarea) -> Node { }); let handler_disable_auto_resize = disable_auto_resize; - let text_input_handler = ev(update_event, move |event| { - event.stop_propagation(); + let text_input_handler = { + let id = id.clone(); + ev(update_event, move |event| { + event.stop_propagation(); - let value = event - .target() - .map(|target| seed::to_textarea(&target).value()) - .unwrap_or_default(); + let value = event + .target() + .map(|target| seed::to_textarea(&target).value()) + .unwrap_or_default(); - if handler_disable_auto_resize && value.contains('\n') { - event.prevent_default(); - } + if handler_disable_auto_resize && value.contains('\n') { + event.prevent_default(); + } - Some(Msg::StrInputChanged( - id, - if handler_disable_auto_resize { - value.trim().to_string() - } else { - value - }, - )) - }); - - class_list.push("textAreaInput"); + Some(Msg::StrInputChanged( + id, + if handler_disable_auto_resize { + value.trim().to_string() + } else { + value + }, + )) + }) + }; div![ + id![format!("styledTextArea-{}", id)], C!["styledTextArea"], div![C!["textAreaHeading"]], textarea![ + C![class_list, "textAreaInput"], attrs![ - At::Class => class_list.join(" "); At::AutoFocus => "true"; At::Style => style_list.join(";"); - At::Placeholder => placeholder.unwrap_or_default(); + At::Placeholder => placeholder; At::Rows => if disable_auto_resize { "5" } else { "auto" } ], value, diff --git a/jirs-client/src/components/styled_tooltip.rs b/jirs-client/src/components/styled_tooltip.rs index 914c57cc..9ba41b41 100644 --- a/jirs-client/src/components/styled_tooltip.rs +++ b/jirs-client/src/components/styled_tooltip.rs @@ -4,7 +4,7 @@ use { }; #[derive(Debug, Copy, Clone)] -pub enum Variant { +pub enum TooltipVariant { About, Messages, TableBuilder, @@ -12,35 +12,35 @@ pub enum Variant { DateTimeBuilder, } -impl Default for Variant { +impl Default for TooltipVariant { fn default() -> Self { - Variant::Messages + TooltipVariant::Messages } } -impl Variant { +impl TooltipVariant { pub fn to_str(&self) -> &'static str { match self { - Variant::About => "about", - Variant::Messages => "messages", - Variant::TableBuilder => "tableTooltip", - Variant::CodeBuilder => "codeTooltip", - Variant::DateTimeBuilder => "dateTimeTooltip", + TooltipVariant::About => "about", + TooltipVariant::Messages => "messages", + TooltipVariant::TableBuilder => "tableTooltip", + TooltipVariant::CodeBuilder => "codeTooltip", + 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 { f.write_str(self.to_str()) } } pub struct StyledTooltip<'l> { - visible: bool, - class_list: Vec<&'l str>, - children: Vec>, - variant: Variant, + pub visible: bool, + pub class_list: &'l str, + pub children: Vec>, + pub variant: TooltipVariant, } 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>, - 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) -> 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 { let StyledTooltip { visible, @@ -122,10 +57,7 @@ pub fn render(values: StyledTooltip) -> Node { variant, } = values; if visible { - div![ - attrs![At::Class => format!("styledTooltip {} {}", class_list.join(" "), variant)], - children - ] + div![C!["styledTooltip", class_list, variant.to_str()], children] } else { empty!() } diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index ad52b9a4..07d7694a 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -6,7 +6,7 @@ use { styled_date_time_input::StyledDateTimeChanged, styled_select::StyledSelectChanged, styled_tooltip, - styled_tooltip::{Variant as StyledTooltip, Variant}, + styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant}, }, model::{ModalType, Model, Page}, shared::{go_to_board, go_to_login}, @@ -24,6 +24,7 @@ mod changes; mod components; mod fields; mod images; +mod location; mod modals; mod model; mod pages; @@ -217,15 +218,15 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { model.page = *page; } Msg::ToggleTooltip(variant) => match variant { - styled_tooltip::Variant::About => { + styled_tooltip::TooltipVariant::About => { model.about_tooltip_visible = !model.about_tooltip_visible; } - styled_tooltip::Variant::Messages => { + styled_tooltip::TooltipVariant::Messages => { model.messages_tooltip_visible = !model.messages_tooltip_visible; } - styled_tooltip::Variant::CodeBuilder => {} - Variant::TableBuilder => {} - Variant::DateTimeBuilder => {} + styled_tooltip::TooltipVariant::CodeBuilder => {} + TooltipVariant::TableBuilder => {} + TooltipVariant::DateTimeBuilder => {} }, _ => (), } @@ -305,16 +306,8 @@ fn resolve_page(url: Url) -> Option { Some(page) } -pub static mut HOST_URL: String = String::new(); -pub static mut WS_URL: String = String::new(); - #[wasm_bindgen] -pub fn render(host_url: String, ws_url: String) { - unsafe { - HOST_URL = host_url; - WS_URL = ws_url; - } - +pub fn render() { 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) -> Model { - let host_url = unsafe { HOST_URL.clone() }; - let ws_url = unsafe { WS_URL.clone() }; - let mut model = Model::new(host_url, ws_url); - unsafe { - HOST_URL = "".to_string(); - WS_URL = "".to_string(); - } + let mut model = Model::new( + location::host_url().to_string(), + location::ws_url().to_string(), + ); model.page = resolve_page(url).unwrap_or(Page::Project); open_socket(&mut model, orders); diff --git a/jirs-client/src/modals/comments_delete/view.rs b/jirs-client/src/modals/comments_delete/view.rs index defa3345..90b2a17c 100644 --- a/jirs-client/src/modals/comments_delete/view.rs +++ b/jirs-client/src/modals/comments_delete/view.rs @@ -6,11 +6,12 @@ use { pub fn view(_model: &model::Model, modal: &super::Model) -> Node { let comment_id: CommentId = modal.comment_id; - StyledConfirmModal::build() - .title("Are you sure you want to delete this comment?") - .message("Once you delete, it's gone for good.") - .confirm_text("Delete comment") - .on_confirm(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))) - .build() - .into_node() + StyledConfirmModal { + title: "Are you sure you want to delete this comment?", + message: "Once you delete, it's gone for good.", + confirm_text: "Delete comment", + on_confirm: Some(mouse_ev(Ev::Click, move |_| Msg::DeleteComment(comment_id))), + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/modals/debug/view.rs b/jirs-client/src/modals/debug/view.rs index 7cf46f96..4ce850c7 100644 --- a/jirs-client/src/modals/debug/view.rs +++ b/jirs-client/src/modals/debug/view.rs @@ -6,11 +6,11 @@ use { pub fn view(model: &Model) -> Node { let text = format!("{:#?}", model); let code = pre![text]; - StyledModal::build() - .width(1200) - .add_class("debugModal") - .center() - .children(vec![code].into_iter()) - .build() - .into_node() + StyledModal { + width: Some(1200), + class_list: "debugModal", + children: vec![code], + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/modals/epic_field.rs b/jirs-client/src/modals/epic_field.rs index a2f44f04..314f8ddf 100644 --- a/jirs-client/src/modals/epic_field.rs +++ b/jirs-client/src/modals/epic_field.rs @@ -9,36 +9,40 @@ use { seed::prelude::Node, }; +use crate::components::styled_select::SelectVariant; + pub fn epic_field(model: &Model, modal: &Modal, field_id: FieldId) -> Option> where Modal: IssueModal, { if model.epics.is_empty() { - 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(), - ) + return None; } + 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(), + ) } diff --git a/jirs-client/src/modals/epics_delete/view.rs b/jirs-client/src/modals/epics_delete/view.rs index 7a0941b5..eaa78b49 100644 --- a/jirs-client/src/modals/epics_delete/view.rs +++ b/jirs-client/src/modals/epics_delete/view.rs @@ -11,25 +11,26 @@ use { pub fn view(model: &model::Model, modal: &Model) -> Node { if modal.related_issues.is_empty() { - StyledConfirmModal::build() - .title("Delete empty epic") - .cancel_text("Cancel") - .confirm_text("Delete epic") - .on_confirm(mouse_ev("click", move |ev| { + StyledConfirmModal { + title: "Delete empty epic", + confirm_text: "Delete epic", + cancel_text: "Cancel", + on_confirm: Some(mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); Msg::DeleteEpic - })) - .build() - .into_node() + })), + ..Default::default() + } + .into_node() } else { - StyledModal::build() - .add_class("deleteEpic") - .center() - .width(600) - .child(warning(model, modal)) - .build() - .into_node() + StyledModal { + children: vec![warning(model, modal)], + width: Some(600), + class_list: "deleteEpic", + ..Default::default() + } + .into_node() } } @@ -39,7 +40,7 @@ fn warning(model: &model::Model, modal: &Model) -> Node { .iter() .flat_map(|id| model.issues_by_id.get(id)) .map(|issue| { - let link = StyledIcon::build(Icon::Link).build().into_node(); + let link = StyledIcon::from(Icon::Link).into_node(); li![div![ C!["relatedIssue"], a![ @@ -51,16 +52,17 @@ fn warning(model: &model::Model, modal: &Model) -> Node { }) .collect(); - let close = StyledButton::build() - .text("Close") - .on_click(mouse_ev("click", move |ev| { + let close = StyledButton { + text: Some("Close"), + on_click: Some(mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); Msg::ModalDropped - })) - .secondary() - .build() - .into_node(); + })), + variant: ButtonVariant::Secondary, + ..Default::default() + } + .into_node(); section![ h3![C!["header"], "Cannot delete epic"], diff --git a/jirs-client/src/modals/epics_edit/view.rs b/jirs-client/src/modals/epics_edit/view.rs index bd098cf3..3e964e4b 100644 --- a/jirs-client/src/modals/epics_edit/view.rs +++ b/jirs-client/src/modals/epics_edit/view.rs @@ -33,30 +33,34 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node { } else { transform_into_unavailable(modal) }; - let close = StyledButton::build() - .on_click(mouse_ev("click", |ev| { + let close = StyledButton { + on_click: Some(mouse_ev("click", |ev| { ev.stop_propagation(); ev.prevent_default(); Msg::ModalDropped - })) - .empty() - .icon(Icon::Close) - .build() - .into_node(); - StyledModal::build() - .center() - .width(600) - .add_class("editEpic") - .child(div![C!["header"], h1!["Edit epic"], close]) - .child( - StyledInput::build() - .state(&modal.name) - .build(FieldId::EditEpic(EpicFieldId::Name)) - .into_node(), - ) - .child(transform) - .build() - .into_node() + })), + variant: ButtonVariant::Empty, + icon: Some(Icon::Close.into_node()), + ..Default::default() + } + .into_node(); + StyledModal { + width: Some(600), + class_list: "editEpic", + children: vec![ + div![C!["header"], h1!["Edit epic"], close], + StyledInput { + value: modal.name.value.as_str(), + valid: modal.name.is_valid(), + id: Some(FieldId::EditEpic(EpicFieldId::Name)), + ..Default::default() + } + .into_node(), + transform, + ], + ..Default::default() + } + .into_node() } fn transform_into_available(modal: &super::Model) -> Node { @@ -69,15 +73,16 @@ fn transform_into_available(modal: &super::Model) -> Node { .state(&modal.transform_into) .build(FieldId::EditEpic(EpicFieldId::TransformInto)) .into_node(); - let execute = StyledButton::build() - .on_click(mouse_ev("click", |ev| { + let execute = StyledButton { + on_click: Some(mouse_ev("click", |ev| { ev.stop_propagation(); ev.prevent_default(); Msg::TransformEpic - })) - .text("Transform") - .build() - .into_node(); + })), + text: Some("Transform"), + ..Default::default() + } + .into_node(); div![C!["transform available"], div![types], div![execute]] } diff --git a/jirs-client/src/modals/issue_statuses_delete/view.rs b/jirs-client/src/modals/issue_statuses_delete/view.rs index 08ea9fc1..3c3e9369 100644 --- a/jirs-client/src/modals/issue_statuses_delete/view.rs +++ b/jirs-client/src/modals/issue_statuses_delete/view.rs @@ -5,14 +5,14 @@ use { }; pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node { - StyledConfirmModal::build() - .title("Delete column") - .cancel_text("No") - .confirm_text("Yes") - .on_confirm(mouse_ev(Ev::Click, move |_| { + StyledConfirmModal { + title: "Delete column", + message: "Are you sure you want to delete column?", + confirm_text: "Yes", + cancel_text: "No", + on_confirm: Some(mouse_ev(Ev::Click, move |_| { Msg::DeleteIssueStatus(issue_status_id) - })) - .message("Are you sure you want to delete column?") - .build() - .into_node() + })), + } + .into_node() } diff --git a/jirs-client/src/modals/issues_create/model.rs b/jirs-client/src/modals/issues_create/model.rs index 50c8fff6..4292f83f 100644 --- a/jirs-client/src/modals/issues_create/model.rs +++ b/jirs-client/src/modals/issues_create/model.rs @@ -67,25 +67,26 @@ impl<'l> IntoChild<'l> for Type { let type_icon = { use crate::components::styled_icon::*; - let icon = { - match self { + StyledIcon { + icon: match self { Type::Task => Icon::Task, Type::Bug => Icon::Bug, Type::Story => Icon::Story, Type::Epic => Icon::Epic, - } - }; - crate::components::styled_icon::StyledIcon::build(icon) - .add_class(name) - .build() - .into_node() + }, + class_list: name, + ..Default::default() + } + .into_node() }; - StyledSelectChild::build() - .add_class(name) - .text(name) - .icon(type_icon) - .value(value) + StyledSelectChildBuilder { + class_list: name, + text: Some(name), + icon: Some(type_icon), + value, + ..Default::default() + } } } diff --git a/jirs-client/src/modals/issues_create/view.rs b/jirs-client/src/modals/issues_create/view.rs index 54cb0973..7c7bebcc 100644 --- a/jirs-client/src/modals/issues_create/view.rs +++ b/jirs-client/src/modals/issues_create/view.rs @@ -18,6 +18,9 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; +use crate::components::styled_select::SelectVariant; + pub fn view(model: &Model, modal: &AddIssueModal) -> Node { let issue_type = modal .type_state @@ -80,55 +83,54 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { }; let submit = { - StyledButton::build() - .primary() - .text(issue_type.submit_label()) - .add_class("action") - .add_class("submit") - .add_class("actionButton") - .on_click(mouse_ev(Ev::Click, move |ev| { + StyledButton { + variant: ButtonVariant::Primary, + text: Some(issue_type.submit_label()), + class_list: "action submit actionButton", + on_click: Some(mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); ev.prevent_default(); Some(issue_type.submit_action()) - })) - .build() - .into_node() + })), + ..Default::default() + } + .into_node() }; - let cancel = StyledButton::build() - .empty() - .add_class("action") - .add_class("cancel") - .add_class("actionButton") - .text("Cancel") - .on_click(mouse_ev(Ev::Click, |ev| { + let cancel = StyledButton { + variant: ButtonVariant::Empty, + class_list: "action cancel actionButton", + text: Some("Cancel"), + on_click: Some(mouse_ev(Ev::Click, |ev| { ev.stop_propagation(); ev.prevent_default(); Some(Msg::ModalDropped) - })) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); let actions = div![attrs![At::Class => "actions"], submit, cancel]; let form = form.add_field(actions).build().into_node(); - StyledModal::build() - .add_class("addIssue") - .width(0) - .variant(crate::components::styled_modal::Variant::Center) - .child(form) - .build() - .into_node() + StyledModal { + class_list: "addIssue", + width: Some(0), + children: vec![form], + ..Default::default() + } + .into_node() } fn issue_type_field(modal: &AddIssueModal) -> Node { - let select_type = StyledSelect::build() - .name("type") - .normal() - .text_filter(modal.type_state.text_filter.as_str()) - .opened(modal.type_state.opened) - .valid(true) - .options(Type::Task.into_iter().map(|t| t.into_child().name("type"))) - .selected(vec![{ + let select_type = StyledSelect { + id: FieldId::AddIssueModal(IssueFieldId::Type), + name: "type", + variant: SelectVariant::Normal, + text_filter: modal.type_state.text_filter.as_str(), + opened: modal.type_state.opened, + valid: true, + options: Some(Type::Task.into_iter().map(|t| t.into_child().name("type"))), + selected: vec![{ let v: Type = modal .type_state .values @@ -139,43 +141,52 @@ fn issue_type_field(modal: &AddIssueModal) -> Node { v } .into_child() - .name("type")]) - .build(FieldId::AddIssueModal(IssueFieldId::Type)) - .into_node(); - StyledField::build() - .label("Issue Type") - .tip("Start typing to get a list of possible matches.") - .input(select_type) - .build() - .into_node() + .name("type")], + ..Default::default() + } + .into_node(); + StyledField { + label: "Issue Type", + tip: Some("Start typing to get a list of possible matches."), + input: select_type, + ..Default::default() + } + .into_node() } #[inline] fn short_summary_field(modal: &AddIssueModal) -> Node { - let short_summary = StyledInput::build() - .state(&modal.title_state) - .build(FieldId::AddIssueModal(IssueFieldId::Title)) - .into_node(); - StyledField::build() - .label("Short Summary") - .tip("Concisely summarize the issue in one or two sentences.") - .input(short_summary) - .build() - .into_node() + let short_summary = StyledInput { + value: modal.title_state.value.as_str(), + valid: modal.title_state.is_valid(), + id: Some(FieldId::AddIssueModal(IssueFieldId::Title)), + ..Default::default() + } + .into_node(); + StyledField { + label: "Short Summary", + tip: Some("Concisely summarize the issue in one or two sentences."), + input: short_summary, + ..Default::default() + } + .into_node() } fn description_field() -> Node { - let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description)) - .height(110) - .add_class("textarea") - .build() - .into_node(); - StyledField::build() - .label("Description") - .tip("Describe the issue in as much detail as you'd like.") - .input(description) - .build() - .into_node() + let description = StyledTextarea { + id: Some(FieldId::AddIssueModal(IssueFieldId::Description)), + height: 110, + class_list: "textarea", + ..Default::default() + } + .into_node(); + StyledField { + label: "Description", + tip: Some("Describe the issue in as much detail as you'd like."), + input: description, + ..Default::default() + } + .into_node() } fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { @@ -183,58 +194,60 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { .reporter_id .or_else(|| model.user.as_ref().map(|u| u.id)) .unwrap_or_default(); - let reporter = StyledSelect::build() - .normal() - .text_filter(modal.reporter_state.text_filter.as_str()) - .opened(modal.reporter_state.opened) - .options(model.users.iter().map(|u| u.to_child().name("reporter"))) - .selected( - model - .users - .iter() - .filter_map(|user| { - if user.id == reporter_id { - Some(user.to_child().name("reporter")) - } else { - None - } - }) - .collect(), - ) - .valid(true) - .build(FieldId::AddIssueModal(IssueFieldId::Reporter)) - .into_node(); - StyledField::build() - .input(reporter) - .label("Reporter") - .tip("") - .build() - .into_node() + let reporter = StyledSelect { + id: FieldId::AddIssueModal(IssueFieldId::Reporter), + variant: SelectVariant::Normal, + text_filter: modal.reporter_state.text_filter.as_str(), + opened: modal.reporter_state.opened, + options: Some(model.users.iter().map(|u| u.to_child().name("reporter"))), + selected: model + .users + .iter() + .filter_map(|user| { + if user.id == reporter_id { + Some(user.to_child().name("reporter")) + } else { + None + } + }) + .collect(), + + valid: true, + ..Default::default() + } + .into_node(); + StyledField { + input: reporter, + label: "Reporter", + ..Default::default() + } + .into_node() } fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { - let assignees = StyledSelect::build() - .normal() - .multi() - .text_filter(modal.assignees_state.text_filter.as_str()) - .opened(modal.assignees_state.opened) - .options(model.users.iter().map(|u| u.to_child().name("assignees"))) - .selected( - model - .users - .iter() - .filter_map(|user| { - if modal.user_ids.contains(&user.id) { - Some(user.to_child().name("assignees")) - } else { - None - } - }) - .collect(), - ) - .valid(true) - .build(FieldId::AddIssueModal(IssueFieldId::Assignees)) - .into_node(); + let assignees = StyledSelect { + id: FieldId::AddIssueModal(IssueFieldId::Assignees), + variant: SelectVariant::Normal, + is_multi: true, + text_filter: modal.assignees_state.text_filter.as_str(), + opened: modal.assignees_state.opened, + options: Some(model.users.iter().map(|u| u.to_child().name("assignees"))), + selected: model + .users + .iter() + .filter_map(|user| { + if modal.user_ids.contains(&user.id) { + Some(user.to_child().name("assignees")) + } else { + None + } + }) + .collect(), + + valid: true, + ..Default::default() + } + .into_node(); StyledField::build() .input(assignees) .label("Assignees") @@ -245,33 +258,40 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { fn issue_priority_field(modal: &AddIssueModal) -> Node { let priorities = IssuePriority::default().into_iter(); - let select_priority = StyledSelect::build() - .name("priority") - .normal() - .text_filter(modal.priority_state.text_filter.as_str()) - .opened(modal.priority_state.opened) - .valid(true) - .options(priorities.map(|p| p.into_child().name("priority"))) - .selected(vec![modal.priority.into_child().name("priority")]) - .build(FieldId::AddIssueModal(IssueFieldId::Priority)) - .into_node(); - StyledField::build() - .label("Issue Type") - .tip("Priority in relation to other issues.") - .input(select_priority) - .build() - .into_node() + let select_priority = StyledSelect { + id: FieldId::AddIssueModal(IssueFieldId::Priority), + name: "priority", + variant: SelectVariant::Normal, + text_filter: modal.priority_state.text_filter.as_str(), + opened: modal.priority_state.opened, + valid: true, + options: Some(priorities.map(|p| p.into_child().name("priority"))), + selected: vec![modal.priority.into_child().name("priority")], + ..Default::default() + } + .into_node(); + StyledField { + label: "Issue Type", + tip: Some("Priority in relation to other issues."), + input: select_priority, + ..Default::default() + } + .into_node() } fn name_field(modal: &AddIssueModal) -> Node { - let name = StyledInput::build() - .state(&modal.title_state) - .build(FieldId::AddIssueModal(IssueFieldId::Title)) - .into_node(); - StyledField::build() - .label("Epic name") - .tip("Describe upcoming feature.") - .input(name) - .build() - .into_node() + let name = StyledInput { + value: modal.title_state.value.as_str(), + valid: modal.title_state.is_valid(), + id: Some(FieldId::AddIssueModal(IssueFieldId::Title)), + ..Default::default() + } + .into_node(); + StyledField { + label: "Epic name", + tip: Some("Describe upcoming feature."), + input: name, + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/modals/issues_delete/view.rs b/jirs-client/src/modals/issues_delete/view.rs index 067e7439..d0f098a5 100644 --- a/jirs-client/src/modals/issues_delete/view.rs +++ b/jirs-client/src/modals/issues_delete/view.rs @@ -9,14 +9,12 @@ pub fn view(model: &model::Model) -> Node { _ => return Node::Empty, }; - let handle_issue_delete = mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id)); - - StyledConfirmModal::build() - .title("Are you sure you want to delete this issue?") - .message("Once you delete, it's gone for good.") - .confirm_text("Delete issue") - .cancel_text("Cancel") - .on_confirm(handle_issue_delete) - .build() - .into_node() + StyledConfirmModal { + title: "Are you sure you want to delete this issue?", + message: "Once you delete, it's gone for good.", + confirm_text: "Delete issue", + cancel_text: "Cancel", + on_confirm: Some(mouse_ev(Ev::Click, move |_| Msg::DeleteIssue(issue_id))), + } + .into_node() } diff --git a/jirs-client/src/modals/issues_edit/view.rs b/jirs-client/src/modals/issues_edit/view.rs index 2e0dc861..9802dfc8 100644 --- a/jirs-client/src/modals/issues_edit/view.rs +++ b/jirs-client/src/modals/issues_edit/view.rs @@ -17,6 +17,11 @@ use { 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; pub fn view(model: &Model, modal: &EditIssueModal) -> Node { @@ -88,53 +93,87 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node { Msg::ModalOpened(ModalType::DeleteIssueConfirm(Some(issue_id))) }); - let copy_button = StyledButton::build() - .empty() - .icon(Icon::Link) - .on_click(click_handler) - .children(vec![span![if *link_copied { + let copy_button = StyledButton { + variant: ButtonVariant::Empty, + icon: Some(Icon::Link.into_node()), + on_click: Some(click_handler), + children: vec![span![if *link_copied { "Link Copied" } else { "Copy link" - }]]) - .build() - .into_node(); - let delete_button = StyledButton::build() - .empty() - .icon(Icon::Trash.into_styled_builder().size(19).build()) - .on_click(delete_confirmation_handler) - .build() - .into_node(); - let close_button = StyledButton::build() - .empty() - .icon(Icon::Close.into_styled_builder().size(24).build()) - .on_click(close_handler) - .build() - .into_node(); + }]], + ..Default::default() + } + .into_node(); + let delete_button = StyledButton { + variant: ButtonVariant::Empty, + icon: Some( + StyledIcon { + icon: Icon::Trash, + size: Some(19), + ..Default::default() + } + .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() - .dropdown_width(150) - .name("type") - .text_filter(top_type_state.text_filter.as_str()) - .opened(top_type_state.opened) - .valid(true) - .options( - IssueType::default() - .into_iter() - .map(|t| t.into_child().name("type")), - ) - .selected(vec![{ - let id = modal.id; - let issue_type = &payload.issue_type; - issue_type - .into_child() - .name("type") - .text_owned(format!("{} - {}", issue_type, id)) - }]) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Type, - ))) - .into_node(); + let issue_type_select = { + let id = modal.id; + let issue_type = &payload.issue_type; + let text = format!("{} - {}", issue_type, id); + + StyledSelect { + id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), + name: "type", + text_filter: top_type_state.text_filter.as_str(), + dropdown_width: Some(150), + valid: true, + opened: top_type_state.opened, + options: Some( + IssueType::default() + .into_iter() + .map(|t| t.into_child().name("type")), + ), + selected: vec![{ + let name = payload.issue_type.to_label(); + + let type_icon = StyledIcon { + icon: payload.issue_type.clone().into(), + 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![ C!["topActions"], @@ -156,41 +195,40 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .. } = modal; - let title = StyledInput::build() - .add_input_class("issueSummary") - .add_wrapper_class("issueSummary") - .add_wrapper_class("textarea") - .state(&modal.title_state) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + let title = StyledInput { + input_class_list: "issueSummary", + wrapper_class_list: "issueSummary textarea", + value: modal.title_state.value.as_str(), + valid: modal.title_state.is_valid(), + id: Some(FieldId::EditIssueModal(EditIssueModalSection::Issue( IssueFieldId::Title, - ))) - .into_node(); + ))), + ..Default::default() + } + .into_node(); let description = { - StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Description, - ))) - .initial_text(description_state.initial_text.as_str()) - .html(payload.description.as_ref().cloned().unwrap_or_default()) - .mode(description_state.mode.clone()) - .update_on(Ev::Change) - .build() + StyledEditor { + id: Some(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Description, + ))), + initial_text: description_state.initial_text.as_str(), + text: description_state.initial_text.as_str(), + html: payload.description.as_deref().unwrap_or_default(), + mode: description_state.mode.clone(), + update_event: Ev::Change, + } .into_node() }; let description_field = StyledField::build().input(description).build().into_node(); - let user_avatar = StyledAvatar::build() - .add_class("userAvatar") - .size(32) - .avatar_url( - model - .user - .as_ref() - .and_then(|u| u.avatar_url.as_deref()) - .unwrap_or_default(), - ) - .build() - .into_node(); + let user_avatar = StyledAvatar { + avatar_url: model.user.as_ref().and_then(|u| u.avatar_url.as_deref()), + size: 32, + class_list: "userAvatar", + ..StyledAvatar::default() + } + .into_node(); let create_comment = if comment_form.creating && comment_form.id.is_none() { build_comment_form(comment_form) @@ -206,11 +244,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { vec![div![C!["fakeTextArea"], "Add a comment...", handler]] }; - let comments: Vec> = model - .comments - .iter() - .flat_map(|c| comment(model, modal, c)) - .collect(); + let comments = model.comments.iter().flat_map(|c| comment(model, modal, c)); div![ C!["left"], @@ -249,110 +283,105 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .. } = modal; - let status = StyledSelect::build() - .name("status") - .opened(status_state.opened) - .normal() - .text_filter(status_state.text_filter.as_str()) - .options( + let status = StyledSelect { + id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), + name: "status", + opened: status_state.opened, + variant: SelectVariant::Normal, + text_filter: status_state.text_filter.as_str(), + options: Some( model .issue_statuses .iter() .map(|opt| opt.to_child().name("status")), - ) - .selected( - model - .issue_statuses - .iter() - .filter(|is| is.id == payload.issue_status_id) - .map(|is| is.to_child().name("status")) - .collect(), - ) - .valid(true) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::IssueStatusId, - ))) - .into_node(); + ), + selected: model + .issue_statuses + .iter() + .filter(|is| is.id == payload.issue_status_id) + .map(|is| is.to_child().name("status")) + .collect(), + + valid: true, + ..Default::default() + } + .into_node(); let status_field = StyledField::build() .input(status) .label("Status") .build() .into_node(); - let assignees = StyledSelect::build() - .name("assignees") - .opened(assignees_state.opened) - .empty() - .multi() - .text_filter(assignees_state.text_filter.as_str()) - .options( + let assignees = StyledSelect { + id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), + name: "assignees", + variant: SelectVariant::Empty, + is_multi: true, + opened: assignees_state.opened, + text_filter: assignees_state.text_filter.as_str(), + options: Some( model .users .iter() .map(|user| user.to_child().name("assignees")), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.user_ids.contains(&user.id)) - .map(|user| user.to_child().name("assignees")) - .collect(), - ) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Assignees, - ))) - .into_node(); + ), + selected: model + .users + .iter() + .filter(|user| payload.user_ids.contains(&user.id)) + .map(|user| user.to_child().name("assignees")) + .collect(), + ..Default::default() + } + .into_node(); let assignees_field = StyledField::build() .input(assignees) .label("Assignees") .build() .into_node(); - let reporter = StyledSelect::build() - .name("reporter") - .opened(reporter_state.opened) - .empty() - .text_filter(reporter_state.text_filter.as_str()) - .options( + let reporter = StyledSelect { + id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), + name: "reporter", + opened: reporter_state.opened, + variant: SelectVariant::Empty, + text_filter: reporter_state.text_filter.as_str(), + options: Some( model .users .iter() .map(|user| user.to_child().name("reporter")), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.reporter_id == user.id) - .map(|user| user.to_child().name("reporter")) - .collect(), - ) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Reporter, - ))) - .into_node(); + ), + selected: model + .users + .iter() + .filter(|user| payload.reporter_id == user.id) + .map(|user| user.to_child().name("reporter")) + .collect(), + ..Default::default() + } + .into_node(); let reporter_field = StyledField::build() .input(reporter) .label("Reporter") .build() .into_node(); - let priority = StyledSelect::build() - .name("priority") - .opened(priority_state.opened) - .empty() - .text_filter(priority_state.text_filter.as_str()) - .options( + let priority = StyledSelect { + id: FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), + name: "priority", + variant: SelectVariant::Empty, + opened: priority_state.opened, + text_filter: priority_state.text_filter.as_str(), + options: Some( IssuePriority::default() .into_iter() .map(|p| p.into_child().name("priority")), - ) - .selected(vec![payload.priority.into_child().name("priority")]) - .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Priority, - ))) - .into_node(); + ), + selected: vec![payload.priority.into_child().name("priority")], + ..Default::default() + } + .into_node(); let priority_field = StyledField::build() .input(priority) .label("Priority") diff --git a/jirs-client/src/modals/issues_edit/view/comments.rs b/jirs-client/src/modals/issues_edit/view/comments.rs index 07bbf1e7..a95e1a2a 100644 --- a/jirs-client/src/modals/issues_edit/view/comments.rs +++ b/jirs-client/src/modals/issues_edit/view/comments.rs @@ -13,6 +13,8 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; + pub fn build_comment_form(form: &CommentForm) -> Vec> { let submit_comment_form = mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); @@ -26,26 +28,30 @@ pub fn build_comment_form(form: &CommentForm) -> Vec> { )) }); - let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment( - CommentFieldId::Body, - ))) - .value(form.body.as_str()) - .placeholder("Add a comment...") - .build() + let text_area = StyledTextarea { + id: Some(FieldId::EditIssueModal(EditIssueModalSection::Comment( + CommentFieldId::Body, + ))), + value: form.body.as_str(), + placeholder: "Add a comment...", + ..Default::default() + } .into_node(); - let submit = StyledButton::build() - .primary() - .on_click(submit_comment_form) - .text("Save") - .build() - .into_node(); - let cancel = StyledButton::build() - .empty() - .on_click(close_comment_form) - .text("Cancel") - .build() - .into_node(); + let submit = StyledButton { + variant: ButtonVariant::Primary, + on_click: Some(submit_comment_form), + text: Some("Save"), + ..Default::default() + } + .into_node(); + let cancel = StyledButton { + variant: ButtonVariant::Empty, + on_click: Some(close_comment_form), + text: Some("Cancel"), + ..Default::default() + } + .into_node(); 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 avatar = StyledAvatar::build() - .size(32) - .avatar_url(user.avatar_url.as_deref()?) - .add_class("userAvatar") - .build() - .into_node(); + let avatar = StyledAvatar { + avatar_url: user.avatar_url.as_deref(), + size: 32, + class_list: "userAvatar", + ..StyledAvatar::default() + } + .into_node(); let buttons = if model.user.as_ref().map(|u| u.id) == Some(comment.user_id) { let comment_id = comment.id; @@ -68,26 +75,28 @@ pub fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Opti ev.stop_propagation(); Msg::ModalOpened(ModalType::DeleteCommentConfirm(Some(comment_id))) }); - let edit_button = StyledButton::build() - .add_class("editButton") - .on_click(mouse_ev(Ev::Click, move |_| { + let edit_button = StyledButton { + class_list: "editButton", + on_click: Some(mouse_ev(Ev::Click, move |_| { Msg::ModalChanged(FieldChange::EditComment( FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), comment_id, )) - })) - .text("Edit") - .empty() - .build() - .into_node(); + })), + text: Some("Edit"), + variant: ButtonVariant::Empty, + ..Default::default() + } + .into_node(); - let cancel_button = StyledButton::build() - .add_class("deleteButton") - .on_click(delete_comment_handler) - .text("Delete") - .empty() - .build() - .into_node(); + let cancel_button = StyledButton { + class_list: "deleteButton", + on_click: Some(delete_comment_handler), + text: Some("Delete"), + variant: ButtonVariant::Empty, + ..Default::default() + } + .into_node(); vec![edit_button, cancel_button] } else { diff --git a/jirs-client/src/modals/time_tracking/view.rs b/jirs-client/src/modals/time_tracking/view.rs index e855fbe3..b5883657 100644 --- a/jirs-client/src/modals/time_tracking/view.rs +++ b/jirs-client/src/modals/time_tracking/view.rs @@ -69,18 +69,20 @@ pub fn view(model: &Model, modal: &super::Model) -> Node { div![C!["inputContainer"], time_remaining_field] ]; - let close = StyledButton::build() - .text("Done") - .on_click(mouse_ev(Ev::Click, |_| Msg::ModalDropped)) - .build() - .into_node(); + let close = StyledButton { + text: Some("Done"), + on_click: Some(mouse_ev(Ev::Click, |_| Msg::ModalDropped)), + ..Default::default() + } + .into_node(); - StyledModal::build() - .add_class("timeTrackingModal") - .children(vec![modal_title, tracking, inputs, div![C!["actions"], close]].into_iter()) - .width(400) - .build() - .into_node() + StyledModal { + class_list: "timeTrackingModal", + children: vec![modal_title, tracking, inputs, div![C!["actions"], close]], + width: Some(400), + ..Default::default() + } + .into_node() } #[inline] @@ -94,27 +96,32 @@ pub fn time_tracking_field( let fibonacci_values = fibonacci_values(); let input = match time_tracking_type { TimeTracking::Untracked => empty![], - TimeTracking::Fibonacci => StyledSelect::build() - .selected( - select_state - .values - .iter() - .map(|n| (*n).to_child()) - .collect(), - ) - .state(select_state) - .options(fibonacci_values.iter().map(|v| v.to_child())) - .build(field_id) - .into_node(), - TimeTracking::Hourly => StyledInput::build() - .state(input_state) - .valid(true) - .build(field_id) - .into_node(), + TimeTracking::Fibonacci => StyledSelect { + id: field_id, + selected: select_state + .values + .iter() + .map(|n| (*n).to_child()) + .collect(), + + text_filter: select_state.text_filter.as_str(), + opened: select_state.opened, + options: Some(fibonacci_values.iter().map(|v| v.to_child())), + ..Default::default() + } + .into_node(), + TimeTracking::Hourly => StyledInput { + valid: input_state.is_valid(), + value: input_state.value.as_str(), + id: Some(field_id), + ..Default::default() + } + .into_node(), }; - StyledField::build() - .input(input) - .label(label) - .build() - .into_node() + StyledField { + input, + label, + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index da4a18b5..3c1b157c 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -142,6 +142,12 @@ macro_rules! match_page { _ => return, } }; + ($model: ident, $ty: ident; Empty) => { + match &$model.page_content { + PageContent::$ty(page) => page, + _ => return Node::Empty, + } + }; } #[macro_export] macro_rules! match_page_mut { diff --git a/jirs-client/src/pages/invite_page/view.rs b/jirs-client/src/pages/invite_page/view.rs index b19e810a..561501d9 100644 --- a/jirs-client/src/pages/invite_page/view.rs +++ b/jirs-client/src/pages/invite_page/view.rs @@ -4,6 +4,7 @@ use { styled_button::StyledButton, styled_field::StyledField, styled_form::StyledForm, styled_input::StyledInput, }, + match_page, model::{Model, PageContent}, pages::invite_page::InvitePage, shared::{outer_layout, ToNode}, @@ -14,11 +15,10 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; + pub fn view(model: &Model) -> Node { - let page = match &model.page_content { - PageContent::Invite(page) => page, - _ => return empty![], - }; + let page = match_page!(model, Invite; Empty); let token_field = token_field(page); let submit_field = submit(page); @@ -43,24 +43,32 @@ pub fn view(model: &Model) -> Node { } fn submit(_page: &InvitePage) -> Node { - let submit = StyledButton::build() - .text("Accept") - .primary() - .build() - .into_node(); - StyledField::build().input(submit).build().into_node() + let submit = StyledButton { + text: Some("Accept"), + variant: ButtonVariant::Primary, + ..Default::default() + } + .into_node(); + StyledField { + input: submit, + ..Default::default() + } + .into_node() } fn token_field(page: &InvitePage) -> Node { - let token = StyledInput::build() - .valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none()) - .value(page.token.as_str()) - .build(FieldId::Invite(InviteFieldId::Token)) - .into_node(); + let input = StyledInput { + valid: !page.token_touched || is_token(page.token.as_str()) && page.error.is_none(), + id: Some(FieldId::Invite(InviteFieldId::Token)), + value: page.token.as_str(), + ..Default::default() + } + .into_node(); - StyledField::build() - .input(token) - .label("Your invite token") - .build() - .into_node() + StyledField { + input, + label: "Your invite token", + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/pages/profile_page/view.rs b/jirs-client/src/pages/profile_page/view.rs index 75970305..2b471f5f 100644 --- a/jirs-client/src/pages/profile_page/view.rs +++ b/jirs-client/src/pages/profile_page/view.rs @@ -15,53 +15,65 @@ use { 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 { let page = match &model.page_content { PageContent::Profile(profile_page) => profile_page, _ => return empty![], }; - let avatar = StyledImageInput::build(FieldId::Profile(UsersFieldId::Avatar)) - .add_class("avatar") - .state(&page.avatar) - .build() - .into_node(); + let avatar = StyledImageInput { + id: FieldId::Profile(UsersFieldId::Avatar), + class_list: "avatar", + url: page.avatar.url.as_deref(), + } + .into_node(); - let username = StyledInput::build() - .state(&page.name) - .valid(true) - .primary() - .build(FieldId::Profile(UsersFieldId::Username)) - .into_node(); - let username_field = StyledField::build() - .label("Username") - .input(username) - .build() - .into_node(); + let username = StyledInput { + id: Some(FieldId::Profile(UsersFieldId::Username)), + valid: page.name.is_valid(), + value: page.name.value.as_str(), + variant: InputVariant::Primary, + ..Default::default() + } + .into_node(); + let username_field = StyledField { + label: "Username", + input: username, + ..Default::default() + } + .into_node(); - let email = StyledInput::build() - .state(&page.email) - .valid(true) - .primary() - .build(FieldId::Profile(UsersFieldId::Username)) - .into_node(); - let email_field = StyledField::build() - .label("E-Mail") - .input(email) - .build() - .into_node(); + let email = StyledInput { + id: Some(FieldId::Profile(UsersFieldId::Email)), + valid: page.email.is_valid(), + value: page.email.value.as_str(), + variant: InputVariant::Primary, + ..Default::default() + } + .into_node(); + let email_field = StyledField { + label: "E-Mail", + input: email, + ..Default::default() + } + .into_node(); let current_project = build_current_project(model, page); - let submit = StyledButton::build() - .primary() - .text("Save") - .on_click(mouse_ev(Ev::Click, |ev| { + let submit = StyledButton { + variant: ButtonVariant::Primary, + text: Some("Save"), + on_click: Some(mouse_ev(Ev::Click, |ev| { ev.prevent_default(); Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm)) - })) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); let submit_field = StyledField::build().input(submit).build().into_node(); let content = StyledForm::build() @@ -81,44 +93,47 @@ pub fn view(model: &Model) -> Node { } fn build_current_project(model: &Model, page: &ProfilePage) -> Node { - let inner = - if model.projects.len() <= 1 { - let name = model - .project - .as_ref() - .map(|p| p.name.as_str()) - .unwrap_or_default(); - span![name] - } else { - let mut project_by_id = HashMap::new(); - for p in model.projects.iter() { - project_by_id.insert(p.id, p); - } - let mut joined_projects = HashMap::new(); - for p in model.user_projects.iter() { - joined_projects.insert(p.project_id, p); - } + let inner = if model.projects.len() <= 1 { + let name = model + .project + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or_default(); + span![name] + } else { + let mut project_by_id = HashMap::new(); + for p in model.projects.iter() { + project_by_id.insert(p.id, p); + } + let mut joined_projects = HashMap::new(); + for p in model.user_projects.iter() { + joined_projects.insert(p.project_id, p); + } - StyledSelect::build() - .name("current_project") - .normal() - .options(model.projects.iter().filter_map(|project| { - joined_projects.get(&project.id).map(|_| project.to_child()) - })) - .selected( - page.current_project - .values - .iter() - .filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child())) - .collect(), - ) - .state(&page.current_project) - .build(FieldId::Profile(UsersFieldId::CurrentProject)) - .into_node() - }; - StyledField::build() - .label("Current project") - .input(div![C!["project-name"], inner]) - .build() + StyledSelect { + id: FieldId::Profile(UsersFieldId::CurrentProject), + name: "current_project", + valid: true, + opened: page.current_project.opened, + text_filter: page.current_project.text_filter.as_str(), + variant: SelectVariant::Normal, + options: Some(model.projects.iter().filter_map(|project| { + joined_projects.get(&project.id).map(|_| project.to_child()) + })), + selected: page + .current_project + .values + .iter() + .filter_map(|id| project_by_id.get(&((*id) as i32)).map(|p| p.to_child())) + .collect(), + ..Default::default() + } .into_node() + }; + StyledField { + label: "Current project", + input: div![C!["project-name"], inner], + ..Default::default() + } + .into_node() } diff --git a/jirs-client/src/pages/project_page/view/board.rs b/jirs-client/src/pages/project_page/view/board.rs index 5fd16575..f13f97e4 100644 --- a/jirs-client/src/pages/project_page/view/board.rs +++ b/jirs-client/src/pages/project_page/view/board.rs @@ -9,6 +9,8 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; + pub fn project_board_lists(model: &Model) -> Node { let project_page = match &model.page_content { PageContent::Project(project_page) => project_page, @@ -35,10 +37,10 @@ pub fn project_board_lists(model: &Model) -> Node { let epic_name = match per_epic.epic_ref.as_ref() { Some((id, name)) => { let id = *id; - let edit_button = StyledButton::build() - .empty() - .icon(Icon::EditAlt) - .on_click(mouse_ev("click", move |ev| { + let edit_button = StyledButton { + variant: ButtonVariant::Empty, + icon: Some(Icon::EditAlt.into_node()), + on_click: Some(mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); seed::Url::new() @@ -46,13 +48,14 @@ pub fn project_board_lists(model: &Model) -> Node { .add_path_part(id.to_string()) .go_and_push(); Msg::ChangePage(Page::EditEpic(id)) - })) - .build() - .into_node(); - let delete_button = StyledButton::build() - .empty() - .icon(Icon::DeleteAlt) - .on_click(mouse_ev("click", move |ev| { + })), + ..Default::default() + } + .into_node(); + let delete_button = StyledButton { + variant: ButtonVariant::Empty, + icon: Some(Icon::DeleteAlt.into_node()), + on_click: Some(mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); seed::Url::new() @@ -60,9 +63,10 @@ pub fn project_board_lists(model: &Model) -> Node { .add_path_part(id.to_string()) .go_and_push(); Msg::ChangePage(Page::DeleteEpic(id)) - })) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); div![ C!["epicHeader"], @@ -130,26 +134,31 @@ fn project_issue(model: &Model, issue: &Issue) -> Node { .iter() .filter_map(|id| model.users_by_id.get(id)) .map(|user| { - StyledAvatar::build() - .size(24) - .name(user.name.as_str()) - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .user_index(0) - .build() - .into_node() + StyledAvatar { + avatar_url: user.avatar_url.as_deref(), + size: 24, + name: &user.name, + ..StyledAvatar::default() + } + .into_node() }) .collect(); - let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into()) - .with_color(issue.issue_type.to_str()) - .build() - .into_node(); + let issue_type_icon = StyledIcon { + icon: issue.issue_type.into(), + class_list: issue.issue_type.to_str(), + color: Some(issue.issue_type.to_str()), + ..Default::default() + } + .into_node(); - let priority_icon = StyledIcon::build(issue.priority.into()) - .add_class(issue.priority.to_str()) - .with_color(issue.priority.to_str()) - .build() - .into_node(); + let priority_icon = StyledIcon { + icon: issue.priority.into(), + class_list: issue.priority.to_str(), + color: Some(issue.priority.to_str()), + ..Default::default() + } + .into_node(); let issue_id = issue.id; let drag_started = drag_ev(Ev::DragStart, move |ev| { diff --git a/jirs-client/src/pages/project_page/view/filters.rs b/jirs-client/src/pages/project_page/view/filters.rs index 654106d5..1bc6ec63 100644 --- a/jirs-client/src/pages/project_page/view/filters.rs +++ b/jirs-client/src/pages/project_page/view/filters.rs @@ -14,29 +14,33 @@ pub fn project_board_filters(model: &Model) -> Node { _ => return empty![], }; - let search_input = StyledInput::build() - .icon(Icon::Search) - .valid(true) - .value(project_page.text_filter.as_str()) - .build(FieldId::TextFilterBoard) - .into_node(); + let search_input = StyledInput { + value: project_page.text_filter.as_str(), + valid: true, + id: Some(FieldId::TextFilterBoard), + icon: Some(Icon::Search), + ..Default::default() + } + .into_node(); - let only_my = StyledButton::build() - .empty() - .active(project_page.only_my_filter) - .text("Only My Issues") - .add_class("filterChild") - .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)) - .build() - .into_node(); + let only_my = StyledButton { + variant: ButtonVariant::Empty, + active: project_page.only_my_filter, + text: Some("Only My Issues"), + class_list: "filterChild", + on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)), + ..Default::default() + } + .into_node(); - let recently_updated = StyledButton::build() - .empty() - .text("Recently Updated") - .add_class("filterChild") - .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)) - .build() - .into_node(); + let recently_updated = StyledButton { + variant: ButtonVariant::Empty, + text: Some("Recently Updated"), + class_list: "filterChild", + on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)), + ..Default::default() + } + .into_node(); let clear_all = if project_page.only_my_filter || project_page.recently_updated_filter @@ -75,17 +79,18 @@ pub fn avatars_filters(model: &Model) -> Node { .map(|(idx, user)| { let user_id = user.id; let active = active_avatar_filters.contains(&user_id); - let styled_avatar = StyledAvatar::build() - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .on_click(mouse_ev(Ev::Click, move |_| { + let styled_avatar = StyledAvatar { + avatar_url: user.avatar_url.as_deref(), + name: &user.name, + on_click: Some(mouse_ev(Ev::Click, move |_| { Msg::ProjectAvatarFilterChanged(user_id, active) - })) - .name(user.name.as_str()) - .user_index(idx) - .build() - .into_node(); + })), + user_index: idx, + ..StyledAvatar::default() + } + .into_node(); div![ - if active { Some(C!["isActive"]) } else { None }, + IF![active => C!["isActive"]], C!["avatarIsActiveBorder"], styled_avatar ] diff --git a/jirs-client/src/pages/project_settings_page/view.rs b/jirs-client/src/pages/project_settings_page/view.rs index 36157503..6a037893 100644 --- a/jirs-client/src/pages/project_settings_page/view.rs +++ b/jirs-client/src/pages/project_settings_page/view.rs @@ -21,6 +21,10 @@ use { 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; static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt"); @@ -60,29 +64,31 @@ pub fn view(model: &model::Model) -> Node { .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) .into_node(); let time_tracking_type: TimeTracking = page.time_tracking.value.into(); - let time_tracking_field = StyledField::build() - .input(time_tracking) - .tip(match time_tracking_type { + let time_tracking_field = StyledField { + input: time_tracking, + tip: Some(match time_tracking_type { TimeTracking::Fibonacci => TIME_TRACKING_FIBONACCI, TimeTracking::Hourly => TIME_TRACKING_HOURLY, _ => "", - }) - .build() - .into_node(); + }), + ..Default::default() + } + .into_node(); let columns_field = columns_section(model, page); - let save_button = StyledButton::build() - .add_class("actionButton") - .on_click(mouse_ev(Ev::Click, |ev| { + let save_button = StyledButton { + class_list: "actionButton", + on_click: Some(mouse_ev(Ev::Click, |ev| { ev.prevent_default(); Msg::PageChanged(PageChanged::ProjectSettings( ProjectPageChange::SubmitProjectSettingsForm, )) - })) - .text("Save changes") - .build() - .into_node(); + })), + text: Some("Save changes"), + ..Default::default() + } + .into_node(); let form = StyledForm::build() .heading("Project Details") @@ -110,13 +116,15 @@ pub fn view(model: &model::Model) -> Node { /// Build project name input with styled field wrapper fn name_field(page: &ProjectSettingsPage) -> Node { - let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name)) - .value(page.payload.name.as_deref().unwrap_or_default()) - .height(39) - .max_height(39) - .disable_auto_resize() - .build() - .into_node(); + let name = StyledTextarea { + id: Some(FieldId::ProjectSettings(ProjectFieldId::Name)), + value: page.payload.name.as_deref().unwrap_or_default(), + height: 39, + max_height: 39, + disable_auto_resize: true, + ..Default::default() + } + .into_node(); StyledField::build() .label("Name") .input(name) @@ -127,13 +135,15 @@ fn name_field(page: &ProjectSettingsPage) -> Node { /// Build project url input with styled field wrapper fn url_field(page: &ProjectSettingsPage) -> Node { - let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url)) - .height(39) - .max_height(39) - .disable_auto_resize() - .value(page.payload.url.as_deref().unwrap_or_default()) - .build() - .into_node(); + let url = StyledTextarea { + id: Some(FieldId::ProjectSettings(ProjectFieldId::Url)), + height: 39, + max_height: 39, + disable_auto_resize: true, + value: page.payload.url.as_deref().unwrap_or_default(), + ..Default::default() + } + .into_node(); StyledField::build() .label("Url") .input(url) @@ -144,52 +154,53 @@ fn url_field(page: &ProjectSettingsPage) -> Node { /// Build project description text area with styled field wrapper fn description_field(page: &ProjectSettingsPage) -> Node { - let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description)) - .text( - page.payload - .description - .as_ref() - .cloned() - .unwrap_or_default(), - ) - .update_on(Ev::Change) - .mode(page.description_mode.clone()) - .build() - .into_node(); - StyledField::build() - .input(description) - .label("Description") - .tip("Describe the project in as much detail as you'd like.") - .build() - .into_node() + let description = StyledEditor { + id: Some(FieldId::ProjectSettings(ProjectFieldId::Description)), + initial_text: page.payload.description.as_deref().unwrap_or_default(), + text: page.payload.description.as_deref().unwrap_or_default(), + html: page.payload.description.as_deref().unwrap_or_default(), + mode: page.description_mode.clone(), + update_event: Ev::Change, + } + .into_node(); + StyledField { + label: "Description", + tip: Some("Describe the project in as much detail as you'd like."), + input: description, + ..Default::default() + } + .into_node() } /// Build project category dropdown with styled field wrapper fn category_field(page: &ProjectSettingsPage) -> Node { - let category = StyledSelect::build() - .opened(page.project_category_state.opened) - .text_filter(page.project_category_state.text_filter.as_str()) - .valid(true) - .normal() - .options( + let category = StyledSelect { + id: FieldId::ProjectSettings(ProjectFieldId::Category), + opened: page.project_category_state.opened, + text_filter: page.project_category_state.text_filter.as_str(), + valid: true, + variant: SelectVariant::Normal, + options: Some( ProjectCategory::default() .into_iter() .map(|c| c.into_child()), - ) - .selected(vec![page + ), + selected: vec![page .payload .category .as_ref() .cloned() .unwrap_or_default() - .into_child()]) - .build(FieldId::ProjectSettings(ProjectFieldId::Category)) - .into_node(); - StyledField::build() - .label("Project Category") - .input(category) - .build() - .into_node() + .into_child()], + ..Default::default() + } + .into_node(); + StyledField { + input: category, + label: "Project Category", + ..Default::default() + } + .into_node() } /// Build draggable columns preview with option to remove and add new columns @@ -216,13 +227,13 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node { add_column(page, column_style.as_str()) ] ]; - StyledField::build() - .add_class("columnsField") - .input(columns_section) - .label("Columns") - .tip("Double-click on name to change it.") - .build() - .into_node() + StyledField { + label: "Columns", + tip: Some("Double-click on name to change it."), + input: columns_section, + class_list: "columnsField", + } + .into_node() } #[inline] @@ -246,20 +257,23 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node { ))) }); - let input = StyledInput::build() - .state(&page.name) - .primary() - .auto_focus() - .on_input_ev(blur) - .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) - .into_node(); + let input = StyledInput { + value: page.name.value.as_str(), + valid: page.name.is_valid(), + auto_focus: true, + variant: InputVariant::Primary, + id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)), + input_handlers: vec![blur], + ..Default::default() + } + .into_node(); div![ C!["columnPreview"], div![C!["columnName"], form![on_submit, input]] ] } else { - let add_column = StyledIcon::build(Icon::Plus).build().into_node(); + let add_column = StyledIcon::from(Icon::Plus).into_node(); div![ C!["columnPreview"], attrs![At::Style => column_style], @@ -282,13 +296,16 @@ fn column_preview( ProjectPageChange::EditIssueStatusName(None), )) }); - let input = StyledInput::build() - .state(&page.name) - .primary() - .auto_focus() - .on_input_ev(blur) - .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) - .into_node(); + let input = StyledInput { + value: page.name.value.as_str(), + valid: page.name.is_valid(), + variant: InputVariant::Primary, + auto_focus: true, + input_handlers: vec![blur], + id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)), + ..Default::default() + } + .into_node(); div![C!["columnPreview"], div![C!["columnName"], input]] } else { @@ -337,13 +354,14 @@ fn show_column_preview( ev.stop_propagation(); Msg::ModalOpened(ModalType::DeleteIssueStatusModal(Some(id))) }); - let delete = StyledButton::build() - .primary() - .add_class("removeColumn") - .icon(Icon::Trash) - .on_click(on_delete) - .build() - .into_node(); + let delete = StyledButton { + variant: ButtonVariant::Primary, + class_list: "removeColumn", + icon: Some(Icon::Trash.into_node()), + on_click: Some(on_delete), + ..Default::default() + } + .into_node(); div![C!["removeColumn"], delete] } else { div![ diff --git a/jirs-client/src/pages/reports_page/view.rs b/jirs-client/src/pages/reports_page/view.rs index 73428113..b524c19a 100644 --- a/jirs-client/src/pages/reports_page/view.rs +++ b/jirs-client/src/pages/reports_page/view.rs @@ -12,6 +12,8 @@ use { std::collections::HashMap, }; +use crate::components::styled_icon::Icon; + const SVG_MARGIN_X: u32 = 10; const SVG_DRAWABLE_HEIGHT: u32 = 300; 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; let day = date.format("%Y-%m-%d").to_string(); - let type_icon = StyledIcon::build(issue_type.clone().into()) - .build() + let type_icon = StyledIcon::from(Icon::from(issue_type.clone())) .into_node(); - let priority_icon = StyledIcon::build(priority.clone().into()) - .build() + let priority_icon = StyledIcon::from(Icon::from(priority.clone())) .into_node(); let desc = Node::from_html(None, - description - .as_deref() - .unwrap_or_default() + description + .as_deref() + .unwrap_or_default(), ); - let link = StyledLink::build() - .with_icon() - .text(format!("{}-{}", project_name, id).as_str()) - .href(format!("/issues/{}", id).as_str()) - .build() - .into_node(); + let link = StyledLink { + children: vec![ + Icon::Link.into_node(), + span![format!("{}-{}", project_name, id).as_str()] + ], + class_list: "withIcon", + href: format!("/issues/{}", id).as_str(), + }.into_node(); li![ - C!["issue"], - C![selection_state.to_str()], + C!["issue", selection_state.to_str()], div![C!["number"], link], div![C!["type"], type_icon], IF!( selection_state != SelectionState::NotSelected => div![C!["priority"], priority_icon]), diff --git a/jirs-client/src/pages/sign_in_page/view.rs b/jirs-client/src/pages/sign_in_page/view.rs index 81a4cc6c..8a0d1b14 100644 --- a/jirs-client/src/pages/sign_in_page/view.rs +++ b/jirs-client/src/pages/sign_in_page/view.rs @@ -16,28 +16,34 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; + pub fn view(model: &model::Model) -> Node { let page = match &model.page_content { PageContent::SignIn(page) => page, _ => return empty![], }; - let username = StyledInput::build() - .value(page.username.as_str()) - .valid(is_valid_username(page.username_touched, &page.username)) - .build(FieldId::SignIn(SignInFieldId::Username)) - .into_node(); + let username = StyledInput { + value: page.username.as_str(), + valid: is_valid_username(page.username_touched, &page.username), + id: Some(FieldId::SignIn(SignInFieldId::Username)), + ..Default::default() + } + .into_node(); let username_field = StyledField::build() .label("Username") .input(username) .build() .into_node(); - let email = StyledInput::build() - .value(page.email.as_str()) - .valid(is_valid_email(page.email_touched, page.email.as_str())) - .build(FieldId::SignIn(SignInFieldId::Email)) - .into_node(); + let email = StyledInput { + value: page.email.as_str(), + valid: is_valid_email(page.email_touched, page.email.as_str()), + id: Some(FieldId::SignIn(SignInFieldId::Email)), + ..Default::default() + } + .into_node(); let email_field = StyledField::build() .label("E-Mail") .input(email) @@ -45,33 +51,39 @@ pub fn view(model: &model::Model) -> Node { .into_node(); let submit = if page.login_success { - StyledButton::build() - .success() - .text("✓ Please check your mail") + StyledButton { + variant: ButtonVariant::Success, + text: Some("✓ Please check your mail"), + ..Default::default() + } } else { - StyledButton::build() - .primary() - .text("Sign In") - .on_click(mouse_ev(Ev::Click, |_| Msg::SignInRequest)) + StyledButton { + variant: ButtonVariant::Primary, + text: Some("Sign In"), + 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(); - 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) - .add_class("noPasswordHelp") - .size(22) - .build() - .into_node(); + let help_icon = StyledIcon { + icon: Icon::Help, + class_list: "noPasswordHelp", + size: Some(22), + ..Default::default() + } + .into_node(); let no_pass_section = div![ C!["noPasswordSection"], @@ -80,19 +92,16 @@ pub fn view(model: &model::Model) -> Node { span!["Why I don't see password?"] ]; - let sign_in_form = StyledForm::build() - .heading("Sign In to your account") - .on_submit(ev(Ev::Submit, |ev| { + let sign_in_form = StyledForm { + heading: "Sign In to your account", + fields: vec![username_field, email_field, submit_field, no_pass_section], + on_submit: Some(ev(Ev::Submit, |ev| { ev.stop_propagation(); ev.prevent_default(); Msg::SignInRequest - })) - .add_field(username_field) - .add_field(email_field) - .add_field(submit_field) - .add_field(no_pass_section) - .build() - .into_node(); + })), + } + .into_node(); let token = StyledInput::new_with_id_and_value_and_valid( FieldId::SignIn(SignInFieldId::Token), @@ -105,12 +114,13 @@ pub fn view(model: &model::Model) -> Node { .input(token) .build() .into_node(); - let submit_token = StyledButton::build() - .primary() - .text("Authorize") - .on_click(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)) - .build() - .into_node(); + let submit_token = StyledButton { + variant: ButtonVariant::Primary, + text: Some("Authorize"), + on_click: Some(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)), + ..Default::default() + } + .into_node(); let submit_token_field = StyledField::build().input(submit_token).build().into_node(); let bind_token_form = StyledForm::build() diff --git a/jirs-client/src/pages/sign_up_page/view.rs b/jirs-client/src/pages/sign_up_page/view.rs index c05eefe6..9fb122a0 100644 --- a/jirs-client/src/pages/sign_up_page/view.rs +++ b/jirs-client/src/pages/sign_up_page/view.rs @@ -8,6 +8,7 @@ use { styled_input::StyledInput, styled_link::StyledLink, }, + match_page, model::{self, PageContent}, shared::{outer_layout, ToNode}, validations::is_email, @@ -17,64 +18,75 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; + pub fn view(model: &model::Model) -> Node { - let page = match &model.page_content { - PageContent::SignUp(page) => page, - _ => return empty![], - }; + let page = match_page!(model, SignUp; Empty); - let username = StyledInput::build() - .value(page.username.as_str()) - .valid(!page.username_touched || page.username.len() > 1) - .build(FieldId::SignUp(SignUpFieldId::Username)) - .into_node(); - let username_field = StyledField::build() - .label("Username") - .input(username) - .build() - .into_node(); - - 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)) + let username = StyledInput { + value: page.username.as_str(), + valid: !page.username_touched || page.username.len() > 1, + id: Some(FieldId::SignUp(SignUpFieldId::Username)), + ..Default::default() + } + .into_node(); + let username_field = StyledField { + label: "Username", + input: username, + ..Default::default() } - .build() .into_node(); - let sign_in_link = StyledLink::build() - .text("Sign In") - .href("/login") - .add_class("signInLink") - .build() - .into_node(); + let email = StyledInput { + value: page.email.as_str(), + valid: !page.email_touched || is_email(page.email.as_str()), + id: Some(FieldId::SignUp(SignUpFieldId::Email)), + ..Default::default() + } + .into_node(); + let email_field = StyledField { + label: "E-Mail", + input: email, + ..Default::default() + } + .into_node(); - let submit_field = StyledField::build() - .input(div![C!["twoRow"], submit, sign_in_link,]) - .build() - .into_node(); + let submit = if page.sign_up_success { + StyledButton { + variant: ButtonVariant::Success, + 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) - .add_class("noPasswordHelp") - .size(22) - .build() - .into_node(); + let sign_in_link = StyledLink { + children: vec![span!["Sign In"]], + class_list: "signInLink", + href: "/login", + } + .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![ C!["noPasswordSection"], diff --git a/jirs-client/src/pages/users_page/update.rs b/jirs-client/src/pages/users_page/update.rs index 68227d6d..75d30c80 100644 --- a/jirs-client/src/pages/users_page/update.rs +++ b/jirs-client/src/pages/users_page/update.rs @@ -7,13 +7,17 @@ use { FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged, }, 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) { + log!(model); + if model.user.is_none() { + return; + } + if let Msg::ChangePage(Page::Users) = msg { build_page_content(model); - // return; } let page = match &mut model.page_content { diff --git a/jirs-client/src/pages/users_page/view.rs b/jirs-client/src/pages/users_page/view.rs index f46f785b..c0588f70 100644 --- a/jirs-client/src/pages/users_page/view.rs +++ b/jirs-client/src/pages/users_page/view.rs @@ -13,76 +13,89 @@ use { seed::{prelude::*, *}, }; -pub fn view(model: &Model) -> Node { - if model.user.is_none() { - return empty![]; - } +use crate::components::styled_button::ButtonVariant; +use crate::components::styled_input::InputVariant; +use crate::components::styled_select::SelectVariant; +pub fn view(model: &Model) -> Node { let page = match &model.page_content { PageContent::Users(page) => page, _ => return empty![], }; - let name = StyledInput::build() - .valid(!page.name_touched || page.name.len() >= 3) - .value(page.name.as_str()) - .build(FieldId::Users(UsersFieldId::Username)) - .into_node(); - let name_field = StyledField::build() - .input(name) - .label("Name") - .build() - .into_node(); + let name = StyledInput { + valid: !page.name_touched || page.name.len() >= 3, + value: page.name.as_str(), + variant: InputVariant::Normal, + id: Some(FieldId::Users(UsersFieldId::Username)), + ..Default::default() + } + .into_node(); + let name_field = StyledField { + label: "Name", + input: name, + ..Default::default() + } + .into_node(); - let email = StyledInput::build() - .valid(!page.email_touched || is_email(page.email.as_str())) - .value(page.email.as_str()) - .build(FieldId::Users(UsersFieldId::Email)) - .into_node(); - let email_field = StyledField::build() - .input(email) - .label("E-Mail") - .build() - .into_node(); + let email = StyledInput { + id: Some(FieldId::Users(UsersFieldId::Email)), + valid: !page.email_touched || is_email(page.email.as_str()), + value: page.email.as_str(), + variant: InputVariant::Normal, + ..Default::default() + } + .into_node(); + let email_field = StyledField { + input: email, + label: "E-Mail", + ..Default::default() + } + .into_node(); - let user_role = StyledSelect::build() - .name("user_role") - .valid(true) - .normal() - .state(&page.user_role_state) - .selected(vec![page.user_role.into_child()]) - .options( + let user_role = StyledSelect { + id: FieldId::Users(UsersFieldId::UserRole), + name: "user_role", + valid: true, + variant: SelectVariant::Normal, + text_filter: page.user_role_state.text_filter.as_str(), + opened: page.user_role_state.opened, + selected: vec![page.user_role.into_child()], + options: Some( UserRole::default() .into_iter() .map(|role| role.into_child()), - ) - .build(FieldId::Users(UsersFieldId::UserRole)) - .into_node(); + ), + ..Default::default() + } + .into_node(); let user_role_field = StyledField::build() .input(user_role) .label("Role") .build() .into_node(); - let submit = StyledButton::build() - .add_class("submitUserInvite") - .active(page.form_state != InvitationFormState::Sent) - .primary() - .text("Invite user") - .build() - .into_node(); + let submit = StyledButton { + text: Some("Invite user"), + variant: ButtonVariant::Primary, + class_list: "submitUserInvite", + active: page.form_state != InvitationFormState::Sent, + ..Default::default() + } + .into_node(); let submit_supplement = match page.form_state { - InvitationFormState::Succeed => StyledButton::build() - .add_class("resetUserInvite") - .active(true) - .empty() - .set_type_reset() - .on_click(mouse_ev(Ev::Click, |_| { + InvitationFormState::Succeed => StyledButton { + variant: ButtonVariant::Empty, + class_list: "resetUserInvite", + active: true, + on_click: Some(mouse_ev(Ev::Click, |_| { Msg::PageChanged(PageChanged::Users(UsersPageChange::ResetForm)) - })) - .text("Reset") - .build() - .into_node(), + })), + text: Some("Reset"), + button_type: "reset", + ..Default::default() + } + .into_node(), InvitationFormState::Failed => div![C!["error"], "There was an error"], _ => empty![], }; @@ -109,13 +122,14 @@ pub fn view(model: &Model) -> Node { .iter() .map(|user| { let user_id = user.id; - let remove = StyledButton::build() - .text("Remove") - .on_click(mouse_ev(Ev::Click, move |_| { + let remove = StyledButton { + text: Some("Remove"), + on_click: Some(mouse_ev(Ev::Click, move |_| { Msg::InvitedUserRemove(user_id) - })) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); let role = page .invitations .iter() @@ -144,15 +158,21 @@ pub fn view(model: &Model) -> Node { .iter() .map(|invitation| { let id = invitation.id; - let revoke = StyledButton::build() - .text("Revoke") - .disabled(invitation.state == InvitationState::Revoked) - .on_click(mouse_ev(Ev::Click, move |_| Msg::InviteRevokeRequest(id))) - .build() - .into_node(); + let revoke = StyledButton { + disabled: invitation.state == InvitationState::Revoked, + text: Some("Revoke"), + on_click: Some(mouse_ev(Ev::Click, move |_| Msg::InviteRevokeRequest(id))), + ..Default::default() + } + .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![ - C!["invitation"], - attrs![At::Class => format!("{}", invitation.state)], + C!["invitation", format!("{}", invitation.state)], span![invitation.name.as_str()], span![invitation.email.as_str()], span![format!("{}", invitation.state)], diff --git a/jirs-client/src/shared/aside.rs b/jirs-client/src/shared/aside.rs index 286711dd..56169790 100644 --- a/jirs-client/src/shared/aside.rs +++ b/jirs-client/src/shared/aside.rs @@ -98,7 +98,7 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option) None }; 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| { mouse_ev("click", move |ev| { ev.stop_propagation(); @@ -111,10 +111,9 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option) }); li![ - C!["linkItem"], + C!["linkItem", icon.to_str()], active_flag, allow_flag, - C![icon.to_str()], a![ attrs![At::Href => path], on_click, diff --git a/jirs-client/src/shared/navbar_left.rs b/jirs-client/src/shared/navbar_left.rs index 06c91b8b..72ae8f9b 100644 --- a/jirs-client/src/shared/navbar_left.rs +++ b/jirs-client/src/shared/navbar_left.rs @@ -15,6 +15,9 @@ use { seed::{prelude::*, *}, }; +use crate::components::styled_button::ButtonVariant; +use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant}; + trait IntoNavItemIcon { fn into_nav_item_icon(self) -> Node; } @@ -27,7 +30,12 @@ impl IntoNavItemIcon for Node { impl IntoNavItemIcon for Icon { fn into_nav_item_icon(self) -> Node { - 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> { ]; let user_icon = match model.user.as_ref() { - Some(user) => i![ - C!["styledIcon"], - StyledAvatar::build() - .size(27) - .name(user.name.as_str()) - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .build() - .into_node() - ], - _ => StyledIcon::build(Icon::User).size(21).build().into_node(), + Some(user) => { + let avatar = StyledAvatar { + avatar_url: user.avatar_url.as_deref(), + size: 27, + name: &user.name, + ..StyledAvatar::default() + } + .into_node(); + i![C!["styledIcon"], avatar] + } + _ => StyledIcon { + icon: Icon::User, + size: Some(21), + ..Default::default() + } + .into_node(), }; let messages = if model.messages.is_empty() { @@ -68,7 +82,7 @@ pub fn render(model: &Model) -> Vec> { None, Some(mouse_ev(Ev::Click, |ev| { 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) -> Node { let on_click: EventHandler = ev(Ev::Click, move |_| { - Some(Msg::ToggleTooltip(styled_tooltip::Variant::About)) + Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About)) }); div![C!["aboutTooltip"], on_click, children] } fn messages_tooltip_popup(model: &Model) -> Node { let on_click: EventHandler = ev(Ev::Click, move |_| { - Some(Msg::ToggleTooltip(styled_tooltip::Variant::Messages)) + Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::Messages)) }); let mut messages: Vec> = vec![]; for (idx, message) in model.messages.iter().enumerate() { @@ -163,13 +177,13 @@ fn messages_tooltip_popup(model: &Model) -> Node { }; } let body = div![on_click, C!["messagesList"], messages]; - styled_tooltip::StyledTooltip::build() - .add_class("messagesPopup") - .visible(model.messages_tooltip_visible) - .messages_tooltip() - .add_child(body) - .build() - .into_node() + styled_tooltip::StyledTooltip { + visible: model.messages_tooltip_visible, + class_list: "messagesPopup", + children: vec![body], + variant: TooltipVariant::Messages, + } + .into_node() } fn message_ui(model: &Model, message: &Message) -> Option> { @@ -186,7 +200,7 @@ fn message_ui(model: &Model, message: &Message) -> Option> { let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with('#') { empty![] } else { - let link_icon = StyledIcon::build(Icon::Link).build().into_node(); + let link_icon = StyledIcon::from(Icon::Link).into_node(); div![ C!["hyperlink"], a![ @@ -199,16 +213,17 @@ fn message_ui(model: &Model, message: &Message) -> Option> { }; let message_description = parse_description(model, description.as_str()); - let close_button = StyledButton::build() - .icon(Icon::Close) - .empty() - .on_click(mouse_ev(Ev::Click, move |ev| { + let close_button = StyledButton { + variant: ButtonVariant::Empty, + icon: Some(Icon::Close.into_node()), + on_click: Some(mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); ev.prevent_default(); Some(Msg::MessageSeen(message_id)) - })) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); let top = div![ C!["top"], div![C!["summary"], summary], @@ -218,30 +233,32 @@ fn message_ui(model: &Model, message: &Message) -> Option> { let node = match message_type { MessageType::ReceivedInvitation => { let token: InvitationToken = hyper_link.trim_start_matches('#').parse().ok()?; - let accept = StyledButton::build() - .primary() - .text("Accept") - .active(true) - .icon(Icon::Check) - .on_click(mouse_ev(Ev::Click, move |ev| { + let accept = StyledButton { + variant: ButtonVariant::Primary, + active: true, + text: Some("Accept"), + icon: Some(Icon::Check.into_node()), + on_click: Some(mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); ev.prevent_default(); Some(Msg::MessageInvitationApproved(token)) - })) - .build() - .into_node(); - let reject = StyledButton::build() - .danger() - .text("Dismiss") - .icon(Icon::Close) - .on_click(mouse_ev(Ev::Click, move |ev| { + })), + ..Default::default() + } + .into_node(); + let reject = StyledButton { + variant: ButtonVariant::Danger, + active: true, + text: Some("Dismiss"), + icon: Some(Icon::Close.into_node()), + on_click: Some(mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); ev.prevent_default(); Some(Msg::MessageInvitationDismiss(token)) - })) - .active(true) - .build() - .into_node(); + })), + ..Default::default() + } + .into_node(); div![ C!["message"], attrs![At::Class => format!("{}", message_type)], @@ -267,28 +284,29 @@ fn message_ui(model: &Model, message: &Message) -> Option> { } fn about_tooltip_popup(model: &Model) -> Node { - let visit_website = StyledButton::build() - .text("Visit Website") - .primary() - .build() - .into_node(); - let github_repo = StyledButton::build() - .text("Github Repo") - .secondary() - .icon(Icon::Github) - .build() - .into_node(); + let visit_website = StyledButton { + variant: ButtonVariant::Primary, + text: Some("Visit Website"), + ..Default::default() + } + .into_node(); + let github_repo = StyledButton { + variant: ButtonVariant::Secondary, + text: Some("Github Repo"), + icon: Some(Icon::Github.into_node()), + ..Default::default() + } + .into_node(); let on_click = mouse_ev(Ev::Click, |_| { - Msg::ToggleTooltip(styled_tooltip::Variant::About) + Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About) }); let body = div![ on_click, C!["feedbackDropdown"], div![ - C!["feedbackImageCont"], + C!["feedbackImageCont feedbackImage"], img![attrs![At::Src => "/feedback.png"]], - C!["feedbackImage"], ], div![ C!["feedbackParagraph"], @@ -321,13 +339,13 @@ fn about_tooltip_popup(model: &Model) -> Node { ] ]; - styled_tooltip::StyledTooltip::build() - .visible(model.about_tooltip_visible) - .about_tooltip() - .add_class("aboutTooltipPopup") - .add_child(body) - .build() - .into_node() + StyledTooltip { + visible: model.about_tooltip_visible, + class_list: "aboutTooltipPopup", + children: vec![body], + variant: TooltipVariant::About, + } + .into_node() } fn parse_description(model: &Model, desc: &str) -> Node { @@ -342,12 +360,13 @@ fn parse_description(model: &Model, desc: &str) -> Node { .find(|(_, user)| user.email == email) }) .map(|(index, user)| { - let avatar = StyledAvatar::build() - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .user_index(index) - .size(16) - .build() - .into_node(); + let avatar = StyledAvatar { + avatar_url: user.avatar_url.as_deref(), + size: 16, + user_index: index, + ..StyledAvatar::default() + } + .into_node(); span![C!["mention"], avatar, user.name.as_str()] }) .unwrap_or_else(|| span![word]); diff --git a/jirs-client/src/shared/tracking_widget.rs b/jirs-client/src/shared/tracking_widget.rs index c430bcbb..26d45ec6 100644 --- a/jirs-client/src/shared/tracking_widget.rs +++ b/jirs-client/src/shared/tracking_widget.rs @@ -47,11 +47,13 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node { .. } = modal; - let icon = StyledIcon::build(Icon::Stopwatch) - .add_class("watchIcon") - .size(32) - .build() - .into_node(); + let icon = StyledIcon { + icon: Icon::Stopwatch, + class_list: "watchIcon", + size: Some(32), + ..Default::default() + } + .into_node(); let bar_width = calc_bar_width(*estimate, *time_spent, *time_remaining); let spent_text = match (time_spent, time_tracking_type) { diff --git a/jirs-client/static/index.js b/jirs-client/static/index.js index 5a13bc02..09f7090b 100644 --- a/jirs-client/static/index.js +++ b/jirs-client/static/index.js @@ -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 => { // window.module = module; await module.default(); - const host_url = `${ location.protocol }//${ process.env.JIRS_SERVER_BIND }:${ process.env.JIRS_SERVER_PORT }`; - module.render(host_url, wsUrl()); + module.render(); document.querySelector('main').className = ''; const spinner = document.querySelector('.spinner'); spinner && spinner.remove();