Start edit epic

This commit is contained in:
eraden 2021-01-17 00:53:56 +01:00
parent b137b461a8
commit 82eb025359
24 changed files with 528 additions and 237 deletions

View File

@ -18,7 +18,7 @@ pub enum BoardPageChange {
IssueDragStarted(EpicId), IssueDragStarted(EpicId),
IssueDragStopped(EpicId), IssueDragStopped(EpicId),
DragLeave(EpicId), DragLeave(EpicId),
ExchangePosition(EpicId), ChangePosition(EpicId),
IssueDragOverStatus(IssueStatusId), IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId), IssueDropZone(IssueStatusId),
} }

View File

@ -1,6 +1,6 @@
use jirs_data::{ use jirs_data::{
CommentFieldId, InviteFieldId, IssueFieldId, ProjectFieldId, SignInFieldId, SignUpFieldId, CommentFieldId, EpicFieldId, InviteFieldId, IssueFieldId, ProjectFieldId, SignInFieldId,
UsersFieldId, SignUpFieldId, UsersFieldId,
}; };
pub type AvatarFilterActive = bool; pub type AvatarFilterActive = bool;
@ -92,6 +92,8 @@ pub enum FieldId {
// issue // issue
AddIssueModal(IssueFieldId), AddIssueModal(IssueFieldId),
EditIssueModal(EditIssueModalSection), EditIssueModal(EditIssueModalSection),
// epic
EditEpic(EpicFieldId),
// project boards // project boards
TextFilterBoard, TextFilterBoard,
CopyButtonLabel, CopyButtonLabel,
@ -202,6 +204,12 @@ impl std::fmt::Display for FieldId {
UsersFieldId::Avatar => f.write_str("profile-avatar"), UsersFieldId::Avatar => f.write_str("profile-avatar"),
UsersFieldId::CurrentProject => f.write_str("profile-currentProject"), 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"), FieldId::Rte(..) => f.write_str("rte"),
} }
} }

View File

@ -238,9 +238,11 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
crate::modals::update(&msg, model, orders); crate::modals::update(&msg, model, orders);
match model.page { match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) | Page::DeleteEpic(..) => { Page::Project
pages::project_page::update(msg, model, orders) | 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::ProjectSettings => pages::project_settings_page::update(msg, model, orders),
Page::SignIn => pages::sign_in_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), 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<Msg>) {
fn view(model: &model::Model) -> Node<Msg> { fn view(model: &model::Model) -> Node<Msg> {
match model.page { match model.page {
Page::Project | Page::AddIssue | Page::DeleteEpic(..) | Page::EditIssue(..) => { Page::Project
pages::project_page::view(model) | Page::AddIssue
} | Page::EditIssue(..)
| Page::DeleteEpic(..)
| Page::EditEpic(..) => pages::project_page::view(model),
Page::ProjectSettings => pages::project_settings_page::view(model), Page::ProjectSettings => pages::project_settings_page::view(model),
Page::SignIn => pages::sign_in_page::view(model), Page::SignIn => pages::sign_in_page::view(model),
Page::SignUp => pages::sign_up_page::view(model), Page::SignUp => pages::sign_up_page::view(model),
@ -280,10 +284,6 @@ fn resolve_page(url: Url) -> Option<Page> {
Some(Ok(id)) => Page::EditIssue(id), Some(Ok(id)) => Page::EditIssue(id),
_ => return None, _ => return None,
}, },
"delete-epic" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Page::DeleteEpic(id),
_ => return None,
},
"profile" => Page::Profile, "profile" => Page::Profile,
"add-issue" => Page::AddIssue, "add-issue" => Page::AddIssue,
"project-settings" => Page::ProjectSettings, "project-settings" => Page::ProjectSettings,
@ -292,6 +292,14 @@ fn resolve_page(url: Url) -> Option<Page> {
"invite" => Page::Invite, "invite" => Page::Invite,
"users" => Page::Users, "users" => Page::Users,
"reports" => Page::Reports, "reports" => Page::Reports,
"delete-epic" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Page::DeleteEpic(id),
_ => return None,
},
"edit-epic" => match url.path().get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Page::EditEpic(id),
_ => return None,
},
_ => Page::Project, _ => Page::Project,
}; };
Some(page) Some(page)

View File

@ -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<IssueId>,
}
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,
}
}
}

View File

@ -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<IssueId>,
}
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,
}
}
}

View File

@ -1,7 +1,7 @@
use { use {
crate::{ crate::{
components::{styled_button::*, styled_confirm_modal::*, styled_icon::*, styled_modal::*}, components::{styled_button::*, styled_confirm_modal::*, styled_icon::*, styled_modal::*},
modals::epic_delete::Model, modals::epics_delete::Model,
model, model,
shared::ToNode, shared::ToNode,
Msg, Msg,

View File

@ -0,0 +1,5 @@
pub use {model::*, update::*, view::*};
mod model;
mod update;
mod view;

View File

@ -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<IssueId>,
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![],
),
}
}
}

View File

@ -0,0 +1,5 @@
use {crate::Msg, seed::prelude::*};
pub fn update(_msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Orders<Msg>) {
let _modal = crate::match_modal_mut!(model, DeleteEpic);
}

View File

@ -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<Msg> {
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()
}

View File

@ -2,7 +2,8 @@ pub use {epic_field::*, update::*, view::*};
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub mod debug; pub mod debug;
pub mod epic_delete; pub mod epics_delete;
pub mod epics_edit;
pub mod issue_statuses_delete; pub mod issue_statuses_delete;
pub mod issues_create; pub mod issues_create;
pub mod issues_delete; pub mod issues_delete;
@ -12,3 +13,35 @@ pub mod time_tracking;
mod epic_field; mod epic_field;
mod update; mod update;
mod view; 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,
}
};
}

View File

@ -5,7 +5,7 @@ use {
ws::send_ws_msg, ws::send_ws_msg,
FieldChange, FieldId, Msg, OperationKind, ResourceKind, FieldChange, FieldId, Msg, OperationKind, ResourceKind,
}, },
jirs_data::{TimeTracking, WsMsg}, jirs_data::{EpicId, IssueId, TimeTracking, WsMsg},
seed::{prelude::*, *}, seed::{prelude::*, *},
}; };
@ -46,6 +46,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(Page::DeleteEpic(issue_id)) => { Msg::ChangePage(Page::DeleteEpic(issue_id)) => {
push_delete_epic_modal(*issue_id, model, orders) 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)] #[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => { Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
@ -69,7 +70,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
issues_create::update(msg, model, orders); issues_create::update(msg, model, orders);
issues_edit::update(msg, model, orders); issues_edit::update(msg, model, orders);
issue_statuses_delete::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<Msg>) {
}))); })));
} }
fn push_delete_epic_modal(issue_id: i32, model: &mut Model, _orders: &mut impl Orders<Msg>) { fn push_edit_modal(issue_id: EpicId, model: &mut Model, orders: &mut impl Orders<Msg>) {
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<Msg>) {
let time_tracking_type = model let time_tracking_type = model
.project .project
.as_ref() .as_ref()
@ -113,3 +109,15 @@ fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Ms
); );
model.modals.push(modal); model.modals.push(modal);
} }
fn push_edit_epic_modal(epic_id: EpicId, model: &mut Model, _orders: &mut impl Orders<Msg>) {
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<Msg>) {
use crate::modals::epics_delete::Model;
let modal = Model::new(issue_id, model);
model.modals.push(ModalType::DeleteEpic(Box::new(modal)));
}

View File

@ -14,6 +14,10 @@ pub fn view(model: &Model) -> Node<Msg> {
.modals .modals
.iter() .iter()
.map(|modal| match modal { .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) => { ModalType::EditIssue(issue_id, modal) => {
if let Some(_issue) = model.issues_by_id.get(issue_id) { if let Some(_issue) = model.issues_by_id.get(issue_id) {
let details = issues_edit::view(model, modal.as_ref()); let details = issues_edit::view(model, modal.as_ref());
@ -29,6 +33,7 @@ pub fn view(model: &Model) -> Node<Msg> {
} }
ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model), ModalType::DeleteIssueConfirm(_id) => crate::modals::issues_delete::view(model),
ModalType::AddIssue(modal) => issues_create::view(model, modal), ModalType::AddIssue(modal) => issues_create::view(model, modal),
// comment
ModalType::DeleteCommentConfirm(comment_id) => { ModalType::DeleteCommentConfirm(comment_id) => {
let comment_id = *comment_id; let comment_id = *comment_id;
StyledConfirmModal::build() StyledConfirmModal::build()
@ -47,7 +52,6 @@ pub fn view(model: &Model) -> Node<Msg> {
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
ModalType::DebugModal => crate::modals::debug::view(model), ModalType::DebugModal => crate::modals::debug::view(model),
ModalType::DeleteEpic(modal) => crate::modals::epic_delete::view(model, modal),
}) })
.collect(); .collect();
section![id!["modals"], modals] section![id!["modals"], modals]

View File

@ -26,9 +26,13 @@ pub trait IssueModal {
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType { pub enum ModalType {
// issue
AddIssue(Box<crate::modals::issues_create::Model>), AddIssue(Box<crate::modals::issues_create::Model>),
EditIssue(EpicId, Box<crate::modals::issues_edit::Model>), EditIssue(EpicId, Box<crate::modals::issues_edit::Model>),
DeleteEpic(Box<crate::modals::epic_delete::Model>), // epic
DeleteEpic(Box<crate::modals::epics_delete::Model>),
EditEpic(Box<crate::modals::epics_edit::Model>),
DeleteIssueConfirm(EpicId), DeleteIssueConfirm(EpicId),
DeleteCommentConfirm(CommentId), DeleteCommentConfirm(CommentId),
TimeTracking(EpicId), TimeTracking(EpicId),
@ -47,10 +51,15 @@ pub struct CommentForm {
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)] #[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub enum Page { pub enum Page {
Project, Project,
EditIssue(EpicId), // epic
DeleteEpic(EpicId), DeleteEpic(EpicId),
EditEpic(EpicId),
// issue
EditIssue(EpicId),
AddIssue, AddIssue,
// settings
ProjectSettings, ProjectSettings,
// auth
SignIn, SignIn,
SignUp, SignUp,
Invite, Invite,
@ -63,8 +72,9 @@ impl Page {
pub fn to_path(self) -> String { pub fn to_path(self) -> String {
match self { match self {
Page::Project => "/board".to_string(), Page::Project => "/board".to_string(),
Page::EditIssue(id) => format!("/issues/{id}", id = id),
Page::DeleteEpic(id) => format!("/delete-epic/{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::AddIssue => "/add-issue".to_string(),
Page::ProjectSettings => "/project-settings".to_string(), Page::ProjectSettings => "/project-settings".to_string(),
Page::SignIn => "/login".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)] #[derive(Debug)]
pub enum PageContent { pub enum PageContent {
SignIn(Box<SignInPage>), SignIn(Box<SignInPage>),
@ -145,27 +174,39 @@ pub struct Model {
pub page: Page, pub page: Page,
pub page_content: PageContent, pub page_content: PageContent,
pub project: Option<Project>,
pub current_user_project: Option<UserProject>, pub current_user_project: Option<UserProject>,
pub issues: Vec<Issue>, // issues
issues: Vec<Issue>,
pub issues_by_id: HashMap<EpicId, Issue>, pub issues_by_id: HashMap<EpicId, Issue>,
// users
pub user: Option<User>, pub user: Option<User>,
pub users: Vec<User>, pub users: Vec<User>,
pub users_by_id: HashMap<UserId, User>, pub users_by_id: HashMap<UserId, User>,
// comments
pub comments: Vec<Comment>, pub comments: Vec<Comment>,
pub comments_by_id: HashMap<CommentId, Comment>,
// issue_statuses
pub issue_statuses: Vec<IssueStatus>, pub issue_statuses: Vec<IssueStatus>,
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>,
// messages
pub messages: Vec<Message>, pub messages: Vec<Message>,
// user_projects
pub user_projects: Vec<UserProject>, pub user_projects: Vec<UserProject>,
// projects
pub project: Option<Project>,
pub projects: Vec<Project>, pub projects: Vec<Project>,
// epics
pub epics: Vec<Epic>, pub epics: Vec<Epic>,
pub epics_by_id: HashMap<EpicId, Epic>,
pub show_extras: bool, pub show_extras: bool,
} }
@ -194,6 +235,7 @@ impl Model {
users: vec![], users: vec![],
users_by_id: Default::default(), users_by_id: Default::default(),
comments: vec![], comments: vec![],
comments_by_id: Default::default(),
issue_statuses: vec![], issue_statuses: vec![],
issue_statuses_by_id: Default::default(), issue_statuses_by_id: Default::default(),
issue_statuses_by_name: Default::default(), issue_statuses_by_name: Default::default(),
@ -203,13 +245,52 @@ impl Model {
epics: vec![], epics: vec![],
issues_by_id: Default::default(), issues_by_id: Default::default(),
show_extras: false, 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<Issue> {
&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<User> {
&self.user
}
pub fn current_user_role(&self) -> UserRole { pub fn current_user_role(&self) -> UserRole {
self.current_user_project self.current_user_project
.as_ref() .as_ref()
.map(|up| up.role) .map(|up| up.role)
.unwrap_or_default() .unwrap_or_default()
} }
pub fn epic_issue_ids(&self, epic_id: EpicId) -> Vec<IssueId> {
self.issues()
.iter()
.filter_map(|issue| {
if issue.epic_id == Some(epic_id) {
Some(issue.id)
} else {
None
}
})
.collect()
}
} }

View File

@ -83,6 +83,66 @@ impl ProjectPage {
} }
self.visible_issues = map; self.visible_issues = map;
} }
pub fn visible_issues(
page: &ProjectPage,
epics: &[Epic],
statuses: &[IssueStatus],
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 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] #[inline]

View File

@ -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 { let mut rebuild_visible = false;
PageContent::Project(project_page) => project_page, {
_ => return, let project_page = crate::match_page_mut!(model, Project);
};
match msg { match msg {
Msg::UserChanged(..) Msg::UserChanged(..)
| Msg::ProjectChanged(Some(..)) | Msg::ProjectChanged(Some(..))
| Msg::ChangePage(Page::Project) | Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue) | Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => { | Msg::ChangePage(Page::EditIssue(..)) => {
board_load(model, orders); 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;
} }
} Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleRemoved, ..) => {
Msg::StrInputChanged(FieldId::TextFilterBoard, text) => { orders.skip().send_msg(Msg::ModalDropped);
project_page.text_filter = text; rebuild_visible = true;
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);
} }
project_page.rebuild_visible( Msg::ResourceChanged(
&model.epics, ResourceKind::Issue
&model.issue_statuses, | ResourceKind::Project
&model.issues, | ResourceKind::IssueStatus
&model.user, | 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; if rebuild_visible {
project_page.rebuild_visible( let visible_issues = ProjectPage::visible_issues(
&model.epics, crate::match_page!(model, Project),
&model.issue_statuses, model.epics(),
&model.issues, model.issue_statuses(),
&model.user, model.issues(),
); model.user(),
} );
Msg::ProjectToggleRecentlyUpdated => { crate::match_page_mut!(model, Project).visible_issues = visible_issues;
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,
);
}
_ => (),
} }
} }

View File

@ -38,6 +38,15 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
let edit_button = StyledButton::build() let edit_button = StyledButton::build()
.empty() .empty()
.icon(Icon::EditAlt) .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() .build()
.into_node(); .into_node();
let delete_button = StyledButton::build() let delete_button = StyledButton::build()
@ -159,7 +168,7 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
ev.prevent_default(); ev.prevent_default();
ev.stop_propagation(); ev.stop_propagation();
Some(Msg::PageChanged(PageChanged::Board( Some(Msg::PageChanged(PageChanged::Board(
BoardPageChange::ExchangePosition(issue_id), BoardPageChange::ChangePosition(issue_id),
))) )))
}); });

View File

@ -198,7 +198,7 @@ fn columns_section(model: &Model, page: &ProjectSettingsPage) -> Node<Msg> {
let width = 100f64 / (model.issue_statuses.len() + 1) as f64; let width = 100f64 / (model.issue_statuses.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 mut per_column_issue_count = HashMap::new(); let mut per_column_issue_count = HashMap::new();
for issue in model.issues.iter() { for issue in model.issues().iter() {
*per_column_issue_count *per_column_issue_count
.entry(issue.issue_status_id) .entry(issue.issue_status_id)
.or_insert(0) += 1; .or_insert(0) += 1;

View File

@ -196,7 +196,7 @@ fn issue_list(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
fn this_month_updated<'a>(model: &'a Model, page: &ReportsPage) -> Vec<&'a Issue> { fn this_month_updated<'a>(model: &'a Model, page: &ReportsPage) -> Vec<&'a Issue> {
model model
.issues .issues()
.iter() .iter()
.filter(|issue| { .filter(|issue| {
issue.updated_at.date() >= page.first_day && issue.updated_at.date() <= page.last_day issue.updated_at.date() >= page.first_day && issue.updated_at.date() <= page.last_day

View File

@ -1,6 +1,7 @@
use { use {
crate::{ crate::{
model::{Model, PageContent}, model::{Model, PageContent},
pages::project_page::ProjectPage,
ws::send_ws_msg, ws::send_ws_msg,
Msg, Msg,
}, },
@ -16,7 +17,7 @@ pub fn drag_started(issue_id: EpicId, model: &mut Model) {
project_page.issue_drag.drag(issue_id); 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 { let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page, PageContent::Project(project_page) => project_page,
_ => return, _ => return,
@ -30,7 +31,7 @@ pub fn exchange_position(below_id: EpicId, model: &mut Model) {
} }
let (issue_status_id, epic_id) = model let (issue_status_id, epic_id) = model
.issues .issues()
.iter() .iter()
.find_map(|issue| { .find_map(|issue| {
if issue.id == dragged_id { if issue.id == dragged_id {
@ -42,7 +43,7 @@ pub fn exchange_position(below_id: EpicId, model: &mut Model) {
.unwrap_or_default(); .unwrap_or_default();
let mut issues: Vec<Issue> = model let mut issues: Vec<Issue> = model
.issues .issues_mut()
.drain_filter(|issue| issue.issue_status_id == issue_status_id && issue.epic_id == epic_id) .drain_filter(|issue| issue.issue_status_id == issue_status_id && issue.epic_id == epic_id)
.collect(); .collect();
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); 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; iss.list_position = issue.list_position;
} }
changed.push((issue.id, 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 { if let PageContent::Project(project_page) = &mut model.page_content {
project_page.rebuild_visible( project_page.visible_issues = visible;
&model.epics,
&model.issue_statuses,
&model.issues,
&model.user,
);
for (id, _) in changed.iter() { for (id, _) in changed.iter() {
project_page.issue_drag.mark_dirty(*id); project_page.issue_drag.mark_dirty(*id);
} }
@ -107,18 +110,16 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
model.ws.as_ref(), model.ws.as_ref(),
orders, orders,
); );
if let PageContent::Project(project_page) = &mut model.page_content { crate::match_page_mut!(model, Project).issue_drag.clear();
project_page.issue_drag.clear()
};
} }
pub fn change_status(status_id: IssueStatusId, model: &mut Model) { pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
let project_page = match &mut model.page_content { let dragged_id = match crate::match_page!(model, Project)
PageContent::Project(project_page) => project_page, .issue_drag
_ => return, .dragged_id
}; .as_ref()
.cloned()
let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() { {
Some(issue_id) => issue_id, Some(issue_id) => issue_id,
_ => return error!("Nothing is dragged"), _ => return error!("Nothing is dragged"),
}; };
@ -132,7 +133,7 @@ pub fn change_status(status_id: IssueStatusId, model: &mut Model) {
} }
let mut issues: Vec<Issue> = model let mut issues: Vec<Issue> = model
.issues .issues_mut()
.drain_filter(|issue| { .drain_filter(|issue| {
if issue.id == dragged_id { if issue.id == dragged_id {
issue.issue_status_id = status_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)); issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
let mut dirty = vec![];
for mut issue in issues { for mut issue in issues {
if issue.id == dragged_id { if issue.id == dragged_id {
issue.issue_status_id = status_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; 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( let visible = ProjectPage::visible_issues(
&model.epics, crate::match_page!(model, Project),
&model.issue_statuses, model.epics(),
&model.issues, model.issue_statuses(),
&model.user, model.issues(),
model.user(),
); );
crate::match_page_mut!(model, Project).visible_issues = visible;
} }

View File

@ -219,9 +219,11 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// issues // issues
WsMsg::ProjectIssuesLoaded(mut v) => { WsMsg::ProjectIssuesLoaded(mut v) => {
v.sort_by(|a, b| (a.list_position as i64).cmp(&(b.list_position as i64))); 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(); model.issues_by_id.clear();
for issue in model.issues.iter() { for issue in v {
model.issues_by_id.insert(issue.id, issue.clone()); 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<Msg>) {
None, None,
)); ));
} }
WsMsg::IssueUpdated(mut issue) => { WsMsg::IssueUpdated(issue) => {
let id = issue.id; let id = issue.id;
model.issues_by_id.remove(&id); model.issues_by_id.remove(&id);
model.issues_by_id.insert(id, issue.clone()); model.issues_by_id.insert(id, issue.clone());
if let Some(idx) = model.issues.iter().position(|i| i.id == issue.id) { if let Some(idx) = model.issues().iter().position(|i| i.id == issue.id) {
std::mem::swap(&mut model.issues[idx], &mut issue); let _ = std::mem::replace(&mut model.issues_mut()[idx], issue);
} }
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue, ResourceKind::Issue,
@ -246,12 +248,12 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
WsMsg::IssueDeleted(id, _count) => { WsMsg::IssueDeleted(id, _count) => {
let mut old = vec![]; let mut old = vec![];
std::mem::swap(&mut model.issues, &mut old); std::mem::swap(model.issues_mut(), &mut old);
for is in old { for is in old {
if is.id == id { if is.id == id {
continue; continue;
} }
model.issues.push(is); model.issues_mut().push(is);
} }
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Issue, ResourceKind::Issue,
@ -282,27 +284,33 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
return; return;
} }
comments.sort_by(|a, b| a.updated_at.cmp(&b.updated_at)); 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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment, ResourceKind::Comment,
OperationKind::ListLoaded, OperationKind::ListLoaded,
None, 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) { 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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment, ResourceKind::Comment,
OperationKind::SingleModified, OperationKind::SingleModified,
Some(comment.id), Some(comment_id),
)); ));
} }
WsMsg::CommentDeleted(comment_id, _count) => { WsMsg::CommentDeleted(comment_id, _count) => {
if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) { if let Some(idx) = model.comments.iter().position(|c| c.id == comment_id) {
model.comments.remove(idx); model.comments.remove(idx);
} }
model.comments_by_id.remove(&comment_id);
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Comment, ResourceKind::Comment,
OperationKind::SingleRemoved, OperationKind::SingleRemoved,
@ -317,7 +325,7 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
if let Some(me) = model.user.as_mut() { if let Some(me) = model.user.as_mut() {
if me.id == user_id { if me.id == user_id {
me.avatar_url = Some(avatar_url.clone()); me.avatar_url = Some(avatar_url);
} }
} }
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
@ -361,7 +369,10 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// epics // epics
WsMsg::EpicsLoaded(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( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::ListLoaded, OperationKind::ListLoaded,
@ -370,29 +381,33 @@ pub fn update(msg: WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
WsMsg::EpicCreated(epic) => { WsMsg::EpicCreated(epic) => {
let id = epic.id; 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.sort_by(|a, b| a.id.cmp(&b.id));
model.epics_by_id.insert(epic.id, epic);
orders.send_msg(Msg::ResourceChanged( orders.send_msg(Msg::ResourceChanged(
ResourceKind::Epic, ResourceKind::Epic,
OperationKind::SingleCreated, OperationKind::SingleCreated,
Some(id), 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) { 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)); 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,
Some(epic.id), Some(epic_id),
)); ));
} }
WsMsg::EpicDeleted(id, _count) => { WsMsg::EpicDeleted(id, _count) => {
if let Some(idx) = model.epics.iter().position(|e| e.id == id) { if let Some(idx) = model.epics.iter().position(|e| e.id == id) {
model.epics.remove(idx); model.epics.remove(idx);
} }
model.epics_by_id.remove(&id);
model.epics.sort_by(|a, b| a.id.cmp(&b.id)); 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,

View File

@ -65,4 +65,5 @@ pub enum EpicFieldId {
Name, Name,
StartsAt, StartsAt,
EndsAt, EndsAt,
TransformInto,
} }