Reduce memory usage, remove Rc

This commit is contained in:
Adrian Woźniak 2020-10-18 10:35:41 +02:00
parent 8811c39e18
commit 46fafb619c
48 changed files with 1656 additions and 1124 deletions

938
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,13 @@ repository = "https://gitlab.com/adrian.wozniak/jirs"
license = "MPL-2.0" license = "MPL-2.0"
#license-file = "../LICENSE" #license-file = "../LICENSE"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
[[bin]]
name = "print"
path = "./src/print.rs"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
name = "jirs_client" name = "jirs_client"
@ -23,21 +30,29 @@ opt-level = 's'
[dependencies] [dependencies]
jirs-data = { path = "../jirs-data" } jirs-data = { path = "../jirs-data" }
wee_alloc = "*"
seed = { version = "0.7.0" } seed = { version = "0.7.0" }
serde = "*"
bincode = "1.2.1" serde = { version = "*" }
chrono = { version = "0.4", features = ["serde", "wasmbind"] } serde_json = { version = "*" }
bincode = { version = "*" }
ron = { version = "*" }
bson = "1.1.0"
serde_cbor = "0.11.1"
chrono = { version = "0.4", default-features = false, features = ["serde", "wasmbind"] }
uuid = { version = "0.8.1", features = ["serde"] } uuid = { version = "0.8.1", features = ["serde"] }
futures = "^0.1.26" futures = "^0.1.26"
comrak = "*" comrak = "*"
wee_alloc = "*"
num-traits = { version = "*" } num-traits = { version = "*" }
lazy_static = "*" lazy_static = "*"
syntect = { version = "*", default-features = false, features = ["html", "regex-fancy", "dump-load-rs"] } syntect = { version = "*", default-features = false, features = ["html", "regex-fancy", "dump-load-rs"] }
[dependencies.wasm-bindgen] [dependencies.wasm-bindgen]
version = "0.2.66" version = "*"
features = ["enable-interning"] features = ["enable-interning"]
[dependencies.js-sys] [dependencies.js-sys]
@ -45,7 +60,7 @@ version = "*"
default-features = false default-features = false
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.22" version = "*"
default-features = false default-features = false
features = [ features = [
# elements # elements

View File

@ -9,6 +9,32 @@
<link rel="stylesheet" type="text/css" href="/styles.css"> <link rel="stylesheet" type="text/css" href="/styles.css">
</head> </head>
<body> <body>
<main id="app"></main> <main id="app">
<style>
.lds-hourglass {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-hourglass:after {
content: " ";
display: block;
border-radius: 50%;
width: 0;
height: 0;
margin: 8px;
box-sizing: border-box;
border: 32px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-hourglass 1.2s infinite;
}
@keyframes lds-hourglass {
0% { transform: rotate(0); animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); }
50% { transform: rotate(900deg); animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
100% { transform: rotate(1800deg); } }
</style>
<div class="lds-hourglass"></div>
</main>
</body> </body>
</html> </html>

View File

@ -8,9 +8,65 @@
<title>JIRS</title> <title>JIRS</title>
<link href="/styles.css" rel="stylesheet" type="text/css"> <link href="/styles.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
main#app.loading {
background: rgb(0, 82, 204);
width: 100%;
height: 100%;
display: block;
}
.spinner {
color: #fff;
width: 180px;
height: 80px;
margin: 0 auto;
}
.lds-hourglass {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
margin: 4rem auto;
}
.lds-hourglass:after {
content: " ";
display: block;
border-radius: 50%;
width: 0;
height: 0;
margin: 8px;
box-sizing: border-box;
border: 32px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-hourglass 1.2s infinite;
}
@keyframes lds-hourglass {
0% {
transform: rotate(0);
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
}
50% {
transform: rotate(900deg);
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
100% {
transform: rotate(1800deg);
}
}
</style>
</head> </head>
<body> <body>
<main id="app"></main> <main class="loading" id="app">
<div class="spinner">
<div class="lds-hourglass"></div>
<div>Loading....</div>
<div>Please wait</div>
</div>
</main>
<script src="/index.js" type="module"></script> <script src="/index.js" type="module"></script>
</body> </body>
</html> </html>

View File

@ -1,18 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PROJECT_ROOT=$(git rev-parse --show-toplevel)
CLIENT_ROOT=${PROJECT_ROOT}/jirs-client
cd ${CLIENT_ROOT}
. .env . .env
cargo watch -s ${CLIENT_ROOT}/scripts/run-wasm-pack.sh -w ${CLIENT_ROOT}/src -w ${CLIENT_ROOT}/Cargo.toml -w ./static -w js
rm -Rf tmp
mkdir -p tmp
mkdir -p target
wasm-pack build --mode force --dev --out-name jirs --out-dir ./tmp --target web -- --verbose
../target/debug/jirs-css -i ./js/styles.css -O ./tmp/styles.css
cp -r ./static/* ./tmp
cat ./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" &> ./tmp/index.js
cp ./js/template.html ./tmp/index.html

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -ex
. .env
rm -Rf tmp
mkdir -p tmp
mkdir -p target
wasm-pack build --mode force --dev --out-name jirs --out-dir ./tmp --target web -- --verbose
../target/debug/jirs-css -i ./js/styles.css -O ./tmp/styles.css
cp -r ./static/* ./tmp
cat ./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" &>./tmp/index.js
cp ./js/template.html ./tmp/index.html

View File

@ -16,53 +16,49 @@ impl JirsCodeBuilder {
#[wasm_bindgen] #[wasm_bindgen]
pub fn hi_code(&mut self, lang: &str, code: &str) -> String { pub fn hi_code(&mut self, lang: &str, code: &str) -> String {
let syntax = match crate::hi::SYNTAX_SET.find_syntax_by_name(lang) { let syntax = {
Some(s) => s, match crate::hi::syntax_set::load().find_syntax_by_name(lang) {
_ => { Some(s) => s.clone(),
return code.to_string(); _ => {
return code.to_string();
}
} }
}; };
let mut buffer = String::new(); let mut buffer: Vec<String> = Vec::with_capacity(code.lines().count() * 2);
for line in code.lines() { for line in code.lines() {
buffer.push_str(self.hi(syntax, line).as_str()); self.hi(&syntax, line, &mut buffer);
buffer.push_str("<br />"); buffer.push("<br />".to_string());
} }
buffer buffer.join("")
} }
fn hi(&mut self, syntax: &SyntaxReference, line: &str) -> String { fn hi<'l>(&mut self, syntax: &SyntaxReference, line: &'l str, buffer: &mut Vec<String>) {
let mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github let mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github
let tokens = h.highlight(line, &crate::hi::SYNTAX_SET); let tokens = { h.highlight(line, &crate::hi::syntax_set::load()) };
let parts: Vec<String> = tokens for (style, token) in tokens.into_iter() {
.into_iter() let Style {
.map(|(style, token)| { foreground: f,
let Style { background: b,
foreground: f, font_style,
background: b, } = style;
font_style, let fs = if font_style == FontStyle::BOLD {
} = style; "font-weight: bold"
let fs = if font_style == FontStyle::BOLD { } else if font_style == FontStyle::ITALIC {
"font-weight: bold" "font-style: italic"
} else if font_style == FontStyle::ITALIC { } else if font_style == FontStyle::UNDERLINE {
"font-style: italic" "text-decoration: underline"
} else if font_style == FontStyle::UNDERLINE { } else {
"text-decoration: underline" ""
} else { };
""
}; buffer.push(format!(
let f = format!("rgba({}, {}, {}, {})", f.r, f.g, f.b, f.a); r#"<span style="color: rgba({f_r}, {f_g}, {f_b}, {f_a});background:rgba({b_r}, {b_g}, {b_b}, {b_a}); {fs}">{t}</span>"#,
let b = format!("rgba({}, {}, {}, {})", b.r, b.g, b.b, b.a); t = if token.is_empty() { "&nbsp;" } else { token },
format!( f_r = f.r, f_g = f.g, f_b = f.b, f_a = f.a, b_r = b.r, b_g = b.g, b_b = b.b, b_a = b.a,
r#"<span style="color: {f};background:{b}; {fs}">{t}</span>"#, fs = fs
t = if token.is_empty() { "&nbsp;" } else { token }, ));
f = f, }
b = b,
fs = fs
)
})
.collect();
parts.join("")
} }
} }
@ -91,6 +87,7 @@ pub fn define() {
const lang = this.getAttribute('lang') || ''; const lang = this.getAttribute('lang') || '';
setTimeout(() => {{ setTimeout(() => {{
const code = (this.innerHTML || '').trim(); const code = (this.innerHTML || '').trim();
console.log('connected');
shadow.querySelector('#view').innerHTML = runtime.hi_code(lang, code); shadow.querySelector('#view').innerHTML = runtime.hi_code(lang, code);
}}, 1); }}, 1);
"#, "#,

View File

@ -1,9 +1,13 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use syntect::dumps::from_binary; use syntect::{dumps::*, highlighting::ThemeSet};
use syntect::highlighting::ThemeSet;
use syntect::parsing::SyntaxSet; pub mod syntax_set;
//
// pub use syntax::get_syntax;
// pub use syntax::SYNTAX_LIST;
// pub use syntax::SYNTAX_NAMES;
lazy_static! { lazy_static! {
pub static ref SYNTAX_SET: SyntaxSet = from_binary(include_bytes!("./newlines.packdump"));
pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("./all.themedump")); pub static ref THEME_SET: ThemeSet = from_binary(include_bytes!("./all.themedump"));
// pub static ref SYNTAX_SET: SyntaxSet = syntax_set::load();
} }

Binary file not shown.

View File

@ -0,0 +1,10 @@
use syntect::parsing::SyntaxSet;
static mut SET: Option<SyntaxSet> = None;
pub fn load() -> &'static SyntaxSet {
if unsafe { SET.as_ref() }.is_none() {
unsafe { SET = serde_cbor::from_slice(include_bytes!("./syntax_set.cbor")).ok() };
}
unsafe { SET.as_ref() }.unwrap()
}

View File

@ -1,4 +1,4 @@
#![feature(or_patterns)] #![feature(or_patterns, type_ascription)]
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use web_sys::File; use web_sys::File;
@ -33,6 +33,11 @@ mod users;
pub mod validations; pub mod validations;
mod ws; mod ws;
#[link(wasm_import_module = "__wbindgen_thread_xform__")]
extern "C" {
fn __wbindgen_thread_id() -> u32;
}
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
@ -80,7 +85,7 @@ pub enum Msg {
StrInputChanged(FieldId, String), StrInputChanged(FieldId, String),
U32InputChanged(FieldId, u32), U32InputChanged(FieldId, u32),
FileInputChanged(FieldId, Vec<File>), FileInputChanged(FieldId, Vec<File>),
Rte(RteMsg, FieldId), Rte(FieldId, RteMsg),
// issues // issues
AddIssue, AddIssue,
@ -143,7 +148,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
WebSocketChanged::WebSocketMessageLoaded(v) => { WebSocketChanged::WebSocketMessageLoaded(v) => {
match bincode::deserialize(v.as_slice()) { match bincode::deserialize(v.as_slice()) {
Ok(WsMsg::Ping | WsMsg::Pong) => { Ok(WsMsg::Ping | WsMsg::Pong) => {
orders.perform_cmd(cmds::timeout(1000, || { orders.perform_cmd(cmds::timeout(300, || {
Msg::WebSocketChange(WebSocketChanged::SendPing) Msg::WebSocketChange(WebSocketChanged::SendPing)
})); }));
} }
@ -267,11 +272,14 @@ pub static mut WS_URL: String = String::new();
#[wasm_bindgen] #[wasm_bindgen]
pub fn render(host_url: String, ws_url: String) { pub fn render(host_url: String, ws_url: String) {
elements::define();
unsafe { unsafe {
HOST_URL = host_url; HOST_URL = host_url;
WS_URL = ws_url; WS_URL = ws_url;
} }
{
// crate::hi::syntax_set::load();
}
elements::define();
let _app = seed::App::builder(update, view) let _app = seed::App::builder(update, view)
.routes(routes) .routes(routes)

View File

@ -73,10 +73,10 @@ impl Type {
} }
} }
impl ToChild for Type { impl<'l> ToChild<'l> for Type {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let name = match self { let name = match self {
Type::Task => "Task", Type::Task => "Task",
Type::Bug => "Bug", Type::Bug => "Bug",

View File

@ -426,6 +426,7 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let issue_types = IssueType::ordered();
let issue_type_select = StyledSelect::build() let issue_type_select = StyledSelect::build()
.dropdown_width(150) .dropdown_width(150)
.name("type") .name("type")
@ -433,8 +434,8 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.opened(top_type_state.opened) .opened(top_type_state.opened)
.valid(true) .valid(true)
.options( .options(
IssueType::ordered() issue_types
.into_iter() .iter()
.map(|t| t.to_child().name("type")) .map(|t| t.to_child().name("type"))
.collect(), .collect(),
) )
@ -444,7 +445,7 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
issue_type issue_type
.to_child() .to_child()
.name("type") .name("type")
.text(format!("{} - {}", issue_type, id)) .text_owned(format!("{} - {}", issue_type, id))
}]) }])
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Type, IssueFieldId::Type,
@ -500,7 +501,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
model model
.user .user
.as_ref() .as_ref()
.and_then(|u| u.avatar_url.clone()) .and_then(|u| u.avatar_url.as_deref())
.unwrap_or_default(), .unwrap_or_default(),
) )
.build() .build()
@ -597,7 +598,7 @@ fn comment(model: &Model, modal: &EditIssueModal, comment: &Comment) -> Option<N
let avatar = StyledAvatar::build() let avatar = StyledAvatar::build()
.size(32) .size(32)
.avatar_url(user.avatar_url.as_ref().cloned()?) .avatar_url(user.avatar_url.as_deref()?)
.add_class("userAvatar") .add_class("userAvatar")
.build() .build()
.into_node(); .into_node();
@ -751,14 +752,15 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let issue_priorities = IssuePriority::ordered();
let priority = StyledSelect::build() let priority = StyledSelect::build()
.name("priority") .name("priority")
.opened(priority_state.opened) .opened(priority_state.opened)
.empty() .empty()
.text_filter(priority_state.text_filter.as_str()) .text_filter(priority_state.text_filter.as_str())
.options( .options(
IssuePriority::ordered() issue_priorities
.into_iter() .iter()
.map(|p| p.to_child().name("priority")) .map(|p| p.to_child().name("priority"))
.collect(), .collect(),
) )

View File

@ -89,6 +89,7 @@ pub fn time_tracking_field(
input_state: &StyledInputState, input_state: &StyledInputState,
select_state: &StyledSelectState, select_state: &StyledSelectState,
) -> Node<Msg> { ) -> Node<Msg> {
let fibonacci_values = fibonacci_values();
let input = match time_tracking_type { let input = match time_tracking_type {
TimeTracking::Untracked => empty![], TimeTracking::Untracked => empty![],
TimeTracking::Fibonacci => StyledSelect::build() TimeTracking::Fibonacci => StyledSelect::build()
@ -100,12 +101,7 @@ pub fn time_tracking_field(
.collect(), .collect(),
) )
.state(select_state) .state(select_state)
.options( .options(fibonacci_values.iter().map(|v| v.to_child()).collect())
fibonacci_values()
.into_iter()
.map(|v| v.to_child())
.collect(),
)
.build(field_id) .build(field_id)
.into_node(), .into_node(),
TimeTracking::Hourly => StyledInput::build() TimeTracking::Hourly => StyledInput::build()

View File

@ -8,16 +8,16 @@ use uuid::Uuid;
use jirs_data::*; use jirs_data::*;
use crate::modal::time_tracking::value_for_time_tracking; use crate::{
use crate::shared::drag::DragState; modal::time_tracking::value_for_time_tracking,
use crate::shared::styled_checkbox::StyledCheckboxState; shared::{
use crate::shared::styled_date_time_input::StyledDateTimeInputState; drag::DragState, styled_checkbox::StyledCheckboxState,
use crate::shared::styled_editor::Mode; styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
use crate::shared::styled_image_input::StyledImageInputState; styled_image_input::StyledImageInputState, styled_input::StyledInputState,
use crate::shared::styled_input::StyledInputState; styled_rte::StyledRteState, styled_select::StyledSelectState,
use crate::shared::styled_rte::StyledRteState; },
use crate::shared::styled_select::StyledSelectState; EditIssueModalSection, FieldId, Msg, ProjectFieldId,
use crate::{EditIssueModalSection, FieldId, Msg, ProjectFieldId}; };
pub trait IssueModal { pub trait IssueModal {
fn epic_id_value(&self) -> Option<u32>; fn epic_id_value(&self) -> Option<u32>;

11
jirs-client/src/print.rs Normal file
View File

@ -0,0 +1,11 @@
fn main() {
std::fs::create_dir_all("./jirs-client/src/hi/syntax").unwrap();
use syntect::{dumps::*, parsing::*};
let syntax_set: SyntaxSet = from_binary(include_bytes!("./hi/newlines.packdump"));
std::fs::write(
"./jirs-client/src/hi/syntax_set.cbor",
serde_cbor::ser::to_vec(&syntax_set).unwrap(),
)
.unwrap();
}

View File

@ -41,7 +41,7 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
fn header() -> Node<Msg> { fn header() -> Node<Msg> {
let button = StyledButton::build() let button = StyledButton::build()
.secondary() .secondary()
.text("Github Repo".to_string()) .text("Github Repo")
.icon(Icon::Github) .icon(Icon::Github)
.build() .build()
.into_node(); .into_node();
@ -124,7 +124,7 @@ fn avatars_filters(model: &Model) -> Node<Msg> {
class_list.push("isActive"); class_list.push("isActive");
} }
let styled_avatar = StyledAvatar::build() let styled_avatar = StyledAvatar::build()
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default()) .avatar_url(user.avatar_url.as_deref().unwrap_or_default())
.on_click(mouse_ev(Ev::Click, move |_| { .on_click(mouse_ev(Ev::Click, move |_| {
Msg::ProjectAvatarFilterChanged(user_id, active) Msg::ProjectAvatarFilterChanged(user_id, active)
})) }))
@ -270,7 +270,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
StyledAvatar::build() StyledAvatar::build()
.size(24) .size(24)
.name(user.name.as_str()) .name(user.name.as_str())
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default()) .avatar_url(user.avatar_url.as_deref().unwrap_or_default())
.user_index(idx) .user_index(idx)
.build() .build()
.into_node() .into_node()
@ -279,10 +279,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
let issue_type_icon = { let issue_type_icon = {
StyledIcon::build(issue.issue_type.clone().into()) StyledIcon::build(issue.issue_type.clone().into())
.add_style(format!( .with_color(issue.issue_type.to_str())
"color: var(--{issue_type})",
issue_type = issue.issue_type.to_string()
))
.build() .build()
.into_node() .into_node()
}; };
@ -292,7 +289,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
_ => Icon::ArrowUp, _ => Icon::ArrowUp,
}; };
StyledIcon::build(icon) StyledIcon::build(icon)
.add_style(format!("color: var(--{})", issue.priority)) .with_color(issue.priority.to_str())
.build() .build()
.into_node() .into_node()
}; };

View File

@ -0,0 +1 @@
Tracking employees time carries the risk of having them feel like they are being spied on. This is one of the most common fears that employees have when a time tracking system is implemented. No one likes to feel like theyre always being watched.

View File

@ -0,0 +1 @@
Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging.

View File

@ -18,8 +18,8 @@ use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{inner_layout, ToChild, ToNode}; use crate::shared::{inner_layout, ToChild, ToNode};
use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange}; use crate::{model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange};
static TIME_TRACKING_FIBONACCI: &str = "Tracking employees time carries the risk of having them feel like they are being spied on. This is one of the most common fears that employees have when a time tracking system is implemented. No one likes to feel like theyre always being watched."; static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
static TIME_TRACKING_HOURLY: &str = "Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging."; static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt");
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content { let page = match &model.page_content {
@ -106,7 +106,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
/// Build project name input with styled field wrapper /// Build project name input with styled field wrapper
fn name_field(page: &ProjectSettingsPage) -> Node<Msg> { fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name)) let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name))
.value(page.payload.name.as_ref().cloned().unwrap_or_default()) .value(page.payload.name.as_deref().unwrap_or_default())
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
@ -126,7 +126,7 @@ fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default()) .value(page.payload.url.as_deref().unwrap_or_default())
.build() .build()
.into_node(); .into_node();
StyledField::build() StyledField::build()
@ -161,17 +161,13 @@ fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
/// Build project category dropdown with styled field wrapper /// Build project category dropdown with styled field wrapper
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> { fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
let project_categories = ProjectCategory::ordered();
let category = StyledSelect::build() let category = StyledSelect::build()
.opened(page.project_category_state.opened) .opened(page.project_category_state.opened)
.text_filter(page.project_category_state.text_filter.as_str()) .text_filter(page.project_category_state.text_filter.as_str())
.valid(true) .valid(true)
.normal() .normal()
.options( .options(project_categories.iter().map(|c| c.to_child()).collect())
ProjectCategory::ordered()
.into_iter()
.map(|c| c.to_child())
.collect(),
)
.selected(vec![page .selected(vec![page
.payload .payload
.category .category

View File

@ -14,8 +14,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
vec![ vec![
WsMsg::UserProjectsLoad, WsMsg::UserProjectsLoad,
WsMsg::ProjectsLoad, WsMsg::ProjectsLoad,
WsMsg::MessagesLoad,
WsMsg::ProjectUsersLoad, WsMsg::ProjectUsersLoad,
WsMsg::MessagesLoad,
], ],
model.ws.as_ref(), model.ws.as_ref(),
orders, orders,
@ -85,16 +85,17 @@ pub fn render(model: &Model) -> Node<Msg> {
fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>) -> Node<Msg> { fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>) -> Node<Msg> {
let path = page.map(|ref p| p.to_path()).unwrap_or_default(); let path = page.map(|ref p| p.to_path()).unwrap_or_default();
let mut class_list = vec!["linkItem".to_string(), icon.to_string()]; let mut class_list = vec![];
if page.is_none() { if page.is_none() {
class_list.push("notAllowed".to_string()); class_list.push("notAllowed");
}; };
if Some(model.page) == page { if Some(model.page) == page {
class_list.push("active".to_string()); class_list.push("active");
} }
let icon_node = StyledIcon::build(icon).build().into_node(); let icon_node = StyledIcon::build(icon).build().into_node();
li![ li![
attrs![At::Class => class_list.join(" ")], class!["linkItem"],
class![icon.to_str()],
a![ a![
attrs![At::Href => path], attrs![At::Href => path],
icon_node, icon_node,

View File

@ -31,10 +31,10 @@ pub mod styled_textarea;
pub mod styled_tooltip; pub mod styled_tooltip;
pub mod tracking_widget; pub mod tracking_widget;
pub trait ToChild { pub trait ToChild<'l> {
type Builder; type Builder: 'l;
fn to_child(&self) -> Self::Builder; fn to_child<'m: 'l>(&'m self) -> Self::Builder;
} }
pub fn go_to_board(orders: &mut impl Orders<Msg>) { pub fn go_to_board(orders: &mut impl Orders<Msg>) {
@ -51,7 +51,7 @@ pub fn go_to(url: &str) {
seed::push_route(Url::from_str(url).unwrap()); seed::push_route(Url::from_str(url).unwrap());
} }
pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> { pub fn find_issue<'l>(model: &'l Model, issue_id: IssueId) -> Option<&'l Issue> {
model.issues.iter().find(|issue| issue.id == issue_id) model.issues.iter().find(|issue| issue.id == issue_id)
} }

View File

@ -61,7 +61,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
StyledAvatar::build() StyledAvatar::build()
.size(27) .size(27)
.name(user.name.as_str()) .name(user.name.as_str())
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default()) .avatar_url(user.avatar_url.as_deref().unwrap_or_default())
.build() .build()
.into_node() .into_node()
], ],
@ -257,12 +257,12 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
fn about_tooltip_popup(model: &Model) -> Node<Msg> { fn about_tooltip_popup(model: &Model) -> Node<Msg> {
let visit_website = StyledButton::build() let visit_website = StyledButton::build()
.text("Visit Website".to_string()) .text("Visit Website")
.primary() .primary()
.build() .build()
.into_node(); .into_node();
let github_repo = StyledButton::build() let github_repo = StyledButton::build()
.text("Github Repo".to_string()) .text("Github Repo")
.secondary() .secondary()
.icon(Icon::Github) .icon(Icon::Github)
.build() .build()
@ -332,7 +332,7 @@ fn parse_description(model: &Model, desc: &str) -> Node<Msg> {
}) })
.map(|(index, user)| { .map(|(index, user)| {
let avatar = StyledAvatar::build() let avatar = StyledAvatar::build()
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default()) .avatar_url(user.avatar_url.as_deref().unwrap_or_default())
.user_index(index) .user_index(index)
.size(16) .size(16)
.build() .build()

View File

@ -3,21 +3,21 @@ use seed::{prelude::*, *};
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
pub struct StyledAvatar { pub struct StyledAvatar<'l> {
avatar_url: Option<String>, avatar_url: Option<&'l str>,
size: u32, size: u32,
name: String, name: &'l str,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
user_index: usize, user_index: usize,
} }
impl Default for StyledAvatar { impl<'l> Default for StyledAvatar<'l> {
fn default() -> Self { fn default() -> Self {
Self { Self {
avatar_url: None, avatar_url: None,
size: 32, size: 32,
name: "".to_string(), name: "",
on_click: None, on_click: None,
class_list: vec![], class_list: vec![],
user_index: 0, user_index: 0,
@ -25,12 +25,12 @@ impl Default for StyledAvatar {
} }
} }
impl StyledAvatar { impl<'l> StyledAvatar<'l> {
pub fn build() -> StyledAvatarBuilder { pub fn build() -> StyledAvatarBuilder<'l> {
StyledAvatarBuilder { StyledAvatarBuilder {
avatar_url: None, avatar_url: None,
size: None, size: None,
name: "".to_string(), name: "",
on_click: None, on_click: None,
class_list: vec![], class_list: vec![],
user_index: 0, user_index: 0,
@ -38,29 +38,25 @@ impl StyledAvatar {
} }
} }
impl ToNode for StyledAvatar { impl<'l> ToNode for StyledAvatar<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
pub struct StyledAvatarBuilder { pub struct StyledAvatarBuilder<'l> {
avatar_url: Option<String>, avatar_url: Option<&'l str>,
size: Option<u32>, size: Option<u32>,
name: String, name: &'l str,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
user_index: usize, user_index: usize,
} }
impl StyledAvatarBuilder { impl<'l> StyledAvatarBuilder<'l> {
pub fn avatar_url<S>(mut self, avatar_url: S) -> Self pub fn avatar_url<'m: 'l>(mut self, avatar_url: &'m str) -> Self {
where if !avatar_url.is_empty() {
S: Into<String>, self.avatar_url = Some(avatar_url);
{
let url = avatar_url.into();
if !url.is_empty() {
self.avatar_url = Some(url);
} }
self self
} }
@ -70,11 +66,8 @@ impl StyledAvatarBuilder {
self self
} }
pub fn name<S>(mut self, name: S) -> Self pub fn name<'m: 'l>(mut self, name: &'m str) -> Self {
where self.name = name;
S: Into<String>,
{
self.name = name.into();
self self
} }
@ -83,11 +76,8 @@ impl StyledAvatarBuilder {
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
@ -96,7 +86,7 @@ impl StyledAvatarBuilder {
self self
} }
pub fn build(self) -> StyledAvatar { pub fn build(self) -> StyledAvatar<'l> {
StyledAvatar { StyledAvatar {
avatar_url: self.avatar_url, avatar_url: self.avatar_url,
size: self.size.unwrap_or(32), size: self.size.unwrap_or(32),
@ -120,10 +110,10 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
let index = user_index % 8; let index = user_index % 8;
class_list.push("styledAvatar".to_string()); class_list.push("styledAvatar");
match avatar_url { match avatar_url {
Some(_) => class_list.push("image".to_string()), Some(_) => class_list.push("image"),
_ => class_list.push("letter".to_string()), _ => class_list.push("letter"),
}; };
let shared_style = format!("width: {size}px; height: {size}px", size = size); let shared_style = format!("width: {size}px; height: {size}px", size = size);
@ -155,10 +145,13 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
shared = shared_style, shared = shared_style,
size = size size = size
); );
class_list.push("letter".to_string());
class_list.push(format!("avatarColor{}", index + 1));
div![ div![
attrs![At::Class => class_list.join(" "), At::Style => style], class!["letter"],
attrs![
At::Class => format!("avatarColor{}", index + 1),
At::Class => class_list.join(" "),
At::Style => style
],
span![letter], span![letter],
handler, handler,
] ]

View File

@ -12,8 +12,8 @@ enum Variant {
Empty, Empty,
} }
impl ToString for Variant { impl Variant {
fn to_string(&self) -> String { fn to_str(&self) -> &'static str {
match self { match self {
Variant::Primary => "primary", Variant::Primary => "primary",
Variant::Success => "success", Variant::Success => "success",
@ -21,24 +21,29 @@ impl ToString for Variant {
Variant::Secondary => "secondary", Variant::Secondary => "secondary",
Variant::Empty => "empty", Variant::Empty => "empty",
} }
.to_string() }
}
impl ToString for Variant {
fn to_string(&self) -> String {
self.to_str().to_string()
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledButtonBuilder { pub struct StyledButtonBuilder<'l> {
variant: Option<Variant>, variant: Option<Variant>,
disabled: Option<bool>, disabled: Option<bool>,
active: Option<bool>, active: Option<bool>,
text: Option<String>, text: Option<&'l str>,
icon: Option<Node<Msg>>, icon: Option<Node<Msg>>,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
children: Option<Vec<Node<Msg>>>, children: Option<Vec<Node<Msg>>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
button_type: Option<String>, button_type: Option<&'l str>,
} }
impl StyledButtonBuilder { impl<'l> StyledButtonBuilder<'l> {
fn variant(mut self, value: Variant) -> Self { fn variant(mut self, value: Variant) -> Self {
self.variant = Some(value); self.variant = Some(value);
self self
@ -74,11 +79,8 @@ impl StyledButtonBuilder {
self self
} }
pub fn text<S>(mut self, value: S) -> Self pub fn text(mut self, value: &'l str) -> Self {
where self.text = Some(value);
S: Into<String>,
{
self.text = Some(value.into());
self self
} }
@ -100,20 +102,17 @@ impl StyledButtonBuilder {
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn set_type_reset(mut self) -> Self { pub fn set_type_reset(mut self) -> Self {
self.button_type = Some("reset".to_string()); self.button_type = Some("reset");
self self
} }
pub fn build(self) -> StyledButton { pub fn build(self) -> StyledButton<'l> {
StyledButton { StyledButton {
variant: self.variant.unwrap_or_else(|| Variant::Primary), variant: self.variant.unwrap_or_else(|| Variant::Primary),
disabled: self.disabled.unwrap_or_else(|| false), disabled: self.disabled.unwrap_or_else(|| false),
@ -123,30 +122,30 @@ impl StyledButtonBuilder {
on_click: self.on_click, on_click: self.on_click,
children: self.children.unwrap_or_default(), children: self.children.unwrap_or_default(),
class_list: self.class_list, class_list: self.class_list,
button_type: self.button_type.unwrap_or_else(|| "submit".to_string()), button_type: self.button_type.unwrap_or_else(|| "submit"),
} }
} }
} }
pub struct StyledButton { pub struct StyledButton<'l> {
variant: Variant, variant: Variant,
disabled: bool, disabled: bool,
active: bool, active: bool,
text: Option<String>, text: Option<&'l str>,
icon: Option<Node<Msg>>, icon: Option<Node<Msg>>,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
button_type: String, button_type: &'l str,
} }
impl StyledButton { impl<'l> StyledButton<'l> {
pub fn build() -> StyledButtonBuilder { pub fn build() -> StyledButtonBuilder<'l> {
StyledButtonBuilder::default() StyledButtonBuilder::default()
} }
} }
impl ToNode for StyledButton { impl<'l> ToNode for StyledButton<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
@ -164,16 +163,16 @@ pub fn render(values: StyledButton) -> Node<Msg> {
mut class_list, mut class_list,
button_type, button_type,
} = values; } = values;
class_list.push("styledButton".to_string()); class_list.push("styledButton");
class_list.push(variant.to_string()); class_list.push(variant.to_str());
if children.is_empty() && text.is_none() { if children.is_empty() && text.is_none() {
class_list.push("iconOnly".to_string()); class_list.push("iconOnly");
} }
if active { if active {
class_list.push("isActive".to_string()); class_list.push("isActive");
} }
if icon.is_some() { if icon.is_some() {
class_list.push("withIcon".to_string()); class_list.push("withIcon");
} }
let handler = match on_click { let handler = match on_click {
Some(h) if !disabled => vec![h], Some(h) if !disabled => vec![h],

View File

@ -27,21 +27,21 @@ impl StyledCheckboxState {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ChildBuilder { pub struct ChildBuilder<'l> {
field_id: Option<FieldId>, field_id: Option<FieldId>,
name: String, name: &'l str,
label: String, label: &'l str,
value: u32, value: u32,
selected: bool, selected: bool,
class_list: Vec<String>, class_list: Vec<String>,
} }
impl Default for ChildBuilder { impl<'l> Default for ChildBuilder<'l> {
fn default() -> Self { fn default() -> Self {
Self { Self {
field_id: None, field_id: None,
name: "".to_string(), name: "",
label: "".to_string(), label: "",
value: 0, value: 0,
selected: false, selected: false,
class_list: vec![], class_list: vec![],
@ -49,25 +49,19 @@ impl Default for ChildBuilder {
} }
} }
impl ChildBuilder { impl<'l> ChildBuilder<'l> {
pub fn value(mut self, value: u32) -> Self { pub fn value(mut self, value: u32) -> Self {
self.value = value; self.value = value;
self self
} }
pub fn name<S>(mut self, name: S) -> Self pub fn name(mut self, name: &'l str) -> Self {
where self.name = name;
S: Into<String>,
{
self.name = name.into();
self self
} }
pub fn label<S>(mut self, label: S) -> Self pub fn label(mut self, label: &'l str) -> Self {
where self.label = label;
S: Into<String>,
{
self.label = label.into();
self self
} }
@ -90,7 +84,7 @@ impl ChildBuilder {
} }
} }
impl ToNode for ChildBuilder { impl<'l> ToNode for ChildBuilder<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
let ChildBuilder { let ChildBuilder {
field_id, field_id,
@ -111,9 +105,9 @@ impl ToNode for ChildBuilder {
class_list.push(if selected { "selected" } else { "" }.to_string()); class_list.push(if selected { "selected" } else { "" }.to_string());
let input_attrs = if selected { let input_attrs = if selected {
attrs![At::Type => "radio", At::Name => name.as_str(), At::Checked => selected, At::Id => format!("{}-{}", id, name)] attrs![At::Type => "radio", At::Name => name, At::Checked => selected, At::Id => format!("{}-{}", id, name)]
} else { } else {
attrs![At::Type => "radio", At::Name => name.as_str(), At::Id => format!("{}-{}", id, name)] attrs![At::Type => "radio", At::Name => name, At::Id => format!("{}-{}", id, name)]
}; };
div![ div![
@ -126,21 +120,21 @@ impl ToNode for ChildBuilder {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledCheckbox { pub struct StyledCheckbox<'l> {
id: FieldId, id: FieldId,
options: Vec<ChildBuilder>, options: Vec<ChildBuilder<'l>>,
selected: u32, selected: u32,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl ToNode for StyledCheckbox { impl<'l> ToNode for StyledCheckbox<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledCheckbox { impl<'l> StyledCheckbox<'l> {
pub fn build() -> StyledCheckboxBuilder { pub fn build() -> StyledCheckboxBuilder<'l> {
StyledCheckboxBuilder { StyledCheckboxBuilder {
options: vec![], options: vec![],
selected: 0, selected: 0,
@ -149,32 +143,29 @@ impl StyledCheckbox {
} }
} }
pub struct StyledCheckboxBuilder { pub struct StyledCheckboxBuilder<'l> {
options: Vec<ChildBuilder>, options: Vec<ChildBuilder<'l>>,
selected: u32, selected: u32,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl StyledCheckboxBuilder { impl<'l> StyledCheckboxBuilder<'l> {
pub fn state(mut self, state: &StyledCheckboxState) -> Self { pub fn state(mut self, state: &StyledCheckboxState) -> Self {
self.selected = state.value; self.selected = state.value;
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn options(mut self, options: Vec<ChildBuilder>) -> Self { pub fn options(mut self, options: Vec<ChildBuilder<'l>>) -> Self {
self.options = options; self.options = options;
self self
} }
pub fn build(self, field_id: FieldId) -> StyledCheckbox { pub fn build(self, field_id: FieldId) -> StyledCheckbox<'l> {
StyledCheckbox { StyledCheckbox {
id: field_id, id: field_id,
options: self.options, options: self.options,
@ -204,10 +195,10 @@ fn render(values: StyledCheckbox) -> Node<Msg> {
] ]
} }
impl ToChild for TimeTracking { impl<'l> ToChild<'l> for TimeTracking {
type Builder = ChildBuilder; type Builder = ChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
Self::Builder::default() Self::Builder::default()
.label(match self { .label(match self {
TimeTracking::Untracked => "No tracking", TimeTracking::Untracked => "No tracking",

View File

@ -12,65 +12,53 @@ const CONFIRM_TEXT: &str = "Confirm";
const CANCEL_TEXT: &str = "Cancel"; const CANCEL_TEXT: &str = "Cancel";
#[derive(Debug)] #[derive(Debug)]
pub struct StyledConfirmModal { pub struct StyledConfirmModal<'l> {
title: String, title: &'l str,
message: String, message: &'l str,
confirm_text: String, confirm_text: &'l str,
cancel_text: String, cancel_text: &'l str,
on_confirm: Option<EventHandler<Msg>>, on_confirm: Option<EventHandler<Msg>>,
} }
impl StyledConfirmModal { impl<'l> StyledConfirmModal<'l> {
pub fn build() -> StyledConfirmModalBuilder { pub fn build() -> StyledConfirmModalBuilder<'l> {
StyledConfirmModalBuilder::default() StyledConfirmModalBuilder::default()
} }
} }
impl ToNode for StyledConfirmModal { impl<'l> ToNode for StyledConfirmModal<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledConfirmModalBuilder { pub struct StyledConfirmModalBuilder<'l> {
title: Option<String>, title: Option<&'l str>,
message: Option<String>, message: Option<&'l str>,
confirm_text: Option<String>, confirm_text: Option<&'l str>,
cancel_text: Option<String>, cancel_text: Option<&'l str>,
on_confirm: Option<EventHandler<Msg>>, on_confirm: Option<EventHandler<Msg>>,
} }
impl StyledConfirmModalBuilder { impl<'l> StyledConfirmModalBuilder<'l> {
pub fn title<S>(mut self, title: S) -> Self pub fn title(mut self, title: &'l str) -> Self {
where self.title = Some(title);
S: Into<String>,
{
self.title = Some(title.into());
self self
} }
pub fn message<S>(mut self, message: S) -> Self pub fn message(mut self, message: &'l str) -> Self {
where self.message = Some(message);
S: Into<String>,
{
self.message = Some(message.into());
self self
} }
pub fn confirm_text<S>(mut self, confirm_text: S) -> Self pub fn confirm_text(mut self, confirm_text: &'l str) -> Self {
where self.confirm_text = Some(confirm_text);
S: Into<String>,
{
self.confirm_text = Some(confirm_text.into());
self self
} }
pub fn cancel_text<S>(mut self, cancel_text: S) -> Self pub fn cancel_text(mut self, cancel_text: &'l str) -> Self {
where self.cancel_text = Some(cancel_text);
S: Into<String>,
{
self.cancel_text = Some(cancel_text.into());
self self
} }
@ -79,14 +67,12 @@ impl StyledConfirmModalBuilder {
self self
} }
pub fn build(self) -> StyledConfirmModal { pub fn build(self) -> StyledConfirmModal<'l> {
StyledConfirmModal { StyledConfirmModal {
title: self.title.unwrap_or_else(|| TITLE.to_string()), title: self.title.unwrap_or_else(|| TITLE),
message: self.message.unwrap_or_else(|| MESSAGE.to_string()), message: self.message.unwrap_or_else(|| MESSAGE),
confirm_text: self confirm_text: self.confirm_text.unwrap_or_else(|| CONFIRM_TEXT),
.confirm_text cancel_text: self.cancel_text.unwrap_or_else(|| CANCEL_TEXT),
.unwrap_or_else(|| CONFIRM_TEXT.to_string()),
cancel_text: self.cancel_text.unwrap_or_else(|| CANCEL_TEXT.to_string()),
on_confirm: self.on_confirm, on_confirm: self.on_confirm,
} }
} }
@ -132,7 +118,7 @@ pub fn render(values: StyledConfirmModal) -> Node<Msg> {
cancel_button cancel_button
], ],
]) ])
.add_class("confirmModal".to_string()) .add_class("confirmModal")
.build() .build()
.into_node() .into_node()
} }

View File

@ -257,7 +257,7 @@ fn render(values: StyledDateTimeInput) -> Node<Msg> {
.to_string(); .to_string();
StyledButton::build() StyledButton::build()
.on_click(on_focus) .on_click(on_focus)
.text(text) .text(text.as_str())
.empty() .empty()
.build() .build()
.into_node() .into_node()

View File

@ -4,47 +4,41 @@ use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
#[derive(Debug)] #[derive(Debug)]
pub struct StyledField { pub struct StyledField<'l> {
label: String, label: &'l str,
tip: Option<String>, tip: Option<&'l str>,
input: Node<Msg>, input: Node<Msg>,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl StyledField { impl<'l> StyledField<'l> {
pub fn build() -> StyledFieldBuilder { pub fn build() -> StyledFieldBuilder<'l> {
StyledFieldBuilder::default() StyledFieldBuilder::default()
} }
} }
impl ToNode for StyledField { impl<'l> ToNode for StyledField<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct StyledFieldBuilder { pub struct StyledFieldBuilder<'l> {
label: Option<String>, label: Option<&'l str>,
tip: Option<String>, tip: Option<&'l str>,
input: Option<Node<Msg>>, input: Option<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl StyledFieldBuilder { impl<'l> StyledFieldBuilder<'l> {
pub fn label<S>(mut self, label: S) -> Self pub fn label(mut self, label: &'l str) -> Self {
where self.label = Some(label);
S: Into<String>,
{
self.label = Some(label.into());
self self
} }
pub fn tip<S>(mut self, tip: S) -> Self pub fn tip(mut self, tip: &'l str) -> Self {
where self.tip = Some(tip);
S: Into<String>,
{
self.tip = Some(tip.into());
self self
} }
@ -53,15 +47,12 @@ impl StyledFieldBuilder {
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn build(self) -> StyledField { pub fn build(self) -> StyledField<'l> {
StyledField { StyledField {
label: self.label.unwrap_or_default(), label: self.label.unwrap_or_default(),
tip: self.tip, tip: self.tip,
@ -76,16 +67,15 @@ pub fn render(values: StyledField) -> Node<Msg> {
label, label,
tip, tip,
input, input,
mut class_list, class_list,
} = values; } = values;
let tip_node = match tip { let tip_node = match tip {
Some(s) => div![attrs![At::Class => "styledTip"], s], Some(s) => div![attrs![At::Class => "styledTip"], s],
_ => empty![], _ => empty![],
}; };
class_list.push("styledField".to_string());
div![ div![
attrs![At::Class => class_list.join(" ")], attrs![At::Class => class_list.join(" "), At::Class => "styledField"],
seed::label![attrs![At::Class => "styledLabel"], label], seed::label![attrs![At::Class => "styledLabel"], label],
input, input,
tip_node, tip_node,

View File

@ -4,32 +4,32 @@ use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledForm { pub struct StyledForm<'l> {
heading: String, heading: &'l str,
fields: Vec<Node<Msg>>, fields: Vec<Node<Msg>>,
on_submit: Option<EventHandler<Msg>>, on_submit: Option<EventHandler<Msg>>,
} }
impl StyledForm { impl<'l> StyledForm<'l> {
pub fn build() -> StyledFormBuilder { pub fn build() -> StyledFormBuilder<'l> {
StyledFormBuilder::default() StyledFormBuilder::default()
} }
} }
impl ToNode for StyledForm { impl<'l> ToNode for StyledForm<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct StyledFormBuilder { pub struct StyledFormBuilder<'l> {
fields: Vec<Node<Msg>>, fields: Vec<Node<Msg>>,
heading: String, heading: &'l str,
on_submit: Option<EventHandler<Msg>>, on_submit: Option<EventHandler<Msg>>,
} }
impl StyledFormBuilder { impl<'l> StyledFormBuilder<'l> {
pub fn add_field(mut self, node: Node<Msg>) -> Self { pub fn add_field(mut self, node: Node<Msg>) -> Self {
self.fields.push(node); self.fields.push(node);
self self
@ -42,11 +42,8 @@ impl StyledFormBuilder {
self self
} }
pub fn heading<S>(mut self, heading: S) -> Self pub fn heading(mut self, heading: &'l str) -> Self {
where self.heading = heading;
S: Into<String>,
{
self.heading = heading.into();
self self
} }
@ -55,7 +52,7 @@ impl StyledFormBuilder {
self self
} }
pub fn build(self) -> StyledForm { pub fn build(self) -> StyledForm<'l> {
StyledForm { StyledForm {
heading: self.heading, heading: self.heading,
fields: self.fields, fields: self.fields,

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{IssuePriority, IssueType}; use jirs_data::{IssuePriority, IssueType};
@ -100,21 +102,19 @@ pub enum Icon {
} }
impl Icon { impl Icon {
pub fn to_color(self) -> Option<String> { pub fn to_color(&self) -> Option<String> {
match self { match self {
Icon::Bug | Icon::Task | Icon::Story | Icon::Epic => Some(format!("var(--{})", self)), Icon::Bug | Icon::Task | Icon::Story | Icon::Epic => Some(format!("var(--{})", self)),
_ => None, _ => None,
} }
} }
pub fn into_styled_builder(self) -> StyledIconBuilder { pub fn into_styled_builder<'l>(self) -> StyledIconBuilder<'l> {
StyledIcon::build(self) StyledIcon::build(self)
} }
}
impl std::fmt::Display for Icon { pub fn to_str<'l>(&self) -> &'l str {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self {
let code = match self {
Icon::Bug => "bug", Icon::Bug => "bug",
Icon::Stopwatch => "stopwatch", Icon::Stopwatch => "stopwatch",
Icon::Task => "task", Icon::Task => "task",
@ -202,8 +202,13 @@ impl std::fmt::Display for Icon {
Icon::DoubleLeft => "double-left", Icon::DoubleLeft => "double-left",
Icon::DoubleRight => "double-right", Icon::DoubleRight => "double-right",
}; }
f.write_str(code) }
}
impl std::fmt::Display for Icon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str())
} }
} }
@ -230,8 +235,8 @@ impl From<IssuePriority> for Icon {
} }
} }
impl Into<StyledIcon> for Icon { impl<'l> Into<StyledIcon<'l>> for Icon {
fn into(self) -> StyledIcon { fn into(self) -> StyledIcon<'l> {
StyledIcon::build(self).build() StyledIcon::build(self).build()
} }
} }
@ -243,33 +248,28 @@ impl ToNode for Icon {
} }
} }
pub struct StyledIconBuilder { pub struct StyledIconBuilder<'l> {
icon: Icon, icon: Icon,
size: Option<i32>, size: Option<i32>,
class_list: Vec<String>, class_list: Vec<Cow<'l, str>>,
style_list: Vec<String>, style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
} }
impl StyledIconBuilder { impl<'l> StyledIconBuilder<'l> {
pub fn size(mut self, size: i32) -> Self { pub fn size(mut self, size: i32) -> Self {
self.size = Some(size); self.size = Some(size);
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(Cow::Borrowed(name));
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn add_style<S>(mut self, name: S) -> Self pub fn with_color(mut self, color: &'l str) -> Self {
where self.color = Some(Cow::Borrowed(color));
S: Into<String>,
{
self.style_list.push(name.into());
self self
} }
@ -278,10 +278,11 @@ impl StyledIconBuilder {
self self
} }
pub fn build(self) -> StyledIcon { pub fn build(self) -> StyledIcon<'l> {
StyledIcon { StyledIcon {
icon: self.icon, icon: self.icon,
size: self.size, size: self.size,
color: self.color,
class_list: self.class_list, class_list: self.class_list,
style_list: self.style_list, style_list: self.style_list,
on_click: self.on_click, on_click: self.on_click,
@ -289,27 +290,29 @@ impl StyledIconBuilder {
} }
} }
pub struct StyledIcon { pub struct StyledIcon<'l> {
icon: Icon, icon: Icon,
size: Option<i32>, size: Option<i32>,
class_list: Vec<String>, class_list: Vec<Cow<'l, str>>,
style_list: Vec<String>, style_list: Vec<Cow<'l, str>>,
color: Option<Cow<'l, str>>,
on_click: Option<EventHandler<Msg>>, on_click: Option<EventHandler<Msg>>,
} }
impl StyledIcon { impl<'l> StyledIcon<'l> {
pub fn build(icon: Icon) -> StyledIconBuilder { pub fn build(icon: Icon) -> StyledIconBuilder<'l> {
StyledIconBuilder { StyledIconBuilder {
icon, icon,
size: None, size: None,
class_list: vec![], class_list: vec![],
style_list: vec![], style_list: vec![],
color: None,
on_click: None, on_click: None,
} }
} }
} }
impl ToNode for StyledIcon { impl<'l> ToNode for StyledIcon<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
@ -319,23 +322,56 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
let StyledIcon { let StyledIcon {
icon, icon,
size, size,
mut class_list, color,
mut style_list, class_list,
style_list,
on_click, on_click,
} = values; } = values;
if let Some(s) = icon.to_color() { let styles: Vec<Attrs> = vec![
style_list.push(format!("color: {}", s)); size.map(|s| {
} let font_size = format!("font-size: {}", s);
attrs![At::Style => font_size]
}),
icon.to_color().map(|s| {
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]
}),
]
.into_iter()
.filter(Option::is_some)
.map(|o| o.unwrap())
.collect();
if let Some(size) = size { let class_list: Vec<seed::Attrs> = class_list
style_list.push(format!("font-size: {s}px", s = size)); .into_iter()
} .map(|s| match s {
Cow::Borrowed(s) => class![s],
class_list.push(format!("styledIcon {}", icon)); Cow::Owned(s) => class![s.as_str()],
})
.collect();
let style_list = style_list
.iter()
.map(|s| match s {
Cow::Borrowed(s) => s,
Cow::Owned(s) => s.as_str(),
})
.collect::<Vec<&str>>()
.join(";");
i![ i![
attrs![At::Class => class_list.join(" "), At::Style => style_list.join(";")], class!["styledIcon"],
class_list,
class![icon.to_str()],
styles,
attrs![ At::Style => style_list ],
on_click, on_click,
"" ""
] ]

View File

@ -30,14 +30,14 @@ impl StyledImageInputState {
} }
} }
pub struct StyledImageInput { pub struct StyledImageInput<'l> {
id: FieldId, id: FieldId,
class_list: Vec<String>, class_list: Vec<&'l str>,
url: Option<String>, url: Option<String>,
} }
impl StyledImageInput { impl<'l> StyledImageInput<'l> {
pub fn build(field_id: FieldId) -> StyledInputInputBuilder { pub fn build(field_id: FieldId) -> StyledInputInputBuilder<'l> {
StyledInputInputBuilder { StyledInputInputBuilder {
id: field_id, id: field_id,
class_list: vec![], class_list: vec![],
@ -46,24 +46,21 @@ impl StyledImageInput {
} }
} }
impl ToNode for StyledImageInput { impl<'l> ToNode for StyledImageInput<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
pub struct StyledInputInputBuilder { pub struct StyledInputInputBuilder<'l> {
id: FieldId, id: FieldId,
class_list: Vec<String>, class_list: Vec<&'l str>,
url: Option<String>, url: Option<String>,
} }
impl StyledInputInputBuilder { impl<'l> StyledInputInputBuilder<'l> {
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
@ -72,7 +69,7 @@ impl StyledInputInputBuilder {
self self
} }
pub fn build(self) -> StyledImageInput { pub fn build(self) -> StyledImageInput<'l> {
StyledImageInput { StyledImageInput {
id: self.id, id: self.id,
class_list: self.class_list, class_list: self.class_list,

View File

@ -10,13 +10,18 @@ pub enum Variant {
Primary, Primary,
} }
impl ToString for Variant { impl Variant {
fn to_string(&self) -> String { pub fn to_str<'l>(&self) -> &'l str {
match self { match self {
Variant::Normal => "normal", Variant::Normal => "normal",
Variant::Primary => "primary", Variant::Primary => "primary",
} }
.to_string() }
}
impl ToString for Variant {
fn to_string(&self) -> String {
self.to_str().to_string()
} }
} }
@ -67,21 +72,21 @@ impl StyledInputState {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledInput { pub struct StyledInput<'l> {
id: FieldId, id: FieldId,
icon: Option<Icon>, icon: Option<Icon>,
valid: bool, valid: bool,
value: Option<String>, value: Option<String>,
input_type: Option<String>, input_type: Option<&'l str>,
input_class_list: Vec<String>, input_class_list: Vec<&'l str>,
wrapper_class_list: Vec<String>, wrapper_class_list: Vec<&'l str>,
variant: Variant, variant: Variant,
auto_focus: bool, auto_focus: bool,
input_handlers: Vec<EventHandler<Msg>>, input_handlers: Vec<EventHandler<Msg>>,
} }
impl StyledInput { impl<'l> StyledInput<'l> {
pub fn build() -> StyledInputBuilder { pub fn build() -> StyledInputBuilder<'l> {
StyledInputBuilder { StyledInputBuilder {
icon: None, icon: None,
valid: None, valid: None,
@ -97,19 +102,19 @@ impl StyledInput {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledInputBuilder { pub struct StyledInputBuilder<'l> {
icon: Option<Icon>, icon: Option<Icon>,
valid: Option<bool>, valid: Option<bool>,
value: Option<String>, value: Option<String>,
input_type: Option<String>, input_type: Option<&'l str>,
input_class_list: Vec<String>, input_class_list: Vec<&'l str>,
wrapper_class_list: Vec<String>, wrapper_class_list: Vec<&'l str>,
variant: Variant, variant: Variant,
auto_focus: bool, auto_focus: bool,
input_handlers: Vec<EventHandler<Msg>>, input_handlers: Vec<EventHandler<Msg>>,
} }
impl StyledInputBuilder { impl<'l> StyledInputBuilder<'l> {
pub fn icon(mut self, icon: Icon) -> Self { pub fn icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon); self.icon = Some(icon);
self self
@ -133,19 +138,13 @@ impl StyledInputBuilder {
.valid(!state.touched || !state.value.is_empty()) .valid(!state.touched || !state.value.is_empty())
} }
pub fn add_input_class<S>(mut self, name: S) -> Self pub fn add_input_class(mut self, name: &'l str) -> Self {
where self.input_class_list.push(name);
S: Into<String>,
{
self.input_class_list.push(name.into());
self self
} }
pub fn add_wrapper_class<S>(mut self, name: S) -> Self pub fn add_wrapper_class(mut self, name: &'l str) -> Self {
where self.wrapper_class_list.push(name);
S: Into<String>,
{
self.wrapper_class_list.push(name.into());
self self
} }
@ -164,7 +163,7 @@ impl StyledInputBuilder {
self self
} }
pub fn build(self, id: FieldId) -> StyledInput { pub fn build(self, id: FieldId) -> StyledInput<'l> {
StyledInput { StyledInput {
id, id,
icon: self.icon, icon: self.icon,
@ -180,7 +179,7 @@ impl StyledInputBuilder {
} }
} }
impl ToNode for StyledInput { impl<'l> ToNode for StyledInput<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
@ -200,15 +199,14 @@ pub fn render(values: StyledInput) -> Node<Msg> {
input_handlers, input_handlers,
} = values; } = values;
wrapper_class_list.push(variant.to_string()); wrapper_class_list.push(variant.to_str());
wrapper_class_list.push(format!("{}", id));
if !valid { if !valid {
wrapper_class_list.push("invalid".to_string()); wrapper_class_list.push("invalid");
} }
input_class_list.push(variant.to_string()); input_class_list.push(variant.to_str());
if icon.is_some() { if icon.is_some() {
input_class_list.push("withIcon".to_string()); input_class_list.push("withIcon");
} }
let icon = match icon { let icon = match icon {
@ -240,7 +238,10 @@ pub fn render(values: StyledInput) -> Node<Msg> {
div![ div![
C!["styledInput"], C!["styledInput"],
attrs!(At::Class => wrapper_class_list.join(" ")), attrs!(
At::Class => wrapper_class_list.join(" "),
At::Class => format!("{}", id),
),
icon, icon,
on_click, on_click,
on_keyup, on_keyup,
@ -250,7 +251,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
At::Id => format!("{}", id), At::Id => format!("{}", id),
At::Class => input_class_list.join(" "), At::Class => input_class_list.join(" "),
At::Value => value.unwrap_or_default(), At::Value => value.unwrap_or_default(),
At::Type => input_type.unwrap_or_else(|| "text".to_string()), At::Type => input_type.unwrap_or_else(|| "text"),
], ],
if auto_focus { if auto_focus {

View File

@ -3,56 +3,46 @@ use seed::{prelude::*, *};
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
pub struct StyledLink { pub struct StyledLink<'l> {
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
href: String, href: &'l str,
} }
impl StyledLink { impl<'l> StyledLink<'l> {
pub fn build() -> StyledLinkBuilder { pub fn build() -> StyledLinkBuilder<'l> {
StyledLinkBuilder::default() StyledLinkBuilder::default()
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledLinkBuilder { pub struct StyledLinkBuilder<'l> {
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
href: String, href: &'l str,
} }
impl StyledLinkBuilder { impl<'l> StyledLinkBuilder<'l> {
pub fn add_child(mut self, child: Node<Msg>) -> Self { pub fn add_child(mut self, child: Node<Msg>) -> Self {
self.children.push(child); self.children.push(child);
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn href<S>(mut self, href: S) -> Self pub fn href(mut self, href: &'l str) -> Self {
where self.href = href;
S: Into<String>,
{
self.href = href.into();
self self
} }
pub fn text<S>(self, s: S) -> Self pub fn text(self, s: &'l str) -> Self {
where self.add_child(span![s])
S: Into<String>,
{
let text: String = s.into();
self.add_child(span![text])
} }
pub fn build(self) -> StyledLink { pub fn build(self) -> StyledLink<'l> {
StyledLink { StyledLink {
children: self.children, children: self.children,
class_list: self.class_list, class_list: self.class_list,
@ -61,7 +51,7 @@ impl StyledLinkBuilder {
} }
} }
impl ToNode for StyledLink { impl<'l> ToNode for StyledLink<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
@ -70,12 +60,12 @@ impl ToNode for StyledLink {
pub fn render(values: StyledLink) -> Node<Msg> { pub fn render(values: StyledLink) -> Node<Msg> {
let StyledLink { let StyledLink {
children, children,
mut class_list, class_list,
href, href,
} = values; } = values;
class_list.push("styledLink".to_string());
a![ a![
class!["styledLink"],
attrs![ attrs![
At::Class => class_list.join(" "), At::Class => class_list.join(" "),
At::Href => href, At::Href => href,

View File

@ -28,36 +28,36 @@ impl Variant {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledModal { pub struct StyledModal<'l> {
variant: Variant, variant: Variant,
width: Option<usize>, width: Option<usize>,
with_icon: bool, with_icon: bool,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl ToNode for StyledModal { impl<'l> ToNode for StyledModal<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledModal { impl<'l> StyledModal<'l> {
pub fn build() -> StyledModalBuilder { pub fn build() -> StyledModalBuilder<'l> {
Default::default() Default::default()
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledModalBuilder { pub struct StyledModalBuilder<'l> {
variant: Option<Variant>, variant: Option<Variant>,
width: Option<usize>, width: Option<usize>,
with_icon: Option<bool>, with_icon: Option<bool>,
children: Option<Vec<Node<Msg>>>, children: Option<Vec<Node<Msg>>>,
class_list: Vec<String>, class_list: Vec<&'l str>,
} }
impl StyledModalBuilder { impl<'l> StyledModalBuilder<'l> {
pub fn variant(mut self, variant: Variant) -> Self { pub fn variant(mut self, variant: Variant) -> Self {
self.variant = Some(variant); self.variant = Some(variant);
self self
@ -82,15 +82,12 @@ impl StyledModalBuilder {
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn build(self) -> StyledModal { pub fn build(self) -> StyledModal<'l> {
StyledModal { StyledModal {
variant: self.variant.unwrap_or_else(|| Variant::Center), variant: self.variant.unwrap_or_else(|| Variant::Center),
width: self.width, width: self.width,
@ -126,7 +123,8 @@ pub fn render(values: StyledModal) -> Node<Msg> {
}); });
let clickable_class = format!("clickableOverlay {}", variant.to_class_name()); let clickable_class = format!("clickableOverlay {}", variant.to_class_name());
class_list.push(format!("styledModal {}", variant.to_class_name())); class_list.push("styledModal");
class_list.push(variant.to_class_name());
let styled_modal_style = match width { let styled_modal_style = match width {
Some(0) => "".to_string(), Some(0) => "".to_string(),
Some(n) => format!("max-width: {width}px", width = n), Some(n) => format!("max-width: {width}px", width = n),

View File

@ -26,8 +26,8 @@ impl HeadingSize {
} }
} }
impl ToString for HeadingSize { impl HeadingSize {
fn to_string(&self) -> String { fn as_str<'l>(&self) -> &'l str {
use HeadingSize::*; use HeadingSize::*;
match self { match self {
@ -39,7 +39,6 @@ impl ToString for HeadingSize {
H5 => "H5", H5 => "H5",
H6 => "H6", H6 => "H6",
} }
.to_string()
} }
} }
@ -88,28 +87,18 @@ pub enum RteMsg {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ExecCommand { pub struct ExecCommand<'l> {
pub(crate) name: String, pub(crate) name: &'l str,
pub(crate) param: String, pub(crate) param: &'l str,
} }
impl ExecCommand { impl<'l> ExecCommand<'l> {
pub fn new<S>(name: S) -> Self pub fn new(name: &'l str) -> Self {
where
S: Into<String>,
{
Self::new_with_param(name, "") Self::new_with_param(name, "")
} }
pub fn new_with_param<S1, S2>(name: S1, param: S2) -> Self pub fn new_with_param(name: &'l str, param: &'l str) -> Self {
where Self { name, param }
S1: Into<String>,
S2: Into<String>,
{
Self {
name: name.into(),
param: param.into(),
}
} }
} }
@ -136,9 +125,7 @@ impl RteMsg {
| HeadingSize::H3 | HeadingSize::H3
| HeadingSize::H4 | HeadingSize::H4
| HeadingSize::H5 | HeadingSize::H5
| HeadingSize::H6 => { | HeadingSize::H6 => Some(ExecCommand::new_with_param("heading", heading.as_str())),
Some(ExecCommand::new_with_param("heading", heading.to_string()))
}
HeadingSize::Normal => Some(ExecCommand::new_with_param("formatBlock", "div")), HeadingSize::Normal => Some(ExecCommand::new_with_param("formatBlock", "div")),
}, },
RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")), RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")),
@ -193,7 +180,7 @@ pub struct StyledRteCodeState {
impl StyledRteCodeState { impl StyledRteCodeState {
pub fn new(field_id: FieldId) -> Self { pub fn new(field_id: FieldId) -> Self {
let mut languages: Vec<String> = crate::hi::SYNTAX_SET let mut languages: Vec<String> = crate::hi::syntax_set::load()
.syntaxes() .syntaxes()
.iter() .iter()
.map(|s| s.name.clone()) .map(|s| s.name.clone())
@ -202,7 +189,7 @@ impl StyledRteCodeState {
Self { Self {
visible: false, visible: false,
lang: StyledSelectState::new( lang: StyledSelectState::new(
FieldId::Rte(RteField::CodeLang(Box::new(field_id.clone()))), FieldId::Rte(RteField::CodeLang(Box::new(field_id))),
vec![], vec![],
), ),
code: "".to_string(), code: "".to_string(),
@ -221,6 +208,33 @@ impl StyledRteCodeState {
} }
} }
struct RteTableBodyBuilder {
cols: u16,
rows: u16,
}
impl RteTableBodyBuilder {
pub fn new(cols: u16, rows: u16) -> Self {
Self { cols, rows }
}
}
impl ToString for RteTableBodyBuilder {
fn to_string(&self) -> String {
let RteTableBodyBuilder { cols, rows } = self;
let mut buff = "<tbody>".to_string();
for _c in 0..(*cols) {
buff.push_str("<tr>");
for _r in 0..(*rows) {
buff.push_str("<td>&nbsp;</td>")
}
buff.push_str("</tr>");
}
buff.push_str("</tbody>");
buff
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct StyledRteState { pub struct StyledRteState {
pub value: String, pub value: String,
@ -250,17 +264,19 @@ impl StyledRteState {
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.code_tooltip.lang.update(msg, orders); self.code_tooltip.lang.update(msg, orders);
let m = match msg { let m = match msg {
Msg::Rte(m, field) if field == &self.field_id => m, Msg::Rte(field, m) if field == &self.field_id => m,
_ => return, _ => return,
}; };
match m.to_command() { match m.to_command() {
Some(ExecCommand { name, param }) => { Some(ExecCommand { name, param }) => {
self.store_range(); self.store_range();
match seed::html_document().exec_command_with_show_ui_and_value( let doc = match web_sys::window().and_then(|w| w.document()).map(|d| {
name.as_str(), wasm_bindgen::JsValue::from(d).unchecked_into::<web_sys::HtmlDocument>()
false, }) {
param.as_str(), Some(doc) => doc,
) { _ => return,
};
match doc.exec_command_with_show_ui_and_value(name, false, param) {
Ok(_) => {} Ok(_) => {}
Err(e) => log!(e), Err(e) => log!(e),
} }
@ -345,16 +361,9 @@ impl StyledRteState {
Ok(t) => t, Ok(t) => t,
_ => return, _ => return,
}; };
let mut buff = "<tbody>".to_string(); table.set_inner_html(
for _c in 0..(*cols) { RteTableBodyBuilder::new(*cols, *rows).to_string().as_str(),
buff.push_str("<tr>"); );
for _r in 0..(*rows) {
buff.push_str("<td>&nbsp;</td>")
}
buff.push_str("</tr>");
}
buff.push_str("</tbody>");
table.set_inner_html(buff.as_str());
if let Err(e) = r.insert_node(&table) { if let Err(e) = r.insert_node(&table) {
log!(e); log!(e);
} }
@ -363,6 +372,10 @@ impl StyledRteState {
_ => log!(m), _ => log!(m),
}, },
}; };
// orders.skip().send_msg(Msg::StrInputChanged(
// self.field_id.clone(),
// self.value.clone(),
// ));
} }
fn store_range(&mut self) { fn store_range(&mut self) {
@ -396,58 +409,54 @@ impl StyledRteState {
let field_id = self.field_id.clone(); let field_id = self.field_id.clone();
let identifier = self.identifier; let identifier = self.identifier;
orders.perform_cmd(cmds::timeout(200, move || { orders.perform_cmd(cmds::timeout(200, move || {
Msg::Rte(RteMsg::RequestFocus(identifier), field_id) Msg::Rte(field_id, RteMsg::RequestFocus(identifier))
})); }));
} }
} }
pub struct StyledRte { pub struct StyledRte<'component> {
field_id: FieldId, field_id: FieldId,
table_tooltip: StyledRteTableState, table_tooltip: Option<&'component StyledRteTableState>,
identifier: Option<uuid::Uuid>, identifier: Option<uuid::Uuid>,
code_tooltip: StyledRteCodeState, code_tooltip: Option<&'component StyledRteCodeState>,
} }
impl StyledRte { impl<'component> StyledRte<'component> {
pub fn build(field_id: FieldId) -> StyledRteBuilder { pub fn build(field_id: FieldId) -> StyledRteBuilder<'component> {
StyledRteBuilder { StyledRteBuilder {
field_id: field_id.clone(), field_id: field_id.clone(),
value: String::new(), value: String::new(),
table_tooltip: StyledRteTableState { table_tooltip: None,
visible: false, code_tooltip: None,
rows: 0,
cols: 0,
},
code_tooltip: StyledRteCodeState::new(field_id),
identifier: None, identifier: None,
} }
} }
} }
impl ToNode for StyledRte { impl<'component> ToNode for StyledRte<'component> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
pub struct StyledRteBuilder { pub struct StyledRteBuilder<'outer> {
field_id: FieldId, field_id: FieldId,
value: String, value: String,
table_tooltip: StyledRteTableState, table_tooltip: Option<&'outer StyledRteTableState>,
code_tooltip: StyledRteCodeState, code_tooltip: Option<&'outer StyledRteCodeState>,
identifier: Option<uuid::Uuid>, identifier: Option<uuid::Uuid>,
} }
impl StyledRteBuilder { impl<'outer> StyledRteBuilder<'outer> {
pub fn state(mut self, state: &StyledRteState) -> Self { pub fn state(mut self, state: &'outer StyledRteState) -> Self {
self.value = state.value.clone(); self.value = state.value.clone();
self.table_tooltip = state.table_tooltip.clone(); self.table_tooltip = Some(&state.table_tooltip);
self.code_tooltip = state.code_tooltip.clone(); self.code_tooltip = Some(&state.code_tooltip);
self.identifier = Some(state.identifier); self.identifier = Some(state.identifier);
self self
} }
pub fn build(self) -> StyledRte { pub fn build(self) -> StyledRte<'outer> {
StyledRte { StyledRte {
field_id: self.field_id, field_id: self.field_id,
table_tooltip: self.table_tooltip, table_tooltip: self.table_tooltip,
@ -458,7 +467,7 @@ impl StyledRteBuilder {
} }
pub fn render(values: StyledRte) -> Node<Msg> { pub fn render(values: StyledRte) -> Node<Msg> {
{ /*{
let _brush_button = styled_rte_button( let _brush_button = styled_rte_button(
"Brush", "Brush",
Icon::Brush, Icon::Brush,
@ -533,12 +542,17 @@ pub fn render(values: StyledRte) -> Node<Msg> {
None as Option<Msg> None as Option<Msg>
}), }),
); );
} }*/
// let field_id = values.field_id.clone();
let capture_event = ev(Ev::KeyDown, |ev| { let capture_event = ev(Ev::KeyDown, |ev| {
ev.stop_propagation(); ev.stop_propagation();
None as Option<Msg> None as Option<Msg>
}); });
// let capture_change = ev(Ev::Input, |ev| {
// ev.stop_propagation();
// Some(Msg::StrInputChanged(field_id, "".to_string()))
// });
let id = values.identifier.unwrap_or_default().to_string(); let id = values.identifier.unwrap_or_default().to_string();
@ -563,7 +577,8 @@ pub fn render(values: StyledRte) -> Node<Msg> {
div![ div![
C!["editor"], C!["editor"],
attrs![At::ContentEditable => true], attrs![At::ContentEditable => true],
capture_event capture_event,
// capture_change
], ],
] ]
] ]
@ -578,7 +593,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyFull, field_id)) Some(Msg::Rte(field_id, RteMsg::JustifyFull))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -587,7 +602,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::JustifyCenter, Icon::JustifyCenter,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyCenter, field_id)) Some(Msg::Rte(field_id, RteMsg::JustifyCenter))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -596,7 +611,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::JustifyLeft, Icon::JustifyLeft,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyLeft, field_id)) Some(Msg::Rte(field_id, RteMsg::JustifyLeft))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -606,7 +621,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyRight, field_id)) Some(Msg::Rte(field_id, RteMsg::JustifyRight))
}), }),
); );
div![ div![
@ -625,7 +640,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Redo, Icon::Redo,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Redo, field_id)) Some(Msg::Rte(field_id, RteMsg::Redo))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -634,7 +649,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Undo, Icon::Undo,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Undo, field_id)) Some(Msg::Rte(field_id, RteMsg::Undo))
}), }),
); );
/*let field_id = values.field_id.clone(); /*let field_id = values.field_id.clone();
@ -681,7 +696,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::EraserAlt, Icon::EraserAlt,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::RemoveFormat, field_id)) Some(Msg::Rte(field_id, RteMsg::RemoveFormat))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -690,7 +705,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Bold, Icon::Bold,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Bold, field_id)) Some(Msg::Rte(field_id, RteMsg::Bold))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -699,7 +714,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Italic, Icon::Italic,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Italic, field_id)) Some(Msg::Rte(field_id, RteMsg::Italic))
}), }),
); );
@ -710,7 +725,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Underline, Icon::Underline,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Underscore, field_id)) Some(Msg::Rte(field_id, RteMsg::Underscore))
}), }),
) )
}; };
@ -722,7 +737,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::StrikeThrough, Icon::StrikeThrough,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Strikethrough, field_id)) Some(Msg::Rte(field_id, RteMsg::Strikethrough))
}), }),
) )
}; };
@ -734,7 +749,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Subscript, Icon::Subscript,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Subscript, field_id)) Some(Msg::Rte(field_id, RteMsg::Subscript))
}), }),
) )
}; };
@ -746,7 +761,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Superscript, Icon::Superscript,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::Superscript, field_id)) Some(Msg::Rte(field_id, RteMsg::Superscript))
}), }),
) )
}; };
@ -818,10 +833,10 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
.map(|h| { .map(|h| {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
let button = StyledButton::build() let button = StyledButton::build()
.text(h.to_string()) .text(h.as_str())
.on_click(mouse_ev(Ev::Click, move |ev| { .on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertHeading(h), field_id)) Some(Msg::Rte(field_id, RteMsg::InsertHeading(h)))
})) }))
.empty() .empty()
.build() .build()
@ -867,7 +882,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::ListingDots, Icon::ListingDots,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertUnorderedList, field_id)) Some(Msg::Rte(field_id, RteMsg::InsertUnorderedList))
}), }),
); );
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
@ -876,7 +891,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::ListingNumber, Icon::ListingNumber,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertOrderedList, field_id)) Some(Msg::Rte(field_id, RteMsg::InsertOrderedList))
}), }),
); );
/*let field_id = values.field_id.clone(); /*let field_id = values.field_id.clone();
@ -896,7 +911,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::Table, Icon::Table,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::TableSetVisibility(true), field_id)) Some(Msg::Rte(field_id, RteMsg::TableSetVisibility(true)))
}), }),
) )
}; };
@ -909,7 +924,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::Paragraph, Icon::Paragraph,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertParagraph, field_id)) Some(Msg::Rte(field_id, RteMsg::InsertParagraph))
}), }),
) )
}; };
@ -920,7 +935,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::CodeAlt, Icon::CodeAlt,
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertCode(true), field_id)) Some(Msg::Rte(field_id, RteMsg::InsertCode(true)))
}), }),
) )
}; };
@ -945,8 +960,8 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte( Some(Msg::Rte(
RteMsg::ChangeIndent(RteIndentMsg::Increase),
field_id, field_id,
RteMsg::ChangeIndent(RteIndentMsg::Increase),
)) ))
}), }),
); );
@ -957,8 +972,8 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte( Some(Msg::Rte(
RteMsg::ChangeIndent(RteIndentMsg::Decrease),
field_id, field_id,
RteMsg::ChangeIndent(RteIndentMsg::Decrease),
)) ))
}), }),
); );
@ -975,18 +990,23 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
} }
fn table_tooltip(values: &StyledRte) -> Node<Msg> { fn table_tooltip(values: &StyledRte) -> Node<Msg> {
let StyledRteTableState { let (visible, rows, cols) = values
visible, .table_tooltip
rows, .map(
cols, |StyledRteTableState {
} = values.table_tooltip; visible,
rows,
cols,
}| (*visible, *rows, *cols),
)
.unwrap_or_default();
let on_rows_change = { let on_rows_change = {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
input_ev(Ev::Change, move |v| { input_ev(Ev::Change, move |v| {
v.parse::<u16>() v.parse::<u16>()
.ok() .ok()
.map(|n| Msg::Rte(RteMsg::TableSetRows(n), field_id)) .map(|n| Msg::Rte(field_id, RteMsg::TableSetRows(n)))
}) })
}; };
@ -995,7 +1015,7 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
input_ev(Ev::Change, move |v| { input_ev(Ev::Change, move |v| {
v.parse::<u16>() v.parse::<u16>()
.ok() .ok()
.map(|n| Msg::Rte(RteMsg::TableSetColumns(n), field_id)) .map(|n| Msg::Rte(field_id, RteMsg::TableSetColumns(n)))
}) })
}; };
@ -1006,7 +1026,7 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
.icon(Icon::Close) .icon(Icon::Close)
.on_click(mouse_ev(Ev::Click, move |ev| { .on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::TableSetVisibility(false), field_id)) Some(Msg::Rte(field_id, RteMsg::TableSetVisibility(false)))
})) }))
.build() .build()
.into_node() .into_node()
@ -1016,7 +1036,7 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
mouse_ev(Ev::Click, move |ev| { mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertTable { rows, cols }, field_id)) Some(Msg::Rte(field_id, RteMsg::InsertTable { rows, cols }))
}) })
}; };
@ -1070,9 +1090,16 @@ fn styled_rte_button(title: &str, icon: Icon, handler: EventHandler<Msg>) -> Nod
} }
fn code_tooltip(values: &StyledRte) -> Node<Msg> { fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let StyledRteCodeState { visible, lang, .. } = &values.code_tooltip; let (visible, lang) = values
.code_tooltip
.as_ref()
.map(|StyledRteCodeState { visible, lang, .. }| (*visible, Some(lang)))
.unwrap_or_default();
let languages = values.code_tooltip.languages(); let languages = values
.code_tooltip
.map(|ct| ct.languages().as_slice())
.unwrap_or_default();
let options: Vec<(String, u32)> = languages let options: Vec<(String, u32)> = languages
.iter() .iter()
@ -1081,15 +1108,14 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
.collect(); .collect();
let select_lang_node = StyledSelect::build() let select_lang_node = StyledSelect::build()
.state(lang) .try_state(lang)
.selected( .selected(
lang.values lang.and_then(|l| l.values.get(0))
.get(0)
.and_then(|n| options.get(*n as usize)) .and_then(|n| options.get(*n as usize))
.map(|v| vec![v.to_child()]) .map(|v| vec![v.to_child()])
.unwrap_or_default(), .unwrap_or_default(),
) )
.options(options.into_iter().map(|opt| opt.to_child()).collect()) .options(options.iter().map(|opt| opt.to_child()).collect())
.normal() .normal()
.build(FieldId::Rte(RteField::CodeLang(Box::new( .build(FieldId::Rte(RteField::CodeLang(Box::new(
values.field_id.clone(), values.field_id.clone(),
@ -1103,7 +1129,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
.icon(Icon::Close) .icon(Icon::Close)
.on_click(mouse_ev(Ev::Click, move |ev| { .on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertCode(false), field_id)) Some(Msg::Rte(field_id, RteMsg::InsertCode(false)))
})) }))
.build() .build()
.into_node() .into_node()
@ -1116,7 +1142,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let target = ev.target().unwrap(); let target = ev.target().unwrap();
let textarea = seed::to_textarea(&target); let textarea = seed::to_textarea(&target);
let code = textarea.value(); let code = textarea.value();
Msg::Rte(RteMsg::CodeChanged(code), field_id) Msg::Rte(field_id, RteMsg::CodeChanged(code))
}); });
seed::textarea![on_change] seed::textarea![on_change]
}; };
@ -1126,7 +1152,8 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let on_insert = ev(Ev::Click, move |ev| { let on_insert = ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
Msg::Rte(RteMsg::InjectCode, field_id) ev.target().unwrap();
Msg::Rte(field_id, RteMsg::InjectCode)
}); });
let insert = StyledButton::build() let insert = StyledButton::build()
.on_click(on_insert) .on_click(on_insert)
@ -1138,7 +1165,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
StyledTooltip::build() StyledTooltip::build()
.code_tooltip() .code_tooltip()
.visible(*visible) .visible(visible)
.add_child(h2!["Insert Code", close_tooltip]) .add_child(h2!["Insert Code", close_tooltip])
.add_child(select_lang_node) .add_child(select_lang_node)
.add_child(input) .add_child(input)

View File

@ -25,12 +25,18 @@ impl Default for Variant {
} }
} }
impl Variant {
pub fn to_str<'l>(&self) -> &'l str {
match self {
Variant::Empty => "empty",
Variant::Normal => "normal",
}
}
}
impl std::fmt::Display for Variant { impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.to_str())
Variant::Empty => f.write_str("empty"),
Variant::Normal => f.write_str("normal"),
}
} }
} }
@ -61,34 +67,30 @@ impl StyledSelectState {
} }
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) {
let ref id = self.field_id; let field_id = match msg {
Msg::StyledSelectChanged(field_id, ..) => field_id,
_ => return,
};
if &self.field_id != field_id {
return;
}
match msg { match msg {
Msg::StyledSelectChanged(field_id, StyledSelectChanged::DropDownVisibility(b)) Msg::StyledSelectChanged(_, StyledSelectChanged::DropDownVisibility(b)) => {
if field_id == id =>
{
self.opened = *b; self.opened = *b;
if !self.opened { if !self.opened {
self.text_filter.clear(); self.text_filter.clear();
} }
} }
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Text(text)) Msg::StyledSelectChanged(_, StyledSelectChanged::Text(text)) => {
if field_id == id =>
{
self.text_filter = text.clone(); self.text_filter = text.clone();
} }
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(Some(v))) Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(Some(v))) => {
if field_id == id =>
{
self.values = vec![*v]; self.values = vec![*v];
} }
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None)) Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(None)) => {
if field_id == id =>
{
self.values.clear(); self.values.clear();
} }
Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(v)) Msg::StyledSelectChanged(_, StyledSelectChanged::RemoveMulti(v)) => {
if field_id == id =>
{
let mut old = vec![]; let mut old = vec![];
std::mem::swap(&mut old, &mut self.values); std::mem::swap(&mut old, &mut self.values);
@ -103,28 +105,28 @@ impl StyledSelectState {
} }
} }
pub struct StyledSelect { pub struct StyledSelect<'l> {
id: FieldId, id: FieldId,
variant: Variant, variant: Variant,
dropdown_width: Option<usize>, dropdown_width: Option<usize>,
name: Option<String>, name: Option<&'l str>,
valid: bool, valid: bool,
is_multi: bool, is_multi: bool,
options: Vec<StyledSelectChildBuilder>, options: Vec<StyledSelectChildBuilder<'l>>,
selected: Vec<StyledSelectChildBuilder>, selected: Vec<StyledSelectChildBuilder<'l>>,
text_filter: String, text_filter: &'l str,
opened: bool, opened: bool,
clearable: bool, clearable: bool,
} }
impl ToNode for StyledSelect { impl<'l> ToNode for StyledSelect<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledSelect { impl<'l> StyledSelect<'l> {
pub fn build() -> StyledSelectBuilder { pub fn build() -> StyledSelectBuilder<'l> {
StyledSelectBuilder { StyledSelectBuilder {
variant: None, variant: None,
dropdown_width: None, dropdown_width: None,
@ -141,21 +143,21 @@ impl StyledSelect {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledSelectBuilder { pub struct StyledSelectBuilder<'l> {
variant: Option<Variant>, variant: Option<Variant>,
dropdown_width: Option<usize>, dropdown_width: Option<usize>,
name: Option<String>, name: Option<&'l str>,
valid: Option<bool>, valid: Option<bool>,
is_multi: Option<bool>, is_multi: Option<bool>,
options: Option<Vec<StyledSelectChildBuilder>>, options: Option<Vec<StyledSelectChildBuilder<'l>>>,
selected: Option<Vec<StyledSelectChildBuilder>>, selected: Option<Vec<StyledSelectChildBuilder<'l>>>,
text_filter: Option<String>, text_filter: Option<&'l str>,
opened: Option<bool>, opened: Option<bool>,
clearable: bool, clearable: bool,
} }
impl StyledSelectBuilder { impl<'l> StyledSelectBuilder<'l> {
pub fn build(self, id: FieldId) -> StyledSelect { pub fn build(self, id: FieldId) -> StyledSelect<'l> {
StyledSelect { StyledSelect {
id, id,
variant: self.variant.unwrap_or_default(), variant: self.variant.unwrap_or_default(),
@ -171,7 +173,15 @@ impl StyledSelectBuilder {
} }
} }
pub fn state(self, state: &StyledSelectState) -> Self { pub fn try_state<'state: 'l>(self, state: Option<&'state StyledSelectState>) -> Self {
if let Some(s) = state {
self.state(s)
} else {
self
}
}
pub fn state<'state: 'l>(self, state: &'state StyledSelectState) -> Self {
self.opened(state.opened) self.opened(state.opened)
.text_filter(state.text_filter.as_str()) .text_filter(state.text_filter.as_str())
} }
@ -181,19 +191,13 @@ impl StyledSelectBuilder {
self self
} }
pub fn name<S>(mut self, name: S) -> Self pub fn name(mut self, name: &'l str) -> Self {
where self.name = Some(name);
S: Into<String>,
{
self.name = Some(name.into());
self self
} }
pub fn text_filter<S>(mut self, text_filter: S) -> Self pub fn text_filter(mut self, text_filter: &'l str) -> Self {
where self.text_filter = Some(text_filter);
S: Into<String>,
{
self.text_filter = Some(text_filter.into());
self self
} }
@ -207,12 +211,12 @@ impl StyledSelectBuilder {
self self
} }
pub fn options(mut self, options: Vec<StyledSelectChildBuilder>) -> Self { pub fn options(mut self, options: Vec<StyledSelectChildBuilder<'l>>) -> Self {
self.options = Some(options); self.options = Some(options);
self self
} }
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder>) -> Self { pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder<'l>>) -> Self {
self.selected = Some(selected); self.selected = Some(selected);
self self
} }
@ -301,7 +305,7 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
let children: Vec<Node<Msg>> = options let children: Vec<Node<Msg>> = options
.into_iter() .into_iter()
.filter(|o| !selected.contains(&o) && o.match_text(text_filter.as_str())) .filter(|o| !selected.contains(&o) && o.match_text(text_filter))
.map(|child| { .map(|child| {
let child = child.build(DisplayType::SelectOption); let child = child.build(DisplayType::SelectOption);
let value = child.value(); let value = child.value();

View File

@ -1,3 +1,5 @@
use std::borrow::Cow;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::shared::styled_select::Variant; use crate::shared::styled_select::Variant;
@ -9,18 +11,18 @@ pub enum DisplayType {
SelectValue, SelectValue,
} }
pub struct StyledSelectChild { pub struct StyledSelectChild<'l> {
name: Option<String>, name: Option<&'l str>,
icon: Option<Node<Msg>>, icon: Option<Node<Msg>>,
text: Option<String>, text: Option<std::borrow::Cow<'l, str>>,
display_type: DisplayType, display_type: DisplayType,
value: u32, value: u32,
class_list: Vec<String>, class_list: Vec<std::borrow::Cow<'l, str>>,
variant: Variant, variant: Variant,
} }
impl StyledSelectChild { impl<'l> StyledSelectChild<'l> {
pub fn build() -> StyledSelectChildBuilder { pub fn build() -> StyledSelectChildBuilder<'l> {
StyledSelectChildBuilder { StyledSelectChildBuilder {
icon: None, icon: None,
text: None, text: None,
@ -37,47 +39,46 @@ impl StyledSelectChild {
} }
} }
impl ToNode for StyledSelectChild { impl<'l> ToNode for StyledSelectChild<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledSelectChildBuilder { pub struct StyledSelectChildBuilder<'l> {
icon: Option<Node<Msg>>, icon: Option<Node<Msg>>,
text: Option<String>, text: Option<std::borrow::Cow<'l, str>>,
name: Option<String>, name: Option<&'l str>,
value: u32, value: u32,
class_list: Vec<String>, class_list: Vec<std::borrow::Cow<'l, str>>,
variant: Variant, variant: Variant,
} }
impl PartialEq for StyledSelectChildBuilder { impl<'l> PartialEq for StyledSelectChildBuilder<'l> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.value == other.value self.value == other.value
} }
} }
impl StyledSelectChildBuilder { impl<'l> StyledSelectChildBuilder<'l> {
pub fn icon(mut self, icon: Node<Msg>) -> Self { pub fn icon(mut self, icon: Node<Msg>) -> Self {
self.icon = Some(icon); self.icon = Some(icon);
self self
} }
pub fn text<S>(mut self, text: S) -> Self pub fn text<'m: 'l>(mut self, text: &'m str) -> Self {
where self.text = Some(std::borrow::Cow::Borrowed(text));
S: Into<String>,
{
self.text = Some(text.into());
self self
} }
pub fn name<S>(mut self, name: S) -> Self pub fn text_owned(mut self, text: String) -> Self {
where self.text = Some(std::borrow::Cow::Owned(text));
S: Into<String>, self
{ }
self.name = Some(name.into());
pub fn name(mut self, name: &'l str) -> Self {
self.name = Some(name);
self self
} }
@ -93,15 +94,12 @@ impl StyledSelectChildBuilder {
.unwrap_or(true) .unwrap_or(true)
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class<'m: 'l>(mut self, name: &'m str) -> Self {
where self.class_list.push(Cow::Borrowed(name));
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
pub fn build(self, display_type: DisplayType) -> StyledSelectChild { pub fn build(self, display_type: DisplayType) -> StyledSelectChild<'l> {
StyledSelectChild { StyledSelectChild {
name: self.name, name: self.name,
icon: self.icon, icon: self.icon,
@ -121,45 +119,27 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
text, text,
display_type, display_type,
value: _, value: _,
mut class_list, class_list,
variant, variant,
} = values; } = values;
class_list.push(format!("{}", variant));
let label_class = match display_type { let label_class = match display_type {
DisplayType::SelectOption => vec![ DisplayType::SelectOption => vec![
"optionLabel".to_string(), "optionLabel",
variant.to_string(), variant.to_str(),
name.as_ref().cloned().unwrap_or_default(), name.as_deref().unwrap_or_default(),
name.as_ref()
.map(|s| format!("{}Label", s))
.unwrap_or_default(),
class_list.join(" "),
], ],
DisplayType::SelectValue => vec![ DisplayType::SelectValue => vec![
"selectItemLabel".to_string(), "selectItemLabel",
variant.to_string(), variant.to_str(),
name.as_ref().cloned().unwrap_or_default(), name.as_deref().unwrap_or_default(),
name.as_ref()
.map(|s| format!("{}Label", s))
.unwrap_or_default(),
class_list.join(" "),
], ],
} }
.join(" "); .join(" ");
let wrapper_class = match display_type { let wrapper_class = match display_type {
DisplayType::SelectOption => vec![ DisplayType::SelectOption => vec!["optionItem", name.as_deref().unwrap_or_default()],
"optionItem".to_string(), DisplayType::SelectValue => vec!["selectItem", name.as_deref().unwrap_or_default()],
name.as_ref().cloned().unwrap_or_default(),
class_list.join(" "),
],
DisplayType::SelectValue => vec![
"selectItem".to_string(),
name.as_ref().cloned().unwrap_or_default(),
class_list.join(" "),
],
} }
.join(" "); .join(" ");
@ -169,19 +149,32 @@ pub fn render(values: StyledSelectChild) -> Node<Msg> {
}; };
let label_node = match text { let label_node = match text {
Some(text) => div![class![label_class.as_str()], text], Some(text) => div![
attrs![
At::Class => name.as_deref().map(|s| format!("{}Label", s)).unwrap_or_default(),
At::Class => class_list.join(" "),
],
class![label_class.as_str()],
text
],
_ => empty![], _ => empty![],
}; };
div![class![wrapper_class.as_str()], icon_node, label_node] div![
class![variant.to_str()],
class![wrapper_class.as_str()],
attrs![At::Class => class_list.join(" ")],
icon_node,
label_node
]
} }
impl ToChild for jirs_data::User { impl<'l> ToChild<'l> for jirs_data::User {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let avatar = crate::shared::styled_avatar::StyledAvatar::build() let avatar = crate::shared::styled_avatar::StyledAvatar::build()
.avatar_url(self.avatar_url.as_ref().cloned().unwrap_or_default()) .avatar_url(self.avatar_url.as_deref().unwrap_or_default())
.size(20) .size(20)
.name(self.name.as_str()) .name(self.name.as_str())
.build() .build()
@ -193,110 +186,106 @@ impl ToChild for jirs_data::User {
} }
} }
impl ToChild for jirs_data::IssuePriority { impl<'l> ToChild<'l> for jirs_data::IssuePriority {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> StyledSelectChildBuilder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into()) let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
.add_class(self.to_string()) .add_class(self.to_str())
.build() .build()
.into_node(); .into_node();
let text = self.to_string(); let text = self.to_str();
StyledSelectChild::build() StyledSelectChild::build()
.icon(icon) .icon(icon)
.value(self.clone().into()) .value(self.clone().into())
.text(text) .text(text)
.add_class(format!("{}", self)) .add_class(self.to_str())
} }
} }
impl ToChild for jirs_data::IssueStatus { impl<'l> ToChild<'l> for jirs_data::IssueStatus {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> StyledSelectChildBuilder {
let text = &self.name;
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChild::build()
.value(self.id as u32) .value(self.id as u32)
.add_class(text) .add_class(self.name.as_str())
.text(text.as_str()) .text(self.name.as_str())
} }
} }
impl ToChild for jirs_data::IssueType { impl<'l> ToChild<'l> for jirs_data::IssueType {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> StyledSelectChildBuilder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let name = self.to_label().to_owned(); let name = self.to_label();
let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into()) let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
.add_class(name.as_str()) .add_class(name)
.build() .build()
.into_node(); .into_node();
StyledSelectChild::build() StyledSelectChild::build()
.add_class(name.as_str()) .add_class(name)
.text(name) .text(name)
.icon(type_icon) .icon(type_icon)
.value(self.clone().into()) .value(self.clone().into())
} }
} }
impl ToChild for jirs_data::ProjectCategory { impl<'l> ToChild<'l> for jirs_data::ProjectCategory {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> StyledSelectChildBuilder {
let name = self.to_string();
fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChild::build()
.add_class(name.as_str()) .add_class(self.to_str())
.text(name) .text(self.to_str())
.value(self.clone().into()) .value(self.clone().into())
} }
} }
impl ToChild for jirs_data::UserRole { impl<'l> ToChild<'l> for jirs_data::UserRole {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> StyledSelectChildBuilder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let name = self.to_string(); let name = self.to_str();
StyledSelectChild::build() StyledSelectChild::build()
.add_class(name.as_str()) .add_class(name)
.add_class("capitalize") .add_class("capitalize")
.text(name) .text(name)
.value(self.clone().into()) .value(self.clone().into())
} }
} }
impl ToChild for jirs_data::Project { impl<'l> ToChild<'l> for jirs_data::Project {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChild::build()
.text(self.name.as_str()) .text(self.name.as_str())
.value(self.id as u32) .value(self.id as u32)
} }
} }
impl ToChild for jirs_data::Epic { impl<'l> ToChild<'l> for jirs_data::Epic {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChild::build()
.text(self.name.as_str()) .text(self.name.as_str())
.value(self.id as u32) .value(self.id as u32)
} }
} }
impl ToChild for u32 { impl<'l> ToChild<'l> for u32 {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
let name = self.to_string(); let name = stringify!(self);
StyledSelectChild::build() StyledSelectChild::build()
.add_class(name.as_str()) .add_class(name)
.text(name) .text(name)
.value(*self) .value(*self)
} }
@ -305,10 +294,10 @@ impl ToChild for u32 {
pub type Label = String; pub type Label = String;
pub type Value = u32; pub type Value = u32;
impl ToChild for (Label, Value) { impl<'l> ToChild<'l> for (Label, Value) {
type Builder = StyledSelectChildBuilder; type Builder = StyledSelectChildBuilder<'l>;
fn to_child(&self) -> Self::Builder { fn to_child<'m: 'l>(&'m self) -> Self::Builder {
StyledSelectChild::build() StyledSelectChild::build()
.text(self.0.as_str()) .text(self.0.as_str())
.value(self.1) .value(self.1)

View File

@ -4,31 +4,31 @@ use crate::shared::ToNode;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
#[derive(Debug)] #[derive(Debug)]
pub struct StyledTextarea { pub struct StyledTextarea<'l> {
id: FieldId, id: FieldId,
height: usize, height: usize,
max_height: usize, max_height: usize,
value: String, value: &'l str,
class_list: Vec<String>, class_list: Vec<&'l str>,
update_event: Ev, update_event: Ev,
placeholder: Option<String>, placeholder: Option<&'l str>,
disable_auto_resize: bool, disable_auto_resize: bool,
} }
impl ToNode for StyledTextarea { impl<'l> ToNode for StyledTextarea<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledTextarea { impl<'l> StyledTextarea<'l> {
pub fn build(field_id: FieldId) -> StyledTextareaBuilder { pub fn build(field_id: FieldId) -> StyledTextareaBuilder<'l> {
StyledTextareaBuilder { StyledTextareaBuilder {
id: field_id, id: field_id,
height: None, height: None,
max_height: None, max_height: None,
on_change: None, on_change: None,
value: "".to_string(), value: "",
class_list: vec![], class_list: vec![],
update_event: None, update_event: None,
placeholder: None, placeholder: None,
@ -38,19 +38,19 @@ impl StyledTextarea {
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledTextareaBuilder { pub struct StyledTextareaBuilder<'l> {
id: FieldId, id: FieldId,
height: Option<usize>, height: Option<usize>,
max_height: Option<usize>, max_height: Option<usize>,
on_change: Option<EventHandler<Msg>>, on_change: Option<EventHandler<Msg>>,
value: String, value: &'l str,
class_list: Vec<String>, class_list: Vec<&'l str>,
update_event: Option<Ev>, update_event: Option<Ev>,
placeholder: Option<String>, placeholder: Option<&'l str>,
disable_auto_resize: bool, disable_auto_resize: bool,
} }
impl StyledTextareaBuilder { impl<'l> StyledTextareaBuilder<'l> {
#[inline] #[inline]
pub fn height(mut self, height: usize) -> Self { pub fn height(mut self, height: usize) -> Self {
self.height = Some(height); self.height = Some(height);
@ -64,20 +64,14 @@ impl StyledTextareaBuilder {
} }
#[inline] #[inline]
pub fn value<S>(mut self, value: S) -> Self pub fn value(mut self, value: &'l str) -> Self {
where self.value = value;
S: Into<String>,
{
self.value = value.into();
self self
} }
#[inline] #[inline]
pub fn add_class<S>(mut self, value: S) -> Self pub fn add_class(mut self, value: &'l str) -> Self {
where self.class_list.push(value);
S: Into<String>,
{
self.class_list.push(value.into());
self self
} }
@ -86,11 +80,8 @@ impl StyledTextareaBuilder {
self self
} }
pub fn placeholder<S>(mut self, placeholder: S) -> Self pub fn placeholder(mut self, placeholder: &'l str) -> Self {
where self.placeholder = Some(placeholder);
S: Into<String>,
{
self.placeholder = Some(placeholder.into());
self self
} }
@ -100,7 +91,7 @@ impl StyledTextareaBuilder {
} }
#[inline] #[inline]
pub fn build(self) -> StyledTextarea { pub fn build(self) -> StyledTextarea<'l> {
StyledTextarea { StyledTextarea {
id: self.id, id: self.id,
value: self.value, value: self.value,
@ -140,7 +131,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
} = values; } = values;
let mut style_list = vec![]; let mut style_list = vec![];
let min_height = get_min_height(value.as_str(), height as f64, disable_auto_resize); let min_height = get_min_height(value, height as f64, disable_auto_resize);
if min_height > 0f64 { if min_height > 0f64 {
style_list.push(format!("min-height: {}px", min_height)); style_list.push(format!("min-height: {}px", min_height));
} }
@ -197,7 +188,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
) )
}); });
class_list.push("textAreaInput".to_string()); class_list.push("textAreaInput");
div![ div![
attrs![At::Class => "styledTextArea"], attrs![At::Class => "styledTextArea"],

View File

@ -18,56 +18,59 @@ impl Default for Variant {
} }
} }
impl std::fmt::Display for Variant { impl Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { pub fn to_str(&self) -> &'static str {
match self { match self {
Variant::About => f.write_str("about"), Variant::About => "about",
Variant::Messages => f.write_str("messages"), Variant::Messages => "messages",
Variant::TableBuilder => f.write_str("tableTooltip"), Variant::TableBuilder => "tableTooltip",
Variant::CodeBuilder => f.write_str("codeTooltip"), Variant::CodeBuilder => "codeTooltip",
Variant::DateTimeBuilder => f.write_str("dateTimeTooltip"), Variant::DateTimeBuilder => "dateTimeTooltip",
} }
} }
} }
pub struct StyledTooltip { impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.to_str())
}
}
pub struct StyledTooltip<'l> {
visible: bool, visible: bool,
class_name: String, class_list: Vec<&'l str>,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
variant: Variant, variant: Variant,
} }
impl ToNode for StyledTooltip { impl<'l> ToNode for StyledTooltip<'l> {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
impl StyledTooltip { impl<'l> StyledTooltip<'l> {
pub fn build() -> StyledTooltipBuilder { pub fn build() -> StyledTooltipBuilder<'l> {
StyledTooltipBuilder::default() StyledTooltipBuilder::default()
} }
} }
#[derive(Default)] #[derive(Default)]
pub struct StyledTooltipBuilder { pub struct StyledTooltipBuilder<'l> {
visible: bool, visible: bool,
class_list: Vec<String>, class_list: Vec<&'l str>,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
variant: Variant, variant: Variant,
} }
impl StyledTooltipBuilder { impl<'l> StyledTooltipBuilder<'l> {
pub fn visible(mut self, b: bool) -> Self { pub fn visible(mut self, b: bool) -> Self {
self.visible = b; self.visible = b;
self self
} }
pub fn add_class<S>(mut self, name: S) -> Self pub fn add_class(mut self, name: &'l str) -> Self {
where self.class_list.push(name);
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }
@ -101,10 +104,10 @@ impl StyledTooltipBuilder {
self self
} }
pub fn build(self) -> StyledTooltip { pub fn build(self) -> StyledTooltip<'l> {
StyledTooltip { StyledTooltip {
visible: self.visible, visible: self.visible,
class_name: self.class_list.join(" "), class_list: self.class_list,
children: self.children, children: self.children,
variant: self.variant, variant: self.variant,
} }
@ -114,13 +117,13 @@ impl StyledTooltipBuilder {
pub fn render(values: StyledTooltip) -> Node<Msg> { pub fn render(values: StyledTooltip) -> Node<Msg> {
let StyledTooltip { let StyledTooltip {
visible, visible,
class_name, class_list,
children, children,
variant, variant,
} = values; } = values;
if visible { if visible {
div![ div![
attrs![At::Class => format!("styledTooltip {} {}", class_name, variant)], attrs![At::Class => format!("styledTooltip {} {}", class_list.join(" "), variant)],
children children
] ]
} else { } else {

View File

@ -44,18 +44,14 @@ pub fn view(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let roles = UserRole::ordered();
let user_role = StyledSelect::build() let user_role = StyledSelect::build()
.name("user_role") .name("user_role")
.valid(true) .valid(true)
.normal() .normal()
.state(&page.user_role_state) .state(&page.user_role_state)
.selected(vec![page.user_role.to_child()]) .selected(vec![page.user_role.to_child()])
.options( .options(roles.iter().map(|role| role.to_child()).collect())
UserRole::ordered()
.into_iter()
.map(|role| role.to_child())
.collect(),
)
.build(FieldId::Users(UsersFieldId::UserRole)) .build(FieldId::Users(UsersFieldId::UserRole))
.into_node(); .into_node();
let user_role_field = StyledField::build() let user_role_field = StyledField::build()

View File

@ -42,9 +42,12 @@ pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>, orders: &mut impl Orders<
return; return;
} }
}; };
// orders.perform_cmd(seed::app::cmds::timeout(10, move || {
// let ws = ws.clone();
let binary = bincode::serialize(&msg).unwrap(); let binary = bincode::serialize(&msg).unwrap();
ws.send_bytes(binary.as_slice()) ws.send_bytes(binary.as_slice())
.expect("Failed to send ws msg"); .expect("Failed to send ws msg");
// }));
} }
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -64,10 +67,21 @@ pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
let url = model.ws_url.as_str(); let url = model.ws_url.as_str();
model.ws = WebSocket::builder(url, orders) model.ws = WebSocket::builder(url, orders)
.on_message(|msg| Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(msg))) .on_message(|msg| {
.on_open(|| Msg::WebSocketChange(WebSocketChanged::WebSocketOpened)) Some(Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(
.on_close(|_| Msg::WebSocketChange(WebSocketChanged::WebSocketClosed)) msg,
.on_error(|| {}) )))
})
.on_open(|| {
log!("open_socket opened");
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
})
.on_close(|_| Some(Msg::WebSocketChange(WebSocketChanged::WebSocketClosed)))
.on_error(|| {
error!("Failed to open WebSocket");
None as Option<Msg>
})
.protocols(&["jirs"])
.build_and_open() .build_and_open()
.ok(); .ok();
} }

View File

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

View File

@ -101,13 +101,19 @@ impl Into<IssueType> for u32 {
} }
} }
impl IssueType {
pub fn to_str(&self) -> &'static str {
match self {
IssueType::Task => "task",
IssueType::Bug => "bug",
IssueType::Story => "story",
}
}
}
impl std::fmt::Display for IssueType { impl std::fmt::Display for IssueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.to_str())
IssueType::Task => f.write_str("task"),
IssueType::Bug => f.write_str("bug"),
IssueType::Story => f.write_str("story"),
}
} }
} }
@ -157,15 +163,21 @@ impl Default for IssuePriority {
} }
} }
impl IssuePriority {
pub fn to_str(&self) -> &'static str {
match self {
IssuePriority::Highest => "highest",
IssuePriority::High => "high",
IssuePriority::Medium => "medium",
IssuePriority::Low => "low",
IssuePriority::Lowest => "lowest",
}
}
}
impl std::fmt::Display for IssuePriority { impl std::fmt::Display for IssuePriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.to_str())
IssuePriority::Highest => f.write_str("highest"),
IssuePriority::High => f.write_str("high"),
IssuePriority::Medium => f.write_str("medium"),
IssuePriority::Low => f.write_str("low"),
IssuePriority::Lowest => f.write_str("lowest"),
}
} }
} }
@ -245,13 +257,19 @@ impl Default for UserRole {
} }
} }
impl UserRole {
pub fn to_str<'l>(&self) -> &'l str {
match self {
UserRole::User => "user",
UserRole::Manager => "manager",
UserRole::Owner => "owner",
}
}
}
impl std::fmt::Display for UserRole { impl std::fmt::Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.to_str())
UserRole::User => f.write_str("user"),
UserRole::Manager => f.write_str("manager"),
UserRole::Owner => f.write_str("owner"),
}
} }
} }
@ -316,13 +334,19 @@ impl Default for ProjectCategory {
} }
} }
impl ProjectCategory {
pub fn to_str<'l>(&self) -> &'l str {
match self {
ProjectCategory::Software => "software",
ProjectCategory::Marketing => "marketing",
ProjectCategory::Business => "business",
}
}
}
impl std::fmt::Display for ProjectCategory { impl std::fmt::Display for ProjectCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { f.write_str(self.to_str())
ProjectCategory::Software => f.write_str("software"),
ProjectCategory::Marketing => f.write_str("marketing"),
ProjectCategory::Business => f.write_str("business"),
}
} }
} }

View File

@ -26,7 +26,6 @@ default = [
] ]
[dependencies] [dependencies]
serde = { version = "*", features = ["derive"] }
actix = { version = "*" } actix = { version = "*" }
actix-web = { version = "*" } actix-web = { version = "*" }
actix-cors = { version = "*" } actix-cors = { version = "*" }
@ -35,14 +34,14 @@ actix-rt = "1"
actix-web-actors = "*" actix-web-actors = "*"
actix-multipart = { version = "*" } actix-multipart = { version = "*" }
dotenv = { version = "*" }
byteorder = "1.0"
chrono = { version = "0.4", features = [ "serde" ] }
libc = { version = "0.2.0" }
pq-sys = { version = ">=0.3.0, <0.5.0" } pq-sys = { version = ">=0.3.0, <0.5.0" }
serde_json = { version = ">=0.8.0, <2.0" } r2d2 = { version = ">= 0.8, < 0.9" }
toml = "0.5.6"
bincode = "1.2.1" dotenv = { version = "*" }
byteorder = "1.0"
chrono = { version = "0.4", features = ["serde"] }
libc = { version = "0.2.0" }
time = { version = "0.1" } time = { version = "0.1" }
url = { version = "2.1.0" } url = { version = "2.1.0" }
percent-encoding = { version = "2.1.0" } percent-encoding = { version = "2.1.0" }
@ -53,14 +52,23 @@ num-traits = { version = "0.2" }
num-integer = { version = "0.1.32" } num-integer = { version = "0.1.32" }
bigdecimal = { version = ">= 0.0.10, <= 0.1.0" } bigdecimal = { version = ">= 0.0.10, <= 0.1.0" }
bitflags = { version = "1.0" } bitflags = { version = "1.0" }
r2d2 = { version = ">= 0.8, < 0.9" }
serde = { version = "*", features = ["derive"] }
serde_json = { version = ">=0.8.0, <2.0" }
toml = "0.5.6"
bincode = "1.2.1"
log = "0.4" log = "0.4"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
env_logger = "0.7" env_logger = "0.7"
async-trait = { version = "*" }
futures = { version = "*" } futures = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] }
lettre = { version = "*" } lettre = { version = "*" }
lettre_email = { version = "*" } lettre_email = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] }
[dependencies.diesel] [dependencies.diesel]
version = "1.4.4" version = "1.4.4"
@ -82,4 +90,4 @@ version = "0.43.0"
# Local storage # Local storage
[dependencies.actix-files] [dependencies.actix-files]
optional = true optional = true
version = "0.2.1" version = "*"

View File

@ -53,7 +53,7 @@ where
} }
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
let pool: Db = match req.app_data() { let pool: &Db = match req.app_data::<Db>() {
Some(d) => d, Some(d) => d,
_ => { _ => {
return async move { return async move {
@ -66,7 +66,7 @@ where
} }
}; };
match check_token(req.headers(), pool) { match check_token(req.headers(), pool.clone()) {
std::result::Result::Err(e) => { std::result::Result::Err(e) => {
return async move { return async move {
let res = e.into_http_response().into_body(); let res = e.into_http_response().into_body();

View File

@ -82,7 +82,7 @@ impl WsMessageSender for ws::WebsocketContext<WebSocketActor> {
impl Handler<InnerMsg> for WebSocketActor { impl Handler<InnerMsg> for WebSocketActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: InnerMsg, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: InnerMsg, ctx: &mut <Self as Actor>::Context) -> Self::Result {
if let InnerMsg::Transfer(msg) = msg { if let InnerMsg::Transfer(msg) = msg {
ctx.send_msg(&msg) ctx.send_msg(&msg)
}; };
@ -252,10 +252,10 @@ impl WebSocketActor {
} }
fn require_user(&self) -> Result<&User, WsMsg> { fn require_user(&self) -> Result<&User, WsMsg> {
self.current_user.as_ref().map(|u| u).ok_or_else(|| { self.current_user
let _x = 1; .as_ref()
WsMsg::AuthorizeExpired .map(|u| u)
}) .ok_or_else(|| WsMsg::AuthorizeExpired)
} }
fn require_user_project(&self) -> Result<&UserProject, WsMsg> { fn require_user_project(&self) -> Result<&UserProject, WsMsg> {
@ -300,7 +300,11 @@ impl WebSocketActor {
} }
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor { impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) { fn handle(
&mut self,
msg: Result<ws::Message, ws::ProtocolError>,
ctx: &mut <Self as Actor>::Context,
) {
match msg { match msg {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg), Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
Ok(ws::Message::Text(text)) => ctx.text(text), Ok(ws::Message::Text(text)) => ctx.text(text),
@ -322,7 +326,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
} }
} }
fn finished(&mut self, ctx: &mut Self::Context) { fn finished(&mut self, ctx: &mut <Self as Actor>::Context) {
info!("Disconnected"); info!("Disconnected");
if let (Some(user), Some(up)) = ( if let (Some(user), Some(up)) = (
self.current_user.as_ref(), self.current_user.as_ref(),
@ -380,7 +384,7 @@ impl Actor for WsServer {
impl Handler<InnerMsg> for WsServer { impl Handler<InnerMsg> for WsServer {
type Result = (); type Result = ();
fn handle(&mut self, msg: InnerMsg, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: InnerMsg, _ctx: &mut <Self as Actor>::Context) -> Self::Result {
debug!("receive {:?}", msg); debug!("receive {:?}", msg);
match msg { match msg {
InnerMsg::Join(project_id, user_id, recipient) => { InnerMsg::Join(project_id, user_id, recipient) => {
@ -408,8 +412,12 @@ impl Handler<InnerMsg> for WsServer {
self.sessions.remove(&user_id); self.sessions.remove(&user_id);
} else { } else {
let v = self.sessions.entry(user_id).or_insert_with(Vec::new); let v = self.sessions.entry(user_id).or_insert_with(Vec::new);
if v.remove_item(&recipient).is_none() { let mut old = vec![];
debug!("Can't remove recipient"); std::mem::swap(&mut old, v);
for r in old {
if r != recipient {
v.push(r);
}
} }
} }
} }

View File