Add delete epic modal

This commit is contained in:
Adrian Woźniak 2021-01-16 14:31:31 +01:00
parent 915ff46281
commit 87d2ea48a3
24 changed files with 267 additions and 78 deletions

View File

@ -28,21 +28,21 @@ pub struct LoadProjectIssues {
#[derive(Default, Execute)]
#[db_exec(result = "Issue", schema = "issues")]
pub struct UpdateIssue {
pub issue_id: i32,
pub issue_id: jirs_data::IssueId,
pub title: Option<String>,
pub issue_type: Option<IssueType>,
pub priority: Option<IssuePriority>,
pub list_position: Option<i32>,
pub list_position: Option<jirs_data::ListPosition>,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: Option<i32>,
pub user_ids: Option<Vec<i32>>,
pub reporter_id: Option<i32>,
pub issue_status_id: Option<i32>,
pub epic_id: Option<Option<i32>>,
pub project_id: Option<jirs_data::ProjectId>,
pub user_ids: Option<Vec<jirs_data::UserId>>,
pub reporter_id: Option<jirs_data::UserId>,
pub issue_status_id: Option<jirs_data::IssueStatusId>,
pub epic_id: Option<Option<jirs_data::EpicId>>,
}
impl UpdateIssue {

View File

@ -1,4 +1,3 @@
use jirs_data::{EpicId, IssueStatusId, ListPosition};
use {
crate::{WebSocketActor, WsHandler, WsResult},
database_actor::{
@ -6,7 +5,10 @@ use {
issues::{LoadProjectIssues, UpdateIssue},
},
futures::executor::block_on,
jirs_data::{CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, PayloadVariant, WsMsg},
jirs_data::{
CreateIssuePayload, IssueAssignee, IssueFieldId, IssueId, IssueStatusId, ListPosition,
PayloadVariant, WsMsg,
},
std::collections::HashMap,
};
@ -257,7 +259,7 @@ impl WsHandler<LoadIssues> for WebSocketActor {
}
}
pub struct SyncIssueListPosition(pub Vec<(IssueId, ListPosition, IssueStatusId, Option<EpicId>)>);
pub struct SyncIssueListPosition(pub Vec<(IssueId, ListPosition, IssueStatusId, Option<IssueId>)>);
impl WsHandler<SyncIssueListPosition> for WebSocketActor {
fn handle_msg(&mut self, msg: SyncIssueListPosition, ctx: &mut Self::Context) -> WsResult {

View File

@ -108,6 +108,62 @@
&.debugModal {
padding-left: 15px;
}
&.deleteEpic {
> section {
> .header {
background: var(--danger);
color: var(--secondary);
padding: {
top: 15px;
right: 40px;
left: 40px;
bottom: 15px;
};
}
> .warning {
margin: {
bottom: 10px;
top: 10px;
}
padding: {
right: 40px;
left: 40px;
};
}
> .relatedList {
list-style: none;
padding: {
right: 40px;
left: 40px;
bottom: 20px;
};
> li {
> .relatedIssue {
list-style: none;
line-height: 22px;
font-size: 14px;
> a {
color: var(--textLink);
.styledIcon.link {
font-size: 14px;
margin: {
right: 5px;
}
}
}
}
}
}
}
}
}
}
}

View File

@ -1,6 +1,6 @@
use seed::prelude::WebSocketMessage;
use jirs_data::{IssueId, IssueStatusId, WsMsg};
use jirs_data::{EpicId, IssueStatusId, WsMsg};
use crate::shared::styled_editor::Mode as TabMode;
use crate::FieldId;
@ -16,10 +16,10 @@ pub enum FieldChange {
#[derive(Clone, Debug, PartialEq)]
pub enum BoardPageChange {
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
DragLeave(IssueId),
ExchangePosition(IssueId),
IssueDragStarted(EpicId),
IssueDragStopped(EpicId),
DragLeave(EpicId),
ExchangePosition(EpicId),
IssueDragOverStatus(IssueStatusId),
IssueDropZone(IssueStatusId),
}

View File

@ -104,7 +104,7 @@ pub enum Msg {
// issues
AddIssue,
DeleteIssue(IssueId),
DeleteIssue(EpicId),
// epics
AddEpic,
@ -237,7 +237,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
crate::modals::update(&msg, model, orders);
match model.page {
Page::Project | Page::AddIssue | Page::EditIssue(..) => {
Page::Project | Page::AddIssue | Page::EditIssue(..) | Page::DeleteEpic(..) => {
pages::project_page::update(msg, model, orders)
}
Page::ProjectSettings => pages::project_settings_page::update(msg, model, orders),
@ -255,8 +255,9 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
fn view(model: &model::Model) -> Node<Msg> {
match model.page {
Page::Project | Page::AddIssue => pages::project_page::view(model),
Page::EditIssue(_id) => pages::project_page::view(model),
Page::Project | Page::AddIssue | Page::DeleteEpic(..) | Page::EditIssue(..) => {
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),
@ -278,6 +279,10 @@ fn resolve_page(url: Url) -> Option<Page> {
Some(Ok(id)) => Page::EditIssue(id),
_ => 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,
"add-issue" => Page::AddIssue,
"project-settings" => Page::ProjectSettings,

View File

@ -14,7 +14,7 @@ pub fn view(model: &Model) -> Node<Msg> {
.width(1200)
.add_class("debugModal")
.center()
.children(vec![code])
.children(vec![code].into_iter())
.build()
.into_node()
}

View File

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

View File

@ -0,0 +1,30 @@
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,10 @@
use {
crate::{shared::go_to_board, Msg},
seed::prelude::*,
};
pub fn update(msg: &Msg, _model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
if let Msg::ModalDropped = msg {
go_to_board(orders);
};
}

View File

@ -0,0 +1,55 @@
use {
crate::{
modals::epic_delete::Model,
model,
shared::{styled_confirm_modal::*, styled_icon::*, styled_modal::*, ToNode},
Msg,
},
seed::{prelude::*, *},
};
pub fn view(model: &model::Model, modal: &Model) -> Node<Msg> {
if modal.related_issues.is_empty() {
StyledConfirmModal::build()
.title("Delete empty epic")
.cancel_text("Cancel")
.confirm_text("Delete epic")
.build()
.into_node()
} else {
StyledModal::build()
.add_class("deleteEpic")
.center()
.width(600)
.child(warning(model, modal))
.build()
.into_node()
}
}
fn warning(model: &model::Model, modal: &Model) -> Node<Msg> {
let issues: Vec<Node<Msg>> = modal
.related_issues
.iter()
.flat_map(|id| model.issues_by_id.get(id))
.map(|issue| {
let link = StyledIcon::build(Icon::Link).build().into_node();
li![div![
C!["relatedIssue"],
a![
attrs! {"href" => format!("/issues/{}", issue.id)},
link,
issue.title.as_str()
]
]]
})
.collect();
section![
h3![C!["header"], "Cannot delete epic"],
div![
C!["warning"],
"This epic have related issues. Please move or delete them first."
],
ol![C!["relatedList"], issues]
]
}

View File

@ -114,7 +114,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.add_class("addIssue")
.width(0)
.variant(crate::shared::styled_modal::Variant::Center)
.children(vec![form])
.child(form)
.build()
.into_node()
}

View File

@ -9,13 +9,13 @@ use {
},
EditIssueModalSection, FieldId, Msg,
},
jirs_data::{Issue, IssueFieldId, IssueId, TimeTracking, UpdateIssuePayload},
jirs_data::{EpicId, Issue, IssueFieldId, TimeTracking, UpdateIssuePayload},
seed::prelude::*,
};
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct Model {
pub id: IssueId,
pub id: EpicId,
pub link_copied: bool,
pub payload: UpdateIssuePayload,
pub top_type_state: StyledSelectState,

View File

@ -2,6 +2,7 @@ pub use {epic_field::*, update::*, view::*};
#[cfg(debug_assertions)]
pub mod debug;
pub mod epic_delete;
pub mod issue_statuses_delete;
pub mod issues_create;
pub mod issues_delete;

View File

@ -12,7 +12,7 @@ use {
},
EditIssueModalSection, FieldId, Msg,
},
jirs_data::{IssueFieldId, IssueId, TimeTracking},
jirs_data::{EpicId, IssueFieldId, TimeTracking},
seed::{prelude::*, *},
};
@ -25,7 +25,7 @@ pub fn value_for_time_tracking(v: &Option<i32>, time_tracking_type: &TimeTrackin
}
}
pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
pub fn view(model: &Model, issue_id: EpicId) -> Node<Msg> {
if model.issues_by_id.get(&issue_id).is_none() {
return Node::Empty;
}
@ -73,12 +73,7 @@ pub fn view(model: &Model, issue_id: IssueId) -> Node<Msg> {
StyledModal::build()
.add_class("timeTrackingModal")
.children(vec![
modal_title,
tracking,
inputs,
div![C!["actions"], close],
])
.children(vec![modal_title, tracking, inputs, div![C!["actions"], close]].into_iter())
.width(400)
.build()
.into_node()

View File

@ -43,6 +43,9 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders),
Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders),
Msg::ChangePage(Page::DeleteEpic(issue_id)) => {
push_delete_epic_modal(*issue_id, model, orders)
}
#[cfg(debug_assertions)]
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
@ -61,10 +64,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
_ => (),
}
use crate::modals::{issue_statuses_delete, issues_create, issues_edit};
issues_create::update(msg, model, orders);
issues_edit::update(msg, model, orders);
issue_statuses_delete::update(msg, model, orders);
{
use crate::modals::*;
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);
}
}
fn push_add_modal(model: &mut Model, _orders: &mut impl Orders<Msg>) {
@ -75,12 +81,18 @@ 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>) {
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
.project
.as_ref()
.map(|p| p.time_tracking)
.unwrap_or_else(|| TimeTracking::Untracked);
.unwrap_or(TimeTracking::Untracked);
let modal = {
let issue = match model.issues_by_id.get(&issue_id) {
Some(issue) => issue,

View File

@ -18,7 +18,7 @@ pub fn view(model: &Model) -> Node<Msg> {
StyledModal::build()
.variant(crate::shared::styled_modal::Variant::Center)
.width(1040)
.children(vec![details])
.child(details)
.build()
.into_node()
} else {
@ -45,6 +45,7 @@ pub fn view(model: &Model) -> Node<Msg> {
}
#[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]

View File

@ -27,10 +27,11 @@ pub trait IssueModal {
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType {
AddIssue(Box<crate::modals::issues_create::Model>),
EditIssue(IssueId, Box<crate::modals::issues_edit::Model>),
DeleteIssueConfirm(IssueId),
EditIssue(EpicId, Box<crate::modals::issues_edit::Model>),
DeleteEpic(Box<crate::modals::epic_delete::Model>),
DeleteIssueConfirm(EpicId),
DeleteCommentConfirm(CommentId),
TimeTracking(IssueId),
TimeTracking(EpicId),
DeleteIssueStatusModal(Box<crate::modals::issue_statuses_delete::Model>),
#[cfg(debug_assertions)]
DebugModal,
@ -46,7 +47,8 @@ pub struct CommentForm {
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)]
pub enum Page {
Project,
EditIssue(IssueId),
EditIssue(EpicId),
DeleteEpic(EpicId),
AddIssue,
ProjectSettings,
SignIn,
@ -62,6 +64,7 @@ impl Page {
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::AddIssue => "/add-issue".to_string(),
Page::ProjectSettings => "/project-settings".to_string(),
Page::SignIn => "/login".to_string(),
@ -147,7 +150,7 @@ pub struct Model {
pub current_user_project: Option<UserProject>,
pub issues: Vec<Issue>,
pub issues_by_id: HashMap<IssueId, Issue>,
pub issues_by_id: HashMap<EpicId, Issue>,
pub user: Option<User>,
pub users: Vec<User>,

View File

@ -4,12 +4,12 @@ use {crate::shared::drag::DragState, jirs_data::*, std::collections::HashMap};
pub struct StatusIssueIds {
pub status_id: IssueStatusId,
pub status_name: IssueStatusName,
pub issue_ids: Vec<IssueId>,
pub issue_ids: Vec<EpicId>,
}
#[derive(Default, Debug)]
pub struct EpicIssuePerStatus {
pub epic_name: Option<EpicName>,
pub epic_ref: Option<(EpicId, EpicName)>,
pub per_status_issues: Vec<StatusIssueIds>,
}
@ -57,7 +57,7 @@ impl ProjectPage {
for epic in epics {
let mut per_epic_map = EpicIssuePerStatus {
epic_name: epic.map(|(_, name)| name.to_string()),
epic_ref: epic.map(|(id, name)| (id, name.to_string())),
..Default::default()
};

View File

@ -1,4 +1,3 @@
use crate::shared::styled_button::StyledButton;
use {
crate::{
model::PageContent,
@ -9,6 +8,8 @@ use {
seed::{prelude::*, *},
};
use crate::shared::styled_button::StyledButton;
pub fn project_board_lists(model: &Model) -> Node<Msg> {
let project_page = match &model.page_content {
PageContent::Project(project_page) => project_page,
@ -32,8 +33,9 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
)
})
.collect();
let epic_name = match per_epic.epic_name.as_deref() {
Some(name) => {
let epic_name = match per_epic.epic_ref.as_ref() {
Some((id, name)) => {
let id = *id;
let edit_button = StyledButton::build()
.empty()
.icon(Icon::EditAlt)
@ -42,6 +44,15 @@ pub fn project_board_lists(model: &Model) -> Node<Msg> {
let delete_button = StyledButton::build()
.empty()
.icon(Icon::DeleteAlt)
.on_click(mouse_ev("click", move |ev| {
ev.stop_propagation();
ev.prevent_default();
seed::Url::new()
.add_path_part("delete-epic")
.add_path_part(id.to_string())
.go_and_push();
Msg::ChangePage(Page::DeleteEpic(id))
}))
.build()
.into_node();

View File

@ -109,15 +109,9 @@ pub fn render(values: StyledConfirmModal) -> Node<Msg> {
StyledModal::build()
.width(600)
.children(vec![
div![attrs![At::Class => "title"], title],
message_node,
div![
attrs![At::Class => "actions"],
confirm_button,
cancel_button
],
])
.child(div![C!["title"], title])
.child(message_node)
.child(div![C!["actions"], confirm_button, cancel_button])
.add_class("confirmModal")
.build()
.into_node()

View File

@ -58,38 +58,48 @@ pub struct StyledModalBuilder<'l> {
}
impl<'l> StyledModalBuilder<'l> {
#[inline]
pub fn variant(mut self, variant: Variant) -> Self {
self.variant = Some(variant);
self
}
#[inline]
pub fn center(self) -> Self {
self.variant(Variant::Center)
}
#[inline]
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
// pub fn with_icon(mut self, with_icon: bool) -> Self {
// self.with_icon = Some(with_icon);
// self
// }
pub fn children(mut self, children: Vec<Node<Msg>>) -> Self {
self.children = Some(children);
#[inline]
pub fn child(mut self, child: Node<Msg>) -> Self {
self.children.get_or_insert(vec![]).push(child);
self
}
#[inline]
pub fn children<ChildIter>(mut self, children: ChildIter) -> Self
where
ChildIter: Iterator<Item = Node<Msg>>,
{
self.children.get_or_insert(vec![]).extend(children);
self
}
#[inline]
pub fn add_class(mut self, name: &'l str) -> Self {
self.class_list.push(name);
self
}
#[inline]
pub fn build(self) -> StyledModal<'l> {
StyledModal {
variant: self.variant.unwrap_or_else(|| Variant::Center),
variant: self.variant.unwrap_or(Variant::Center),
width: self.width,
with_icon: self.with_icon.unwrap_or_default(),
children: self.children.unwrap_or_default(),
@ -98,6 +108,7 @@ impl<'l> StyledModalBuilder<'l> {
}
}
#[inline]
pub fn render(values: StyledModal) -> Node<Msg> {
let StyledModal {
variant,

View File

@ -8,7 +8,7 @@ use {
seed::{prelude::Orders, *},
};
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
pub fn drag_started(issue_id: EpicId, model: &mut Model) {
let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page,
_ => return,
@ -16,7 +16,7 @@ pub fn drag_started(issue_id: IssueId, model: &mut Model) {
project_page.issue_drag.drag(issue_id);
}
pub fn exchange_position(below_id: IssueId, model: &mut Model) {
pub fn exchange_position(below_id: EpicId, model: &mut Model) {
let project_page = match &mut model.page_content {
PageContent::Project(project_page) => project_page,
_ => return,
@ -88,7 +88,7 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
_ => return,
};
let changes: Vec<(IssueId, ListPosition, IssueStatusId, Option<EpicId>)> = dirty
let changes: Vec<(EpicId, ListPosition, IssueStatusId, Option<EpicId>)> = dirty
.into_iter()
.filter_map(|id| {
model.issues_by_id.get(&id).map(|issue| {

View File

@ -1,6 +1,8 @@
#[cfg(feature = "backend")]
use diesel::*;
#[cfg(feature = "backend")]
use derive_enum_sql::EnumSql;
use {
chrono::NaiveDateTime,
derive_enum_iter::EnumIter,
@ -16,9 +18,6 @@ pub mod fields;
pub mod msg;
mod payloads;
#[cfg(feature = "backend")]
use derive_enum_sql::EnumSql;
pub type NumberOfDeleted = usize;
pub type IssueId = i32;
pub type ListPosition = i32;
@ -223,7 +222,7 @@ pub struct Project {
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
pub struct Issue {
pub id: IssueId,
pub id: EpicId,
pub title: String,
pub issue_type: IssueType,
pub priority: IssuePriority,
@ -275,7 +274,7 @@ pub struct Comment {
pub id: CommentId,
pub body: String,
pub user_id: UserId,
pub issue_id: IssueId,
pub issue_id: EpicId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
@ -320,7 +319,7 @@ pub struct Token {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct IssueAssignee {
pub id: i32,
pub issue_id: IssueId,
pub issue_id: EpicId,
pub user_id: UserId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,

View File

@ -1,10 +1,9 @@
use crate::ListPosition;
use {
crate::{
AvatarUrl, BindToken, Code, Comment, CommentId, CreateCommentPayload, CreateIssuePayload,
EmailString, Epic, EpicId, HighlightedCode, Invitation, InvitationId, InvitationToken,
Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId, Lang, Message, MessageId,
NameString, NumberOfDeleted, PayloadVariant, Position, Project, TitleString,
Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId, Lang, ListPosition, Message,
MessageId, NameString, NumberOfDeleted, PayloadVariant, Position, Project, TitleString,
UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, UserProjectId,
UserRole, UsernameString,
},
@ -187,7 +186,7 @@ pub enum WsMsg {
IssueDeleted(IssueId, NumberOfDeleted),
IssueCreate(CreateIssuePayload),
IssueCreated(Issue),
IssueSyncListPosition(Vec<(IssueId, ListPosition, IssueStatusId, Option<EpicId>)>),
IssueSyncListPosition(Vec<(IssueId, ListPosition, IssueStatusId, Option<IssueId>)>),
// issue status
IssueStatusesLoad,