Refactor
This commit is contained in:
parent
a4c7f916be
commit
e807dae81e
@ -1,6 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum ProjectFieldId {
|
||||
Name,
|
||||
Url,
|
||||
@ -11,20 +11,20 @@ pub enum ProjectFieldId {
|
||||
IssueStatusName,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum SignInFieldId {
|
||||
Username,
|
||||
Email,
|
||||
Token,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum SignUpFieldId {
|
||||
Username,
|
||||
Email,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum UsersFieldId {
|
||||
Username,
|
||||
Email,
|
||||
@ -34,17 +34,17 @@ pub enum UsersFieldId {
|
||||
TextEditorMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum InviteFieldId {
|
||||
Token,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum CommentFieldId {
|
||||
Body,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum IssueFieldId {
|
||||
Type,
|
||||
Title,
|
||||
@ -62,7 +62,7 @@ pub enum IssueFieldId {
|
||||
EpicEndsAt,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum EpicFieldId {
|
||||
Name,
|
||||
StartsAt,
|
||||
|
@ -35,6 +35,18 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .styledField {
|
||||
.emailSend {
|
||||
text-align: center;
|
||||
|
||||
> div > svg {
|
||||
width: 160px;
|
||||
margin: 0 auto;
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .noPasswordSection {
|
||||
line-height: 32px;
|
||||
margin-top: 15px;
|
||||
@ -58,6 +70,18 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
> p {
|
||||
line-height: 1.4285;
|
||||
color: var(--danger);
|
||||
font-family: var(--font-medium);
|
||||
text-align: center;
|
||||
font-size: 14.5px;
|
||||
border-top: 1px solid var(--danger);
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1240px) {
|
||||
|
193
web/src/components/events.rs
Normal file
193
web/src/components/events.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::prelude::{ev, Ev, EventHandler};
|
||||
|
||||
use crate::components::styled_date_time_input::StyledDateTimeChanged;
|
||||
use crate::components::styled_md_editor::MdEditorMode;
|
||||
use crate::components::styled_select::StyledSelectChanged;
|
||||
use crate::{resolve_page, FieldChange};
|
||||
|
||||
type EvHandler = EventHandler<crate::Msg>;
|
||||
|
||||
pub fn on_click_change_number_input(field_id: crate::FieldId, value: u32) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
|
||||
crate::Msg::U32InputChanged(field_id, value)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_change_set_str_input(field_id: crate::FieldId) -> EvHandler {
|
||||
ev(Ev::Change, move |ev| {
|
||||
ev.stop_propagation();
|
||||
|
||||
let target = ev.target().unwrap();
|
||||
crate::Msg::StrInputChanged(field_id, seed::to_input(&target).value())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_drop_modal() -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
|
||||
crate::Msg::ModalDropped
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_day(field_id: crate::FieldId, date: chrono::NaiveDateTime) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
|
||||
// log::info!("{:?}", date);
|
||||
crate::Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::DayChanged(Some(date)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_date_time_visibility(field_id: crate::FieldId, visible: bool) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
crate::Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::PopupVisibilityChanged(!visible),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_month(field_id: crate::FieldId, date: chrono::NaiveDateTime) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
|
||||
crate::Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::MonthChanged(Some(date)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_change_image_input(field_id: crate::FieldId) -> EvHandler {
|
||||
ev(Ev::Change, move |ev| {
|
||||
let target = ev.target().unwrap();
|
||||
let input = seed::to_input(&target);
|
||||
let v = input
|
||||
.files()
|
||||
.map(|list| {
|
||||
(0..list.length())
|
||||
.filter_map(|i| list.get(i))
|
||||
.collect::<Vec<web_sys::File>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
crate::Msg::FileInputChanged(field_id, v)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_keyup_change_select_text(field_id: crate::FieldId) -> EvHandler {
|
||||
ev(Ev::KeyUp, move |ev| {
|
||||
ev.stop_propagation();
|
||||
|
||||
let target = ev.target().unwrap();
|
||||
let value = seed::to_input(&target).value();
|
||||
crate::Msg::StyledSelectChanged(field_id, StyledSelectChanged::Text(value))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_select_dropdown_visibility(
|
||||
field_id: crate::FieldId,
|
||||
opened: bool,
|
||||
) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
|
||||
crate::Msg::StyledSelectChanged(field_id, StyledSelectChanged::DropDownVisibility(!opened))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_select_selected(field_id: crate::FieldId, value: Option<u32>) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
crate::Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(value))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_select_remove_multi(field_id: crate::FieldId, value: u32) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
crate::Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_page(href: String) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
if href.starts_with('/') {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
|
||||
if let Ok(url) = seed::Url::from_str(href.as_str()) {
|
||||
url.go_and_push();
|
||||
return resolve_page(url).map(crate::Msg::ChangePage);
|
||||
}
|
||||
}
|
||||
|
||||
None as Option<crate::Msg>
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_change_tab(field_id: crate::FieldId, new_mode: MdEditorMode) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
|
||||
crate::Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_noop() -> EvHandler {
|
||||
noop(Ev::Click)
|
||||
}
|
||||
|
||||
pub fn on_keyup_noop() -> EvHandler {
|
||||
noop(Ev::KeyUp)
|
||||
}
|
||||
|
||||
fn noop(event: Ev) -> EvHandler {
|
||||
ev(event, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
None as Option<crate::Msg>
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_event_change_text_value(
|
||||
field_id: crate::FieldId,
|
||||
update_event: Ev,
|
||||
handler_disable_auto_resize: bool,
|
||||
) -> EvHandler {
|
||||
ev(update_event, move |event| {
|
||||
event.stop_propagation();
|
||||
|
||||
let value = event
|
||||
.target()
|
||||
.map(|target| seed::to_textarea(&target).value())
|
||||
.unwrap_or_default();
|
||||
|
||||
if handler_disable_auto_resize && value.contains('\n') {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
crate::Msg::StrInputChanged(
|
||||
field_id,
|
||||
if handler_disable_auto_resize {
|
||||
value.trim().to_string()
|
||||
} else {
|
||||
value
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
@ -18,3 +18,4 @@ pub mod styled_select_child;
|
||||
pub mod styled_textarea;
|
||||
pub mod styled_tip;
|
||||
pub mod styled_tooltip;
|
||||
mod events;
|
||||
|
@ -32,7 +32,7 @@ impl<'l> StyledAvatar<'l> {
|
||||
.chars()
|
||||
.rev()
|
||||
.last()
|
||||
.map(|c| c.to_string())
|
||||
.map(String::from)
|
||||
.unwrap_or_default();
|
||||
match avatar_url {
|
||||
Some(url) => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
|
||||
use super::events;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -56,15 +57,7 @@ impl<'l> ChildBuilder<'l> {
|
||||
#[inline(always)]
|
||||
pub fn render(self) -> Node<Msg> {
|
||||
let id = self.field_id.to_string();
|
||||
let handler: EventHandler<Msg> = {
|
||||
let id = self.field_id;
|
||||
let value = self.value;
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::U32InputChanged(id, value)
|
||||
})
|
||||
};
|
||||
let handler = events::on_click_change_number_input(self.field_id, self.value);
|
||||
|
||||
div![
|
||||
C![
|
||||
|
@ -48,7 +48,7 @@ impl<'l> StyledConfirmModal<'l> {
|
||||
_ => cancel_text,
|
||||
}),
|
||||
variant: ButtonVariant::Secondary,
|
||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ModalDropped)),
|
||||
on_click: Some(super::events::on_click_drop_modal()),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
@ -105,13 +105,16 @@ impl StyledDateTimeInput {
|
||||
let calendar_start = StyledDateTimeInput::calendar_start(start);
|
||||
let calendar_end = StyledDateTimeInput::calendar_end(end);
|
||||
let current_month_range = start..=end;
|
||||
// let current = calendar_start;
|
||||
let mut weeks = vec![];
|
||||
let mut current_week = vec![];
|
||||
|
||||
let mut weeks = Vec::with_capacity(7);
|
||||
let mut current_week = Vec::with_capacity(6);
|
||||
|
||||
for current in DateRange(calendar_start, calendar_end) {
|
||||
if current.weekday() == Weekday::Mon && !current_week.is_empty() {
|
||||
weeks.push(div![C!["week"], std::mem::take(&mut current_week)]);
|
||||
weeks.push(div![
|
||||
C!["week"],
|
||||
std::mem::replace(&mut current_week, Vec::with_capacity(7))
|
||||
]);
|
||||
}
|
||||
|
||||
current_week.push(
|
||||
@ -130,21 +133,14 @@ impl StyledDateTimeInput {
|
||||
}
|
||||
|
||||
let left_action = {
|
||||
let field_id = self.field_id.clone();
|
||||
let current = timestamp;
|
||||
let on_click_left = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
let last_day_of_prev_month = current.with_day0(0).unwrap() - Duration::days(1);
|
||||
let on_click_left = {
|
||||
let last_day_of_prev_month = timestamp.with_day0(0).unwrap() - Duration::days(1);
|
||||
|
||||
let date = last_day_of_prev_month
|
||||
.with_day0(timestamp.day0())
|
||||
.unwrap_or(last_day_of_prev_month);
|
||||
Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::MonthChanged(Some(date)),
|
||||
)
|
||||
});
|
||||
super::events::on_click_change_month(self.field_id.clone(), date)
|
||||
};
|
||||
StyledButton {
|
||||
on_click: Some(on_click_left),
|
||||
icon: Some(StyledIcon::from(Icon::DoubleLeft).render()),
|
||||
@ -153,13 +149,11 @@ impl StyledDateTimeInput {
|
||||
}
|
||||
.render()
|
||||
};
|
||||
|
||||
let right_action = {
|
||||
let field_id = self.field_id.clone();
|
||||
let current = timestamp;
|
||||
let on_click_right = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
let first_day_of_next_month = (current + Duration::days(32)).with_day0(0).unwrap();
|
||||
let on_click_right = {
|
||||
let first_day_of_next_month =
|
||||
(timestamp + Duration::days(32)).with_day0(0).unwrap();
|
||||
let last_day_of_next_month = (first_day_of_next_month + Duration::days(32))
|
||||
.with_day0(0)
|
||||
.unwrap()
|
||||
@ -167,11 +161,8 @@ impl StyledDateTimeInput {
|
||||
let date = first_day_of_next_month
|
||||
.with_day0(timestamp.day0())
|
||||
.unwrap_or(last_day_of_next_month);
|
||||
Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::MonthChanged(Some(date)),
|
||||
)
|
||||
});
|
||||
super::events::on_click_change_month(self.field_id.clone(), date)
|
||||
};
|
||||
StyledButton {
|
||||
on_click: Some(on_click_right),
|
||||
icon: Some(StyledIcon::from(Icon::DoubleRight).render()),
|
||||
@ -210,16 +201,10 @@ impl StyledDateTimeInput {
|
||||
.render();
|
||||
|
||||
let input = {
|
||||
let field_id = self.field_id.clone();
|
||||
let visible = self.popup_visible;
|
||||
let on_focus = ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::PopupVisibilityChanged(!visible),
|
||||
)
|
||||
});
|
||||
let on_focus = super::events::on_click_change_date_time_visibility(
|
||||
self.field_id.clone(),
|
||||
self.popup_visible,
|
||||
);
|
||||
let text = self
|
||||
.timestamp
|
||||
.unwrap_or_else(|| Utc::now().naive_utc())
|
||||
@ -279,25 +264,13 @@ pub struct DayCell<'l> {
|
||||
impl<'l> DayCell<'l> {
|
||||
#[inline(always)]
|
||||
pub fn render(self) -> Node<Msg> {
|
||||
let on_click = {
|
||||
let field_id = self.field_id.clone();
|
||||
let date = *self.current;
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
log::info!("{:?}", date);
|
||||
Msg::StyledDateTimeInputChanged(
|
||||
field_id,
|
||||
StyledDateTimeChanged::DayChanged(Some(date)),
|
||||
)
|
||||
})
|
||||
};
|
||||
let on_click = super::events::on_click_change_day(self.field_id.clone(), *self.current);
|
||||
div![
|
||||
C![
|
||||
"day",
|
||||
format!("{}", self.current.weekday()),
|
||||
IF![self.is_selected() => "selected"],
|
||||
if self.current_month_range.contains(&self.current) {
|
||||
if self.current_month_range.contains(self.current) {
|
||||
"inCurrentMonth"
|
||||
} else {
|
||||
"outCurrentMonth"
|
||||
|
@ -68,6 +68,7 @@ impl StyledEditorState {
|
||||
}
|
||||
|
||||
impl EditorMode {
|
||||
#[inline(always)]
|
||||
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
match self {
|
||||
EditorMode::Md(state) => state.update(msg),
|
||||
@ -167,10 +168,12 @@ fn build_state(
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn build_state_rte(field_id: FieldId) -> EditorMode {
|
||||
EditorMode::Rte(StyledRteState::new(field_id))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn build_state_md(field_id: FieldId, text: &str, html: &str) -> EditorMode {
|
||||
EditorMode::Md(StyledMdEditorState::new(
|
||||
field_id,
|
||||
|
@ -1,5 +1,5 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
use web_sys::File;
|
||||
|
||||
use crate::{FieldId, Msg};
|
||||
@ -45,20 +45,7 @@ impl<'l> StyledImageInput<'l> {
|
||||
url,
|
||||
} = self;
|
||||
|
||||
let field_id = id.clone();
|
||||
let on_change = ev(Ev::Change, move |ev| {
|
||||
let target = ev.target().unwrap();
|
||||
let input = seed::to_input(&target);
|
||||
let v = input
|
||||
.files()
|
||||
.map(|list| {
|
||||
(0..list.length())
|
||||
.filter_map(|i| list.get(i))
|
||||
.collect::<Vec<File>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Msg::FileInputChanged(field_id, v)
|
||||
});
|
||||
let on_change = super::events::on_change_image_input(id.clone());
|
||||
let input_id = id.to_string();
|
||||
|
||||
div![
|
||||
|
@ -160,14 +160,7 @@ impl<'l, 'm: 'l> StyledInput<'l, 'm> {
|
||||
.map(|icon| StyledIcon::from(icon).render())
|
||||
.unwrap_or(Node::Empty);
|
||||
|
||||
let on_change = {
|
||||
let field_id = id.clone();
|
||||
ev(Ev::Change, move |event| {
|
||||
event.stop_propagation();
|
||||
let target = event.target().unwrap();
|
||||
Msg::StrInputChanged(field_id, seed::to_input(&target).value())
|
||||
})
|
||||
};
|
||||
let on_change = super::events::on_change_set_str_input(id.clone());
|
||||
|
||||
div![
|
||||
C![
|
||||
@ -183,7 +176,7 @@ impl<'l, 'm: 'l> StyledInput<'l, 'm> {
|
||||
"inputElement",
|
||||
variant.to_str(),
|
||||
input_class_list,
|
||||
icon.as_ref().map(|_| "withIcon").unwrap_or_default()
|
||||
icon.map(|_| "withIcon")
|
||||
],
|
||||
attrs![
|
||||
"id" => format!("{}", id),
|
||||
|
@ -1,9 +1,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::{resolve_page, Msg};
|
||||
use crate::Msg;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct StyledLink<'l> {
|
||||
@ -16,23 +14,7 @@ pub struct StyledLink<'l> {
|
||||
impl<'l> StyledLink<'l> {
|
||||
#[inline(always)]
|
||||
pub fn render(self) -> Node<Msg> {
|
||||
let on_click = {
|
||||
let href = self.href.to_string();
|
||||
mouse_ev("click", move |ev| {
|
||||
if href.starts_with('/') {
|
||||
ev.prevent_default();
|
||||
ev.stop_propagation();
|
||||
if let Ok(url) = seed::Url::from_str(href.as_str()) {
|
||||
url.go_and_push();
|
||||
if let Some(page) = resolve_page(url) {
|
||||
return Some(Msg::ChangePage(page));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None as Option<Msg>
|
||||
})
|
||||
};
|
||||
let on_click = super::events::on_click_change_page(self.href.to_string());
|
||||
|
||||
a![
|
||||
C!["styledLink", self.class_list],
|
||||
|
@ -1,8 +1,8 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::components::styled_textarea::StyledTextarea;
|
||||
use crate::{FieldChange, FieldId, Msg};
|
||||
use crate::components::styled_textarea::StyledTextarea;
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
||||
#[repr(C)]
|
||||
@ -84,8 +84,9 @@ impl<'l> StyledMdEditor<'l> {
|
||||
} = self;
|
||||
|
||||
let id = id.expect("Styled Editor requires ID");
|
||||
let on_editor_clicked = click_handler(id.clone(), MdEditorMode::Editor);
|
||||
let on_view_clicked = click_handler(id.clone(), MdEditorMode::View);
|
||||
let on_editor_clicked =
|
||||
super::events::on_click_change_tab(id.clone(), MdEditorMode::Editor);
|
||||
let on_view_clicked = super::events::on_click_change_tab(id.clone(), MdEditorMode::View);
|
||||
|
||||
let editor_id = format!("editor-{}", id);
|
||||
let view_id = format!("view-{}", id);
|
||||
@ -140,11 +141,3 @@ impl<'l> StyledMdEditor<'l> {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn click_handler(field_id: FieldId, new_mode: MdEditorMode) -> EventHandler<Msg> {
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::TabChanged(field_id, new_mode))
|
||||
})
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::components::styled_icon::{Icon, StyledIcon};
|
||||
use crate::Msg;
|
||||
@ -76,19 +76,11 @@ impl<'l> StyledModal<'l> {
|
||||
class_list,
|
||||
} = self;
|
||||
|
||||
let close_handler = mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::ModalDropped
|
||||
});
|
||||
let body_handler = mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
None as Option<Msg>
|
||||
});
|
||||
let close_handler = super::events::on_click_drop_modal();
|
||||
let body_handler = super::events::on_click_noop();
|
||||
|
||||
let styled_modal_style = match width {
|
||||
Some(0) => "".to_string(),
|
||||
Some(0) => String::from(""),
|
||||
Some(n) => format!("max-width: {width}px", width = n),
|
||||
_ => format!("max-width: {width}px", width = 130),
|
||||
};
|
||||
|
@ -87,14 +87,15 @@ impl StyledSelectState {
|
||||
self.text_filter = text.clone();
|
||||
}
|
||||
Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(Some(v))) => {
|
||||
self.values = vec![*v];
|
||||
self.values.clear();
|
||||
self.values.push(*v);
|
||||
}
|
||||
Msg::StyledSelectChanged(_, StyledSelectChanged::Changed(None)) => {
|
||||
self.values.clear();
|
||||
}
|
||||
Msg::StyledSelectChanged(_, StyledSelectChanged::RemoveMulti(v)) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut self.values);
|
||||
let len = self.values.len();
|
||||
let old = std::mem::replace(&mut self.values, Vec::with_capacity(len));
|
||||
|
||||
for u in old {
|
||||
if u != *v {
|
||||
@ -164,19 +165,10 @@ where
|
||||
clearable,
|
||||
} = self;
|
||||
|
||||
let on_text = {
|
||||
let field_id = id.clone();
|
||||
input_ev(Ev::KeyUp, move |value| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Text(value))
|
||||
})
|
||||
};
|
||||
let on_text = super::events::on_keyup_change_select_text(id.clone());
|
||||
|
||||
let on_handler = {
|
||||
let field_id = id.clone();
|
||||
mouse_ev(Ev::Click, move |_| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChanged::DropDownVisibility(!opened))
|
||||
})
|
||||
};
|
||||
let on_handler =
|
||||
super::events::on_click_change_select_dropdown_visibility(id.clone(), opened);
|
||||
|
||||
let dropdown_style = dropdown_width.map_or_else(
|
||||
|| "width: 100%;".to_string(),
|
||||
@ -184,14 +176,7 @@ where
|
||||
);
|
||||
|
||||
let action_icon = if clearable && !selected.is_empty() {
|
||||
let on_click = {
|
||||
let field_id = id.clone();
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChanged::Changed(None))
|
||||
})
|
||||
};
|
||||
let on_click = super::events::on_click_change_select_selected(id.clone(), None);
|
||||
StyledIcon {
|
||||
icon: Icon::Close,
|
||||
class_list: "chevronIcon",
|
||||
@ -210,26 +195,24 @@ where
|
||||
empty![]
|
||||
};
|
||||
|
||||
let skip = selected.iter().fold(HashMap::new(), |mut h, o| {
|
||||
h.insert(o.value, true);
|
||||
h
|
||||
});
|
||||
let skip = {
|
||||
let len = selected.len();
|
||||
selected
|
||||
.iter()
|
||||
.fold(HashMap::with_capacity(len), |mut h, o| {
|
||||
h.insert(o.value, true);
|
||||
h
|
||||
})
|
||||
};
|
||||
let children: Vec<Node<Msg>> = if let Some(options) = options {
|
||||
options
|
||||
.filter(|o| !skip.contains_key(&o.value) && o.match_text(text_filter))
|
||||
.map(|child| {
|
||||
let value = child.value();
|
||||
let on_change = super::events::on_click_change_select_selected(
|
||||
id.clone(),
|
||||
Some(child.value()),
|
||||
);
|
||||
let node = child.render_option();
|
||||
|
||||
let on_change = {
|
||||
let field_id = id.clone();
|
||||
mouse_ev(Ev::Click, move |_| {
|
||||
Msg::StyledSelectChanged(
|
||||
field_id,
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
)
|
||||
})
|
||||
};
|
||||
div![C!["option"], on_change, on_handler.clone(), node]
|
||||
})
|
||||
.collect()
|
||||
@ -240,10 +223,7 @@ where
|
||||
seed::div![
|
||||
C!["styledSelect", variant.to_str(), IF![!valid => "invalid"]],
|
||||
attrs![At::Style => dropdown_style.as_str()],
|
||||
keyboard_ev(Ev::KeyUp, |ev| {
|
||||
ev.stop_propagation();
|
||||
None as Option<Msg>
|
||||
}),
|
||||
super::events::on_keyup_noop(),
|
||||
div![
|
||||
C!["valueContainer", variant.to_str()],
|
||||
on_handler,
|
||||
@ -287,18 +267,8 @@ where
|
||||
}
|
||||
|
||||
fn multi_value(child: StyledSelectOption, id: FieldId) -> Node<Msg> {
|
||||
let value = child.value();
|
||||
let handler = super::events::on_click_change_select_remove_multi(id, child.value());
|
||||
|
||||
let opt = child.render_multi_value();
|
||||
|
||||
let handler = {
|
||||
let field_id = id;
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value))
|
||||
})
|
||||
};
|
||||
|
||||
div![C!["valueMultiItem"], opt, handler]
|
||||
div![C!["valueMultiItem"], child.render_multi_value(), handler]
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ pub enum DisplayType {
|
||||
SelectMultiValue,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StyledSelectOption<'l> {
|
||||
pub name: Option<&'l str>,
|
||||
pub icon: Option<Node<Msg>>,
|
||||
@ -22,19 +23,6 @@ pub struct StyledSelectOption<'l> {
|
||||
pub variant: SelectVariant,
|
||||
}
|
||||
|
||||
impl<'l> Default for StyledSelectOption<'l> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: None,
|
||||
icon: None,
|
||||
text: None,
|
||||
value: 0,
|
||||
class_list: "",
|
||||
variant: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> StyledSelectOption<'l> {
|
||||
#[inline(always)]
|
||||
pub fn value(&self) -> u32 {
|
||||
@ -75,31 +63,26 @@ impl<'l> StyledSelectOption<'l> {
|
||||
variant,
|
||||
} = self;
|
||||
|
||||
let label_node = text.map_or_else(
|
||||
|| Node::Empty,
|
||||
|text| {
|
||||
div![
|
||||
C![
|
||||
variant.to_str(),
|
||||
name.as_deref()
|
||||
.map(|s| format!("{}Label", s))
|
||||
.unwrap_or_default(),
|
||||
match display_type {
|
||||
DisplayType::SelectOption => "optionLabel",
|
||||
DisplayType::SelectValue | DisplayType::SelectMultiValue =>
|
||||
"selectItemLabel",
|
||||
},
|
||||
class_list
|
||||
],
|
||||
text
|
||||
]
|
||||
},
|
||||
);
|
||||
let label_node = text.map(|text| {
|
||||
div![
|
||||
C![
|
||||
variant.to_str(),
|
||||
name.map(|s| format!("{}Label", s)),
|
||||
match display_type {
|
||||
DisplayType::SelectOption => "optionLabel",
|
||||
DisplayType::SelectValue | DisplayType::SelectMultiValue =>
|
||||
"selectItemLabel",
|
||||
},
|
||||
class_list
|
||||
],
|
||||
text
|
||||
]
|
||||
});
|
||||
|
||||
div![
|
||||
C![
|
||||
variant.to_str(),
|
||||
name.as_deref().unwrap_or_default(),
|
||||
name,
|
||||
match display_type {
|
||||
DisplayType::SelectOption => "optionItem",
|
||||
DisplayType::SelectValue | DisplayType::SelectMultiValue => "selectItem value",
|
||||
|
@ -49,7 +49,7 @@ impl<'l> StyledTextarea<'l> {
|
||||
disable_auto_resize,
|
||||
} = self;
|
||||
let id = id.expect("Text area requires FieldId");
|
||||
let mut style_list = vec![];
|
||||
let mut style_list = Vec::with_capacity(3);
|
||||
|
||||
let min_height = get_min_height(value, height as f64, disable_auto_resize);
|
||||
if min_height > 0f64 {
|
||||
@ -88,30 +88,11 @@ impl<'l> StyledTextarea<'l> {
|
||||
// });
|
||||
|
||||
let handler_disable_auto_resize = disable_auto_resize;
|
||||
let text_input_handler = {
|
||||
let id = id.clone();
|
||||
ev(update_event, move |event| {
|
||||
event.stop_propagation();
|
||||
|
||||
let value = event
|
||||
.target()
|
||||
.map(|target| seed::to_textarea(&target).value())
|
||||
.unwrap_or_default();
|
||||
|
||||
if handler_disable_auto_resize && value.contains('\n') {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
Some(Msg::StrInputChanged(
|
||||
id,
|
||||
if handler_disable_auto_resize {
|
||||
value.trim().to_string()
|
||||
} else {
|
||||
value
|
||||
},
|
||||
))
|
||||
})
|
||||
};
|
||||
let text_input_handler = super::events::on_event_change_text_value(
|
||||
id.clone(),
|
||||
update_event,
|
||||
handler_disable_auto_resize,
|
||||
);
|
||||
|
||||
div![
|
||||
id![format!("styledTextArea-{}", id)],
|
||||
|
@ -5,7 +5,7 @@ use jirs_data::{
|
||||
|
||||
pub type AvatarFilterActive = bool;
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum EditIssueModalSection {
|
||||
Issue(IssueFieldId),
|
||||
Comment(CommentFieldId),
|
||||
@ -84,7 +84,7 @@ impl ButtonId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum IssuesAndFiltersId {
|
||||
Jql,
|
||||
}
|
||||
|
16
web/src/images/email_send.rs
Normal file
16
web/src/images/email_send.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use seed::prelude::*;
|
||||
|
||||
pub fn render() -> Node<crate::Msg> {
|
||||
use seed::*;
|
||||
|
||||
svg![
|
||||
attrs![
|
||||
"xmlns"=>"http://www.w3.org/2000/svg",
|
||||
"viewBox"=>"0 0 64 64",
|
||||
"xml:space"=>"preserve",
|
||||
],
|
||||
path![attrs![
|
||||
At::D=>"m63.94 39.721-3.956-22.388a3.856 3.856 0 0 0-4.464-3.125l-31.726 5.606c-1.97.348-3.308 2.148-3.15 4.102-.022.162 3.983 22.751 3.983 22.751a3.821 3.821 0 0 0 1.586 2.488 3.827 3.827 0 0 0 2.879.636l31.723-5.605a3.825 3.825 0 0 0 2.488-1.586c.59-.843.816-1.865.637-2.879zm-2.672-3.636L50.6 27.879c3.002-3.233 6.137-6.785 7.702-8.572l2.965 16.778zM24.143 21.783l31.724-5.606a1.855 1.855 0 0 1 1.839.772c-.932 1.072-9.375 10.756-13.433 14.56a3.537 3.537 0 0 1-3.918.631c-4.79-2.213-15.22-7.446-17.727-8.707a1.84 1.84 0 0 1 1.515-1.65zm-1.165 4.064c2.35 1.178 6.616 3.31 10.513 5.218l-7.523 11.702-2.99-16.92zm38.686 15.606a1.838 1.838 0 0 1-1.195.763l-31.725 5.607a1.851 1.851 0 0 1-1.386-.308 1.84 1.84 0 0 1-.762-1.195l-.115-.652 8.82-13.72c1.602.777 3.069 1.479 4.215 2.008a5.529 5.529 0 0 0 6.124-.99c1.005-.94 2.255-2.214 3.586-3.62l12.558 9.66.188 1.063c.086.487-.024.98-.308 1.384zM19.008 29.867a.997.997 0 0 0-1.158-.81L.826 32.065a.999.999 0 1 0 .348 1.969l17.023-3.008a.999.999 0 0 0 .81-1.159zM20.03 35.652a.998.998 0 0 0-1.159-.81L8.657 36.645a.999.999 0 1 0 .348 1.969l10.214-1.805a.999.999 0 0 0 .81-1.158zM19.888 40.597 13.079 41.8a.999.999 0 1 0 .348 1.969l6.808-1.203a.999.999 0 0 0 .81-1.158.996.996 0 0 0-1.157-.811z"
|
||||
]]
|
||||
]
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
pub mod project_avatar;
|
||||
pub mod email_send;
|
||||
pub mod logo;
|
||||
pub mod project_avatar;
|
||||
|
@ -88,6 +88,7 @@ pub enum Msg {
|
||||
AuthTokenErased,
|
||||
SignInRequest,
|
||||
BindClientRequest,
|
||||
InvalidPair,
|
||||
|
||||
// users
|
||||
InviteRequest,
|
||||
|
@ -1,57 +1,49 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use jirs_data::{Issue, IssueStatus};
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{Issue, IssueStatus};
|
||||
|
||||
use crate::components::styled_icon::{Icon, StyledIcon};
|
||||
use crate::model::Model;
|
||||
use crate::shared::inner_layout;
|
||||
use crate::Msg;
|
||||
use crate::shared::inner_layout;
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let page = crate::match_page!(model, Epics; Empty);
|
||||
|
||||
let epics: Vec<Node<Msg>> = model
|
||||
.epics
|
||||
.iter()
|
||||
.map(|epic| {
|
||||
let issues = page
|
||||
.issues(epic.id)
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.filter_map(|i| model.issues_by_id.get(i))
|
||||
.collect::<Vec<&Issue>>()
|
||||
let epics = model.epics.iter().map(|epic| {
|
||||
let issues = page.issues(epic.id).map(|v| {
|
||||
v.iter()
|
||||
.filter_map(|i| model.issues_by_id.get(i))
|
||||
.map(|issue| {
|
||||
render_issue(
|
||||
issue,
|
||||
model.issue_statuses_by_id.get(&issue.issue_status_id),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.collect::<Vec<Node<Msg>>>()
|
||||
});
|
||||
|
||||
li![
|
||||
C!["epic"],
|
||||
li![
|
||||
C!["epic"],
|
||||
div![
|
||||
C!["firstRow"],
|
||||
div![C!["epicName"], &epic.name],
|
||||
div![
|
||||
C!["firstRow"],
|
||||
div![C!["epicName"], &epic.name],
|
||||
div![
|
||||
C!["date"],
|
||||
date_field("Starts at:", "startsAt", epic.starts_at.as_ref()),
|
||||
date_field("Ends at:", "endsAt", epic.ends_at.as_ref()),
|
||||
],
|
||||
div![C!["counter"], "Number of issues:", issues.len()],
|
||||
C!["date"],
|
||||
date_field("Starts at:", "startsAt", epic.starts_at.as_ref()),
|
||||
date_field("Ends at:", "endsAt", epic.ends_at.as_ref()),
|
||||
],
|
||||
div![
|
||||
C!["secondRow"],
|
||||
div![
|
||||
C!["issues"],
|
||||
issues
|
||||
.into_iter()
|
||||
.map(|issue| render_issue(
|
||||
issue,
|
||||
model.issue_statuses_by_id.get(&issue.issue_status_id)
|
||||
))
|
||||
.collect::<Vec<Node<Msg>>>()
|
||||
]
|
||||
]
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
C!["counter"],
|
||||
"Number of issues:",
|
||||
issues.as_ref().map(Vec::len).unwrap_or(0)
|
||||
],
|
||||
],
|
||||
div![C!["secondRow"], div![C!["issues"], issues]]
|
||||
]
|
||||
});
|
||||
|
||||
inner_layout(
|
||||
model,
|
||||
@ -83,12 +75,7 @@ fn render_issue(issue: &Issue, status: Option<&IssueStatus>) -> Node<Msg> {
|
||||
div![
|
||||
C!["issue"],
|
||||
div![C!["name"], issue.title.as_str()],
|
||||
div![
|
||||
C!["status"],
|
||||
status
|
||||
.map(|status| status.name.as_str())
|
||||
.unwrap_or_default()
|
||||
],
|
||||
div![C!["status"], status.map(|s| s.name.as_str())],
|
||||
div![
|
||||
C!["flags"],
|
||||
div![
|
||||
|
@ -1,6 +1,7 @@
|
||||
use jirs_data::{Issue, IssueId};
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{Issue, IssueId};
|
||||
|
||||
use crate::components::styled_icon::*;
|
||||
use crate::components::styled_link::*;
|
||||
@ -163,7 +164,6 @@ fn issue_link(id: IssueId, project_name: &str) -> Node<Msg> {
|
||||
],
|
||||
disabled: true,
|
||||
class_list: "withIcon issueLink",
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
12
web/src/pages/profile_page/events.rs
Normal file
12
web/src/pages/profile_page/events.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use seed::prelude::{ev, Ev, EventHandler};
|
||||
|
||||
type EvHandler = EventHandler<crate::Msg>;
|
||||
|
||||
pub fn on_submit_submit_profile() -> EvHandler {
|
||||
ev(Ev::Submit, |ev| {
|
||||
ev.prevent_default();
|
||||
crate::Msg::PageChanged(crate::PageChanged::Profile(
|
||||
crate::ProfilePageChange::SubmitForm,
|
||||
))
|
||||
})
|
||||
}
|
@ -2,6 +2,7 @@ pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod events;
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
||||
|
@ -77,10 +77,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
|
||||
let content = StyledForm {
|
||||
heading: "Profile",
|
||||
on_submit: Some(ev(Ev::Submit, |ev| {
|
||||
ev.prevent_default();
|
||||
Msg::PageChanged(PageChanged::Profile(ProfilePageChange::SubmitForm))
|
||||
})),
|
||||
on_submit: Some(super::events::on_submit_submit_profile()),
|
||||
fields: vec![
|
||||
avatar,
|
||||
username_field,
|
||||
@ -96,18 +93,14 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
|
||||
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
||||
let inner = if model.projects.len() <= 1 {
|
||||
let name = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.name.as_str())
|
||||
.unwrap_or_default();
|
||||
let name = model.project.as_ref().map(|p| p.name.as_str());
|
||||
span![name]
|
||||
} else {
|
||||
let mut project_by_id = HashMap::new();
|
||||
let mut project_by_id = HashMap::with_capacity(model.projects.len());
|
||||
for p in model.projects.iter() {
|
||||
project_by_id.insert(p.id, p);
|
||||
}
|
||||
let mut joined_projects = HashMap::new();
|
||||
let mut joined_projects = HashMap::with_capacity(model.user_projects.len());
|
||||
for p in model.user_projects.iter() {
|
||||
joined_projects.insert(p.project_id, p);
|
||||
}
|
||||
@ -147,7 +140,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn project_select_option<'l>(project: &'l Project) -> StyledSelectOption<'l> {
|
||||
fn project_select_option(project: &Project) -> StyledSelectOption<'_> {
|
||||
StyledSelectOption {
|
||||
text: Some(project.name.as_str()),
|
||||
value: project.id as u32,
|
||||
@ -160,12 +153,11 @@ fn editor_mode_select(page: &ProfilePage) -> Node<Msg> {
|
||||
let time_tracking = StyledCheckbox {
|
||||
options: Some(
|
||||
vec![
|
||||
TextEditorMode::MdOnly,
|
||||
TextEditorMode::RteOnly,
|
||||
TextEditorMode::Mixed,
|
||||
editor_mode_checkbox_option(TextEditorMode::MdOnly, &page.text_editor_mode),
|
||||
editor_mode_checkbox_option(TextEditorMode::RteOnly, &page.text_editor_mode),
|
||||
editor_mode_checkbox_option(TextEditorMode::Mixed, &page.text_editor_mode),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|tem| editor_mode_checkbox_option(tem, &page.text_editor_mode)),
|
||||
.into_iter(),
|
||||
),
|
||||
class_list: "timeTracking",
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
use jirs_data::{EpicId, UserId};
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::model::Page;
|
||||
use crate::{BoardPageChange, Msg, PageChanged};
|
||||
use crate::{AvatarFilterActive, BoardPageChange, Msg, PageChanged};
|
||||
|
||||
pub type EvHandler = seed::EventHandler<Msg>;
|
||||
|
||||
@ -72,3 +73,55 @@ pub fn on_click_edit_issue(issue_id: i32) -> EvHandler {
|
||||
Msg::ChangePage(Page::EditIssue(issue_id))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_toggle_only_my() -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ProjectToggleOnlyMy
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_toggle_recent() -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ProjectToggleRecentlyUpdated
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_filter_by_user(user_id: UserId, active: AvatarFilterActive) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_clear_filters() -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ProjectClearFilters
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_goto_delete_epic(epic_id: EpicId) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new()
|
||||
.add_path_part("delete-epic")
|
||||
.add_path_part(epic_id.to_string())
|
||||
.go_and_push();
|
||||
Msg::ChangePage(Page::DeleteEpic(epic_id))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_click_goto_edit_epic(epic_id: EpicId) -> EvHandler {
|
||||
ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new()
|
||||
.add_path_part("edit-epic")
|
||||
.add_path_part(epic_id.to_string())
|
||||
.go_and_push();
|
||||
Msg::ChangePage(Page::EditEpic(epic_id))
|
||||
})
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||
C!["breadcrumbsContainer"],
|
||||
span!["Projects"],
|
||||
span![C!["breadcrumbsDivider"], "/"],
|
||||
span![model.project_name().unwrap_or_default()],
|
||||
span![model.project_name()],
|
||||
span![C!["breadcrumbsDivider"], "/"],
|
||||
span!["Kanban Board"]
|
||||
]
|
||||
|
@ -1,13 +1,14 @@
|
||||
use jirs_data::*;
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::{match_page, Model, Msg};
|
||||
use crate::components::styled_avatar::*;
|
||||
use crate::components::styled_button::{ButtonVariant, StyledButton};
|
||||
use crate::components::styled_icon::*;
|
||||
use crate::model::PageContent;
|
||||
use crate::pages::project_page::{events, StatusIssueIds};
|
||||
use crate::{match_page, Model, Msg, Page};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
@ -26,30 +27,14 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
let edit_button = StyledButton {
|
||||
variant: ButtonVariant::Empty,
|
||||
icon: Some(StyledIcon::from(Icon::EditAlt).render()),
|
||||
on_click: Some(mouse_ev("click", move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new()
|
||||
.add_path_part("edit-epic")
|
||||
.add_path_part(id.to_string())
|
||||
.go_and_push();
|
||||
Msg::ChangePage(Page::EditEpic(id))
|
||||
})),
|
||||
on_click: Some(events::on_click_goto_edit_epic(id)),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let delete_button = StyledButton {
|
||||
variant: ButtonVariant::Empty,
|
||||
icon: Some(StyledIcon::from(Icon::DeleteAlt).render()),
|
||||
on_click: Some(mouse_ev("click", move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
seed::Url::new()
|
||||
.add_path_part("delete-epic")
|
||||
.add_path_part(id.to_string())
|
||||
.go_and_push();
|
||||
Msg::ChangePage(Page::DeleteEpic(id))
|
||||
})),
|
||||
on_click: Some(events::on_click_goto_delete_epic(id)),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
@ -1,12 +1,14 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::{FieldId, Model, Msg};
|
||||
use crate::components::styled_avatar::*;
|
||||
use crate::components::styled_button::*;
|
||||
use crate::components::styled_icon::*;
|
||||
use crate::components::styled_input::*;
|
||||
use crate::model::PageContent;
|
||||
use crate::{FieldId, Model, Msg};
|
||||
|
||||
use super::super::events;
|
||||
|
||||
pub fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
let project_page = match &model.page_content {
|
||||
@ -28,7 +30,7 @@ pub fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
active: project_page.only_my_filter,
|
||||
text: Some("Only My Issues"),
|
||||
class_list: "filterChild",
|
||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)),
|
||||
on_click: Some(events::on_click_toggle_only_my()),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
@ -37,7 +39,7 @@ pub fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
variant: ButtonVariant::Empty,
|
||||
text: Some("Recently Updated"),
|
||||
class_list: "filterChild",
|
||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)),
|
||||
on_click: Some(events::on_click_toggle_recent()),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
@ -50,7 +52,7 @@ pub fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
id!["clearAllFilters"],
|
||||
C!["filterChild"],
|
||||
"Clear all",
|
||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||
events::on_click_clear_filters()
|
||||
]
|
||||
} else {
|
||||
empty![]
|
||||
@ -72,7 +74,7 @@ pub fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
_ => return empty![],
|
||||
};
|
||||
let active_avatar_filters = &project_page.active_avatar_filters;
|
||||
let avatars: Vec<Node<Msg>> = model
|
||||
let avatars = model
|
||||
.user_ids
|
||||
.iter()
|
||||
.filter_map(|id| model.users_by_id.get(id))
|
||||
@ -83,9 +85,7 @@ pub fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
let styled_avatar = StyledAvatar {
|
||||
avatar_url: user.avatar_url.as_deref(),
|
||||
name: &user.name,
|
||||
on_click: Some(mouse_ev(Ev::Click, move |_| {
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active)
|
||||
})),
|
||||
on_click: Some(events::on_click_filter_by_user(user_id, active)),
|
||||
user_index: idx,
|
||||
..StyledAvatar::default()
|
||||
}
|
||||
@ -95,8 +95,7 @@ pub fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
C!["avatarIsActiveBorder"],
|
||||
styled_avatar
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
});
|
||||
|
||||
div![id!["avatars"], C!["filterChild"], avatars]
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking};
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use jirs_data::{IssueStatus, ProjectCategory, TimeTracking};
|
||||
|
||||
use crate::{FieldId, Msg, ProjectFieldId};
|
||||
use crate::components::styled_button::{ButtonVariant, StyledButton};
|
||||
use crate::components::styled_checkbox::{ChildBuilder, StyledCheckbox, StyledCheckboxState};
|
||||
use crate::components::styled_editor::render_styled_editor;
|
||||
@ -17,7 +19,6 @@ use crate::components::styled_textarea::StyledTextarea;
|
||||
use crate::model::{self, Model, PageContent};
|
||||
use crate::pages::project_settings_page::{events, ProjectSettingsPage};
|
||||
use crate::shared::inner_layout;
|
||||
use crate::{FieldId, Msg, ProjectFieldId};
|
||||
|
||||
static TIME_TRACKING_FIBONACCI: &str = include_str!("./time_tracking_fibonacci.txt");
|
||||
static TIME_TRACKING_HOURLY: &str = include_str!("./time_tracking_hourly.txt");
|
||||
@ -246,7 +247,6 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
|
||||
auto_focus: true,
|
||||
variant: InputVariant::Primary,
|
||||
id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)),
|
||||
input_handlers: vec![],
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
@ -282,7 +282,6 @@ fn column_preview(
|
||||
valid: page.name.is_valid(),
|
||||
variant: InputVariant::Primary,
|
||||
auto_focus: true,
|
||||
input_handlers: vec![],
|
||||
id: Some(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)),
|
||||
..Default::default()
|
||||
}
|
||||
@ -300,14 +299,16 @@ fn show_column_preview(
|
||||
per_column_issue_count: &HashMap<i32, i32>,
|
||||
column_style: &str,
|
||||
) -> Node<Msg> {
|
||||
let id = is.id;
|
||||
let drag_started = events::on_drag_start_start_drag_column(is.id);
|
||||
let drag_stopped = events::on_drag_end_stop_drag_column(is.id);
|
||||
let drag_over_handler = events::on_drag_over_exchange_position(is.id);
|
||||
let drag_out = events::on_drag_leave_leave_drag_column(is.id);
|
||||
let on_edit = events::on_click_edit_column(is.id);
|
||||
|
||||
let issue_count_in_column = per_column_issue_count.get(&id).cloned().unwrap_or_default();
|
||||
let issue_count_in_column = per_column_issue_count
|
||||
.get(&is.id)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let delete_row = if issue_count_in_column == 0 {
|
||||
let delete = StyledButton {
|
||||
variant: ButtonVariant::Primary,
|
||||
|
@ -55,7 +55,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
||||
let legend_margin_width = (dominant as f64).log10() * SVG_MARGIN_X as f64;
|
||||
|
||||
// shapes, groups and texts
|
||||
let mut svg_parts: Vec<Node<Msg>> = vec![];
|
||||
let mut svg_parts: Vec<Node<Msg>> = Vec::with_capacity(40);
|
||||
|
||||
// each piece is part of column drawable view where number of parts depends on
|
||||
// number of issues which have largest amount of issues
|
||||
@ -68,7 +68,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
||||
/ page.last_day.day() as f64;
|
||||
|
||||
let resolution = 10;
|
||||
let mut legend_parts: Vec<Node<Msg>> = vec![];
|
||||
let mut legend_parts: Vec<Node<Msg>> = Vec::with_capacity((resolution + 1) * 2);
|
||||
for y in 0..(resolution + 1) {
|
||||
let current = dominant as f64 * (y as f64 / resolution as f64);
|
||||
|
||||
@ -86,7 +86,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
||||
At::Width => SVG_WIDTH as f64 - (legend_margin_width + SVG_MARGIN_X as f64),
|
||||
At::Height => 1,
|
||||
At::Style => "fill: var(--textLight);",
|
||||
],]);
|
||||
]]);
|
||||
}
|
||||
svg_parts.push(seed::g![legend_parts]);
|
||||
|
||||
@ -115,7 +115,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
|
||||
|
||||
let selected = page.selected_day;
|
||||
let current_date = day;
|
||||
let on_click = mouse_ev("click", move |ev| {
|
||||
let on_click = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DaySelected(
|
||||
|
37
web/src/pages/sign_in_page/events.rs
Normal file
37
web/src/pages/sign_in_page/events.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use seed::prelude::{ev, Ev, EventHandler};
|
||||
|
||||
use crate::Msg;
|
||||
|
||||
type EvHandler = EventHandler<Msg>;
|
||||
|
||||
pub fn on_submit_send_sign_in() -> EvHandler {
|
||||
send_sign_in(Ev::Submit)
|
||||
}
|
||||
|
||||
pub fn on_click_send_sign_in() -> EvHandler {
|
||||
send_sign_in(Ev::Click)
|
||||
}
|
||||
|
||||
fn send_sign_in(event: Ev) -> EvHandler {
|
||||
ev(event, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::SignInRequest
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_submit_bind_client() -> EvHandler {
|
||||
bind_token(Ev::Submit)
|
||||
}
|
||||
|
||||
pub fn on_click_bind_client() -> EvHandler {
|
||||
bind_token(Ev::Click)
|
||||
}
|
||||
|
||||
fn bind_token(event: Ev) -> EvHandler {
|
||||
ev(event, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::BindClientRequest
|
||||
})
|
||||
}
|
@ -2,6 +2,7 @@ pub use model::*;
|
||||
pub use update::*;
|
||||
pub use view::*;
|
||||
|
||||
mod events;
|
||||
pub mod model;
|
||||
pub mod update;
|
||||
pub mod view;
|
||||
|
@ -9,15 +9,30 @@ pub type UsernameValidator = Touched<Between<4, 36>>;
|
||||
pub type EmailValidator = Touched<Chain<Changed<AtLeast<6>>, Changed<EmailFormat>>>;
|
||||
pub type TokenValidator = Touched<Chain<Between<10, 36>, Changed<UuidFormat>>>;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq)]
|
||||
pub enum SignInState {
|
||||
Initial,
|
||||
RequestSend,
|
||||
EmailSend,
|
||||
InvalidPair,
|
||||
}
|
||||
|
||||
impl Default for SignInState {
|
||||
fn default() -> Self {
|
||||
Self::Initial
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SignInPage {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub token: String,
|
||||
pub login_success: bool,
|
||||
pub bad_token: String,
|
||||
// validators
|
||||
pub username_v: UsernameValidator,
|
||||
pub email_v: EmailValidator,
|
||||
pub token_v: TokenValidator,
|
||||
|
||||
pub state: SignInState,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use jirs_data::msg::WsMsgSession;
|
||||
use jirs_data::msg::{WsError, WsMsgSession};
|
||||
use jirs_data::{SignInFieldId, WsMsg};
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
@ -8,6 +8,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::model::{self, Model, Page, PageContent};
|
||||
use crate::pages::sign_in_page::model::SignInPage;
|
||||
use crate::pages::sign_in_page::SignInState;
|
||||
use crate::shared::validate::*;
|
||||
use crate::shared::write_auth_token;
|
||||
use crate::ws::send_ws_msg;
|
||||
@ -39,11 +40,16 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
|
||||
page.token = value;
|
||||
}
|
||||
Msg::SignInRequest => {
|
||||
if page.email.is_empty() || page.username.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
send_ws_msg(
|
||||
WsMsgSession::AuthenticateRequest(page.email.clone(), page.username.clone()).into(),
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
page.state = SignInState::RequestSend;
|
||||
}
|
||||
Msg::BindClientRequest => {
|
||||
let bind_token: uuid::Uuid = match Uuid::from_str(page.token.as_str()) {
|
||||
@ -59,9 +65,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
|
||||
orders,
|
||||
);
|
||||
}
|
||||
Msg::InvalidPair => {
|
||||
page.state = SignInState::InvalidPair;
|
||||
}
|
||||
Msg::WebSocketChange(change) => match change {
|
||||
WebSocketChanged::WsMsg(WsMsg::Session(WsMsgSession::AuthenticateSuccess)) => {
|
||||
page.login_success = true;
|
||||
page.state = SignInState::EmailSend;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::Error(WsError::InvalidPair(_, _))) => {
|
||||
page.state = SignInState::EmailSend;
|
||||
}
|
||||
WebSocketChanged::WsMsg(WsMsg::Session(WsMsgSession::BindTokenOk(access_token))) => {
|
||||
match write_auth_token(Some(access_token)) {
|
||||
|
@ -7,6 +7,7 @@ use crate::components::styled_form::StyledForm;
|
||||
use crate::components::styled_icon::{Icon, StyledIcon};
|
||||
use crate::components::styled_input::StyledInput;
|
||||
use crate::components::styled_link::StyledLink;
|
||||
use crate::pages::sign_in_page::{events, SignInPage, SignInState};
|
||||
use crate::shared::outer_layout;
|
||||
use crate::shared::validate::Validator;
|
||||
use crate::{match_page, model, FieldId, Msg, SignInFieldId};
|
||||
@ -14,47 +15,107 @@ use crate::{match_page, model, FieldId, Msg, SignInFieldId};
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match_page!(model, SignIn; Empty);
|
||||
|
||||
let username = StyledInput {
|
||||
value: page.username.as_str(),
|
||||
valid: page.username_v.is_valid(),
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Username)),
|
||||
err_msg: page.username_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let username_field = StyledField {
|
||||
label: "Username",
|
||||
input: username,
|
||||
let sign_in_form = if page.state == SignInState::EmailSend {
|
||||
StyledForm {
|
||||
fields: vec![StyledField {
|
||||
input: div![
|
||||
C!["emailSend"],
|
||||
div![crate::images::email_send::render()],
|
||||
div!["E-Mail send"]
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.render()],
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
} else {
|
||||
StyledForm {
|
||||
heading: "Sign In to your account",
|
||||
fields: vec![
|
||||
username_field(page),
|
||||
email_field(page),
|
||||
submit_field(page),
|
||||
if page.state == SignInState::InvalidPair {
|
||||
div![C!["error"], p!["Invalid credentials"]]
|
||||
} else {
|
||||
Node::Empty
|
||||
},
|
||||
no_pass_section(),
|
||||
],
|
||||
on_submit: Some(events::on_submit_send_sign_in()),
|
||||
}
|
||||
.render()
|
||||
};
|
||||
|
||||
let bind_token_form = StyledForm {
|
||||
on_submit: Some(events::on_submit_bind_client()),
|
||||
fields: vec![token_field(page), submit_token_field()],
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let email = StyledInput {
|
||||
value: page.email.as_str(),
|
||||
valid: page.email_v.is_valid(),
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Email)),
|
||||
err_msg: page.email_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let email_field = StyledField {
|
||||
label: "E-Mail",
|
||||
input: email,
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
outer_layout(model, "login", vec![sign_in_form, bind_token_form])
|
||||
}
|
||||
|
||||
let submit = if page.login_success {
|
||||
fn submit_token_field() -> Node<Msg> {
|
||||
StyledField {
|
||||
input: StyledButton {
|
||||
variant: ButtonVariant::Primary,
|
||||
text: Some("Authorize"),
|
||||
on_click: Some(events::on_click_bind_client()),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
fn token_field(page: &SignInPage) -> Node<Msg> {
|
||||
StyledField {
|
||||
label: "Single use token",
|
||||
input: StyledInput {
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Token)),
|
||||
valid: page.token_v.is_valid(),
|
||||
value: page.token.as_str(),
|
||||
err_msg: page.token_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
fn no_pass_section() -> Node<Msg> {
|
||||
let help_icon = StyledIcon {
|
||||
icon: Icon::Help,
|
||||
class_list: "noPasswordHelp",
|
||||
size: Some(22),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
div![
|
||||
C!["noPasswordSection"],
|
||||
attrs![At::Title => "We don't believe password is helping anyone. Instead after user provide correct login and e-mail he'll receive mail with 1-use token."],
|
||||
help_icon,
|
||||
span!["Why I don't see password?"]
|
||||
]
|
||||
}
|
||||
|
||||
fn submit_field(page: &SignInPage) -> Node<Msg> {
|
||||
let submit = if page.state == SignInState::RequestSend {
|
||||
StyledButton {
|
||||
variant: ButtonVariant::Success,
|
||||
text: Some("✓ Please check your mail"),
|
||||
text: Some("Checking..."),
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
StyledButton {
|
||||
variant: ButtonVariant::Primary,
|
||||
text: Some("Sign In"),
|
||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::SignInRequest)),
|
||||
on_click: Some(events::on_click_send_sign_in()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@ -66,76 +127,41 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let submit_field = StyledField {
|
||||
StyledField {
|
||||
input: div![C!["twoRow"], submit, register_link],
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let help_icon = StyledIcon {
|
||||
icon: Icon::Help,
|
||||
class_list: "noPasswordHelp",
|
||||
size: Some(22),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let no_pass_section = div![
|
||||
C!["noPasswordSection"],
|
||||
attrs![At::Title => "We don't believe password is helping anyone. Instead after user provide correct login and e-mail he'll receive mail with 1-use token."],
|
||||
help_icon,
|
||||
span!["Why I don't see password?"]
|
||||
];
|
||||
|
||||
let sign_in_form = StyledForm {
|
||||
heading: "Sign In to your account",
|
||||
fields: vec![username_field, email_field, submit_field, no_pass_section],
|
||||
on_submit: Some(ev(Ev::Submit, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::SignInRequest
|
||||
})),
|
||||
}
|
||||
.render();
|
||||
|
||||
let token = StyledInput {
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Token)),
|
||||
valid: page.token_v.is_valid(),
|
||||
value: page.token.as_str(),
|
||||
err_msg: page.token_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let token_field = StyledField {
|
||||
label: "Single use token",
|
||||
input: token,
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let submit_token = StyledButton {
|
||||
variant: ButtonVariant::Primary,
|
||||
text: Some("Authorize"),
|
||||
on_click: Some(mouse_ev(Ev::Click, |_| Msg::BindClientRequest)),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let submit_token_field = StyledField {
|
||||
input: submit_token,
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let bind_token_form = StyledForm {
|
||||
on_submit: Some(ev(Ev::Submit, |ev| {
|
||||
ev.stop_propagation();
|
||||
ev.prevent_default();
|
||||
Msg::BindClientRequest
|
||||
})),
|
||||
fields: vec![token_field, submit_token_field],
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let children = vec![sign_in_form, bind_token_form];
|
||||
outer_layout(model, "login", children)
|
||||
.render()
|
||||
}
|
||||
|
||||
fn username_field(page: &SignInPage) -> Node<Msg> {
|
||||
StyledField {
|
||||
label: "Username",
|
||||
input: StyledInput {
|
||||
value: page.username.as_str(),
|
||||
valid: page.username_v.is_valid(),
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Username)),
|
||||
err_msg: page.username_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
fn email_field(page: &SignInPage) -> Node<Msg> {
|
||||
StyledField {
|
||||
label: "E-Mail",
|
||||
input: StyledInput {
|
||||
value: page.email.as_str(),
|
||||
valid: page.email_v.is_valid(),
|
||||
id: Some(FieldId::SignIn(SignInFieldId::Email)),
|
||||
err_msg: page.email_v.message(),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
@ -32,6 +32,10 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
|
||||
page.email_touched = true;
|
||||
}
|
||||
Msg::SignUpRequest => {
|
||||
if page.email.is_empty() || page.username.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
send_ws_msg(
|
||||
WsMsgSession::SignUpRequest(page.email.clone(), page.username.clone()).into(),
|
||||
model.ws.as_ref(),
|
||||
@ -47,6 +51,9 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Msg::InvalidPair => {
|
||||
page.error = String::from("Pair already taken");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
@ -15,30 +15,28 @@ use crate::{match_page, model, FieldId, Msg};
|
||||
pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let page = match_page!(model, SignUp; Empty);
|
||||
|
||||
let username = StyledInput {
|
||||
value: page.username.as_str(),
|
||||
valid: !page.username_touched || page.username.len() > 1,
|
||||
id: Some(FieldId::SignUp(SignUpFieldId::Username)),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let username_field = StyledField {
|
||||
label: "Username",
|
||||
input: username,
|
||||
input: StyledInput {
|
||||
value: page.username.as_str(),
|
||||
valid: !page.username_touched || page.username.len() > 1,
|
||||
id: Some(FieldId::SignUp(SignUpFieldId::Username)),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let email = StyledInput {
|
||||
value: page.email.as_str(),
|
||||
valid: !page.email_touched || is_email(page.email.as_str()),
|
||||
id: Some(FieldId::SignUp(SignUpFieldId::Email)),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
let email_field = StyledField {
|
||||
label: "E-Mail",
|
||||
input: email,
|
||||
input: StyledInput {
|
||||
value: page.email.as_str(),
|
||||
valid: !page.email_touched || is_email(page.email.as_str()),
|
||||
id: Some(FieldId::SignUp(SignUpFieldId::Email)),
|
||||
..Default::default()
|
||||
}
|
||||
.render(),
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
@ -59,16 +57,18 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
}
|
||||
.render();
|
||||
|
||||
let sign_in_link = StyledLink {
|
||||
children: vec![span!["Sign In"]],
|
||||
class_list: "signInLink",
|
||||
href: "/login",
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
||||
let submit_field = StyledField {
|
||||
input: div![C!["twoRow"], submit, sign_in_link],
|
||||
input: div![
|
||||
C!["twoRow"],
|
||||
submit,
|
||||
StyledLink {
|
||||
children: vec![span!["Sign In"]],
|
||||
class_list: "signInLink",
|
||||
href: "/login",
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
|
@ -1,12 +1,13 @@
|
||||
pub use init_load_sets::*;
|
||||
use jirs_data::msg::{
|
||||
WsMsgComment, WsMsgEpic, WsMsgIssue, WsMsgIssueStatus, WsMsgMessage, WsMsgProject,
|
||||
WsError, WsMsgComment, WsMsgEpic, WsMsgIssue, WsMsgIssueStatus, WsMsgMessage, WsMsgProject,
|
||||
WsMsgSession, WsMsgUser,
|
||||
};
|
||||
use jirs_data::*;
|
||||
use seed::prelude::*;
|
||||
|
||||
use crate::model::*;
|
||||
use crate::pages::sign_in_page::SignInState;
|
||||
use crate::shared::{go_to_board, write_auth_token};
|
||||
use crate::{Msg, OperationKind, ResourceKind, WebSocketChanged};
|
||||
|
||||
@ -432,7 +433,7 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
|
||||
if let Some(idx) = model.epics.iter().position(|e| e.id == *id) {
|
||||
model.epics.remove(idx);
|
||||
}
|
||||
model.epics_by_id.remove(&id);
|
||||
model.epics_by_id.remove(id);
|
||||
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
orders.send_msg(Msg::ResourceChanged(
|
||||
ResourceKind::Epic,
|
||||
@ -442,7 +443,10 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
|
||||
}
|
||||
WsMsg::Session(WsMsgSession::AuthenticateSuccess) => {
|
||||
let page = crate::match_page_mut!(model, SignIn);
|
||||
page.login_success = true;
|
||||
page.state = SignInState::EmailSend;
|
||||
}
|
||||
WsMsg::Error(WsError::InvalidLoginPair) => {
|
||||
orders.send_msg(Msg::InvalidPair);
|
||||
}
|
||||
WsMsg::Session(WsMsgSession::BindTokenOk(access_token)) => {
|
||||
match write_auth_token(Some(*access_token)) {
|
||||
@ -454,6 +458,9 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
|
||||
}
|
||||
}
|
||||
}
|
||||
WsMsg::Session(WsMsgSession::SignUpPairTaken) => {
|
||||
orders.send_msg(Msg::InvalidPair);
|
||||
}
|
||||
_ => {
|
||||
log::info!(
|
||||
"got web socket message but don't know what to do with it {:?}",
|
||||
|
Loading…
Reference in New Issue
Block a user