diff --git a/jirs-client/js/css/modal.css b/jirs-client/js/css/styledModal.css similarity index 96% rename from jirs-client/js/css/modal.css rename to jirs-client/js/css/styledModal.css index f12bcd5f..ecc228d1 100644 --- a/jirs-client/js/css/modal.css +++ b/jirs-client/js/css/styledModal.css @@ -76,3 +76,7 @@ .modal > .clickableOverlay > .styledModal.aside > .styledIcon.modalVariantAside:hover { color: var(--primary); } + +.styledModal.confirmModal { + padding: 35px 40px 40px; +} diff --git a/jirs-client/js/styles.css b/jirs-client/js/styles.css index 2e31dac6..ba2a134b 100644 --- a/jirs-client/js/styles.css +++ b/jirs-client/js/styles.css @@ -11,7 +11,7 @@ @import "css/styledSelect.css"; @import "css/styledButton.css"; @import "css/styledInput.css"; +@import "css/styledModal.css"; @import "css/app.css"; -@import "css/modal.css"; @import "css/issue.css"; @import "css/project.css"; diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 5b68de67..1ef87994 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -3,7 +3,7 @@ use seed::{prelude::*, *}; use jirs_data::IssueStatus; -use crate::model::Page; +use crate::model::{ModalType, Page}; use crate::shared::styled_select::StyledSelectChange; mod api; @@ -23,6 +23,12 @@ pub type AvatarFilterActive = bool; #[derive(Clone, Debug)] pub enum FieldId { IssueTypeEditModalTop, + CopyButtonLabel, +} + +#[derive(Clone, Debug)] +pub enum FieldChange { + LinkCopied(FieldId, bool), } #[derive(Clone, Debug)] @@ -52,7 +58,9 @@ pub enum Msg { IssueUpdateResult(FetchObject), // modals - PopModal, + ModalOpened(ModalType), + ModalDropped, + ModalChanged(FieldChange), } fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { diff --git a/jirs-client/src/modal/confirm_delete_issue.rs b/jirs-client/src/modal/confirm_delete_issue.rs new file mode 100644 index 00000000..5b50de5b --- /dev/null +++ b/jirs-client/src/modal/confirm_delete_issue.rs @@ -0,0 +1,16 @@ +use seed::{prelude::*, *}; + +use crate::shared::styled_modal::StyledModal; +use crate::shared::ToNode; +use crate::{model, Msg}; + +pub fn view(model: &model::Model) -> Node { + let handle_issue_delete = mouse_ev(Ev::Click, |_| Msg::NoOp); + StyledModal::build() + .title("Are you sure you want to delete this issue?") + .message("Once you delete, it's gone for good.") + .confirm_text("Delete issue") + .on_confirm(handle_issue_delete) + .build() + .into_node() +} diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issue_details.rs index 0f0bcfe8..34c8bf2f 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issue_details.rs @@ -2,12 +2,12 @@ use seed::{prelude::*, *}; use jirs_data::{Issue, IssueType}; -use crate::model::{EditIssueModal, Model}; +use crate::model::{EditIssueModal, ModalType, Model}; use crate::shared::styled_button::StyledButton; use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_select::{StyledSelect, Variant as SelectVariant}; use crate::shared::ToNode; -use crate::{FieldId, IssueId, Msg}; +use crate::{FieldChange, FieldId, IssueId, Msg}; #[derive(PartialOrd, PartialEq, Debug)] struct IssueTypeOption(IssueId, IssueType); @@ -92,10 +92,13 @@ pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node el.set_selection_range(0, 9999).unwrap(); seed::html_document().exec_command("copy").unwrap(); seed::body().remove_child(&el).unwrap(); - Msg::NoOp + Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) + }); + let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped); + let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| { + Msg::ModalOpened(ModalType::DeleteIssueConfirm(issue_id)) }); - let close_handler = mouse_ev(Ev::Click, |_| Msg::PopModal); let copy_button = StyledButton::build() .empty() .icon(Icon::Link) @@ -110,6 +113,7 @@ pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node let delete_button = StyledButton::build() .empty() .icon(Icon::Trash.into_styled_builder().size(19).build()) + .on_click(delete_confirmation_handler) .build() .into_node(); let close_button = StyledButton::build() diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs index 2dfe2666..e976f8ab 100644 --- a/jirs-client/src/modal/mod.rs +++ b/jirs-client/src/modal/mod.rs @@ -7,16 +7,29 @@ use crate::model::{EditIssueModal, ModalType, Page}; use crate::shared::modal::{Modal, Variant as ModalVariant}; use crate::shared::styled_select::StyledSelectChange; use crate::shared::{find_issue, ToNode}; -use crate::{model, FieldId, Msg}; +use crate::{model, FieldChange, FieldId, Msg}; +mod confirm_delete_issue; mod issue_details; pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders) { match msg { - Msg::PopModal => match model.modals.pop() { + Msg::ModalDropped => match model.modals.pop() { _ => (), }, + Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true)) => { + for modal in model.modals.iter_mut() { + if let ModalType::EditIssue(_, edit) = modal { + edit.link_copied = true; + } + } + } + + Msg::ModalOpened(modal_type) => { + model.modals.push(modal_type.clone()); + } + Msg::ChangePage(Page::EditIssue(issue_id)) => { let value = find_issue(model, *issue_id) .map(|issue| issue.issue_type.clone()) @@ -113,6 +126,7 @@ pub fn view(model: &model::Model) -> Node { empty![] } } + ModalType::DeleteIssueConfirm(_id) => confirm_delete_issue::view(model), _ => empty![], }) .collect(); diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 1bb2ceb4..0ff8763d 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -25,7 +25,6 @@ pub struct EditIssueModal { } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] -#[serde(rename_all = "kebab-case")] pub enum Page { Project, EditIssue(IssueId), diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index dcb9c68b..14c1dd9a 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -13,6 +13,7 @@ pub mod styled_avatar; pub mod styled_button; pub mod styled_icon; pub mod styled_input; +pub mod styled_modal; pub mod styled_select; pub mod styled_tooltip; diff --git a/jirs-client/src/shared/modal.rs b/jirs-client/src/shared/modal.rs index ed2f9e62..cdf80545 100644 --- a/jirs-client/src/shared/modal.rs +++ b/jirs-client/src/shared/modal.rs @@ -58,7 +58,7 @@ pub fn render(values: Modal) -> Node { empty![] }; - let close_handler = mouse_ev(Ev::Click, |_| Msg::PopModal); + let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped); let body_handler = mouse_ev(Ev::Click, |ev| { ev.stop_propagation(); Msg::NoOp diff --git a/jirs-client/src/shared/styled_modal.rs b/jirs-client/src/shared/styled_modal.rs new file mode 100644 index 00000000..59a5dcd9 --- /dev/null +++ b/jirs-client/src/shared/styled_modal.rs @@ -0,0 +1,119 @@ +use seed::EventHandler; +use seed::{prelude::*, *}; + +use crate::shared::ToNode; +use crate::Msg; + +const TITLE: &str = "Warning"; +const MESSAGE: &str = "Are you sure you want to continue with this action?"; +const CONFIRM_TEXT: &str = "Confirm"; +const CANCEL_TEXT: &str = "Cancel"; + +#[derive(Debug)] +pub enum Variant { + Primary, +} + +#[derive(Debug)] +pub struct StyledModal { + pub variant: Variant, + pub title: String, + pub message: String, + pub confirm_text: String, + pub cancel_text: String, + pub on_confirm: Option>, +} + +impl StyledModal { + pub fn build() -> StyledModalBuilder { + StyledModalBuilder::default() + } +} + +impl ToNode for StyledModal { + fn into_node(self) -> Node { + render(self) + } +} + +#[derive(Default)] +pub struct StyledModalBuilder { + variant: Option, + title: Option, + message: Option, + confirm_text: Option, + cancel_text: Option, + on_confirm: Option>>, +} + +impl StyledModalBuilder { + pub fn variant(mut self, variant: Variant) -> Self { + self.variant = Some(variant); + self + } + + pub fn title(mut self, title: S) -> Self + where + S: Into, + { + self.title = Some(title.into()); + self + } + + pub fn message(mut self, message: S) -> Self + where + S: Into, + { + self.message = Some(message.into()); + self + } + + pub fn confirm_text(mut self, confirm_text: S) -> Self + where + S: Into, + { + self.confirm_text = Some(confirm_text.into()); + self + } + + pub fn cancel_text(mut self, cancel_text: S) -> Self + where + S: Into, + { + self.cancel_text = Some(cancel_text.into()); + self + } + + pub fn on_confirm(mut self, on_confirm: EventHandler) -> Self { + self.on_confirm = Some(Some(on_confirm)); + self + } + + pub fn build(self) -> StyledModal { + StyledModal { + variant: self.variant.unwrap_or_else(|| Variant::Primary), + title: self.title.unwrap_or_else(|| TITLE.to_string()), + message: self.message.unwrap_or_else(|| MESSAGE.to_string()), + confirm_text: self + .confirm_text + .unwrap_or_else(|| CONFIRM_TEXT.to_string()), + cancel_text: self.cancel_text.unwrap_or_else(|| CANCEL_TEXT.to_string()), + on_confirm: None, + } + } +} + +pub fn render(values: StyledModal) -> Node { + let StyledModal { + variant, + title, + message, + confirm_text, + cancel_text, + on_confirm, + } = values; + div![ + attrs![At::Class => "modal"], + div![attrs![At::Class => "styledModal"]] + ] +} diff --git a/react-client/src/shared/components/ConfirmModal/Styles.js b/react-client/src/shared/components/ConfirmModal/Styles.js index ab9400bb..467affa7 100644 --- a/react-client/src/shared/components/ConfirmModal/Styles.js +++ b/react-client/src/shared/components/ConfirmModal/Styles.js @@ -1,8 +1,8 @@ import styled from 'styled-components'; -import { font } from 'shared/utils/styles'; -import Modal from 'shared/components/Modal'; -import Button from 'shared/components/Button'; +import { font } from '../../../shared/utils/styles'; +import Modal from '../../../shared/components/Modal'; +import Button from '../../../shared/components/Button'; export const StyledConfirmModal = styled(Modal)` padding: 35px 40px 40px; diff --git a/react-client/src/shared/components/Modal/index.jsx b/react-client/src/shared/components/Modal/index.jsx index a88053b4..8ed01a11 100644 --- a/react-client/src/shared/components/Modal/index.jsx +++ b/react-client/src/shared/components/Modal/index.jsx @@ -1,19 +1,19 @@ import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; -import useOnOutsideClick from 'shared/hooks/onOutsideClick'; -import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown'; +import useOnOutsideClick from '../../../shared/hooks/onOutsideClick'; +import useOnEscapeKeyDown from '../../../shared/hooks/onEscapeKeyDown'; import { ClickableOverlay, CloseIcon, ScrollOverlay, StyledModal } from './Styles'; const Modal = ({ - className, - testid, - variant, - width, - withCloseIcon, - isOpen: propsIsOpen, + className, + testid, + variant, + width, + withCloseIcon, + isOpen: propsIsOpen, onClose: tellParentToClose, renderLink, renderContent,