Optimizations
This commit is contained in:
parent
1c666dc26a
commit
82971adb48
792
Cargo.lock
generated
792
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -20,7 +20,7 @@ default = []
|
||||
[dependencies]
|
||||
jirs-data = { path = "../shared/jirs-data", features = ["frontend"] }
|
||||
|
||||
seed = { version = "0.8.0" }
|
||||
seed = { git = "https://github.com/seed-rs/seed.git" }
|
||||
|
||||
serde = { version = "*" }
|
||||
serde_json = { version = "*" }
|
||||
|
@ -162,6 +162,26 @@ pub struct StyledButton<'l> {
|
||||
button_id: Option<ButtonId>,
|
||||
}
|
||||
|
||||
impl<'l> StyledButton<'l> {
|
||||
pub fn secondary_with_text_and_icon<I>(text: &'l str, icon: I) -> Self
|
||||
where
|
||||
I: ToNode,
|
||||
{
|
||||
Self {
|
||||
variant: Variant::Secondary,
|
||||
disabled: false,
|
||||
active: false,
|
||||
text: Some(text),
|
||||
icon: Some(icon.into_node()),
|
||||
on_click: None,
|
||||
children: vec![],
|
||||
class_list: vec![],
|
||||
button_type: "",
|
||||
button_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> StyledButton<'l> {
|
||||
#[inline(always)]
|
||||
pub fn build() -> StyledButtonBuilder<'l> {
|
||||
|
@ -169,7 +169,7 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||
C!["viewRadio"],
|
||||
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
|
||||
],
|
||||
Node::from_html(html.as_str()),
|
||||
Node::from_html(None, html.as_str()),
|
||||
),
|
||||
};
|
||||
|
||||
|
@ -56,7 +56,7 @@ impl<'l> StyledFieldBuilder<'l> {
|
||||
StyledField {
|
||||
label: self.label.unwrap_or_default(),
|
||||
tip: self.tip,
|
||||
input: self.input.unwrap_or_else(|| empty![]),
|
||||
input: self.input.unwrap_or(empty![]),
|
||||
class_list: self.class_list,
|
||||
}
|
||||
}
|
||||
@ -69,10 +69,7 @@ pub fn render(values: StyledField) -> Node<Msg> {
|
||||
input,
|
||||
class_list,
|
||||
} = values;
|
||||
let tip_node = match tip {
|
||||
Some(s) => div![attrs![At::Class => "styledTip"], s],
|
||||
_ => empty![],
|
||||
};
|
||||
let tip_node = tip.map(|s| div![C!["styledTip"], s]).unwrap_or(empty![]);
|
||||
|
||||
div![
|
||||
attrs![At::Class => class_list.join(" "), At::Class => "styledField"],
|
||||
|
@ -113,6 +113,22 @@ pub struct StyledInput<'l, 'm: 'l> {
|
||||
}
|
||||
|
||||
impl<'l, 'm: 'l> StyledInput<'l, 'm> {
|
||||
#[inline]
|
||||
pub fn new_with_id_and_value_and_valid(id: FieldId, value: &'m str, valid: bool) -> Self {
|
||||
Self {
|
||||
id,
|
||||
icon: None,
|
||||
valid,
|
||||
value: Some(value),
|
||||
input_type: None,
|
||||
input_class_list: vec![],
|
||||
wrapper_class_list: vec![],
|
||||
variant: Variant::Normal,
|
||||
auto_focus: false,
|
||||
input_handlers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn build() -> StyledInputBuilder<'l, 'm> {
|
||||
StyledInputBuilder {
|
||||
|
@ -39,6 +39,18 @@ pub struct StyledModal<'l> {
|
||||
class_list: Vec<&'l str>,
|
||||
}
|
||||
|
||||
impl<'l> StyledModal<'l> {
|
||||
pub fn centered_with_width_and_body(width: usize, children: Vec<Node<Msg>>) -> Self {
|
||||
Self {
|
||||
variant: Variant::Center,
|
||||
width: Some(width),
|
||||
with_icon: false,
|
||||
children,
|
||||
class_list: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> ToNode for StyledModal<'l> {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
|
@ -1,4 +1,4 @@
|
||||
#![feature(or_patterns, type_ascription, trait_alias, drain_filter)]
|
||||
#![feature(type_ascription, trait_alias, drain_filter)]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
|
@ -1,4 +1,4 @@
|
||||
pub use {view::*, model::*};
|
||||
pub use {model::*, view::*};
|
||||
|
||||
mod view;
|
||||
mod model;
|
||||
mod view;
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -20,18 +20,13 @@ use {
|
||||
mod comments;
|
||||
|
||||
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let issue_id = modal.id;
|
||||
if let Some(_issue) = model.issues_by_id.get(&issue_id) {
|
||||
let details = details(model, modal);
|
||||
StyledModal::build()
|
||||
.variant(crate::components::styled_modal::Variant::Center)
|
||||
.width(1040)
|
||||
.child(details)
|
||||
.build()
|
||||
.into_node()
|
||||
} else {
|
||||
Node::Empty
|
||||
}
|
||||
model
|
||||
.issues_by_id
|
||||
.get(&modal.id)
|
||||
.map(|_issue| {
|
||||
StyledModal::centered_with_width_and_body(1040, vec![details(model, modal)]).into_node()
|
||||
})
|
||||
.unwrap_or(Node::Empty)
|
||||
}
|
||||
|
||||
pub fn details(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
|
@ -0,0 +1 @@
|
||||
|
@ -4,66 +4,56 @@ use {
|
||||
};
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let mut nodes = Vec::with_capacity(model.modal_stack().len());
|
||||
|
||||
for modal_type in model.modal_stack() {
|
||||
match modal_type {
|
||||
ModalType::AddIssue(_) => {
|
||||
if let Some(modal) = &model.modals().add_issue {
|
||||
let node = crate::modals::issues_create::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::EditIssue(_) => {
|
||||
if let Some(modal) = &model.modals().edit_issue {
|
||||
let node = crate::modals::issues_edit::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::DeleteEpic(_) => {
|
||||
if let Some(modal) = &model.modals().delete_epic {
|
||||
let node = crate::modals::epics_delete::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::EditEpic(_) => {
|
||||
if let Some(modal) = &model.modals().edit_epic {
|
||||
let node = crate::modals::epics_edit::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::DeleteIssueConfirm(_) => {
|
||||
if let Some(_issue_id) = &model.modals().delete_issue_confirm {
|
||||
let node = crate::modals::issues_delete::view(model);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::DeleteCommentConfirm(_) => {
|
||||
if let Some(modal) = &model.modals().delete_comment_confirm {
|
||||
let node = crate::modals::comments_delete::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::TimeTracking(_) => {
|
||||
if let Some(modal) = &model.modals().time_tracking {
|
||||
let node = crate::modals::time_tracking::view(model, modal);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
ModalType::DeleteIssueStatusModal(_) => {
|
||||
if let Some(modal) = &model.modals().delete_issue_status_modal {
|
||||
let node = crate::modals::issue_statuses_delete::view(model, modal.delete_id);
|
||||
nodes.push(node);
|
||||
}
|
||||
}
|
||||
let nodes = model
|
||||
.modal_stack()
|
||||
.iter()
|
||||
.filter_map(|modal_type| match modal_type {
|
||||
ModalType::AddIssue(_) => model
|
||||
.modals()
|
||||
.add_issue
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::issues_create::view(model, modal)),
|
||||
ModalType::EditIssue(_) => model
|
||||
.modals()
|
||||
.edit_issue
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::issues_edit::view(model, modal)),
|
||||
ModalType::DeleteEpic(_) => model
|
||||
.modals()
|
||||
.delete_epic
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::epics_delete::view(model, modal)),
|
||||
ModalType::EditEpic(_) => model
|
||||
.modals()
|
||||
.edit_epic
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::epics_edit::view(model, modal)),
|
||||
ModalType::DeleteIssueConfirm(_) => model
|
||||
.modals()
|
||||
.delete_issue_confirm
|
||||
.as_ref()
|
||||
.map(|_id| crate::modals::issues_delete::view(model)),
|
||||
ModalType::DeleteCommentConfirm(_) => model
|
||||
.modals()
|
||||
.delete_comment_confirm
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::comments_delete::view(model, modal)),
|
||||
ModalType::TimeTracking(_) => model
|
||||
.modals()
|
||||
.time_tracking
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::time_tracking::view(model, modal)),
|
||||
ModalType::DeleteIssueStatusModal(_) => model
|
||||
.modals()
|
||||
.delete_issue_status_modal
|
||||
.as_ref()
|
||||
.map(|modal| crate::modals::issue_statuses_delete::view(model, modal.delete_id)),
|
||||
#[cfg(debug_assertions)]
|
||||
ModalType::DebugModal(_) => {
|
||||
if let Some(true) = &model.modals().debug_modal {
|
||||
let node = crate::modals::debug::view(model);
|
||||
nodes.push(node)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
ModalType::DebugModal(_) => model
|
||||
.modals()
|
||||
.debug_modal
|
||||
.as_ref()
|
||||
.map(|_| crate::modals::debug::view(model)),
|
||||
});
|
||||
section![id!["modals"], nodes]
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use {crate::shared::drag::DragState, jirs_data::*, std::collections::HashMap};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct StatusIssueIds {
|
||||
pub status_id: IssueStatusId,
|
||||
pub status_name: IssueStatusName,
|
||||
pub issue_ids: Vec<EpicId>,
|
||||
pub issue_ids: Vec<IssueId>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@ -31,57 +32,67 @@ impl ProjectPage {
|
||||
issues: &[Issue],
|
||||
user: &Option<User>,
|
||||
) -> Vec<EpicIssuePerStatus> {
|
||||
let mut map = vec![];
|
||||
let epics = vec![None]
|
||||
.into_iter()
|
||||
.chain(epics.iter().map(|s| Some((s.id, s.name.as_str()))));
|
||||
|
||||
let statuses = statuses.iter().map(|s| (s.id, s.name.as_str()));
|
||||
|
||||
let mut issues: Vec<&Issue> = issues.iter().collect();
|
||||
if page.recently_updated_filter {
|
||||
let issues = issues.iter().filter(|issue| {
|
||||
issue_filter_with_avatars(issue, &page.active_avatar_filters)
|
||||
&& issue_filter_with_text(issue, page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, page.only_my_filter, user)
|
||||
});
|
||||
let issues = if page.recently_updated_filter {
|
||||
let mut m = HashMap::new();
|
||||
let mut sorted = vec![];
|
||||
for issue in issues.into_iter() {
|
||||
sorted.push((issue.id, issue.updated_at));
|
||||
let mut sorted: Vec<(IssueId, NaiveDateTime)> = issues
|
||||
.map(|issue| {
|
||||
m.insert(issue.id, issue);
|
||||
}
|
||||
(issue.id, issue.updated_at)
|
||||
})
|
||||
.collect();
|
||||
sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
issues = sorted
|
||||
let mut issues: Vec<&Issue> = sorted
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.flat_map(|(id, _)| m.remove(&id))
|
||||
.collect();
|
||||
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
}
|
||||
issues
|
||||
} else {
|
||||
issues.collect()
|
||||
};
|
||||
|
||||
for epic in epics {
|
||||
let issues_per_epic_id = issues.into_iter().fold(HashMap::new(), |mut m, issue| {
|
||||
m.entry(issue.epic_id).or_insert_with(Vec::new).push(issue);
|
||||
m
|
||||
});
|
||||
|
||||
epics
|
||||
.map(|epic| {
|
||||
let mut per_epic_map = EpicIssuePerStatus {
|
||||
epic_ref: epic.map(|(id, name)| (id, name.to_string())),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (current_status_id, issue_status_name) in statuses.to_owned() {
|
||||
let mut per_status_map = StatusIssueIds {
|
||||
let per_status_map = StatusIssueIds {
|
||||
status_id: current_status_id,
|
||||
status_name: issue_status_name.to_string(),
|
||||
..Default::default()
|
||||
issue_ids: issues_per_epic_id
|
||||
.get(&epic.map(|(id, _)| id))
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.filter(|issue| issue_filter_status(issue, current_status_id))
|
||||
.map(|issue| issue.id)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
for issue in issues.iter() {
|
||||
if issue.epic_id == epic.map(|(id, _)| id)
|
||||
&& issue_filter_status(issue, current_status_id)
|
||||
&& issue_filter_with_avatars(issue, &page.active_avatar_filters)
|
||||
&& issue_filter_with_text(issue, page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, page.only_my_filter, user)
|
||||
{
|
||||
per_status_map.issue_ids.push(issue.id);
|
||||
}
|
||||
}
|
||||
per_epic_map.per_status_issues.push(per_status_map);
|
||||
}
|
||||
map.push(per_epic_map);
|
||||
}
|
||||
map
|
||||
per_epic_map
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,12 +42,7 @@ fn header(model: &Model) -> Node<Msg> {
|
||||
if !model.show_extras {
|
||||
return Node::Empty;
|
||||
}
|
||||
let button = StyledButton::build()
|
||||
.secondary()
|
||||
.text("Repository")
|
||||
.icon(Icon::Github)
|
||||
.build()
|
||||
.into_node();
|
||||
let button = StyledButton::secondary_with_text_and_icon("Repository", Icon::Github).into_node();
|
||||
div![
|
||||
id!["projectBoardHeader"],
|
||||
div![id!["boardName"], C!["headerChild"], "Kanban board"],
|
||||
|
@ -206,7 +206,7 @@ fn issue_list(page: &ReportsPage, project_name: &str, this_month_updated: &[&Iss
|
||||
let priority_icon = StyledIcon::build(priority.clone().into())
|
||||
.build()
|
||||
.into_node();
|
||||
let desc = Node::from_html(
|
||||
let desc = Node::from_html(None,
|
||||
description
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
|
@ -24,7 +24,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
|
||||
let username = StyledInput::build()
|
||||
.value(page.username.as_str())
|
||||
.valid(!page.username_touched || page.username.len() > 1)
|
||||
.valid(is_valid_username(page.username_touched, &page.username))
|
||||
.build(FieldId::SignIn(SignInFieldId::Username))
|
||||
.into_node();
|
||||
let username_field = StyledField::build()
|
||||
@ -35,7 +35,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
|
||||
let email = StyledInput::build()
|
||||
.value(page.email.as_str())
|
||||
.valid(!page.email_touched || is_email(page.email.as_str()))
|
||||
.valid(is_valid_email(page.email_touched, page.email.as_str()))
|
||||
.build(FieldId::SignIn(SignInFieldId::Email))
|
||||
.into_node();
|
||||
let email_field = StyledField::build()
|
||||
@ -94,10 +94,11 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let token = StyledInput::build()
|
||||
.value(page.token.as_str())
|
||||
.valid(!page.token_touched || is_token(page.token.as_str()))
|
||||
.build(FieldId::SignIn(SignInFieldId::Token))
|
||||
let token = StyledInput::new_with_id_and_value_and_valid(
|
||||
FieldId::SignIn(SignInFieldId::Token),
|
||||
&page.token,
|
||||
is_valid_token(page.token_touched, page.token.as_str()),
|
||||
)
|
||||
.into_node();
|
||||
let token_field = StyledField::build()
|
||||
.label("Single use token")
|
||||
@ -126,3 +127,15 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
let children = vec![sign_in_form, bind_token_form];
|
||||
outer_layout(model, "login", children)
|
||||
}
|
||||
|
||||
fn is_valid_username(touched: bool, s: &str) -> bool {
|
||||
!touched || (s.len() > 1 && s.len() < 20)
|
||||
}
|
||||
|
||||
fn is_valid_email(touched: bool, s: &str) -> bool {
|
||||
!touched || (is_email(s) && s.len() < 20)
|
||||
}
|
||||
|
||||
fn is_valid_token(touched: bool, s: &str) -> bool {
|
||||
!touched || is_token(s)
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
#![feature(async_closure)]
|
||||
#![feature(vec_remove_item)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
use {
|
||||
|
@ -1,6 +1,9 @@
|
||||
use {
|
||||
serde::{de::DeserializeOwned, export::PhantomData, Serialize},
|
||||
std::fs::{read_to_string, write},
|
||||
serde::{de::DeserializeOwned, Serialize},
|
||||
std::{
|
||||
fs::{read_to_string, write},
|
||||
marker::PhantomData,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Reader<T: DeserializeOwned + Default + Serialize> {
|
||||
|
Loading…
Reference in New Issue
Block a user