Remove some builders

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

4
.env
View File

@ -1,7 +1,7 @@
DEBUG=true
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

1
.gitignore vendored
View File

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

1
Cargo.lock generated
View File

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

View File

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

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

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

View File

@ -1,14 +1,12 @@
#!/usr/bin/env bash
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 \

View File

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

View File

@ -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<EventHandler<Msg>>,
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<EventHandler<Msg>>,
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<u32>,
name: &'l str,
on_click: Option<EventHandler<Msg>>,
class_list: Vec<&'l str>,
user_index: usize,
}
impl<'l> StyledAvatarBuilder<'l> {
#[inline(always)]
pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
if !avatar_url.is_empty() {
self.avatar_url = Some(avatar_url);
}
self
}
#[inline(always)]
pub fn size(mut self, size: u32) -> Self {
self.size = Some(size);
self
}
#[inline(always)]
pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
self.name = name;
self
}
#[inline(always)]
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
self.on_click = Some(on_click);
self
}
#[inline(always)]
pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
self.class_list.push(name);
self
}
#[inline(always)]
pub fn user_index(mut self, user_index: usize) -> Self {
self.user_index = user_index;
self
}
#[inline(always)]
pub fn build(self) -> StyledAvatar<'l> {
StyledAvatar {
avatar_url: self.avatar_url,
size: self.size.unwrap_or(32),
name: self.name,
on_click: self.on_click,
class_list: self.class_list,
user_index: self.user_index,
}
}
}
pub fn render(values: StyledAvatar) -> Node<Msg> {
let StyledAvatar {
avatar_url,
@ -120,10 +46,6 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
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<Msg> {
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,

View File

@ -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<Variant>,
disabled: Option<bool>,
active: Option<bool>,
text: Option<&'l str>,
icon: Option<Node<Msg>>,
on_click: Option<EventHandler<Msg>>,
children: Option<Vec<Node<Msg>>>,
class_list: Vec<&'l str>,
button_type: Option<&'l str>,
button_id: Option<ButtonId>,
}
impl<'l> StyledButtonBuilder<'l> {
#[inline(always)]
fn variant(mut self, value: Variant) -> Self {
self.variant = Some(value);
self
}
#[inline(always)]
pub fn primary(self) -> Self {
self.variant(Variant::Primary)
}
#[inline(always)]
pub fn success(self) -> Self {
self.variant(Variant::Success)
}
#[inline(always)]
pub fn danger(self) -> Self {
self.variant(Variant::Danger)
}
#[inline(always)]
pub fn secondary(self) -> Self {
self.variant(Variant::Secondary)
}
#[inline(always)]
pub fn empty(self) -> Self {
self.variant(Variant::Empty)
}
// pub fn button_id(mut self, button_id: ButtonId) -> Self {
// self.button_id = Some(button_id);
// self
// }
#[inline(always)]
pub fn disabled(mut self, value: bool) -> Self {
self.disabled = Some(value);
self
}
#[inline(always)]
pub fn active(mut self, value: bool) -> Self {
self.active = Some(value);
self
}
#[inline(always)]
pub fn text(mut self, value: &'l str) -> Self {
self.text = Some(value);
self
}
#[inline(always)]
pub fn icon<I>(mut self, value: I) -> Self
where
I: ToNode,
{
self.icon = Some(value.into_node());
self
}
#[inline(always)]
pub fn on_click(mut self, value: EventHandler<Msg>) -> Self {
self.on_click = Some(value);
self
}
#[inline(always)]
pub fn children(mut self, value: Vec<Node<Msg>>) -> Self {
self.children = Some(value);
self
}
#[inline(always)]
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
#[inline(always)]
pub fn set_type_reset(mut self) -> Self {
self.button_type = Some("reset");
self
}
#[inline(always)]
pub fn build(self) -> StyledButton<'l> {
StyledButton {
variant: self.variant.unwrap_or(Variant::Primary),
disabled: self.disabled.unwrap_or(false),
active: self.active.unwrap_or(false),
text: self.text,
icon: self.icon,
on_click: self.on_click,
children: self.children.unwrap_or_default(),
class_list: self.class_list,
button_type: self.button_type.unwrap_or("submit"),
button_id: self.button_id,
}
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<Node<Msg>>,
on_click: Option<EventHandler<Msg>>,
children: Vec<Node<Msg>>,
class_list: Vec<&'l str>,
button_type: &'l str,
button_id: Option<ButtonId>,
pub variant: ButtonVariant,
pub disabled: bool,
pub active: bool,
pub text: Option<&'l str>,
pub icon: Option<Node<Msg>>,
pub on_click: Option<EventHandler<Msg>>,
pub children: Vec<Node<Msg>>,
pub class_list: &'l str,
pub button_type: &'l str,
pub button_id: Option<ButtonId>,
}
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<Msg> {
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<Msg> {
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,
]

View File

@ -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<EventHandler<Msg>>,
pub title: &'l str,
pub message: &'l str,
pub confirm_text: &'l str,
pub cancel_text: &'l str,
pub on_confirm: Option<EventHandler<Msg>>,
}
impl<'l> Default for StyledConfirmModal<'l> {
fn default() -> Self {
Self {
title: TITLE,
message: MESSAGE,
confirm_text: CONFIRM_TEXT,
cancel_text: CANCEL_TEXT,
on_confirm: None,
}
}
}
impl<'l> StyledConfirmModal<'l> {
@ -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<Msg> {
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()
}

View File

@ -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<NaiveDateTime>),
@ -175,18 +178,19 @@ fn render(values: StyledDateTimeInput) -> Node<Msg> {
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<Msg> {
- 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<Msg> {
.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![

View File

@ -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<FieldId>,
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<Ev>,
}
impl StyledEditorBuilder {
#[inline]
pub fn text<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.text = text.into();
self
}
#[inline]
pub fn initial_text<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.initial_text = text.into();
self
}
#[inline]
pub fn html<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.html = text.into();
self
}
#[inline]
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
#[inline]
pub fn build(self) -> StyledEditor {
StyledEditor {
id: self.id,
initial_text: self.initial_text,
text: self.text,
html: self.html,
mode: self.mode,
update_event: self.update_event.unwrap_or(Ev::KeyUp),
}
}
#[inline]
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
}
impl ToNode for StyledEditor {
impl<'l> ToNode for StyledEditor<'l> {
#[inline]
fn into_node(self) -> Node<Msg> {
render(self)
@ -131,6 +67,7 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
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<Msg> {
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<Msg> {
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]],
],
]
}

View File

@ -5,10 +5,21 @@ use {
#[derive(Debug)]
pub struct StyledField<'l> {
label: &'l str,
tip: Option<&'l str>,
input: Node<Msg>,
class_list: Vec<&'l str>,
pub label: &'l str,
pub tip: Option<&'l str>,
pub input: Node<Msg>,
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<Node<Msg>>,
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<Msg> {
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,
]

View File

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

View File

@ -107,10 +107,6 @@ impl Icon {
}
}
pub fn into_styled_builder<'l>(self) -> StyledIconBuilder<'l> {
StyledIcon::build(self)
}
pub fn to_str<'l>(&self) -> &'l str {
match self {
Icon::Bug => "bug",
@ -233,9 +229,12 @@ impl From<IssuePriority> for Icon {
}
}
impl<'l> Into<StyledIcon<'l>> for Icon {
fn into(self) -> StyledIcon<'l> {
StyledIcon::build(self).build()
impl<'l> From<Icon> 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<i32>,
class_list: Vec<Cow<'l, str>>,
style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>,
on_click: Option<EventHandler<Msg>>,
pub icon: Icon,
pub size: Option<i32>,
pub class_list: &'l str,
pub style_list: Vec<Cow<'l, str>>,
pub color: Option<Cow<'l, str>>,
pub on_click: Option<EventHandler<Msg>>,
}
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<i32>,
class_list: Vec<Cow<'l, str>>,
style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>,
on_click: Option<EventHandler<Msg>>,
pub icon: Icon,
pub size: Option<i32>,
pub class_list: &'l str,
pub style_list: Vec<Cow<'l, str>>,
pub color: Option<&'l str>,
pub on_click: Option<EventHandler<Msg>>,
}
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<Msg> {
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<seed::Attrs> = class_list
.into_iter()
.map(|s| match s {
Cow::Borrowed(s) => C![s],
Cow::Owned(s) => C![s.as_str()],
})
.collect();
let style_list = style_list.into_iter().fold("".to_string(), |mut mem, s| {
match s {
Cow::Borrowed(s) => {
@ -368,9 +343,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
});
i![
C!["styledIcon"],
class_list,
C![icon.to_str()],
C!["styledIcon", class_list, icon.to_str()],
styles,
attrs![ At::Style => style_list ],
on_click,

View File

@ -31,19 +31,9 @@ impl StyledImageInputState {
}
pub struct StyledImageInput<'l> {
id: FieldId,
class_list: Vec<&'l str>,
url: Option<String>,
}
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<String>,
}
impl<'l> StyledInputInputBuilder<'l> {
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
pub fn state(mut self, state: &StyledImageInputState) -> Self {
self.url = state.url.as_ref().cloned();
self
}
pub fn build(self) -> StyledImageInput<'l> {
StyledImageInput {
id: self.id,
class_list: self.class_list,
url: self.url,
}
}
}
fn render(values: StyledImageInput) -> Node<Msg> {
let StyledImageInput {
id,
@ -104,8 +68,7 @@ fn render(values: StyledImageInput) -> Node<Msg> {
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],

View File

@ -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<Icon>,
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<EventHandler<Msg>>,
pub id: Option<FieldId>,
pub icon: Option<Icon>,
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<EventHandler<Msg>>,
}
impl<'l, 'm: 'l> Default for StyledInput<'l, 'm> {
fn default() -> Self {
Self {
id: None,
icon: None,
valid: false,
value: "",
input_type: None,
input_class_list: "",
wrapper_class_list: "",
variant: InputVariant::Normal,
auto_focus: false,
input_handlers: vec![],
}
}
}
impl<'l, 'm: 'l> StyledInput<'l, 'm> {
#[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<Icon>,
valid: Option<bool>,
value: Option<&'m str>,
input_type: Option<&'l str>,
input_class_list: Vec<&'l str>,
wrapper_class_list: Vec<&'l str>,
variant: Variant,
auto_focus: bool,
input_handlers: Vec<EventHandler<Msg>>,
}
impl<'l, 'm: 'l> StyledInputBuilder<'l, 'm> {
#[inline]
pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
#[inline]
pub fn valid(mut self, valid: bool) -> Self {
self.valid = Some(valid);
self
}
#[inline]
pub fn value(mut self, v: &'m str) -> Self {
self.value = Some(v);
self
}
#[inline]
pub fn state(self, state: &'m StyledInputState) -> Self {
self.value(&state.value.as_str()).valid(
match (
state.touched,
state.value.as_str(),
state.min.as_ref(),
state.max.as_ref(),
) {
(false, ..) => true,
(_, s, None, None) => !s.is_empty(),
(_, s, Some(min), None) => s.len() >= *min,
(_, s, None, Some(max)) => s.len() <= *max,
(_, s, Some(min), Some(max)) => s.len() >= *min && s.len() <= *max,
},
)
}
#[inline]
pub fn add_input_class(mut self, name: &'l str) -> Self {
self.input_class_list.push(name);
self
}
#[inline]
pub fn add_wrapper_class(mut self, name: &'l str) -> Self {
self.wrapper_class_list.push(name);
self
}
#[inline]
pub fn primary(mut self) -> Self {
self.variant = Variant::Primary;
self
}
#[inline]
pub fn auto_focus(mut self) -> Self {
self.auto_focus = true;
self
}
#[inline]
pub fn on_input_ev(mut self, handler: EventHandler<Msg>) -> Self {
self.input_handlers.push(handler);
self
}
#[inline]
pub fn build(self, id: FieldId) -> StyledInput<'l, 'm> {
StyledInput {
id,
icon: self.icon,
valid: self.valid.unwrap_or_default(),
value: self.value,
input_type: self.input_type,
input_class_list: self.input_class_list,
wrapper_class_list: self.wrapper_class_list,
variant: self.variant,
auto_focus: self.auto_focus,
input_handlers: self.input_handlers,
}
}
}
impl<'l, 'm: 'l> ToNode for StyledInput<'l, 'm> {
@ -262,9 +182,10 @@ pub fn render(values: StyledInput) -> Node<Msg> {
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<Msg> {
};
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,
],

View File

@ -5,21 +5,21 @@ use {
};
pub struct StyledLink<'l> {
children: Vec<Node<Msg>>,
class_list: Vec<&'l str>,
href: &'l str,
pub children: Vec<Node<Msg>>,
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<Node<Msg>>,
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<Msg> {
};
a![
C!["styledLink"],
attrs![
At::Class => class_list.join(" "),
At::Href => href,
],
C!["styledLink", class_list],
attrs![ At::Href => href, ],
on_click,
children,
]

View File

@ -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<usize>,
with_icon: bool,
children: Vec<Node<Msg>>,
class_list: Vec<&'l str>,
pub variant: ModalVariant,
pub width: Option<usize>,
pub with_icon: bool,
pub children: Vec<Node<Msg>>,
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<Node<Msg>>) -> 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<Variant>,
width: Option<usize>,
with_icon: Option<bool>,
children: Option<Vec<Node<Msg>>>,
class_list: Vec<&'l str>,
}
impl<'l> StyledModalBuilder<'l> {
#[inline]
pub fn variant(mut self, variant: Variant) -> Self {
self.variant = Some(variant);
self
}
#[inline]
pub fn center(self) -> Self {
self.variant(Variant::Center)
}
#[inline]
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
#[inline]
pub fn child(mut self, child: Node<Msg>) -> Self {
self.children.get_or_insert(vec![]).push(child);
self
}
#[inline]
pub fn children<ChildIter>(mut self, children: ChildIter) -> Self
where
ChildIter: Iterator<Item = Node<Msg>>,
{
self.children.get_or_insert(vec![]).extend(children);
self
}
#[inline]
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
#[inline]
pub fn build(self) -> StyledModal<'l> {
StyledModal {
variant: self.variant.unwrap_or(Variant::Center),
width: self.width,
with_icon: self.with_icon.unwrap_or_default(),
children: self.children.unwrap_or_default(),
class_list: self.class_list,
}
}
}
#[inline]
pub fn render(values: StyledModal) -> Node<Msg> {
let StyledModal {
@ -130,14 +76,16 @@ pub fn render(values: StyledModal) -> Node<Msg> {
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<Msg> {
});
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<Msg> {
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

View File

@ -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<Item = StyledSelectChildBuilder<'l>>,
{
id: FieldId,
variant: Variant,
dropdown_width: Option<usize>,
name: Option<&'l str>,
valid: bool,
is_multi: bool,
options: Option<Options>,
selected: Vec<StyledSelectChildBuilder<'l>>,
text_filter: &'l str,
opened: bool,
clearable: bool,
pub id: FieldId,
pub variant: SelectVariant,
pub dropdown_width: Option<usize>,
pub name: &'l str,
pub valid: bool,
pub is_multi: bool,
pub options: Option<Options>,
pub selected: Vec<StyledSelectChildBuilder<'l>>,
pub text_filter: &'l str,
pub opened: bool,
pub clearable: bool,
}
impl<'l, Options> Default for StyledSelect<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
fn default() -> Self {
Self {
id: FieldId::TextFilterBoard,
variant: Default::default(),
dropdown_width: None,
name: "",
valid: true,
is_multi: false,
options: None,
selected: vec![],
text_filter: "",
opened: false,
clearable: false,
}
}
}
impl<'l, Options> ToNode for StyledSelect<'l, Options>
@ -136,124 +157,6 @@ where
}
}
impl<'l, Options> StyledSelect<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
pub fn build() -> StyledSelectBuilder<'l, Options> {
StyledSelectBuilder {
variant: None,
dropdown_width: None,
name: None,
valid: None,
is_multi: None,
options: None,
selected: None,
text_filter: None,
opened: None,
clearable: false,
}
}
}
#[derive(Debug)]
pub struct StyledSelectBuilder<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
variant: Option<Variant>,
dropdown_width: Option<usize>,
name: Option<&'l str>,
valid: Option<bool>,
is_multi: Option<bool>,
options: Option<Options>,
selected: Option<Vec<StyledSelectChildBuilder<'l>>>,
text_filter: Option<&'l str>,
opened: Option<bool>,
clearable: bool,
}
impl<'l, Options> StyledSelectBuilder<'l, Options>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
{
pub fn build(self, id: FieldId) -> StyledSelect<'l, Options> {
StyledSelect {
id,
variant: self.variant.unwrap_or_default(),
dropdown_width: self.dropdown_width,
name: self.name,
valid: self.valid.unwrap_or(true),
is_multi: self.is_multi.unwrap_or_default(),
options: self.options,
selected: self.selected.unwrap_or_default(),
text_filter: self.text_filter.unwrap_or_default(),
opened: self.opened.unwrap_or_default(),
clearable: self.clearable,
}
}
pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self {
self.opened(state.opened)
.text_filter(state.text_filter.as_str())
}
pub fn dropdown_width(mut self, dropdown_width: usize) -> Self {
self.dropdown_width = Some(dropdown_width);
self
}
pub fn name(mut self, name: &'l str) -> Self {
self.name = Some(name);
self
}
pub fn text_filter(mut self, text_filter: &'l str) -> Self {
self.text_filter = Some(text_filter);
self
}
pub fn opened(mut self, opened: bool) -> Self {
self.opened = Some(opened);
self
}
pub fn valid(mut self, valid: bool) -> Self {
self.valid = Some(valid);
self
}
pub fn options(mut self, options: Options) -> Self {
self.options = Some(options);
self
}
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder<'l>>) -> Self {
self.selected = Some(selected);
self
}
pub fn normal(mut self) -> Self {
self.variant = Some(Variant::Normal);
self
}
pub fn empty(mut self) -> Self {
self.variant = Some(Variant::Empty);
self
}
pub fn multi(mut self) -> Self {
self.is_multi = Some(true);
self
}
pub fn clearable(mut self) -> Self {
self.clearable = true;
self
}
}
pub fn render<'l, Options>(values: StyledSelect<'l, Options>) -> Node<Msg>
where
Options: Iterator<Item = StyledSelectChildBuilder<'l>>,
@ -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<Node<Msg>> = if is_multi {
let add_icon = StyledIcon::build(Icon::Plus).build().into_node();
let add_icon = StyledIcon::from(Icon::Plus).into_node();
let mut children: Vec<Node<Msg>> = selected
.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<Msg>
}),
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<Msg>) -> Node<Msg> {
}
fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node<Msg> {
let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node();
let close_icon = StyledIcon {
icon: Icon::Close,
size: Some(14),
..Default::default()
}
.into_node();
let child = opt.build(DisplayType::SelectValue);
let 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]
}

View File

@ -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<Node<Msg>>,
text: Option<std::borrow::Cow<'l, str>>,
display_type: DisplayType,
value: u32,
class_list: Vec<std::borrow::Cow<'l, str>>,
variant: Variant,
pub name: Option<&'l str>,
pub icon: Option<Node<Msg>>,
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<Node<Msg>>,
text: Option<std::borrow::Cow<'l, str>>,
name: Option<&'l str>,
value: u32,
class_list: Vec<std::borrow::Cow<'l, str>>,
variant: Variant,
pub icon: Option<Node<Msg>>,
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<Msg> {
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<Msg> {
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()
}
}
}

View File

@ -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<FieldId>,
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<Msg> {
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<usize>,
max_height: Option<usize>,
on_change: Option<EventHandler<Msg>>,
value: &'l str,
class_list: Vec<&'l str>,
update_event: Option<Ev>,
placeholder: Option<&'l str>,
disable_auto_resize: bool,
}
impl<'l> StyledTextareaBuilder<'l> {
#[inline]
pub fn height(mut self, height: usize) -> Self {
self.height = Some(height);
self
}
#[inline]
pub fn max_height(mut self, height: usize) -> Self {
self.max_height = Some(height);
self
}
#[inline]
pub fn value(mut self, value: &'l str) -> Self {
self.value = value;
self
}
#[inline]
pub fn add_class(mut self, value: &'l str) -> Self {
self.class_list.push(value);
self
}
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
pub fn placeholder(mut self, placeholder: &'l str) -> Self {
self.placeholder = Some(placeholder);
self
}
pub fn disable_auto_resize(mut self) -> Self {
self.disable_auto_resize = true;
self
}
#[inline]
pub fn build(self) -> StyledTextarea<'l> {
StyledTextarea {
id: self.id,
value: self.value,
height: self.height.unwrap_or(110),
class_list: self.class_list,
max_height: self.max_height.unwrap_or_default(),
update_event: self.update_event.unwrap_or(Ev::KeyUp),
placeholder: self.placeholder,
disable_auto_resize: self.disable_auto_resize,
}
impl<'l> ToNode for StyledTextarea<'l> {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
@ -124,11 +55,12 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
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<Msg> {
}
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<Msg>;
@ -167,39 +98,41 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
});
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,

View File

@ -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<Node<Msg>>,
variant: Variant,
pub visible: bool,
pub class_list: &'l str,
pub children: Vec<Node<Msg>>,
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<Node<Msg>>,
variant: Variant,
}
impl<'l> StyledTooltipBuilder<'l> {
pub fn visible(mut self, b: bool) -> Self {
self.visible = b;
self
}
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
pub fn add_child(mut self, child: Node<Msg>) -> Self {
self.children.push(child);
self
}
pub fn about_tooltip(mut self) -> Self {
self.variant = Variant::About;
self
}
pub fn messages_tooltip(mut self) -> Self {
self.variant = Variant::Messages;
self
}
// pub fn table_tooltip(mut self) -> Self {
// self.variant = Variant::TableBuilder;
// self
// }
//
// pub fn code_tooltip(mut self) -> Self {
// self.variant = Variant::CodeBuilder;
// self
// }
pub fn date_time_picker(mut self) -> Self {
self.variant = Variant::DateTimeBuilder;
self
}
pub fn build(self) -> StyledTooltip<'l> {
StyledTooltip {
visible: self.visible,
class_list: self.class_list,
children: self.children,
variant: self.variant,
}
}
}
pub fn render(values: StyledTooltip) -> Node<Msg> {
let StyledTooltip {
visible,
@ -122,10 +57,7 @@ pub fn render(values: StyledTooltip) -> Node<Msg> {
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!()
}

View File

@ -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<Msg>) {
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<Page> {
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<Msg>) -> 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);

View File

@ -6,11 +6,12 @@ use {
pub fn view(_model: &model::Model, modal: &super::Model) -> Node<Msg> {
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()
}

View File

@ -6,11 +6,11 @@ use {
pub fn view(model: &Model) -> Node<Msg> {
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()
}

View File

@ -9,36 +9,40 @@ use {
seed::prelude::Node,
};
use crate::components::styled_select::SelectVariant;
pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>>
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(),
)
}

View File

@ -11,25 +11,26 @@ use {
pub fn view(model: &model::Model, modal: &Model) -> Node<Msg> {
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<Msg> {
.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<Msg> {
})
.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"],

View File

@ -33,30 +33,34 @@ pub fn view(_model: &model::Model, modal: &Model) -> Node<Msg> {
} 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<Msg> {
@ -69,15 +73,16 @@ fn transform_into_available(modal: &super::Model) -> Node<Msg> {
.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]]
}

View File

@ -5,14 +5,14 @@ use {
};
pub fn view(_model: &model::Model, issue_status_id: IssueStatusId) -> Node<Msg> {
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()
}

View File

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

View File

@ -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<Msg> {
let issue_type = modal
.type_state
@ -80,55 +83,54 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
};
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<Msg> {
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<Msg> {
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<Msg> {
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<Msg> {
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<Msg> {
@ -183,58 +194,60 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.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<Msg> {
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<Msg> {
fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
let priorities = IssuePriority::default().into_iter();
let select_priority = StyledSelect::build()
.name("priority")
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
.opened(modal.priority_state.opened)
.valid(true)
.options(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<Msg> {
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()
}

View File

@ -9,14 +9,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => 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()
}

View File

@ -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<Msg> {
@ -88,53 +93,87 @@ fn modal_header(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
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<Msg> {
..
} = 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<Msg> {
vec![div![C!["fakeTextArea"], "Add a comment...", handler]]
};
let comments: Vec<Node<Msg>> = 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<Msg> {
..
} = 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")

View File

@ -13,6 +13,8 @@ use {
seed::{prelude::*, *},
};
use crate::components::styled_button::ButtonVariant;
pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
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<Node<Msg>> {
))
});
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 {

View File

@ -69,18 +69,20 @@ pub fn view(model: &Model, modal: &super::Model) -> Node<Msg> {
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()
}

View File

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

View File

@ -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<Msg> {
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<Msg> {
}
fn submit(_page: &InvitePage) -> Node<Msg> {
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<Msg> {
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()
}

View File

@ -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<Msg> {
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<Msg> {
}
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
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()
}

View File

@ -9,6 +9,8 @@ use {
seed::{prelude::*, *},
};
use crate::components::styled_button::ButtonVariant;
pub fn project_board_lists(model: &Model) -> Node<Msg> {
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<Msg> {
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<Msg> {
.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<Msg> {
.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<Msg> {
.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| {

View File

@ -14,29 +14,33 @@ pub fn project_board_filters(model: &Model) -> Node<Msg> {
_ => 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<Msg> {
.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
]

View File

@ -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<Msg> {
.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<Msg> {
/// Build project name input with styled field wrapper
fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
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<Msg> {
/// Build project url input with styled field wrapper
fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
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<Msg> {
/// Build project description text area with styled field wrapper
fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
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<Msg> {
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<Msg> {
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<Msg> {
)))
});
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![

View File

@ -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]),

View File

@ -16,28 +16,34 @@ use {
seed::{prelude::*, *},
};
use crate::components::styled_button::ButtonVariant;
pub fn view(model: &model::Model) -> Node<Msg> {
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<Msg> {
.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<Msg> {
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<Msg> {
.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()

View File

@ -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<Msg> {
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"],

View File

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

View File

@ -13,76 +13,89 @@ use {
seed::{prelude::*, *},
};
pub fn view(model: &Model) -> Node<Msg> {
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<Msg> {
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<Msg> {
.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<Msg> {
.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)],

View File

@ -98,7 +98,7 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>)
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<Page>)
});
li![
C!["linkItem"],
C!["linkItem", icon.to_str()],
active_flag,
allow_flag,
C![icon.to_str()],
a![
attrs![At::Href => path],
on_click,

View File

@ -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<Msg>;
}
@ -27,7 +30,12 @@ impl IntoNavItemIcon for Node<Msg> {
impl IntoNavItemIcon for Icon {
fn into_nav_item_icon(self) -> Node<Msg> {
StyledIcon::build(self).size(21).build().into_node()
StyledIcon {
icon: self,
size: Some(21),
..Default::default()
}
.into_node()
}
}
@ -47,16 +55,22 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
];
let user_icon = match model.user.as_ref() {
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<Node<Msg>> {
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<Msg>) -> Node<Msg> {
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::About))
Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About))
});
div![C!["aboutTooltip"], on_click, children]
}
fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::Messages))
Some(Msg::ToggleTooltip(styled_tooltip::TooltipVariant::Messages))
});
let mut messages: Vec<Node<Msg>> = vec![];
for (idx, message) in model.messages.iter().enumerate() {
@ -163,13 +177,13 @@ fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
};
}
let body = div![on_click, C!["messagesList"], messages];
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<Node<Msg>> {
@ -186,7 +200,7 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
let hyperlink = if hyper_link.is_empty() && !hyper_link.starts_with('#') {
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<Node<Msg>> {
};
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<Node<Msg>> {
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<Node<Msg>> {
}
fn about_tooltip_popup(model: &Model) -> Node<Msg> {
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<Msg> {
]
];
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<Msg> {
@ -342,12 +360,13 @@ fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
.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]);

View File

@ -47,11 +47,13 @@ pub fn tracking_widget(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
..
} = 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) {

View File

@ -1,12 +1,7 @@
const getWsHostName = () => process.env.JIRS_SERVER_BIND === "0.0.0.0" ? 'localhost' : process.env.JIRS_SERVER_BIND;
const getProtocol = () => window.location.protocol.replace(/^http/, 'ws');
const wsUrl = () => `${getProtocol()}//${getWsHostName()}:${process.env.JIRS_SERVER_PORT}/ws/`;
import("/jirs.js").then(async module => {
// 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();