From d49c92cd532bac2403a6b125522848cbd9d178d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Fri, 15 Oct 2021 14:40:51 +0200 Subject: [PATCH] Reduce model memory usage --- shared/jirs-data/src/lib.rs | 2 +- web/src/modals/epic_field.rs | 12 +- web/src/modals/issues_edit/update.rs | 8 +- web/src/modals/issues_edit/view.rs | 25 +++- web/src/model.rs | 22 +--- web/src/pages/epics_page/view.rs | 62 ++++----- web/src/pages/project_page/model.rs | 26 ++-- web/src/pages/project_settings_page/update.rs | 114 +++++++---------- web/src/pages/project_settings_page/view.rs | 10 +- web/src/shared/navbar_left.rs | 2 +- web/src/ws/issue.rs | 11 +- web/src/ws/mod.rs | 119 ++++++++++-------- 12 files changed, 218 insertions(+), 195 deletions(-) diff --git a/shared/jirs-data/src/lib.rs b/shared/jirs-data/src/lib.rs index 8210e189..90afae93 100644 --- a/shared/jirs-data/src/lib.rs +++ b/shared/jirs-data/src/lib.rs @@ -258,7 +258,7 @@ pub struct Issue { pub struct IssueStatus { pub id: IssueStatusId, pub name: String, - pub position: ProjectId, + pub position: ListPosition, pub project_id: ProjectId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, diff --git a/web/src/modals/epic_field.rs b/web/src/modals/epic_field.rs index ec81ab4d..60baa77e 100644 --- a/web/src/modals/epic_field.rs +++ b/web/src/modals/epic_field.rs @@ -11,7 +11,7 @@ pub fn epic_field(model: &Model, modal: &Modal, field_id: FieldId) -> Opt where Modal: IssueModal, { - if model.epics.is_empty() { + if model.epic_ids.is_empty() { return None; } let input = StyledSelect { @@ -19,10 +19,16 @@ where name: "epic", selected: vec![modal .epic_id_value() - .and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId)) + .and_then(|id| model.epics_by_id.get(&(id as EpicId))) .map(epic_select_option) .unwrap_or_default()], - options: Some(model.epics.iter().map(epic_select_option)), + options: Some( + model + .epic_ids + .iter() + .filter_map(|id| model.epics_by_id.get(id)) + .map(epic_select_option), + ), variant: SelectVariant::Normal, clearable: true, text_filter: modal.epic_state().text_filter.as_str(), diff --git a/web/src/modals/issues_edit/update.rs b/web/src/modals/issues_edit/update.rs index 8f73d49b..b2a0ff56 100644 --- a/web/src/modals/issues_edit/update.rs +++ b/web/src/modals/issues_edit/update.rs @@ -339,15 +339,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), comment_id, )) => { - let id = *comment_id; let body = model - .comments - .iter() - .find(|c| c.id == id) + .comments_by_id + .get(comment_id) .map(|c| c.body.clone()) .unwrap_or_default(); modal.comment_form.body = body; - modal.comment_form.id = Some(id); + modal.comment_form.id = Some(*comment_id); modal.comment_form.creating = true; } Msg::DeleteComment(comment_id) => { diff --git a/web/src/modals/issues_edit/view.rs b/web/src/modals/issues_edit/view.rs index a05128b2..73be9371 100644 --- a/web/src/modals/issues_edit/view.rs +++ b/web/src/modals/issues_edit/view.rs @@ -243,7 +243,11 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { vec![div![C!["fakeTextArea"], "Add a comment...", handler]] }; - let comments = model.comments.iter().flat_map(|c| comment(model, modal, c)); + let comments = model + .comment_ids + .iter() + .flat_map(|id| model.comments_by_id.get(id)) + .flat_map(|c| comment(model, modal, c)); div![ C!["left"], @@ -403,11 +407,22 @@ fn status_select( opened: status_state.opened, variant: SelectVariant::Normal, text_filter: status_state.text_filter.as_str(), - options: Some(model.issue_statuses.iter().map(issue_status_select_option)), + options: Some( + model + .issue_status_ids + .iter() + .filter_map(|id| model.issue_statuses_by_id.get(id)) + .map(issue_status_select_option), + ), selected: model - .issue_statuses + .issue_status_ids .iter() - .filter(|is| is.id == payload.issue_status_id) + .filter_map(|id| { + model + .issue_statuses_by_id + .get(id) + .filter(|is| is.id == payload.issue_status_id) + }) .map(issue_status_select_option) .collect(), @@ -424,7 +439,7 @@ fn status_select( } #[inline(always)] -fn issue_status_select_option<'l>(is: &'l IssueStatus) -> StyledSelectOption<'l> { +fn issue_status_select_option(is: &IssueStatus) -> StyledSelectOption<'_> { StyledSelectOption { value: is.id as u32, class_list: is.name.as_str(), diff --git a/web/src/model.rs b/web/src/model.rs index 55cf0e56..47f5ef33 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -254,11 +254,11 @@ pub struct Model { pub user_settings: Option, // comments - pub comments: Vec, + pub comment_ids: Vec, pub comments_by_id: HashMap, // issue_statuses - pub issue_statuses: Vec, + pub issue_status_ids: Vec, pub issue_statuses_by_id: HashMap, pub issue_statuses_by_name: HashMap, @@ -273,7 +273,7 @@ pub struct Model { pub projects: Vec, // epics - pub epics: Vec, + pub epic_ids: Vec, pub epics_by_id: HashMap, pub key_triggers: std::rc::Rc>>>, @@ -304,15 +304,15 @@ impl Model { user_ids: vec![], users_by_id: HashMap::with_capacity(1_000), user_settings: None, - comments: vec![], + comment_ids: vec![], comments_by_id: HashMap::with_capacity(1_000), - issue_statuses: vec![], + issue_status_ids: vec![], issue_statuses_by_id: HashMap::with_capacity(1_000), issue_statuses_by_name: HashMap::with_capacity(1_000), messages: vec![], user_projects: vec![], projects: vec![], - epics: vec![], + epic_ids: vec![], issues_by_id: HashMap::with_capacity(1_000), show_extras: false, epics_by_id: HashMap::with_capacity(1_000), @@ -323,16 +323,6 @@ impl Model { } } - #[inline(always)] - pub fn issue_statuses(&self) -> &[IssueStatus] { - &self.issue_statuses - } - - #[inline(always)] - pub fn epics(&self) -> &[Epic] { - &self.epics - } - #[inline(always)] pub fn user(&self) -> &Option { &self.user diff --git a/web/src/pages/epics_page/view.rs b/web/src/pages/epics_page/view.rs index 4bd3fbe7..ca9f4a95 100644 --- a/web/src/pages/epics_page/view.rs +++ b/web/src/pages/epics_page/view.rs @@ -11,38 +11,42 @@ use crate::Msg; pub fn view(model: &Model) -> Node { let page = crate::match_page!(model, Epics; Empty); - 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), - ) - }) - .collect::>>() - }); + let epics = model + .epic_ids + .iter() + .filter_map(|id| model.epics_by_id.get(id)) + .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), + ) + }) + .collect::>>() + }); - li![ - C!["epic"], - div![ - C!["firstRow"], - div![C!["epicName"], &epic.name], + li![ + C!["epic"], div![ - C!["date"], - date_field("Starts at:", "startsAt", epic.starts_at.as_ref()), - date_field("Ends at:", "endsAt", epic.ends_at.as_ref()), + 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.as_ref().map(Vec::len).unwrap_or(0) + ], ], - div![ - C!["counter"], - "Number of issues:", - issues.as_ref().map(Vec::len).unwrap_or(0) - ], - ], - div![C!["secondRow"], div![C!["issues"], issues]] - ] - }); + div![C!["secondRow"], div![C!["issues"], issues]] + ] + }); inner_layout( model, diff --git a/web/src/pages/project_page/model.rs b/web/src/pages/project_page/model.rs index 00b360e3..9d21408a 100644 --- a/web/src/pages/project_page/model.rs +++ b/web/src/pages/project_page/model.rs @@ -29,24 +29,26 @@ pub struct ProjectPage { } impl ProjectPage { - pub fn visible_issues<'issue, IssueStream>( + pub fn visible_issues<'model, IssueStream, IssueStatusStream, EpicStream>( page: &ProjectPage, - epics: &[Epic], - statuses: &[IssueStatus], + num_of_epics: usize, + epics: EpicStream, + statuses: IssueStatusStream, issues: IssueStream, user: &Option, ) -> Vec where - IssueStream: std::iter::Iterator, + IssueStream: std::iter::Iterator, + IssueStatusStream: std::iter::Iterator, + EpicStream: std::iter::Iterator, { - let num_of_epics = epics.len(); let epics = vec![None].into_iter().chain( - epics - .iter() - .map(|epic| Some((epic.id, epic.name.as_str(), epic.starts_at, epic.ends_at))), + epics.map(|epic| Some((epic.id, epic.name.as_str(), epic.starts_at, epic.ends_at))), ); - let statuses = statuses.iter().map(|s| (s.id, s.name.as_str())); + let statuses = statuses + .map(|s| (s.id, s.name.as_str())) + .collect::>(); let issues = issues.filter(|issue| { issue_filter_with_avatars(issue, &page.active_avatar_filters) && issue_filter_with_text(issue, page.text_filter.as_str()) @@ -93,15 +95,15 @@ impl ProjectPage { ..Default::default() }; - for (current_status_id, issue_status_name) in statuses.to_owned() { + for (current_status_id, issue_status_name) in statuses.iter() { let per_status_map = StatusIssueIds { - status_id: current_status_id, + status_id: *current_status_id, status_name: issue_status_name.to_string(), issue_ids: issues_per_epic_id .get(&epic.map(|(id, ..)| id)) .map(|v| { v.iter() - .filter(|issue| issue_filter_status(issue, current_status_id)) + .filter(|issue| issue_filter_status(issue, *current_status_id)) .map(|issue| issue.id) .collect() }) diff --git a/web/src/pages/project_settings_page/update.rs b/web/src/pages/project_settings_page/update.rs index 7b6434e1..072b21aa 100644 --- a/web/src/pages/project_settings_page/update.rs +++ b/web/src/pages/project_settings_page/update.rs @@ -7,7 +7,7 @@ use seed::prelude::Orders; use crate::components::styled_select::StyledSelectChanged; use crate::model::{Model, Page, PageContent}; use crate::pages::project_settings_page::ProjectSettingsPage; -use crate::ws::{board_load, send_ws_msg}; +use crate::ws::{board_load, send_ws_msg, sort_issue_statuses}; use crate::{match_page_mut, FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged}; pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { @@ -114,7 +114,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { ))) => page.column_drag.clear_last(), Msg::PageChanged(PageChanged::ProjectSettings( ProjectPageChange::ColumnExchangePosition(issue_bellow_id), - )) => exchange_position(issue_bellow_id, model), + )) => swap_position(issue_bellow_id, model), Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone( _issue_status_id, ))) => { @@ -124,39 +124,35 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { id, ))) => { if page.edit_column_id.is_some() && id.is_none() { - let old_id = page.edit_column_id.as_ref().cloned(); - let name = page.name.value.clone(); - if let Some((id, pos)) = model - .issue_statuses - .iter() - .find(|is| Some(is.id) == old_id) - .map(|is| (is.id, is.position)) - { - send_ws_msg( - WsMsgIssueStatus::IssueStatusUpdate(id, name, pos).into(), - model.ws.as_ref(), - orders, - ); + if let Some(old_id) = page.edit_column_id { + let name = page.name.value.clone(); + if let Some((id, pos)) = model + .issue_statuses_by_id + .get(&old_id) + .map(|is| (is.id, is.position)) + { + send_ws_msg( + WsMsgIssueStatus::IssueStatusUpdate(id, name, pos).into(), + model.ws.as_ref(), + orders, + ); + } } } - page.name.value = model - .issue_statuses - .iter() - .find_map(|is| { - if Some(is.id) == id { - Some(is.name.clone()) - } else { - None - } - }) - .unwrap_or_default(); + if let Some(id) = id { + page.name.value = model + .issue_statuses_by_id + .get(&id) + .map(|is| is.name.clone()) + .unwrap_or_default(); + } page.edit_column_id = id; } Msg::PageChanged(PageChanged::ProjectSettings( ProjectPageChange::SubmitIssueStatusForm, )) => { let name = page.name.value.clone(); - let position = model.issue_statuses.len(); + let position = model.issue_status_ids.len(); let ws_msg = WsMsgIssueStatus::IssueStatusCreate(name, position as i32).into(); send_ws_msg(ws_msg, model.ws.as_ref(), orders); } @@ -164,11 +160,8 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } } -fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) { - let page = match &mut model.page_content { - PageContent::ProjectSettings(page) => page, - _ => return, - }; +fn swap_position(bellow_id: IssueStatusId, model: &mut Model) { + let page = crate::match_page_mut!(model, ProjectSettings); if page.column_drag.dragged_or_last(bellow_id) { return; } @@ -177,41 +170,29 @@ fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) { _ => return log::error!("Nothing is dragged"), }; - let mut below = None; - let mut dragged = None; - let mut issues_statuses = vec![]; - std::mem::swap(&mut issues_statuses, &mut model.issue_statuses); + let bellow = model + .issue_statuses_by_id + .get(&bellow_id) + .map(|is| is.position) + .unwrap_or_default(); + let dragged = model + .issue_statuses_by_id + .get(&dragged_id) + .map(|is| is.position) + .unwrap_or_default(); - for issue_status in issues_statuses.into_iter() { - match issue_status.id { - id if id == bellow_id => below = Some(issue_status), - id if id == dragged_id => dragged = Some(issue_status), - _ => model.issue_statuses.push(issue_status), - }; + if let Some(is) = model.issue_statuses_by_id.get_mut(&dragged_id) { + is.position = bellow; + } + if let Some(is) = model.issue_statuses_by_id.get_mut(&bellow_id) { + is.position = dragged; } - let mut below = match below { - Some(below) => below, - _ => return, - }; - let mut dragged = match dragged { - Some(issue_status) => issue_status, - _ => { - model.issue_statuses.push(below); - return; - } - }; - std::mem::swap(&mut dragged.position, &mut below.position); + page.column_drag.mark_dirty(dragged_id); + page.column_drag.mark_dirty(bellow_id); - page.column_drag.mark_dirty(dragged.id); - page.column_drag.mark_dirty(below.id); - - model.issue_statuses.push(below); - model.issue_statuses.push(dragged); - model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); page.column_drag.last_id = Some(bellow_id); + sort_issue_statuses(model); } fn sync(model: &mut Model, orders: &mut impl Orders) { @@ -224,11 +205,10 @@ fn sync(model: &mut Model, orders: &mut impl Orders) { _ => return log::error!("bad content type"), }; for id in dirty { - let IssueStatus { name, position, .. } = - match model.issue_statuses.iter().find(|is| is.id == id) { - Some(is) => is, - _ => continue, - }; + let IssueStatus { name, position, .. } = match model.issue_statuses_by_id.get(&id) { + Some(is) => is, + _ => continue, + }; send_ws_msg( WsMsgIssueStatus::IssueStatusUpdate(id, name.clone(), *position).into(), model.ws.as_ref(), diff --git a/web/src/pages/project_settings_page/view.rs b/web/src/pages/project_settings_page/view.rs index 6b861fde..ea68895f 100644 --- a/web/src/pages/project_settings_page/view.rs +++ b/web/src/pages/project_settings_page/view.rs @@ -203,7 +203,7 @@ fn category_select_option<'l>(pc: ProjectCategory) -> StyledSelectOption<'l> { /// Build draggable columns preview with option to remove and add new columns #[inline(always)] fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node { - let width = 100f64 / (model.issue_statuses.len() + 1) as f64; + let width = 100f64 / (model.issue_status_ids.len() + 1) as f64; let column_style = format!("width: calc({width}% - 10px)", width = width); let per_column_issue_count = model .issue_ids @@ -216,11 +216,11 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node { h }, ); - let columns: Vec> = model - .issue_statuses + let columns = model + .issue_status_ids .iter() - .map(|is| column_preview(is, page, &per_column_issue_count, column_style.as_str())) - .collect(); + .filter_map(|id| model.issue_statuses_by_id.get(id)) + .map(|is| column_preview(is, page, &per_column_issue_count, column_style.as_str())); let columns_section = section![ C!["columnsSection"], diff --git a/web/src/shared/navbar_left.rs b/web/src/shared/navbar_left.rs index bf03af3d..fe15f684 100644 --- a/web/src/shared/navbar_left.rs +++ b/web/src/shared/navbar_left.rs @@ -81,7 +81,7 @@ pub fn render(model: &Model) -> Vec> { }, ); - let issue_nav = if model.issue_statuses.is_empty() { + let issue_nav = if model.issue_status_ids.is_empty() { vec![] } else { vec![ diff --git a/web/src/ws/issue.rs b/web/src/ws/issue.rs index 9bf97638..6d4e67fc 100644 --- a/web/src/ws/issue.rs +++ b/web/src/ws/issue.rs @@ -168,8 +168,15 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) -> bool { pub fn change_visible(model: &mut Model) { let visible = ProjectPage::visible_issues( crate::match_page!(model, Project), - model.epics(), - model.issue_statuses(), + model.epic_ids.len(), + model + .epic_ids + .iter() + .filter_map(|id| model.epics_by_id.get(id)), + model + .issue_status_ids + .iter() + .filter_map(|id| model.issue_statuses_by_id.get(id)), model .issue_ids .iter() diff --git a/web/src/ws/mod.rs b/web/src/ws/mod.rs index c8aedaf5..101127e0 100644 --- a/web/src/ws/mod.rs +++ b/web/src/ws/mod.rs @@ -197,10 +197,20 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) // issue statuses WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusesLoaded(v)) => { - model.issue_statuses = std::mem::take(v); - model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + let len = v.len(); + let mut issue_statuses = std::mem::take(v); + issue_statuses.sort_by(|a, b| a.position.cmp(&b.position)); + model.issue_status_ids = Vec::with_capacity(len); + + model.issue_statuses_by_id = + issue_statuses + .into_iter() + .fold(HashMap::with_capacity(len), |mut h, is| { + model.issue_status_ids.push(is.id); + h.insert(is.id, is); + h + }); + orders.send_msg(Msg::ResourceChanged( ResourceKind::IssueStatus, OperationKind::ListLoaded, @@ -208,42 +218,33 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) )); } WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusCreated(is)) => { - let id = is.id; - model.issue_statuses.push(is.clone()); - model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + model.issue_status_ids.push(is.id); + model.issue_statuses_by_id.insert(is.id, is.clone()); + orders.send_msg(Msg::ResourceChanged( ResourceKind::IssueStatus, OperationKind::SingleCreated, - Some(id), + Some(is.id), )); } WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusUpdated(changed)) => { - let id = changed.id; - if let Some(idx) = model.issue_statuses.iter().position(|c| c.id == changed.id) { - std::mem::swap(&mut model.issue_statuses[idx], changed); - } model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + .issue_statuses_by_id + .insert(changed.id, changed.clone()); + sort_issue_statuses(model); + orders.send_msg(Msg::ResourceChanged( ResourceKind::IssueStatus, OperationKind::SingleModified, - Some(id), + Some(changed.id), )); } WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusDeleted(dropped_id, _count)) => { - let mut old = vec![]; - std::mem::swap(&mut model.issue_statuses, &mut old); - for is in old { - if is.id != *dropped_id { - model.issue_statuses.push(is); - } - } - model - .issue_statuses - .sort_by(|a, b| a.position.cmp(&b.position)); + model.issue_statuses_by_id.remove(dropped_id); + model.issue_status_ids = std::mem::take(&mut model.issue_status_ids) + .into_iter() + .filter(|id| id != dropped_id) + .collect(); orders.send_msg(Msg::ResourceChanged( ResourceKind::IssueStatus, OperationKind::SingleRemoved, @@ -315,6 +316,11 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) issue.epic_id = o.epic_id; } } + orders.send_msg(Msg::ResourceChanged( + ResourceKind::Issue, + OperationKind::ListLoaded, + None, + )); } // users WsMsg::Project(WsMsgProject::ProjectUsersLoaded(v)) => { @@ -335,12 +341,12 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) Some(modal) => modal.id, _ => return, }; + comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); if comments.iter().any(|c| c.issue_id != issue_id) { return; } - comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); - model.comments = comments.clone(); for comment in std::mem::take(comments) { + model.comment_ids.push(comment.id); model.comments_by_id.insert(comment.id, comment); } orders.send_msg(Msg::ResourceChanged( @@ -351,10 +357,7 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) } WsMsg::Comment(WsMsgComment::CommentUpdated(comment)) => { let comment_id = comment.id; - if let Some(idx) = model.comments.iter().position(|c| c.id == comment.id) { - let _ = std::mem::replace(&mut model.comments[idx], comment.clone()); - model.comments_by_id.insert(comment.id, comment.clone()); - } + model.comments_by_id.insert(comment.id, comment.clone()); orders.send_msg(Msg::ResourceChanged( ResourceKind::Comment, OperationKind::SingleModified, @@ -362,8 +365,8 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) )); } WsMsg::Comment(WsMsgComment::CommentDeleted(comment_id, _count)) => { - if let Some(idx) = model.comments.iter().position(|c| c.id == *comment_id) { - model.comments.remove(idx); + if let Some(idx) = model.comment_ids.iter().position(|id| *id == *comment_id) { + model.comment_ids.remove(idx); } model.comments_by_id.remove(&comment_id); orders.send_msg(Msg::ResourceChanged( @@ -407,10 +410,19 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) // epics WsMsg::Epic(WsMsgEpic::EpicsLoaded(epics)) => { - model.epics = epics.clone(); - for epic in epics { - model.epics_by_id.insert(epic.id, epic.clone()); - } + let epics = std::mem::take(epics); + let len = epics.len(); + + model.epic_ids = Vec::with_capacity(len); + model.epics_by_id = + epics + .into_iter() + .fold(HashMap::with_capacity(len), |mut h, epic| { + model.epic_ids.push(epic.id); + h.insert(epic.id, epic); + h + }); + orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::ListLoaded, @@ -419,9 +431,10 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) } WsMsg::Epic(WsMsgEpic::EpicCreated(epic)) => { let id = epic.id; - model.epics.push(epic.clone()); - model.epics.sort_by(|a, b| a.id.cmp(&b.id)); + model.epic_ids.push(epic.id); + model.epic_ids.sort(); model.epics_by_id.insert(epic.id, epic.clone()); + orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::SingleCreated, @@ -430,11 +443,8 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) } WsMsg::Epic(WsMsgEpic::EpicUpdated(epic)) => { let epic_id = epic.id; - if let Some(idx) = model.epics.iter().position(|e| e.id == epic.id) { - let _ = std::mem::replace(&mut model.epics[idx], epic.clone()); - } model.epics_by_id.insert(epic.id, epic.clone()); - model.epics.sort_by(|a, b| a.id.cmp(&b.id)); + orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::SingleModified, @@ -442,11 +452,11 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) )); } WsMsg::Epic(WsMsgEpic::EpicDeleted(id, _count)) => { - if let Some(idx) = model.epics.iter().position(|e| e.id == *id) { - model.epics.remove(idx); - } model.epics_by_id.remove(id); - model.epics.sort_by(|a, b| a.id.cmp(&b.id)); + model.epic_ids = std::mem::take(&mut model.epic_ids) + .into_iter() + .filter(|epic_id| epic_id != id) + .collect(); orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::SingleRemoved, @@ -481,6 +491,17 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders) }; } +pub fn sort_issue_statuses(model: &mut Model) { + let mut ids = model + .issue_status_ids + .iter() + .filter_map(|id| model.issue_statuses_by_id.get(id)) + .map(|is| (is.id, is.position)) + .collect::>(); + ids.sort_by(|(_, a), (_, b)| a.cmp(b)); + model.issue_status_ids = ids.into_iter().map(|(id, _)| id).collect(); +} + fn init_current_project(model: &mut Model, orders: &mut impl Orders) { if model.projects.is_empty() { return;