diff --git a/jirs-client/src/changes.rs b/jirs-client/src/changes.rs index 7502b808..6678e109 100644 --- a/jirs-client/src/changes.rs +++ b/jirs-client/src/changes.rs @@ -18,7 +18,7 @@ pub enum BoardPageChange { IssueDragStarted(EpicId), IssueDragStopped(EpicId), DragLeave(EpicId), - ExchangePosition(EpicId), + ChangePosition(EpicId), IssueDragOverStatus(IssueStatusId), IssueDropZone(IssueStatusId), } diff --git a/jirs-client/src/fields.rs b/jirs-client/src/fields.rs index 1b74154f..24833b66 100644 --- a/jirs-client/src/fields.rs +++ b/jirs-client/src/fields.rs @@ -1,6 +1,6 @@ use jirs_data::{ - CommentFieldId, InviteFieldId, IssueFieldId, ProjectFieldId, SignInFieldId, SignUpFieldId, - UsersFieldId, + CommentFieldId, EpicFieldId, InviteFieldId, IssueFieldId, ProjectFieldId, SignInFieldId, + SignUpFieldId, UsersFieldId, }; pub type AvatarFilterActive = bool; @@ -92,6 +92,8 @@ pub enum FieldId { // issue AddIssueModal(IssueFieldId), EditIssueModal(EditIssueModalSection), + // epic + EditEpic(EpicFieldId), // project boards TextFilterBoard, CopyButtonLabel, @@ -202,6 +204,12 @@ impl std::fmt::Display for FieldId { UsersFieldId::Avatar => f.write_str("profile-avatar"), UsersFieldId::CurrentProject => f.write_str("profile-currentProject"), }, + FieldId::EditEpic(sub) => match sub { + EpicFieldId::Name => f.write_str("epicEpic-name"), + EpicFieldId::StartsAt => f.write_str("epicEpic-startsAt"), + EpicFieldId::EndsAt => f.write_str("epicEpic-endsAt"), + EpicFieldId::TransformInto => f.write_str("epicEpic-transformInto"), + }, FieldId::Rte(..) => f.write_str("rte"), } } diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 3e142ba6..7ee05076 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -238,9 +238,11 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { crate::modals::update(&msg, model, orders); match model.page { - Page::Project | Page::AddIssue | Page::EditIssue(..) | Page::DeleteEpic(..) => { - pages::project_page::update(msg, model, orders) - } + Page::Project + | Page::AddIssue + | Page::EditIssue(..) + | Page::DeleteEpic(..) + | Page::EditEpic(..) => pages::project_page::update(msg, model, orders), Page::ProjectSettings => pages::project_settings_page::update(msg, model, orders), Page::SignIn => pages::sign_in_page::update(msg, model, orders), Page::SignUp => pages::sign_up_page::update(msg, model, orders), @@ -256,9 +258,11 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { fn view(model: &model::Model) -> Node { match model.page { - Page::Project | Page::AddIssue | Page::DeleteEpic(..) | Page::EditIssue(..) => { - pages::project_page::view(model) - } + Page::Project + | Page::AddIssue + | Page::EditIssue(..) + | Page::DeleteEpic(..) + | Page::EditEpic(..) => pages::project_page::view(model), Page::ProjectSettings => pages::project_settings_page::view(model), Page::SignIn => pages::sign_in_page::view(model), Page::SignUp => pages::sign_up_page::view(model), @@ -280,10 +284,6 @@ fn resolve_page(url: Url) -> Option { Some(Ok(id)) => Page::EditIssue(id), _ => return None, }, - "delete-epic" => match url.path().get(1).as_ref().map(|s| s.parse::()) { - Some(Ok(id)) => Page::DeleteEpic(id), - _ => return None, - }, "profile" => Page::Profile, "add-issue" => Page::AddIssue, "project-settings" => Page::ProjectSettings, @@ -292,6 +292,14 @@ fn resolve_page(url: Url) -> Option { "invite" => Page::Invite, "users" => Page::Users, "reports" => Page::Reports, + "delete-epic" => match url.path().get(1).as_ref().map(|s| s.parse::()) { + Some(Ok(id)) => Page::DeleteEpic(id), + _ => return None, + }, + "edit-epic" => match url.path().get(1).as_ref().map(|s| s.parse::()) { + Some(Ok(id)) => Page::EditEpic(id), + _ => return None, + }, _ => Page::Project, }; Some(page) diff --git a/jirs-client/src/modals/epic_delete/model.rs b/jirs-client/src/modals/epic_delete/model.rs deleted file mode 100644 index 81792b09..00000000 --- a/jirs-client/src/modals/epic_delete/model.rs +++ /dev/null @@ -1,30 +0,0 @@ -use { - crate::model, - jirs_data::{EpicId, IssueId}, -}; - -#[derive(Clone, Debug, PartialOrd, PartialEq)] -pub struct Model { - pub epic_id: EpicId, - pub related_issues: Vec, -} - -impl Model { - pub fn new(issue_id: i32, model: &mut model::Model) -> Self { - let related_issues = model - .issues - .iter() - .filter_map(|issue| { - if issue.epic_id == Some(issue_id) { - Some(issue.id) - } else { - None - } - }) - .collect(); - Self { - epic_id: issue_id, - related_issues, - } - } -} diff --git a/jirs-client/src/modals/epic_delete/mod.rs b/jirs-client/src/modals/epics_delete/mod.rs similarity index 100% rename from jirs-client/src/modals/epic_delete/mod.rs rename to jirs-client/src/modals/epics_delete/mod.rs diff --git a/jirs-client/src/modals/epics_delete/model.rs b/jirs-client/src/modals/epics_delete/model.rs new file mode 100644 index 00000000..399d68c0 --- /dev/null +++ b/jirs-client/src/modals/epics_delete/model.rs @@ -0,0 +1,20 @@ +use { + crate::model, + jirs_data::{EpicId, IssueId}, +}; + +#[derive(Clone, Debug, PartialOrd, PartialEq)] +pub struct Model { + pub epic_id: EpicId, + pub related_issues: Vec, +} + +impl Model { + pub fn new(epic_id: i32, model: &mut model::Model) -> Self { + let related_issues = model.epic_issue_ids(epic_id); + Self { + epic_id, + related_issues, + } + } +} diff --git a/jirs-client/src/modals/epic_delete/update.rs b/jirs-client/src/modals/epics_delete/update.rs similarity index 100% rename from jirs-client/src/modals/epic_delete/update.rs rename to jirs-client/src/modals/epics_delete/update.rs diff --git a/jirs-client/src/modals/epic_delete/view.rs b/jirs-client/src/modals/epics_delete/view.rs similarity index 98% rename from jirs-client/src/modals/epic_delete/view.rs rename to jirs-client/src/modals/epics_delete/view.rs index 43c91fdc..7a0941b5 100644 --- a/jirs-client/src/modals/epic_delete/view.rs +++ b/jirs-client/src/modals/epics_delete/view.rs @@ -1,7 +1,7 @@ use { crate::{ components::{styled_button::*, styled_confirm_modal::*, styled_icon::*, styled_modal::*}, - modals::epic_delete::Model, + modals::epics_delete::Model, model, shared::ToNode, Msg, diff --git a/jirs-client/src/modals/epics_edit/mod.rs b/jirs-client/src/modals/epics_edit/mod.rs new file mode 100644 index 00000000..aecce944 --- /dev/null +++ b/jirs-client/src/modals/epics_edit/mod.rs @@ -0,0 +1,5 @@ +pub use {model::*, update::*, view::*}; + +mod model; +mod update; +mod view; diff --git a/jirs-client/src/modals/epics_edit/model.rs b/jirs-client/src/modals/epics_edit/model.rs new file mode 100644 index 00000000..b7065031 --- /dev/null +++ b/jirs-client/src/modals/epics_edit/model.rs @@ -0,0 +1,47 @@ +use crate::FieldId; +use jirs_data::EpicFieldId; +use { + crate::{ + components::{styled_input::*, styled_select::StyledSelectState}, + model, + }, + jirs_data::{EpicId, IssueId}, +}; + +#[derive(Clone, Debug, PartialOrd, PartialEq)] +pub struct Model { + pub epic_id: EpicId, + pub related_issues: Vec, + pub name: StyledInputState, + pub transform_into: StyledSelectState, +} + +impl Model { + pub fn new(epic_id: i32, model: &mut model::Model) -> Self { + let name = model + .epics_by_id + .get(&epic_id) + .map(|epic| epic.name.as_str()) + .unwrap_or_default(); + let related_issues = model + .issues() + .iter() + .filter_map(|issue| { + if issue.epic_id == Some(epic_id) { + Some(issue.id) + } else { + None + } + }) + .collect(); + Self { + epic_id, + related_issues, + name: StyledInputState::new(FieldId::EditEpic(EpicFieldId::Name), name), + transform_into: StyledSelectState::new( + FieldId::EditEpic(EpicFieldId::StartsAt), + vec![], + ), + } + } +} diff --git a/jirs-client/src/modals/epics_edit/update.rs b/jirs-client/src/modals/epics_edit/update.rs new file mode 100644 index 00000000..d18a2862 --- /dev/null +++ b/jirs-client/src/modals/epics_edit/update.rs @@ -0,0 +1,5 @@ +use {crate::Msg, seed::prelude::*}; + +pub fn update(_msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders) { + let _modal = crate::match_modal_mut!(model, DeleteEpic); +} diff --git a/jirs-client/src/modals/epics_edit/view.rs b/jirs-client/src/modals/epics_edit/view.rs new file mode 100644 index 00000000..5004f0a1 --- /dev/null +++ b/jirs-client/src/modals/epics_edit/view.rs @@ -0,0 +1,30 @@ +use { + crate::{ + components::{styled_input::*, styled_modal::*}, + modals::epics_edit::Model, + model, + shared::ToNode, + FieldId, Msg, + }, + jirs_data::EpicFieldId, + seed::{prelude::*, *}, +}; + +pub fn view(_model: &model::Model, modal: &Model) -> Node { + let transform = if modal.related_issues.is_empty() { + Node::Empty + } else { + div![] + }; + StyledModal::build() + .center() + .child(h1!["Edit epic"]) + .child( + StyledInput::build() + .build(FieldId::EditEpic(EpicFieldId::Name)) + .into_node(), + ) + .child(transform) + .build() + .into_node() +} diff --git a/jirs-client/src/modals/mod.rs b/jirs-client/src/modals/mod.rs index ad8787f7..7566db44 100644 --- a/jirs-client/src/modals/mod.rs +++ b/jirs-client/src/modals/mod.rs @@ -2,7 +2,8 @@ pub use {epic_field::*, update::*, view::*}; #[cfg(debug_assertions)] pub mod debug; -pub mod epic_delete; +pub mod epics_delete; +pub mod epics_edit; pub mod issue_statuses_delete; pub mod issues_create; pub mod issues_delete; @@ -12,3 +13,35 @@ pub mod time_tracking; mod epic_field; mod update; mod view; + +#[macro_export] +macro_rules! match_modal { + ($model: ident, $ty: ident) => { + match $model.modals.iter().find_map(|modal| { + if let crate::model::ModalType::$ty(modal) = modal { + Some(modal) + } else { + None + } + }) { + Some(modal) => modal, + _ => return, + } + }; +} + +#[macro_export] +macro_rules! match_modal_mut { + ($model: ident, $ty: ident) => { + match $model.modals.iter_mut().find_map(|modal| { + if let crate::model::ModalType::$ty(modal) = modal { + Some(modal) + } else { + None + } + }) { + Some(modal) => modal, + _ => return, + } + }; +} diff --git a/jirs-client/src/modals/update.rs b/jirs-client/src/modals/update.rs index 6ffc6f52..d5ed8136 100644 --- a/jirs-client/src/modals/update.rs +++ b/jirs-client/src/modals/update.rs @@ -5,7 +5,7 @@ use { ws::send_ws_msg, FieldChange, FieldId, Msg, OperationKind, ResourceKind, }, - jirs_data::{TimeTracking, WsMsg}, + jirs_data::{EpicId, IssueId, TimeTracking, WsMsg}, seed::{prelude::*, *}, }; @@ -46,6 +46,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { Msg::ChangePage(Page::DeleteEpic(issue_id)) => { push_delete_epic_modal(*issue_id, model, orders) } + Msg::ChangePage(Page::EditEpic(issue_id)) => push_edit_epic_modal(*issue_id, model, orders), #[cfg(debug_assertions)] Msg::GlobalKeyDown { key, .. } if key.eq("#") => { @@ -69,7 +70,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { issues_create::update(msg, model, orders); issues_edit::update(msg, model, orders); issue_statuses_delete::update(msg, model, orders); - epic_delete::update(msg, model, orders); + epics_edit::update(msg, model, orders); + epics_delete::update(msg, model, orders); } } @@ -81,13 +83,7 @@ fn push_add_modal(model: &mut Model, _orders: &mut impl Orders) { }))); } -fn push_delete_epic_modal(issue_id: i32, model: &mut Model, _orders: &mut impl Orders) { - use crate::modals::epic_delete::Model; - let modal = Model::new(issue_id, model); - model.modals.push(ModalType::DeleteEpic(Box::new(modal))); -} - -fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders) { +fn push_edit_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl Orders) { let time_tracking_type = model .project .as_ref() @@ -113,3 +109,15 @@ fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders) { + use crate::modals::epics_edit::Model; + let modal = Model::new(epic_id, model); + model.modals.push(ModalType::EditEpic(Box::new(modal))); +} + +fn push_delete_epic_modal(issue_id: IssueId, model: &mut Model, _orders: &mut impl Orders) { + use crate::modals::epics_delete::Model; + let modal = Model::new(issue_id, model); + model.modals.push(ModalType::DeleteEpic(Box::new(modal))); +} diff --git a/jirs-client/src/modals/view.rs b/jirs-client/src/modals/view.rs index 7e0e38a2..7c3fd1a7 100644 --- a/jirs-client/src/modals/view.rs +++ b/jirs-client/src/modals/view.rs @@ -14,6 +14,10 @@ pub fn view(model: &Model) -> Node { .modals .iter() .map(|modal| match modal { + // epic + ModalType::DeleteEpic(modal) => crate::modals::epics_delete::view(model, modal), + ModalType::EditEpic(modal) => crate::modals::epics_edit::view(model, modal), + // issue ModalType::EditIssue(issue_id, modal) => { if let Some(_issue) = model.issues_by_id.get(issue_id) { let details = issues_edit::view(model, modal.as_ref()); @@ -29,6 +33,7 @@ pub fn view(model: &Model) -> Node { } ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model), ModalType::AddIssue(modal) => issues_create::view(model, modal), + // comment ModalType::DeleteCommentConfirm(comment_id) => { let comment_id = *comment_id; StyledConfirmModal::build() @@ -47,7 +52,6 @@ pub fn view(model: &Model) -> Node { } #[cfg(debug_assertions)] ModalType::DebugModal => crate::modals::debug::view(model), - ModalType::DeleteEpic(modal) => crate::modals::epic_delete::view(model, modal), }) .collect(); section![id!["modals"], modals] diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 29a4fd89..ffc0cea0 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -26,9 +26,13 @@ pub trait IssueModal { #[derive(Clone, Debug, PartialOrd, PartialEq)] pub enum ModalType { + // issue AddIssue(Box), EditIssue(EpicId, Box), - DeleteEpic(Box), + // epic + DeleteEpic(Box), + EditEpic(Box), + DeleteIssueConfirm(EpicId), DeleteCommentConfirm(CommentId), TimeTracking(EpicId), @@ -47,10 +51,15 @@ pub struct CommentForm { #[derive(Copy, Clone, Debug, PartialOrd, PartialEq)] pub enum Page { Project, - EditIssue(EpicId), + // epic DeleteEpic(EpicId), + EditEpic(EpicId), + // issue + EditIssue(EpicId), AddIssue, + // settings ProjectSettings, + // auth SignIn, SignUp, Invite, @@ -63,8 +72,9 @@ impl Page { pub fn to_path(self) -> String { match self { Page::Project => "/board".to_string(), - Page::EditIssue(id) => format!("/issues/{id}", id = id), Page::DeleteEpic(id) => format!("/delete-epic/{id}", id = id), + Page::EditEpic(id) => format!("/edit-epic/{id}", id = id), + Page::EditIssue(id) => format!("/issues/{id}", id = id), Page::AddIssue => "/add-issue".to_string(), Page::ProjectSettings => "/project-settings".to_string(), Page::SignIn => "/login".to_string(), @@ -107,6 +117,25 @@ impl Default for InvitationFormState { } } +#[macro_export] +macro_rules! match_page { + ($model: ident, $ty: ident) => { + match &$model.page_content { + PageContent::$ty(page) => page, + _ => return, + } + }; +} +#[macro_export] +macro_rules! match_page_mut { + ($model: ident, $ty: ident) => { + match &mut $model.page_content { + PageContent::$ty(page) => page, + _ => return, + } + }; +} + #[derive(Debug)] pub enum PageContent { SignIn(Box), @@ -145,27 +174,39 @@ pub struct Model { pub page: Page, pub page_content: PageContent, - pub project: Option, - pub current_user_project: Option, - pub issues: Vec, + // issues + issues: Vec, pub issues_by_id: HashMap, + // users pub user: Option, pub users: Vec, pub users_by_id: HashMap, + // comments pub comments: Vec, + pub comments_by_id: HashMap, + // issue_statuses pub issue_statuses: Vec, pub issue_statuses_by_id: HashMap, pub issue_statuses_by_name: HashMap, + // messages pub messages: Vec, + + // user_projects pub user_projects: Vec, + + // projects + pub project: Option, pub projects: Vec, + + // epics pub epics: Vec, + pub epics_by_id: HashMap, pub show_extras: bool, } @@ -194,6 +235,7 @@ impl Model { users: vec![], users_by_id: Default::default(), comments: vec![], + comments_by_id: Default::default(), issue_statuses: vec![], issue_statuses_by_id: Default::default(), issue_statuses_by_name: Default::default(), @@ -203,13 +245,52 @@ impl Model { epics: vec![], issues_by_id: Default::default(), show_extras: false, + epics_by_id: Default::default(), } } + #[inline(always)] + pub fn issues(&self) -> &[Issue] { + &self.issues + } + + #[inline(always)] + pub fn issues_mut(&mut self) -> &mut Vec { + &mut self.issues + } + + #[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 + } + pub fn current_user_role(&self) -> UserRole { self.current_user_project .as_ref() .map(|up| up.role) .unwrap_or_default() } + + pub fn epic_issue_ids(&self, epic_id: EpicId) -> Vec { + self.issues() + .iter() + .filter_map(|issue| { + if issue.epic_id == Some(epic_id) { + Some(issue.id) + } else { + None + } + }) + .collect() + } } diff --git a/jirs-client/src/pages/project_page/model.rs b/jirs-client/src/pages/project_page/model.rs index 192b6410..2a6fa6c3 100644 --- a/jirs-client/src/pages/project_page/model.rs +++ b/jirs-client/src/pages/project_page/model.rs @@ -83,6 +83,66 @@ impl ProjectPage { } self.visible_issues = map; } + + pub fn visible_issues( + page: &ProjectPage, + epics: &[Epic], + statuses: &[IssueStatus], + issues: &[Issue], + user: &Option, + ) -> Vec { + 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 mut m = HashMap::new(); + let mut sorted = vec![]; + for issue in issues.into_iter() { + sorted.push((issue.id, issue.updated_at)); + m.insert(issue.id, issue); + } + sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time)); + issues = sorted + .into_iter() + .take(10) + .flat_map(|(id, _)| m.remove(&id)) + .collect(); + issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); + } + + for epic in epics { + 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 { + status_id: current_status_id, + status_name: issue_status_name.to_string(), + ..Default::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 + } } #[inline] diff --git a/jirs-client/src/pages/project_page/update.rs b/jirs-client/src/pages/project_page/update.rs index 334d9866..cc25b5c6 100644 --- a/jirs-client/src/pages/project_page/update.rs +++ b/jirs-client/src/pages/project_page/update.rs @@ -25,139 +25,114 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order _ => (), } - let project_page = match &mut model.page_content { - PageContent::Project(project_page) => project_page, - _ => return, - }; + let mut rebuild_visible = false; + { + let project_page = crate::match_page_mut!(model, Project); - match msg { - Msg::UserChanged(..) - | Msg::ProjectChanged(Some(..)) - | Msg::ChangePage(Page::Project) - | Msg::ChangePage(Page::AddIssue) - | Msg::ChangePage(Page::EditIssue(..)) => { - board_load(model, orders); - } - Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleRemoved, ..) => { - orders.skip().send_msg(Msg::ModalDropped); - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::ResourceChanged( - ResourceKind::Issue - | ResourceKind::Project - | ResourceKind::IssueStatus - | ResourceKind::Epic, - .., - ) => { - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::StyledSelectChanged( - FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), - StyledSelectChanged::Text(text), - ) => { - let modal = model - .modals - .iter_mut() - .filter_map(|modal| match modal { - ModalType::EditIssue(_, modal) => Some(modal), - _ => None, - }) - .last(); - if let Some(m) = modal { - m.top_type_state.text_filter = text; + match msg { + Msg::UserChanged(..) + | Msg::ProjectChanged(Some(..)) + | Msg::ChangePage(Page::Project) + | Msg::ChangePage(Page::AddIssue) + | Msg::ChangePage(Page::EditIssue(..)) => { + board_load(model, orders); } - } - Msg::StrInputChanged(FieldId::TextFilterBoard, text) => { - project_page.text_filter = text; - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::ProjectAvatarFilterChanged(user_id, active) => { - if active { - project_page.active_avatar_filters = - std::mem::replace(&mut project_page.active_avatar_filters, vec![]) - .into_iter() - .filter(|id| *id != user_id) - .collect(); - } else { - project_page.active_avatar_filters.push(user_id); + Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleRemoved, ..) => { + orders.skip().send_msg(Msg::ModalDropped); + rebuild_visible = true; } - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); + Msg::ResourceChanged( + ResourceKind::Issue + | ResourceKind::Project + | ResourceKind::IssueStatus + | ResourceKind::Epic, + .., + ) => { + rebuild_visible = true; + } + Msg::StyledSelectChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), + StyledSelectChanged::Text(text), + ) => { + let modal = model + .modals + .iter_mut() + .filter_map(|modal| match modal { + ModalType::EditIssue(_, modal) => Some(modal), + _ => None, + }) + .last(); + if let Some(m) = modal { + m.top_type_state.text_filter = text; + } + } + Msg::StrInputChanged(FieldId::TextFilterBoard, text) => { + project_page.text_filter = text; + rebuild_visible = true; + } + Msg::ProjectAvatarFilterChanged(user_id, active) => { + if active { + project_page.active_avatar_filters = + std::mem::replace(&mut project_page.active_avatar_filters, vec![]) + .into_iter() + .filter(|id| *id != user_id) + .collect(); + } else { + project_page.active_avatar_filters.push(user_id); + } + rebuild_visible = true; + } + Msg::ProjectToggleOnlyMy => { + project_page.only_my_filter = !project_page.only_my_filter; + rebuild_visible = true; + } + Msg::ProjectToggleRecentlyUpdated => { + project_page.recently_updated_filter = !project_page.recently_updated_filter; + rebuild_visible = true; + } + Msg::ProjectClearFilters => { + project_page.active_avatar_filters = vec![]; + project_page.recently_updated_filter = false; + project_page.only_my_filter = false; + rebuild_visible = true; + } + Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => { + crate::ws::issue::drag_started(issue_id, model) + } + Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(_))) => { + crate::ws::issue::sync(model, orders); + } + Msg::PageChanged(PageChanged::Board(BoardPageChange::ChangePosition( + issue_bellow_id, + ))) => crate::ws::issue::change_position(issue_bellow_id, model), + Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(status))) => { + crate::ws::issue::change_status(status, model) + } + Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => { + crate::ws::issue::sync(model, orders) + } + Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(_id))) => { + project_page.issue_drag.clear_last(); + } + Msg::DeleteIssue(issue_id) => { + send_ws_msg( + jirs_data::WsMsg::IssueDelete(issue_id), + model.ws.as_ref(), + orders, + ); + } + _ => (), } - Msg::ProjectToggleOnlyMy => { - project_page.only_my_filter = !project_page.only_my_filter; - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::ProjectToggleRecentlyUpdated => { - project_page.recently_updated_filter = !project_page.recently_updated_filter; - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::ProjectClearFilters => { - project_page.active_avatar_filters = vec![]; - project_page.recently_updated_filter = false; - project_page.only_my_filter = false; - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); - } - Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => { - crate::ws::issue::drag_started(issue_id, model) - } - Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(_))) => { - crate::ws::issue::sync(model, orders); - } - Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition( - issue_bellow_id, - ))) => crate::ws::issue::exchange_position(issue_bellow_id, model), - Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragOverStatus(status))) => { - crate::ws::issue::change_status(status, model) - } - Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => { - crate::ws::issue::sync(model, orders) - } - Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(_id))) => { - project_page.issue_drag.clear_last(); - } - Msg::DeleteIssue(issue_id) => { - send_ws_msg( - jirs_data::WsMsg::IssueDelete(issue_id), - model.ws.as_ref(), - orders, - ); - } - _ => (), + } + if rebuild_visible { + let visible_issues = ProjectPage::visible_issues( + crate::match_page!(model, Project), + model.epics(), + model.issue_statuses(), + model.issues(), + model.user(), + ); + crate::match_page_mut!(model, Project).visible_issues = visible_issues; } } diff --git a/jirs-client/src/pages/project_page/view/board.rs b/jirs-client/src/pages/project_page/view/board.rs index f59f06bf..5fd16575 100644 --- a/jirs-client/src/pages/project_page/view/board.rs +++ b/jirs-client/src/pages/project_page/view/board.rs @@ -38,6 +38,15 @@ pub fn project_board_lists(model: &Model) -> Node { let edit_button = StyledButton::build() .empty() .icon(Icon::EditAlt) + .on_click(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)) + })) .build() .into_node(); let delete_button = StyledButton::build() @@ -159,7 +168,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node { ev.prevent_default(); ev.stop_propagation(); Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::ExchangePosition(issue_id), + BoardPageChange::ChangePosition(issue_id), ))) }); diff --git a/jirs-client/src/pages/project_settings_page/view.rs b/jirs-client/src/pages/project_settings_page/view.rs index 7934bc43..d87be91a 100644 --- a/jirs-client/src/pages/project_settings_page/view.rs +++ b/jirs-client/src/pages/project_settings_page/view.rs @@ -198,7 +198,7 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node { let width = 100f64 / (model.issue_statuses.len() + 1) as f64; let column_style = format!("width: calc({width}% - 10px)", width = width); let mut per_column_issue_count = HashMap::new(); - for issue in model.issues.iter() { + for issue in model.issues().iter() { *per_column_issue_count .entry(issue.issue_status_id) .or_insert(0) += 1; diff --git a/jirs-client/src/pages/reports_page/view.rs b/jirs-client/src/pages/reports_page/view.rs index 665c5e1e..b7c3f428 100644 --- a/jirs-client/src/pages/reports_page/view.rs +++ b/jirs-client/src/pages/reports_page/view.rs @@ -196,7 +196,7 @@ fn issue_list(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node { fn this_month_updated<'a>(model: &'a Model, page: &ReportsPage) -> Vec<&'a Issue> { model - .issues + .issues() .iter() .filter(|issue| { issue.updated_at.date() >= page.first_day && issue.updated_at.date() <= page.last_day diff --git a/jirs-client/src/ws/issue.rs b/jirs-client/src/ws/issue.rs index 6f73ba1a..52e81b8f 100644 --- a/jirs-client/src/ws/issue.rs +++ b/jirs-client/src/ws/issue.rs @@ -1,6 +1,7 @@ use { crate::{ model::{Model, PageContent}, + pages::project_page::ProjectPage, ws::send_ws_msg, Msg, }, @@ -16,7 +17,7 @@ pub fn drag_started(issue_id: EpicId, model: &mut Model) { project_page.issue_drag.drag(issue_id); } -pub fn exchange_position(below_id: EpicId, model: &mut Model) { +pub fn change_position(below_id: EpicId, model: &mut Model) { let project_page = match &mut model.page_content { PageContent::Project(project_page) => project_page, _ => return, @@ -30,7 +31,7 @@ pub fn exchange_position(below_id: EpicId, model: &mut Model) { } let (issue_status_id, epic_id) = model - .issues + .issues() .iter() .find_map(|issue| { if issue.id == dragged_id { @@ -42,7 +43,7 @@ pub fn exchange_position(below_id: EpicId, model: &mut Model) { .unwrap_or_default(); let mut issues: Vec = model - .issues + .issues_mut() .drain_filter(|issue| issue.issue_status_id == issue_status_id && issue.epic_id == epic_id) .collect(); issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); @@ -66,16 +67,18 @@ pub fn exchange_position(below_id: EpicId, model: &mut Model) { iss.list_position = issue.list_position; } changed.push((issue.id, issue.list_position)); - model.issues.push(issue); + model.issues_mut().push(issue); } + let visible = ProjectPage::visible_issues( + crate::match_page!(model, Project), + model.epics(), + model.issue_statuses(), + model.issues(), + model.user(), + ); if let PageContent::Project(project_page) = &mut model.page_content { - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, - ); + project_page.visible_issues = visible; for (id, _) in changed.iter() { project_page.issue_drag.mark_dirty(*id); } @@ -107,18 +110,16 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders) { model.ws.as_ref(), orders, ); - if let PageContent::Project(project_page) = &mut model.page_content { - project_page.issue_drag.clear() - }; + crate::match_page_mut!(model, Project).issue_drag.clear(); } pub fn change_status(status_id: IssueStatusId, model: &mut Model) { - let project_page = match &mut model.page_content { - PageContent::Project(project_page) => project_page, - _ => return, - }; - - let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() { + let dragged_id = match crate::match_page!(model, Project) + .issue_drag + .dragged_id + .as_ref() + .cloned() + { Some(issue_id) => issue_id, _ => return error!("Nothing is dragged"), }; @@ -132,7 +133,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) { } let mut issues: Vec = model - .issues + .issues_mut() .drain_filter(|issue| { if issue.id == dragged_id { issue.issue_status_id = status_id; @@ -143,6 +144,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) { issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); + let mut dirty = vec![]; for mut issue in issues { if issue.id == dragged_id { issue.issue_status_id = status_id; @@ -150,14 +152,24 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) { iss.issue_status_id = status_id; } } - project_page.issue_drag.mark_dirty(issue.id); - model.issues.push(issue); + + dirty.push(issue.id); + model.issues_mut().push(issue); + } + { + let project_page = crate::match_page_mut!(model, Project); + for id in dirty { + project_page.issue_drag.mark_dirty(id); + } } - project_page.rebuild_visible( - &model.epics, - &model.issue_statuses, - &model.issues, - &model.user, + let visible = ProjectPage::visible_issues( + crate::match_page!(model, Project), + model.epics(), + model.issue_statuses(), + model.issues(), + model.user(), ); + + crate::match_page_mut!(model, Project).visible_issues = visible; } diff --git a/jirs-client/src/ws/mod.rs b/jirs-client/src/ws/mod.rs index 68d46da6..6034b663 100644 --- a/jirs-client/src/ws/mod.rs +++ b/jirs-client/src/ws/mod.rs @@ -219,9 +219,11 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { // issues WsMsg::ProjectIssuesLoaded(mut v) => { v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); - model.issues = v; + { + let _ = std::mem::replace(model.issues_mut(), v.clone()); + }; model.issues_by_id.clear(); - for issue in model.issues.iter() { + for issue in v { model.issues_by_id.insert(issue.id, issue.clone()); } @@ -231,12 +233,12 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { None, )); } - WsMsg::IssueUpdated(mut issue) => { + WsMsg::IssueUpdated(issue) => { let id = issue.id; model.issues_by_id.remove(&id); model.issues_by_id.insert(id, issue.clone()); - if let Some(idx) = model.issues.iter().position(|i| i.id == issue.id) { - std::mem::swap(&mut model.issues[idx], &mut issue); + if let Some(idx) = model.issues().iter().position(|i| i.id == issue.id) { + let _ = std::mem::replace(&mut model.issues_mut()[idx], issue); } orders.send_msg(Msg::ResourceChanged( ResourceKind::Issue, @@ -246,12 +248,12 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { } WsMsg::IssueDeleted(id, _count) => { let mut old = vec![]; - std::mem::swap(&mut model.issues, &mut old); + std::mem::swap(model.issues_mut(), &mut old); for is in old { if is.id == id { continue; } - model.issues.push(is); + model.issues_mut().push(is); } orders.send_msg(Msg::ResourceChanged( ResourceKind::Issue, @@ -282,27 +284,33 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { return; } comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); - model.comments = comments; + model.comments = comments.clone(); + for comment in comments { + model.comments_by_id.insert(comment.id, comment); + } orders.send_msg(Msg::ResourceChanged( ResourceKind::Comment, OperationKind::ListLoaded, None, )); } - WsMsg::CommentUpdated(mut comment) => { + WsMsg::CommentUpdated(comment) => { + let comment_id = comment.id; if let Some(idx) = model.comments.iter().position(|c| c.id == comment.id) { - std::mem::swap(&mut model.comments[idx], &mut comment); + let _ = std::mem::replace(&mut model.comments[idx], comment.clone()); + model.comments_by_id.insert(comment.id, comment); } orders.send_msg(Msg::ResourceChanged( ResourceKind::Comment, OperationKind::SingleModified, - Some(comment.id), + Some(comment_id), )); } WsMsg::CommentDeleted(comment_id, _count) => { if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) { model.comments.remove(idx); } + model.comments_by_id.remove(&comment_id); orders.send_msg(Msg::ResourceChanged( ResourceKind::Comment, OperationKind::SingleRemoved, @@ -317,7 +325,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { } if let Some(me) = model.user.as_mut() { if me.id == user_id { - me.avatar_url = Some(avatar_url.clone()); + me.avatar_url = Some(avatar_url); } } orders.send_msg(Msg::ResourceChanged( @@ -361,7 +369,10 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { // epics WsMsg::EpicsLoaded(epics) => { - model.epics = epics; + model.epics = epics.clone(); + for epic in epics { + model.epics_by_id.insert(epic.id, epic); + } orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::ListLoaded, @@ -370,29 +381,33 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders) { } WsMsg::EpicCreated(epic) => { let id = epic.id; - model.epics.push(epic); + model.epics.push(epic.clone()); model.epics.sort_by(|a, b| a.id.cmp(&b.id)); + model.epics_by_id.insert(epic.id, epic); orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::SingleCreated, Some(id), )); } - WsMsg::EpicUpdated(mut epic) => { + WsMsg::EpicUpdated(epic) => { + let epic_id = epic.id; if let Some(idx) = model.epics.iter().position(|e| e.id == epic.id) { - std::mem::swap(&mut model.epics[idx], &mut epic); + let _ = std::mem::replace(&mut model.epics[idx], epic.clone()); } + model.epics_by_id.insert(epic.id, epic); model.epics.sort_by(|a, b| a.id.cmp(&b.id)); orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, OperationKind::SingleModified, - Some(epic.id), + Some(epic_id), )); } WsMsg::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)); orders.send_msg(Msg::ResourceChanged( ResourceKind::Epic, diff --git a/shared/jirs-data/src/fields.rs b/shared/jirs-data/src/fields.rs index 9296705a..05564517 100644 --- a/shared/jirs-data/src/fields.rs +++ b/shared/jirs-data/src/fields.rs @@ -65,4 +65,5 @@ pub enum EpicFieldId { Name, StartsAt, EndsAt, + TransformInto, }