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-file = "../LICENSE"
[package.metadata.wasm-pack.profile.dev]
wasm-opt = false
[[bin]]
name = "print"
path = "./src/print.rs"
[lib]
crate-type = ["cdylib", "rlib"]
name = "jirs_client"
@ -23,21 +30,29 @@ opt-level = 's'
[dependencies]
jirs-data = { path = "../jirs-data" }
wee_alloc = "*"
seed = { version = "0.7.0" }
serde = "*"
bincode = "1.2.1"
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
serde = { version = "*" }
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"] }
futures = "^0.1.26"
comrak = "*"
wee_alloc = "*"
num-traits = { version = "*" }
lazy_static = "*"
syntect = { version = "*", default-features = false, features = ["html", "regex-fancy", "dump-load-rs"] }
[dependencies.wasm-bindgen]
version = "0.2.66"
version = "*"
features = ["enable-interning"]
[dependencies.js-sys]
@ -45,7 +60,7 @@ version = "*"
default-features = false
[dependencies.web-sys]
version = "0.3.22"
version = "*"
default-features = false
features = [
# elements

View File

@ -9,6 +9,32 @@
<link rel="stylesheet" type="text/css" href="/styles.css">
</head>
<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>
</html>

View File

@ -8,9 +8,65 @@
<title>JIRS</title>
<link href="/styles.css" rel="stylesheet" type="text/css">
<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>
<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>
</body>
</html>

View File

@ -1,18 +1,9 @@
#!/usr/bin/env bash
PROJECT_ROOT=$(git rev-parse --show-toplevel)
CLIENT_ROOT=${PROJECT_ROOT}/jirs-client
cd ${CLIENT_ROOT}
. .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
cargo watch -s ${CLIENT_ROOT}/scripts/run-wasm-pack.sh -w ${CLIENT_ROOT}/src -w ${CLIENT_ROOT}/Cargo.toml -w ./static -w js

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]
pub fn hi_code(&mut self, lang: &str, code: &str) -> String {
let syntax = match crate::hi::SYNTAX_SET.find_syntax_by_name(lang) {
Some(s) => s,
_ => {
return code.to_string();
let syntax = {
match crate::hi::syntax_set::load().find_syntax_by_name(lang) {
Some(s) => s.clone(),
_ => {
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() {
buffer.push_str(self.hi(syntax, line).as_str());
buffer.push_str("<br />");
self.hi(&syntax, line, &mut buffer);
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 tokens = h.highlight(line, &crate::hi::SYNTAX_SET);
let tokens = { h.highlight(line, &crate::hi::syntax_set::load()) };
let parts: Vec<String> = tokens
.into_iter()
.map(|(style, token)| {
let Style {
foreground: f,
background: b,
font_style,
} = style;
let fs = if font_style == FontStyle::BOLD {
"font-weight: bold"
} else if font_style == FontStyle::ITALIC {
"font-style: italic"
} else if font_style == FontStyle::UNDERLINE {
"text-decoration: underline"
} else {
""
};
let f = format!("rgba({}, {}, {}, {})", f.r, f.g, f.b, f.a);
let b = format!("rgba({}, {}, {}, {})", b.r, b.g, b.b, b.a);
format!(
r#"<span style="color: {f};background:{b}; {fs}">{t}</span>"#,
t = if token.is_empty() { "&nbsp;" } else { token },
f = f,
b = b,
fs = fs
)
})
.collect();
parts.join("")
for (style, token) in tokens.into_iter() {
let Style {
foreground: f,
background: b,
font_style,
} = style;
let fs = if font_style == FontStyle::BOLD {
"font-weight: bold"
} else if font_style == FontStyle::ITALIC {
"font-style: italic"
} else if font_style == FontStyle::UNDERLINE {
"text-decoration: underline"
} else {
""
};
buffer.push(format!(
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>"#,
t = if token.is_empty() { "&nbsp;" } else { token },
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,
fs = fs
));
}
}
}
@ -91,6 +87,7 @@ pub fn define() {
const lang = this.getAttribute('lang') || '';
setTimeout(() => {{
const code = (this.innerHTML || '').trim();
console.log('connected');
shadow.querySelector('#view').innerHTML = runtime.hi_code(lang, code);
}}, 1);
"#,

View File

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

View File

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

View File

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

View File

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

View File

@ -8,16 +8,16 @@ use uuid::Uuid;
use jirs_data::*;
use crate::modal::time_tracking::value_for_time_tracking;
use crate::shared::drag::DragState;
use crate::shared::styled_checkbox::StyledCheckboxState;
use crate::shared::styled_date_time_input::StyledDateTimeInputState;
use crate::shared::styled_editor::Mode;
use crate::shared::styled_image_input::StyledImageInputState;
use crate::shared::styled_input::StyledInputState;
use crate::shared::styled_rte::StyledRteState;
use crate::shared::styled_select::StyledSelectState;
use crate::{EditIssueModalSection, FieldId, Msg, ProjectFieldId};
use crate::{
modal::time_tracking::value_for_time_tracking,
shared::{
drag::DragState, styled_checkbox::StyledCheckboxState,
styled_date_time_input::StyledDateTimeInputState, styled_editor::Mode,
styled_image_input::StyledImageInputState, styled_input::StyledInputState,
styled_rte::StyledRteState, styled_select::StyledSelectState,
},
EditIssueModalSection, FieldId, Msg, ProjectFieldId,
};
pub trait IssueModal {
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> {
let button = StyledButton::build()
.secondary()
.text("Github Repo".to_string())
.text("Github Repo")
.icon(Icon::Github)
.build()
.into_node();
@ -124,7 +124,7 @@ fn avatars_filters(model: &Model) -> Node<Msg> {
class_list.push("isActive");
}
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 |_| {
Msg::ProjectAvatarFilterChanged(user_id, active)
}))
@ -270,7 +270,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
StyledAvatar::build()
.size(24)
.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)
.build()
.into_node()
@ -279,10 +279,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
let issue_type_icon = {
StyledIcon::build(issue.issue_type.clone().into())
.add_style(format!(
"color: var(--{issue_type})",
issue_type = issue.issue_type.to_string()
))
.with_color(issue.issue_type.to_str())
.build()
.into_node()
};
@ -292,7 +289,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
_ => Icon::ArrowUp,
};
StyledIcon::build(icon)
.add_style(format!("color: var(--{})", issue.priority))
.with_color(issue.priority.to_str())
.build()
.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::{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_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_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt");
pub fn view(model: &model::Model) -> Node<Msg> {
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
fn name_field(page: &ProjectSettingsPage) -> Node<Msg> {
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)
.max_height(39)
.disable_auto_resize()
@ -126,7 +126,7 @@ fn url_field(page: &ProjectSettingsPage) -> Node<Msg> {
.height(39)
.max_height(39)
.disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default())
.value(page.payload.url.as_deref().unwrap_or_default())
.build()
.into_node();
StyledField::build()
@ -161,17 +161,13 @@ fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
/// Build project category dropdown with styled field wrapper
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
let project_categories = ProjectCategory::ordered();
let category = StyledSelect::build()
.opened(page.project_category_state.opened)
.text_filter(page.project_category_state.text_filter.as_str())
.valid(true)
.normal()
.options(
ProjectCategory::ordered()
.into_iter()
.map(|c| c.to_child())
.collect(),
)
.options(project_categories.iter().map(|c| c.to_child()).collect())
.selected(vec![page
.payload
.category

View File

@ -14,8 +14,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
vec![
WsMsg::UserProjectsLoad,
WsMsg::ProjectsLoad,
WsMsg::MessagesLoad,
WsMsg::ProjectUsersLoad,
WsMsg::MessagesLoad,
],
model.ws.as_ref(),
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> {
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() {
class_list.push("notAllowed".to_string());
class_list.push("notAllowed");
};
if Some(model.page) == page {
class_list.push("active".to_string());
class_list.push("active");
}
let icon_node = StyledIcon::build(icon).build().into_node();
li![
attrs![At::Class => class_list.join(" ")],
class!["linkItem"],
class![icon.to_str()],
a![
attrs![At::Href => path],
icon_node,

View File

@ -31,10 +31,10 @@ pub mod styled_textarea;
pub mod styled_tooltip;
pub mod tracking_widget;
pub trait ToChild {
type Builder;
pub trait ToChild<'l> {
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>) {
@ -51,7 +51,7 @@ pub fn go_to(url: &str) {
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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,14 +30,14 @@ impl StyledImageInputState {
}
}
pub struct StyledImageInput {
pub struct StyledImageInput<'l> {
id: FieldId,
class_list: Vec<String>,
class_list: Vec<&'l str>,
url: Option<String>,
}
impl StyledImageInput {
pub fn build(field_id: FieldId) -> StyledInputInputBuilder {
impl<'l> StyledImageInput<'l> {
pub fn build(field_id: FieldId) -> StyledInputInputBuilder<'l> {
StyledInputInputBuilder {
id: field_id,
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> {
render(self)
}
}
pub struct StyledInputInputBuilder {
pub struct StyledInputInputBuilder<'l> {
id: FieldId,
class_list: Vec<String>,
class_list: Vec<&'l str>,
url: Option<String>,
}
impl StyledInputInputBuilder {
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
impl<'l> StyledInputInputBuilder<'l> {
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
@ -72,7 +69,7 @@ impl StyledInputInputBuilder {
self
}
pub fn build(self) -> StyledImageInput {
pub fn build(self) -> StyledImageInput<'l> {
StyledImageInput {
id: self.id,
class_list: self.class_list,

View File

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

View File

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

View File

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

View File

@ -26,8 +26,8 @@ impl HeadingSize {
}
}
impl ToString for HeadingSize {
fn to_string(&self) -> String {
impl HeadingSize {
fn as_str<'l>(&self) -> &'l str {
use HeadingSize::*;
match self {
@ -39,7 +39,6 @@ impl ToString for HeadingSize {
H5 => "H5",
H6 => "H6",
}
.to_string()
}
}
@ -88,28 +87,18 @@ pub enum RteMsg {
}
#[derive(Debug)]
pub struct ExecCommand {
pub(crate) name: String,
pub(crate) param: String,
pub struct ExecCommand<'l> {
pub(crate) name: &'l str,
pub(crate) param: &'l str,
}
impl ExecCommand {
pub fn new<S>(name: S) -> Self
where
S: Into<String>,
{
impl<'l> ExecCommand<'l> {
pub fn new(name: &'l str) -> Self {
Self::new_with_param(name, "")
}
pub fn new_with_param<S1, S2>(name: S1, param: S2) -> Self
where
S1: Into<String>,
S2: Into<String>,
{
Self {
name: name.into(),
param: param.into(),
}
pub fn new_with_param(name: &'l str, param: &'l str) -> Self {
Self { name, param }
}
}
@ -136,9 +125,7 @@ impl RteMsg {
| HeadingSize::H3
| HeadingSize::H4
| HeadingSize::H5
| HeadingSize::H6 => {
Some(ExecCommand::new_with_param("heading", heading.to_string()))
}
| HeadingSize::H6 => Some(ExecCommand::new_with_param("heading", heading.as_str())),
HeadingSize::Normal => Some(ExecCommand::new_with_param("formatBlock", "div")),
},
RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")),
@ -193,7 +180,7 @@ pub struct StyledRteCodeState {
impl StyledRteCodeState {
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()
.iter()
.map(|s| s.name.clone())
@ -202,7 +189,7 @@ impl StyledRteCodeState {
Self {
visible: false,
lang: StyledSelectState::new(
FieldId::Rte(RteField::CodeLang(Box::new(field_id.clone()))),
FieldId::Rte(RteField::CodeLang(Box::new(field_id))),
vec![],
),
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)]
pub struct StyledRteState {
pub value: String,
@ -250,17 +264,19 @@ impl StyledRteState {
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.code_tooltip.lang.update(msg, orders);
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,
};
match m.to_command() {
Some(ExecCommand { name, param }) => {
self.store_range();
match seed::html_document().exec_command_with_show_ui_and_value(
name.as_str(),
false,
param.as_str(),
) {
let doc = match web_sys::window().and_then(|w| w.document()).map(|d| {
wasm_bindgen::JsValue::from(d).unchecked_into::<web_sys::HtmlDocument>()
}) {
Some(doc) => doc,
_ => return,
};
match doc.exec_command_with_show_ui_and_value(name, false, param) {
Ok(_) => {}
Err(e) => log!(e),
}
@ -345,16 +361,9 @@ impl StyledRteState {
Ok(t) => t,
_ => return,
};
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>");
table.set_inner_html(buff.as_str());
table.set_inner_html(
RteTableBodyBuilder::new(*cols, *rows).to_string().as_str(),
);
if let Err(e) = r.insert_node(&table) {
log!(e);
}
@ -363,6 +372,10 @@ impl StyledRteState {
_ => log!(m),
},
};
// orders.skip().send_msg(Msg::StrInputChanged(
// self.field_id.clone(),
// self.value.clone(),
// ));
}
fn store_range(&mut self) {
@ -396,58 +409,54 @@ impl StyledRteState {
let field_id = self.field_id.clone();
let identifier = self.identifier;
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,
table_tooltip: StyledRteTableState,
table_tooltip: Option<&'component StyledRteTableState>,
identifier: Option<uuid::Uuid>,
code_tooltip: StyledRteCodeState,
code_tooltip: Option<&'component StyledRteCodeState>,
}
impl StyledRte {
pub fn build(field_id: FieldId) -> StyledRteBuilder {
impl<'component> StyledRte<'component> {
pub fn build(field_id: FieldId) -> StyledRteBuilder<'component> {
StyledRteBuilder {
field_id: field_id.clone(),
value: String::new(),
table_tooltip: StyledRteTableState {
visible: false,
rows: 0,
cols: 0,
},
code_tooltip: StyledRteCodeState::new(field_id),
table_tooltip: None,
code_tooltip: None,
identifier: None,
}
}
}
impl ToNode for StyledRte {
impl<'component> ToNode for StyledRte<'component> {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
pub struct StyledRteBuilder {
pub struct StyledRteBuilder<'outer> {
field_id: FieldId,
value: String,
table_tooltip: StyledRteTableState,
code_tooltip: StyledRteCodeState,
table_tooltip: Option<&'outer StyledRteTableState>,
code_tooltip: Option<&'outer StyledRteCodeState>,
identifier: Option<uuid::Uuid>,
}
impl StyledRteBuilder {
pub fn state(mut self, state: &StyledRteState) -> Self {
impl<'outer> StyledRteBuilder<'outer> {
pub fn state(mut self, state: &'outer StyledRteState) -> Self {
self.value = state.value.clone();
self.table_tooltip = state.table_tooltip.clone();
self.code_tooltip = state.code_tooltip.clone();
self.table_tooltip = Some(&state.table_tooltip);
self.code_tooltip = Some(&state.code_tooltip);
self.identifier = Some(state.identifier);
self
}
pub fn build(self) -> StyledRte {
pub fn build(self) -> StyledRte<'outer> {
StyledRte {
field_id: self.field_id,
table_tooltip: self.table_tooltip,
@ -458,7 +467,7 @@ impl StyledRteBuilder {
}
pub fn render(values: StyledRte) -> Node<Msg> {
{
/*{
let _brush_button = styled_rte_button(
"Brush",
Icon::Brush,
@ -533,12 +542,17 @@ pub fn render(values: StyledRte) -> Node<Msg> {
None as Option<Msg>
}),
);
}
}*/
// let field_id = values.field_id.clone();
let capture_event = ev(Ev::KeyDown, |ev| {
ev.stop_propagation();
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();
@ -563,7 +577,8 @@ pub fn render(values: StyledRte) -> Node<Msg> {
div![
C!["editor"],
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| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyFull, field_id))
Some(Msg::Rte(field_id, RteMsg::JustifyFull))
}),
);
let field_id = values.field_id.clone();
@ -587,7 +602,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::JustifyCenter,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyCenter, field_id))
Some(Msg::Rte(field_id, RteMsg::JustifyCenter))
}),
);
let field_id = values.field_id.clone();
@ -596,7 +611,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::JustifyLeft,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyLeft, field_id))
Some(Msg::Rte(field_id, RteMsg::JustifyLeft))
}),
);
let field_id = values.field_id.clone();
@ -606,7 +621,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::JustifyRight, field_id))
Some(Msg::Rte(field_id, RteMsg::JustifyRight))
}),
);
div![
@ -625,7 +640,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Redo,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::Redo, field_id))
Some(Msg::Rte(field_id, RteMsg::Redo))
}),
);
let field_id = values.field_id.clone();
@ -634,7 +649,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Undo,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::Undo, field_id))
Some(Msg::Rte(field_id, RteMsg::Undo))
}),
);
/*let field_id = values.field_id.clone();
@ -681,7 +696,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::EraserAlt,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::RemoveFormat, field_id))
Some(Msg::Rte(field_id, RteMsg::RemoveFormat))
}),
);
let field_id = values.field_id.clone();
@ -690,7 +705,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Bold,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::Bold, field_id))
Some(Msg::Rte(field_id, RteMsg::Bold))
}),
);
let field_id = values.field_id.clone();
@ -699,7 +714,7 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Icon::Italic,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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| {
let field_id = values.field_id.clone();
let button = StyledButton::build()
.text(h.to_string())
.text(h.as_str())
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertHeading(h), field_id))
Some(Msg::Rte(field_id, RteMsg::InsertHeading(h)))
}))
.empty()
.build()
@ -867,7 +882,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::ListingDots,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertUnorderedList, field_id))
Some(Msg::Rte(field_id, RteMsg::InsertUnorderedList))
}),
);
let field_id = values.field_id.clone();
@ -876,7 +891,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::ListingNumber,
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertOrderedList, field_id))
Some(Msg::Rte(field_id, RteMsg::InsertOrderedList))
}),
);
/*let field_id = values.field_id.clone();
@ -896,7 +911,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
Icon::Table,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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,
mouse_ev(Ev::Click, move |ev| {
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| {
ev.prevent_default();
Some(Msg::Rte(
RteMsg::ChangeIndent(RteIndentMsg::Increase),
field_id,
RteMsg::ChangeIndent(RteIndentMsg::Increase),
))
}),
);
@ -957,8 +972,8 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(
RteMsg::ChangeIndent(RteIndentMsg::Decrease),
field_id,
RteMsg::ChangeIndent(RteIndentMsg::Decrease),
))
}),
);
@ -975,18 +990,23 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
}
fn table_tooltip(values: &StyledRte) -> Node<Msg> {
let StyledRteTableState {
visible,
rows,
cols,
} = values.table_tooltip;
let (visible, rows, cols) = values
.table_tooltip
.map(
|StyledRteTableState {
visible,
rows,
cols,
}| (*visible, *rows, *cols),
)
.unwrap_or_default();
let on_rows_change = {
let field_id = values.field_id.clone();
input_ev(Ev::Change, move |v| {
v.parse::<u16>()
.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| {
v.parse::<u16>()
.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)
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::TableSetVisibility(false), field_id))
Some(Msg::Rte(field_id, RteMsg::TableSetVisibility(false)))
}))
.build()
.into_node()
@ -1016,7 +1036,7 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
let field_id = values.field_id.clone();
mouse_ev(Ev::Click, move |ev| {
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> {
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
.iter()
@ -1081,15 +1108,14 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
.collect();
let select_lang_node = StyledSelect::build()
.state(lang)
.try_state(lang)
.selected(
lang.values
.get(0)
lang.and_then(|l| l.values.get(0))
.and_then(|n| options.get(*n as usize))
.map(|v| vec![v.to_child()])
.unwrap_or_default(),
)
.options(options.into_iter().map(|opt| opt.to_child()).collect())
.options(options.iter().map(|opt| opt.to_child()).collect())
.normal()
.build(FieldId::Rte(RteField::CodeLang(Box::new(
values.field_id.clone(),
@ -1103,7 +1129,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
.icon(Icon::Close)
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertCode(false), field_id))
Some(Msg::Rte(field_id, RteMsg::InsertCode(false)))
}))
.build()
.into_node()
@ -1116,7 +1142,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let target = ev.target().unwrap();
let textarea = seed::to_textarea(&target);
let code = textarea.value();
Msg::Rte(RteMsg::CodeChanged(code), field_id)
Msg::Rte(field_id, RteMsg::CodeChanged(code))
});
seed::textarea![on_change]
};
@ -1126,7 +1152,8 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let on_insert = ev(Ev::Click, move |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::Rte(RteMsg::InjectCode, field_id)
ev.target().unwrap();
Msg::Rte(field_id, RteMsg::InjectCode)
});
let insert = StyledButton::build()
.on_click(on_insert)
@ -1138,7 +1165,7 @@ fn code_tooltip(values: &StyledRte) -> Node<Msg> {
StyledTooltip::build()
.code_tooltip()
.visible(*visible)
.visible(visible)
.add_child(h2!["Insert Code", close_tooltip])
.add_child(select_lang_node)
.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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Variant::Empty => f.write_str("empty"),
Variant::Normal => f.write_str("normal"),
}
f.write_str(self.to_str())
}
}
@ -61,34 +67,30 @@ impl StyledSelectState {
}
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 {
Msg::StyledSelectChanged(field_id, StyledSelectChanged::DropDownVisibility(b))
if field_id == id =>
{
Msg::StyledSelectChanged(_, StyledSelectChanged::DropDownVisibility(b)) => {
self.opened = *b;
if !self.opened {
self.text_filter.clear();
}
}
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Text(text))
if field_id == id =>
{
Msg::StyledSelectChanged(_, StyledSelectChanged::Text(text)) => {
self.text_filter = text.clone();
}
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(Some(v)))
if field_id == id =>
{
Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(Some(v))) => {
self.values = vec![*v];
}
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None))
if field_id == id =>
{
Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(None)) => {
self.values.clear();
}
Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(v))
if field_id == id =>
{
Msg::StyledSelectChanged(_, StyledSelectChanged::RemoveMulti(v)) => {
let mut old = vec![];
std::mem::swap(&mut old, &mut self.values);
@ -103,28 +105,28 @@ impl StyledSelectState {
}
}
pub struct StyledSelect {
pub struct StyledSelect<'l> {
id: FieldId,
variant: Variant,
dropdown_width: Option<usize>,
name: Option<String>,
name: Option<&'l str>,
valid: bool,
is_multi: bool,
options: Vec<StyledSelectChildBuilder>,
selected: Vec<StyledSelectChildBuilder>,
text_filter: String,
options: Vec<StyledSelectChildBuilder<'l>>,
selected: Vec<StyledSelectChildBuilder<'l>>,
text_filter: &'l str,
opened: bool,
clearable: bool,
}
impl ToNode for StyledSelect {
impl<'l> ToNode for StyledSelect<'l> {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
impl StyledSelect {
pub fn build() -> StyledSelectBuilder {
impl<'l> StyledSelect<'l> {
pub fn build() -> StyledSelectBuilder<'l> {
StyledSelectBuilder {
variant: None,
dropdown_width: None,
@ -141,21 +143,21 @@ impl StyledSelect {
}
#[derive(Debug)]
pub struct StyledSelectBuilder {
pub struct StyledSelectBuilder<'l> {
variant: Option<Variant>,
dropdown_width: Option<usize>,
name: Option<String>,
name: Option<&'l str>,
valid: Option<bool>,
is_multi: Option<bool>,
options: Option<Vec<StyledSelectChildBuilder>>,
selected: Option<Vec<StyledSelectChildBuilder>>,
text_filter: Option<String>,
options: Option<Vec<StyledSelectChildBuilder<'l>>>,
selected: Option<Vec<StyledSelectChildBuilder<'l>>>,
text_filter: Option<&'l str>,
opened: Option<bool>,
clearable: bool,
}
impl StyledSelectBuilder {
pub fn build(self, id: FieldId) -> StyledSelect {
impl<'l> StyledSelectBuilder<'l> {
pub fn build(self, id: FieldId) -> StyledSelect<'l> {
StyledSelect {
id,
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)
.text_filter(state.text_filter.as_str())
}
@ -181,19 +191,13 @@ impl StyledSelectBuilder {
self
}
pub fn name<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.name = Some(name.into());
pub fn name(mut self, name: &'l str) -> Self {
self.name = Some(name);
self
}
pub fn text_filter<S>(mut self, text_filter: S) -> Self
where
S: Into<String>,
{
self.text_filter = Some(text_filter.into());
pub fn text_filter(mut self, text_filter: &'l str) -> Self {
self.text_filter = Some(text_filter);
self
}
@ -207,12 +211,12 @@ impl StyledSelectBuilder {
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
}
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder>) -> Self {
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder<'l>>) -> Self {
self.selected = Some(selected);
self
}
@ -301,7 +305,7 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
let children: Vec<Node<Msg>> = options
.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| {
let child = child.build(DisplayType::SelectOption);
let value = child.value();

View File

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

View File

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

View File

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

View File

@ -44,18 +44,14 @@ pub fn view(model: &Model) -> Node<Msg> {
.build()
.into_node();
let roles = UserRole::ordered();
let user_role = StyledSelect::build()
.name("user_role")
.valid(true)
.normal()
.state(&page.user_role_state)
.selected(vec![page.user_role.to_child()])
.options(
UserRole::ordered()
.into_iter()
.map(|role| role.to_child())
.collect(),
)
.options(roles.iter().map(|role| role.to_child()).collect())
.build(FieldId::Users(UsersFieldId::UserRole))
.into_node();
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;
}
};
// orders.perform_cmd(seed::app::cmds::timeout(10, move || {
// let ws = ws.clone();
let binary = bincode::serialize(&msg).unwrap();
ws.send_bytes(binary.as_slice())
.expect("Failed to send ws 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();
model.ws = WebSocket::builder(url, orders)
.on_message(|msg| Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(msg)))
.on_open(|| Msg::WebSocketChange(WebSocketChanged::WebSocketOpened))
.on_close(|_| Msg::WebSocketChange(WebSocketChanged::WebSocketClosed))
.on_error(|| {})
.on_message(|msg| {
Some(Msg::WebSocketChange(WebSocketChanged::WebSocketMessage(
msg,
)))
})
.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()
.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/`;
import("/jirs.js").then(async module => {
window.module = module;
// window.module = module;
await module.default();
const host_url = `${location.protocol}//${process.env.JIRS_SERVER_BIND}:${process.env.JIRS_SERVER_PORT}`;
module.render(host_url, wsUrl());
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IssueType::Task => f.write_str("task"),
IssueType::Bug => f.write_str("bug"),
IssueType::Story => f.write_str("story"),
}
f.write_str(self.to_str())
}
}
@ -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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
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"),
}
f.write_str(self.to_str())
}
}
@ -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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserRole::User => f.write_str("user"),
UserRole::Manager => f.write_str("manager"),
UserRole::Owner => f.write_str("owner"),
}
f.write_str(self.to_str())
}
}
@ -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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProjectCategory::Software => f.write_str("software"),
ProjectCategory::Marketing => f.write_str("marketing"),
ProjectCategory::Business => f.write_str("business"),
}
f.write_str(self.to_str())
}
}

View File

@ -26,7 +26,6 @@ default = [
]
[dependencies]
serde = { version = "*", features = ["derive"] }
actix = { version = "*" }
actix-web = { version = "*" }
actix-cors = { version = "*" }
@ -35,14 +34,14 @@ actix-rt = "1"
actix-web-actors = "*"
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" }
serde_json = { version = ">=0.8.0, <2.0" }
toml = "0.5.6"
bincode = "1.2.1"
r2d2 = { version = ">= 0.8, < 0.9" }
dotenv = { version = "*" }
byteorder = "1.0"
chrono = { version = "0.4", features = ["serde"] }
libc = { version = "0.2.0" }
time = { version = "0.1" }
url = { 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" }
bigdecimal = { version = ">= 0.0.10, <= 0.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"
pretty_env_logger = "0.4"
env_logger = "0.7"
async-trait = { version = "*" }
futures = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] }
lettre = { version = "*" }
lettre_email = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] }
[dependencies.diesel]
version = "1.4.4"
@ -82,4 +90,4 @@ version = "0.43.0"
# Local storage
[dependencies.actix-files]
optional = true
version = "0.2.1"
version = "*"

View File

@ -53,7 +53,7 @@ where
}
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,
_ => {
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) => {
return async move {
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 {
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 {
ctx.send_msg(&msg)
};
@ -252,10 +252,10 @@ impl WebSocketActor {
}
fn require_user(&self) -> Result<&User, WsMsg> {
self.current_user.as_ref().map(|u| u).ok_or_else(|| {
let _x = 1;
WsMsg::AuthorizeExpired
})
self.current_user
.as_ref()
.map(|u| u)
.ok_or_else(|| WsMsg::AuthorizeExpired)
}
fn require_user_project(&self) -> Result<&UserProject, WsMsg> {
@ -300,7 +300,11 @@ impl 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 {
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
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");
if let (Some(user), Some(up)) = (
self.current_user.as_ref(),
@ -380,7 +384,7 @@ impl Actor for WsServer {
impl Handler<InnerMsg> for WsServer {
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);
match msg {
InnerMsg::Join(project_id, user_id, recipient) => {
@ -408,8 +412,12 @@ impl Handler<InnerMsg> for WsServer {
self.sessions.remove(&user_id);
} else {
let v = self.sessions.entry(user_id).or_insert_with(Vec::new);
if v.remove_item(&recipient).is_none() {
debug!("Can't remove recipient");
let mut old = vec![];
std::mem::swap(&mut old, v);
for r in old {
if r != recipient {
v.push(r);
}
}
}
}

View File