Reduce model memory usage

This commit is contained in:
Adrian Woźniak 2021-10-15 14:40:51 +02:00
parent 5abcfb99a6
commit d49c92cd53
No known key found for this signature in database
GPG Key ID: DE43476F72AD3F6C
12 changed files with 218 additions and 195 deletions

View File

@ -258,7 +258,7 @@ pub struct Issue {
pub struct IssueStatus { pub struct IssueStatus {
pub id: IssueStatusId, pub id: IssueStatusId,
pub name: String, pub name: String,
pub position: ProjectId, pub position: ListPosition,
pub project_id: ProjectId, pub project_id: ProjectId,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,

View File

@ -11,7 +11,7 @@ pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Opt
where where
Modal: IssueModal, Modal: IssueModal,
{ {
if model.epics.is_empty() { if model.epic_ids.is_empty() {
return None; return None;
} }
let input = StyledSelect { let input = StyledSelect {
@ -19,10 +19,16 @@ where
name: "epic", name: "epic",
selected: vec![modal selected: vec![modal
.epic_id_value() .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) .map(epic_select_option)
.unwrap_or_default()], .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, variant: SelectVariant::Normal,
clearable: true, clearable: true,
text_filter: modal.epic_state().text_filter.as_str(), text_filter: modal.epic_state().text_filter.as_str(),

View File

@ -339,15 +339,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
comment_id, comment_id,
)) => { )) => {
let id = *comment_id;
let body = model let body = model
.comments .comments_by_id
.iter() .get(comment_id)
.find(|c| c.id == id)
.map(|c| c.body.clone()) .map(|c| c.body.clone())
.unwrap_or_default(); .unwrap_or_default();
modal.comment_form.body = body; modal.comment_form.body = body;
modal.comment_form.id = Some(id); modal.comment_form.id = Some(*comment_id);
modal.comment_form.creating = true; modal.comment_form.creating = true;
} }
Msg::DeleteComment(comment_id) => { Msg::DeleteComment(comment_id) => {

View File

@ -243,7 +243,11 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
vec![div![C!["fakeTextArea"], "Add a comment...", handler]] 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![ div![
C!["left"], C!["left"],
@ -403,11 +407,22 @@ fn status_select(
opened: status_state.opened, opened: status_state.opened,
variant: SelectVariant::Normal, variant: SelectVariant::Normal,
text_filter: status_state.text_filter.as_str(), 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 selected: model
.issue_statuses .issue_status_ids
.iter() .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) .map(issue_status_select_option)
.collect(), .collect(),
@ -424,7 +439,7 @@ fn status_select(
} }
#[inline(always)] #[inline(always)]
fn issue_status_select_option<'l>(is: &'l IssueStatus) -> StyledSelectOption<'l> { fn issue_status_select_option(is: &IssueStatus) -> StyledSelectOption<'_> {
StyledSelectOption { StyledSelectOption {
value: is.id as u32, value: is.id as u32,
class_list: is.name.as_str(), class_list: is.name.as_str(),

View File

@ -254,11 +254,11 @@ pub struct Model {
pub user_settings: Option<UserSetting>, pub user_settings: Option<UserSetting>,
// comments // comments
pub comments: Vec<Comment>, pub comment_ids: Vec<CommentId>,
pub comments_by_id: HashMap<CommentId, Comment>, pub comments_by_id: HashMap<CommentId, Comment>,
// issue_statuses // issue_statuses
pub issue_statuses: Vec<IssueStatus>, pub issue_status_ids: Vec<IssueStatusId>,
pub issue_statuses_by_id: HashMap<IssueStatusId, IssueStatus>, pub issue_statuses_by_id: HashMap<IssueStatusId, IssueStatus>,
pub issue_statuses_by_name: HashMap<String, IssueStatus>, pub issue_statuses_by_name: HashMap<String, IssueStatus>,
@ -273,7 +273,7 @@ pub struct Model {
pub projects: Vec<Project>, pub projects: Vec<Project>,
// epics // epics
pub epics: Vec<Epic>, pub epic_ids: Vec<EpicId>,
pub epics_by_id: HashMap<EpicId, Epic>, pub epics_by_id: HashMap<EpicId, Epic>,
pub key_triggers: std::rc::Rc<std::cell::RefCell<HashMap<char, Box<dyn BuildMsg>>>>, pub key_triggers: std::rc::Rc<std::cell::RefCell<HashMap<char, Box<dyn BuildMsg>>>>,
@ -304,15 +304,15 @@ impl Model {
user_ids: vec![], user_ids: vec![],
users_by_id: HashMap::with_capacity(1_000), users_by_id: HashMap::with_capacity(1_000),
user_settings: None, user_settings: None,
comments: vec![], comment_ids: vec![],
comments_by_id: HashMap::with_capacity(1_000), 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_id: HashMap::with_capacity(1_000),
issue_statuses_by_name: HashMap::with_capacity(1_000), issue_statuses_by_name: HashMap::with_capacity(1_000),
messages: vec![], messages: vec![],
user_projects: vec![], user_projects: vec![],
projects: vec![], projects: vec![],
epics: vec![], epic_ids: vec![],
issues_by_id: HashMap::with_capacity(1_000), issues_by_id: HashMap::with_capacity(1_000),
show_extras: false, show_extras: false,
epics_by_id: HashMap::with_capacity(1_000), 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)] #[inline(always)]
pub fn user(&self) -> &Option<User> { pub fn user(&self) -> &Option<User> {
&self.user &self.user

View File

@ -11,38 +11,42 @@ use crate::Msg;
pub fn view(model: &Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let page = crate::match_page!(model, Epics; Empty); let page = crate::match_page!(model, Epics; Empty);
let epics = model.epics.iter().map(|epic| { let epics = model
let issues = page.issues(epic.id).map(|v| { .epic_ids
v.iter() .iter()
.filter_map(|i| model.issues_by_id.get(i)) .filter_map(|id| model.epics_by_id.get(id))
.map(|issue| { .map(|epic| {
render_issue( let issues = page.issues(epic.id).map(|v| {
issue, v.iter()
model.issue_statuses_by_id.get(&issue.issue_status_id), .filter_map(|i| model.issues_by_id.get(i))
) .map(|issue| {
}) render_issue(
.collect::<Vec<Node<Msg>>>() issue,
}); model.issue_statuses_by_id.get(&issue.issue_status_id),
)
})
.collect::<Vec<Node<Msg>>>()
});
li![ li![
C!["epic"], C!["epic"],
div![
C!["firstRow"],
div![C!["epicName"], &epic.name],
div![ div![
C!["date"], C!["firstRow"],
date_field("Starts at:", "startsAt", epic.starts_at.as_ref()), div![C!["epicName"], &epic.name],
date_field("Ends at:", "endsAt", epic.ends_at.as_ref()), 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![ div![C!["secondRow"], div![C!["issues"], issues]]
C!["counter"], ]
"Number of issues:", });
issues.as_ref().map(Vec::len).unwrap_or(0)
],
],
div![C!["secondRow"], div![C!["issues"], issues]]
]
});
inner_layout( inner_layout(
model, model,

View File

@ -29,24 +29,26 @@ pub struct ProjectPage {
} }
impl ProjectPage { impl ProjectPage {
pub fn visible_issues<'issue, IssueStream>( pub fn visible_issues<'model, IssueStream, IssueStatusStream, EpicStream>(
page: &ProjectPage, page: &ProjectPage,
epics: &[Epic], num_of_epics: usize,
statuses: &[IssueStatus], epics: EpicStream,
statuses: IssueStatusStream,
issues: IssueStream, issues: IssueStream,
user: &Option<User>, user: &Option<User>,
) -> Vec<EpicIssuePerStatus> ) -> Vec<EpicIssuePerStatus>
where where
IssueStream: std::iter::Iterator<Item = &'issue Issue>, IssueStream: std::iter::Iterator<Item = &'model Issue>,
IssueStatusStream: std::iter::Iterator<Item = &'model IssueStatus>,
EpicStream: std::iter::Iterator<Item = &'model Epic>,
{ {
let num_of_epics = epics.len();
let epics = vec![None].into_iter().chain( let epics = vec![None].into_iter().chain(
epics epics.map(|epic| Some((epic.id, epic.name.as_str(), epic.starts_at, epic.ends_at))),
.iter()
.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::<Vec<(IssueStatusId, &str)>>();
let issues = issues.filter(|issue| { let issues = issues.filter(|issue| {
issue_filter_with_avatars(issue, &page.active_avatar_filters) issue_filter_with_avatars(issue, &page.active_avatar_filters)
&& issue_filter_with_text(issue, page.text_filter.as_str()) && issue_filter_with_text(issue, page.text_filter.as_str())
@ -93,15 +95,15 @@ impl ProjectPage {
..Default::default() ..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 { let per_status_map = StatusIssueIds {
status_id: current_status_id, status_id: *current_status_id,
status_name: issue_status_name.to_string(), status_name: issue_status_name.to_string(),
issue_ids: issues_per_epic_id issue_ids: issues_per_epic_id
.get(&epic.map(|(id, ..)| id)) .get(&epic.map(|(id, ..)| id))
.map(|v| { .map(|v| {
v.iter() v.iter()
.filter(|issue| issue_filter_status(issue, current_status_id)) .filter(|issue| issue_filter_status(issue, *current_status_id))
.map(|issue| issue.id) .map(|issue| issue.id)
.collect() .collect()
}) })

View File

@ -7,7 +7,7 @@ use seed::prelude::Orders;
use crate::components::styled_select::StyledSelectChanged; use crate::components::styled_select::StyledSelectChanged;
use crate::model::{Model, Page, PageContent}; use crate::model::{Model, Page, PageContent};
use crate::pages::project_settings_page::ProjectSettingsPage; 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}; use crate::{match_page_mut, FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -114,7 +114,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
))) => page.column_drag.clear_last(), ))) => page.column_drag.clear_last(),
Msg::PageChanged(PageChanged::ProjectSettings( Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnExchangePosition(issue_bellow_id), ProjectPageChange::ColumnExchangePosition(issue_bellow_id),
)) => exchange_position(issue_bellow_id, model), )) => swap_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone( Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id, _issue_status_id,
))) => { ))) => {
@ -124,39 +124,35 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
id, id,
))) => { ))) => {
if page.edit_column_id.is_some() && id.is_none() { if page.edit_column_id.is_some() && id.is_none() {
let old_id = page.edit_column_id.as_ref().cloned(); if let Some(old_id) = page.edit_column_id {
let name = page.name.value.clone(); let name = page.name.value.clone();
if let Some((id, pos)) = model if let Some((id, pos)) = model
.issue_statuses .issue_statuses_by_id
.iter() .get(&old_id)
.find(|is| Some(is.id) == old_id) .map(|is| (is.id, is.position))
.map(|is| (is.id, is.position)) {
{ send_ws_msg(
send_ws_msg( WsMsgIssueStatus::IssueStatusUpdate(id, name, pos).into(),
WsMsgIssueStatus::IssueStatusUpdate(id, name, pos).into(), model.ws.as_ref(),
model.ws.as_ref(), orders,
orders, );
); }
} }
} }
page.name.value = model if let Some(id) = id {
.issue_statuses page.name.value = model
.iter() .issue_statuses_by_id
.find_map(|is| { .get(&id)
if Some(is.id) == id { .map(|is| is.name.clone())
Some(is.name.clone()) .unwrap_or_default();
} else { }
None
}
})
.unwrap_or_default();
page.edit_column_id = id; page.edit_column_id = id;
} }
Msg::PageChanged(PageChanged::ProjectSettings( Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitIssueStatusForm, ProjectPageChange::SubmitIssueStatusForm,
)) => { )) => {
let name = page.name.value.clone(); 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(); let ws_msg = WsMsgIssueStatus::IssueStatusCreate(name, position as i32).into();
send_ws_msg(ws_msg, model.ws.as_ref(), orders); 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<Msg>) {
} }
} }
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) { fn swap_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content { let page = crate::match_page_mut!(model, ProjectSettings);
PageContent::ProjectSettings(page) => page,
_ => return,
};
if page.column_drag.dragged_or_last(bellow_id) { if page.column_drag.dragged_or_last(bellow_id) {
return; return;
} }
@ -177,41 +170,29 @@ fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
_ => return log::error!("Nothing is dragged"), _ => return log::error!("Nothing is dragged"),
}; };
let mut below = None; let bellow = model
let mut dragged = None; .issue_statuses_by_id
let mut issues_statuses = vec![]; .get(&bellow_id)
std::mem::swap(&mut issues_statuses, &mut model.issue_statuses); .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() { if let Some(is) = model.issue_statuses_by_id.get_mut(&dragged_id) {
match issue_status.id { is.position = bellow;
id if id == bellow_id => below = Some(issue_status), }
id if id == dragged_id => dragged = Some(issue_status), if let Some(is) = model.issue_statuses_by_id.get_mut(&bellow_id) {
_ => model.issue_statuses.push(issue_status), is.position = dragged;
};
} }
let mut below = match below { page.column_drag.mark_dirty(dragged_id);
Some(below) => below, page.column_drag.mark_dirty(bellow_id);
_ => 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(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); page.column_drag.last_id = Some(bellow_id);
sort_issue_statuses(model);
} }
fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) { fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -224,11 +205,10 @@ fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
_ => return log::error!("bad content type"), _ => return log::error!("bad content type"),
}; };
for id in dirty { for id in dirty {
let IssueStatus { name, position, .. } = let IssueStatus { name, position, .. } = match model.issue_statuses_by_id.get(&id) {
match model.issue_statuses.iter().find(|is| is.id == id) { Some(is) => is,
Some(is) => is, _ => continue,
_ => continue, };
};
send_ws_msg( send_ws_msg(
WsMsgIssueStatus::IssueStatusUpdate(id, name.clone(), *position).into(), WsMsgIssueStatus::IssueStatusUpdate(id, name.clone(), *position).into(),
model.ws.as_ref(), model.ws.as_ref(),

View File

@ -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 /// Build draggable columns preview with option to remove and add new columns
#[inline(always)] #[inline(always)]
fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> { fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
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 column_style = format!("width: calc({width}% - 10px)", width = width);
let per_column_issue_count = model let per_column_issue_count = model
.issue_ids .issue_ids
@ -216,11 +216,11 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
h h
}, },
); );
let columns: Vec<Node<Msg>> = model let columns = model
.issue_statuses .issue_status_ids
.iter() .iter()
.map(|is| column_preview(is, page, &per_column_issue_count, column_style.as_str())) .filter_map(|id| model.issue_statuses_by_id.get(id))
.collect(); .map(|is| column_preview(is, page, &per_column_issue_count, column_style.as_str()));
let columns_section = section![ let columns_section = section![
C!["columnsSection"], C!["columnsSection"],

View File

@ -81,7 +81,7 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
}, },
); );
let issue_nav = if model.issue_statuses.is_empty() { let issue_nav = if model.issue_status_ids.is_empty() {
vec![] vec![]
} else { } else {
vec![ vec![

View File

@ -168,8 +168,15 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) -> bool {
pub fn change_visible(model: &mut Model) { pub fn change_visible(model: &mut Model) {
let visible = ProjectPage::visible_issues( let visible = ProjectPage::visible_issues(
crate::match_page!(model, Project), crate::match_page!(model, Project),
model.epics(), model.epic_ids.len(),
model.issue_statuses(), 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 model
.issue_ids .issue_ids
.iter() .iter()

View File

@ -197,10 +197,20 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
// issue statuses // issue statuses
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusesLoaded(v)) => { WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusesLoaded(v)) => {
model.issue_statuses = std::mem::take(v); let len = v.len();
model let mut issue_statuses = std::mem::take(v);
.issue_statuses issue_statuses.sort_by(|a, b| a.position.cmp(&b.position));
.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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::IssueStatus, ResourceKind::IssueStatus,
OperationKind::ListLoaded, OperationKind::ListLoaded,
@ -208,42 +218,33 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
)); ));
} }
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusCreated(is)) => { WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusCreated(is)) => {
let id = is.id; model.issue_status_ids.push(is.id);
model.issue_statuses.push(is.clone()); model.issue_statuses_by_id.insert(is.id, is.clone());
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::IssueStatus, ResourceKind::IssueStatus,
OperationKind::SingleCreated, OperationKind::SingleCreated,
Some(id), Some(is.id),
)); ));
} }
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusUpdated(changed)) => { 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 model
.issue_statuses .issue_statuses_by_id
.sort_by(|a, b| a.position.cmp(&b.position)); .insert(changed.id, changed.clone());
sort_issue_statuses(model);
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::IssueStatus, ResourceKind::IssueStatus,
OperationKind::SingleModified, OperationKind::SingleModified,
Some(id), Some(changed.id),
)); ));
} }
WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusDeleted(dropped_id, _count)) => { WsMsg::IssueStatus(WsMsgIssueStatus::IssueStatusDeleted(dropped_id, _count)) => {
let mut old = vec![]; model.issue_statuses_by_id.remove(dropped_id);
std::mem::swap(&mut model.issue_statuses, &mut old); model.issue_status_ids = std::mem::take(&mut model.issue_status_ids)
for is in old { .into_iter()
if is.id != *dropped_id { .filter(|id| id != dropped_id)
model.issue_statuses.push(is); .collect();
}
}
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::IssueStatus, ResourceKind::IssueStatus,
OperationKind::SingleRemoved, OperationKind::SingleRemoved,
@ -315,6 +316,11 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
issue.epic_id = o.epic_id; issue.epic_id = o.epic_id;
} }
} }
orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue,
OperationKind::ListLoaded,
None,
));
} }
// users // users
WsMsg::Project(WsMsgProject::ProjectUsersLoaded(v)) => { WsMsg::Project(WsMsgProject::ProjectUsersLoaded(v)) => {
@ -335,12 +341,12 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
Some(modal) => modal.id, Some(modal) => modal.id,
_ => return, _ => return,
}; };
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
if comments.iter().any(|c| c.issue_id != issue_id) { if comments.iter().any(|c| c.issue_id != issue_id) {
return; return;
} }
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
model.comments = comments.clone();
for comment in std::mem::take(comments) { for comment in std::mem::take(comments) {
model.comment_ids.push(comment.id);
model.comments_by_id.insert(comment.id, comment); model.comments_by_id.insert(comment.id, comment);
} }
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
@ -351,10 +357,7 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
} }
WsMsg::Comment(WsMsgComment::CommentUpdated(comment)) => { WsMsg::Comment(WsMsgComment::CommentUpdated(comment)) => {
let comment_id = comment.id; let comment_id = comment.id;
if let Some(idx) = model.comments.iter().position(|c| c.id == comment.id) { model.comments_by_id.insert(comment.id, comment.clone());
let _ = std::mem::replace(&mut model.comments[idx], comment.clone());
model.comments_by_id.insert(comment.id, comment.clone());
}
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment, ResourceKind::Comment,
OperationKind::SingleModified, OperationKind::SingleModified,
@ -362,8 +365,8 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
)); ));
} }
WsMsg::Comment(WsMsgComment::CommentDeleted(comment_id, _count)) => { WsMsg::Comment(WsMsgComment::CommentDeleted(comment_id, _count)) => {
if let Some(idx) = model.comments.iter().position(|c| c.id == *comment_id) { if let Some(idx) = model.comment_ids.iter().position(|id| *id == *comment_id) {
model.comments.remove(idx); model.comment_ids.remove(idx);
} }
model.comments_by_id.remove(&comment_id); model.comments_by_id.remove(&comment_id);
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
@ -407,10 +410,19 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
// epics // epics
WsMsg::Epic(WsMsgEpic::EpicsLoaded(epics)) => { WsMsg::Epic(WsMsgEpic::EpicsLoaded(epics)) => {
model.epics = epics.clone(); let epics = std::mem::take(epics);
for epic in epics { let len = epics.len();
model.epics_by_id.insert(epic.id, epic.clone());
} 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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::ListLoaded, OperationKind::ListLoaded,
@ -419,9 +431,10 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
} }
WsMsg::Epic(WsMsgEpic::EpicCreated(epic)) => { WsMsg::Epic(WsMsgEpic::EpicCreated(epic)) => {
let id = epic.id; let id = epic.id;
model.epics.push(epic.clone()); model.epic_ids.push(epic.id);
model.epics.sort_by(|a, b| a.id.cmp(&b.id)); model.epic_ids.sort();
model.epics_by_id.insert(epic.id, epic.clone()); model.epics_by_id.insert(epic.id, epic.clone());
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::SingleCreated, OperationKind::SingleCreated,
@ -430,11 +443,8 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
} }
WsMsg::Epic(WsMsgEpic::EpicUpdated(epic)) => { WsMsg::Epic(WsMsgEpic::EpicUpdated(epic)) => {
let epic_id = epic.id; 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_by_id.insert(epic.id, epic.clone());
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::SingleModified, OperationKind::SingleModified,
@ -442,11 +452,11 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
)); ));
} }
WsMsg::Epic(WsMsgEpic::EpicDeleted(id, _count)) => { 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_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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::SingleRemoved, OperationKind::SingleRemoved,
@ -481,6 +491,17 @@ pub fn update(msg: &mut WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>)
}; };
} }
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::<Vec<_>>();
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<Msg>) { fn init_current_project(model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.projects.is_empty() { if model.projects.is_empty() {
return; return;