Add comments section. Refactor Field ids

This commit is contained in:
Adrian Wozniak 2020-04-12 13:21:47 +02:00
parent 1663d9a485
commit 7262411676
21 changed files with 890 additions and 728 deletions

View File

@ -31,18 +31,26 @@ version = "*"
[dependencies.web-sys]
version = "0.3.22"
features = [
# elements
"Window",
"DataTransfer",
"DragEvent",
"HtmlDivElement",
"DomRect",
"HtmlDocument",
"Document",
"HtmlBodyElement",
# types
"DataTransfer",
"DomRect",
"Selection",
"CssStyleDeclaration",
"WebSocket",
"BinaryType",
"Blob",
"MessageEvent",
"AddEventListenerOptions",
# events
"EventTarget",
"ErrorEvent",
"MessageEvent",
"KeyEvent",
"KeyboardEvent",
"DragEvent",
]

View File

@ -34,6 +34,71 @@
background: var(--backgroundLight);
}
.issueDetails > .content > .left > .comments {
padding-top: 40px;
}
.issueDetails > .content > .left > .comments > .title {
font-family: var(--font-medium);
font-weight: normal;
font-size: 15px
}
.issueDetails > .content > .left > .comments > .create {
position: relative;
margin-top: 25px;
font-size: 15px
}
.issueDetails > .content > .left > .comments > .create > .userAvatar {
position: absolute;
top: 0;
left: 0;
}
.issueDetails > .content > .left > .comments > .create > .right {
padding-left: 44px;
}
.issueDetails > .content > .left > .comments > .create > .right > .fakeTextArea {
padding: 12px 16px;
border-radius: 4px;
border: 1px solid var(--borderLightest);
color: var(--textLight);
cursor: pointer;
user-select: none;
}
.issueDetails > .content > .left > .comments > .create > .right > .fakeTextArea:hover {
border: 1px solid var(--borderLight);
}
.issueDetails > .content > .left > .comments > .create > .right > .proTip {
display: flex;
align-items: center;
padding-top: 8px;
color: var(--textMedium);
font-size: 13px;
}
.issueDetails > .content > .left > .comments > .create > .right > .proTip > .strong {
padding-right: 4px;
}
.issueDetails > .content > .left > .comments > .create > .right > .proTip > .tipLetter {
position: relative;
top: 1px;
display: inline-block;
margin: 0 4px;
padding: 0 4px;
border-radius: 2px;
color: var(--textDarkest);
background: var(--backgroundMedium);
font-family: var(--font-bold);
font-weight: normal;
font-size: 12px
}
.issueDetails > .content > .right {
width: 35%;
padding-top: 5px;

View File

@ -197,58 +197,3 @@
vertical-align: middle;
font-size: 14px;
}
.styledSelect > .dropDown > .options > .option > .optionItem {
display: flex;
align-items: center;
}
.styledSelect > .dropDown > .options > .option > .optionItem > .styledIcon {
font-size: 18px;
}
.styledSelect > .dropDown > .options > .option > .optionItem > .optionLabel {
padding: 0 5px 0 7px;
font-size: 15px;
}
/* issue priority */
.styledSelect > .dropDown > .options > .option > .optionItem.priority > .optionLabel {
padding: 0 5px 0 7px;
font-size: 15px;
text-transform: capitalize;
}
.styledSelect > .valueContainer > .selectItem.priority > .selectItemLabel {
text-transform: capitalize;
}
.styledSelect > .dropDown > .options > .option > .optionItem.priority > .styledIcon {
font-size: 18px;
}
.styledSelect > .valueContainer > .selectItem.priority.highest > .styledIcon,
.styledSelect > .dropDown > .options > .option > .optionItem.priority.highest > .styledIcon {
color: var(--highest);
}
.styledSelect > .valueContainer > .selectItem.priority.high > .styledIcon,
.styledSelect > .dropDown > .options > .option > .optionItem.priority.high > .styledIcon {
color: var(--high);
}
.styledSelect > .valueContainer > .selectItem.priority.medium > .styledIcon,
.styledSelect > .dropDown > .options > .option > .optionItem.priority.medium > .styledIcon {
color: var(--medium);
}
.styledSelect > .valueContainer > .selectItem.priority.low > .styledIcon,
.styledSelect > .dropDown > .options > .option > .optionItem.priority.low > .styledIcon {
color: var(--low);
}
.styledSelect > .valueContainer > .selectItem.priority.lowest > .styledIcon,
.styledSelect > .dropDown > .options > .option > .optionItem.priority.lowest > .styledIcon {
color: var(--lowest);
}

View File

@ -0,0 +1,67 @@
.selectItem {
padding: 4px 8px;
}
.selectItem.priority.highest > .styledIcon,
.optionItem.priority.highest > .styledIcon {
color: var(--highest);
}
.selectItem.priority.high > .styledIcon,
.optionItem.priority.high > .styledIcon {
color: var(--high);
}
.selectItem.priority.medium > .styledIcon,
.optionItem.priority.medium > .styledIcon {
color: var(--medium);
}
.selectItem.priority.low > .styledIcon,
.optionItem.priority.low > .styledIcon {
color: var(--low);
}
.selectItem.priority.lowest > .styledIcon,
.optionItem.priority.lowest > .styledIcon {
color: var(--lowest);
}
.selectItem.priority > .selectItemLabel {
text-transform: capitalize;
}
.optionItem {
display: flex;
align-items: center;
}
.optionItem > .styledIcon {
font-size: 18px;
}
.selectItem > .selectItemLabel,
.optionItem > .optionLabel {
padding: 0 5px 0 7px;
font-size: 15px;
}
.optionItem.priority > .optionLabel {
padding: 0 5px 0 7px;
font-size: 15px;
text-transform: capitalize;
}
.optionItem.priority > .styledIcon {
font-size: 18px;
}
/* edit issue */
.topActions > .styledSelect > .valueContainer,
.topActions > .styledSelect > .valueContainer > .selectItem {
height: 100%;
}
.topActions .selectItem, .topActions .optionItem {
padding: 0 12px;
}

View File

@ -9,6 +9,7 @@
@import "./css/styledTooltip.css";
@import "./css/styledAvatar.css";
@import "./css/styledSelect.css";
@import "./css/styledSelectChild.css";
@import "./css/styledButton.css";
@import "./css/styledInput.css";
@import "./css/styledModal.css";

View File

@ -1,9 +1,8 @@
use std::sync::RwLock;
use seed::fetch::FetchObject;
use seed::{prelude::*, *};
use jirs_data::{IssueStatus, WsMsg};
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{ModalType, Model, Page};
@ -21,70 +20,91 @@ mod register;
mod shared;
mod ws;
pub type UserId = i32;
pub type IssueId = i32;
pub type AvatarFilterActive = bool;
pub type AppType = App<Msg, Model, Node<Msg>>;
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum EditIssueModalFieldId {
IssueType,
Title,
Description,
Status,
Assignees,
Reporter,
Priority,
Estimate,
TimeSpend,
TimeRemaining,
// comment
CommentBody,
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum AddIssueModalFieldId {
IssueType,
Summary,
Description,
Reporter,
Assignees,
Priority,
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId {
// edit issue
IssueTypeEditModalTop,
TitleIssueEditModal,
DescriptionIssueEditModal,
StatusIssueEditModal,
AssigneesIssueEditModal,
ReporterIssueEditModal,
PriorityIssueEditModal,
EstimateIssueEditModal,
TimeSpendIssueEditModal,
TimeRemainingIssueEditModal,
// issue
AddIssueModal(AddIssueModalFieldId),
EditIssueModal(EditIssueModalFieldId),
// project boards
TextFilterBoard,
//
CopyButtonLabel,
// add issue
IssueTypeAddIssueModal,
SummaryAddIssueModal,
DescriptionAddIssueModal,
ReporterAddIssueModal,
AssigneesAddIssueModal,
IssuePriorityAddIssueModal,
}
impl std::fmt::Display for FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldId::IssueTypeEditModalTop => f.write_str("issueTypeEditModalTop"),
FieldId::EditIssueModal(sub) => match sub {
EditIssueModalFieldId::IssueType => f.write_str("issueTypeEditModalTop"),
EditIssueModalFieldId::Title => f.write_str("titleIssueEditModal"),
EditIssueModalFieldId::Description => f.write_str("descriptionIssueEditModal"),
EditIssueModalFieldId::Status => f.write_str("statusIssueEditModal"),
EditIssueModalFieldId::Assignees => f.write_str("assigneesIssueEditModal"),
EditIssueModalFieldId::Reporter => f.write_str("reporterIssueEditModal"),
EditIssueModalFieldId::Priority => f.write_str("priorityIssueEditModal"),
EditIssueModalFieldId::Estimate => f.write_str("estimateIssueEditModal"),
EditIssueModalFieldId::TimeSpend => f.write_str("timeSpendIssueEditModal"),
EditIssueModalFieldId::TimeRemaining => f.write_str("timeRemainingIssueEditModal"),
EditIssueModalFieldId::CommentBody => f.write_str("editIssue-commentBody"),
},
FieldId::AddIssueModal(sub) => match sub {
AddIssueModalFieldId::IssueType => f.write_str("issueTypeAddIssueModal"),
AddIssueModalFieldId::Summary => f.write_str("summaryAddIssueModal"),
AddIssueModalFieldId::Description => f.write_str("descriptionAddIssueModal"),
AddIssueModalFieldId::Reporter => f.write_str("reporterAddIssueModal"),
AddIssueModalFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
AddIssueModalFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
},
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
FieldId::IssueTypeAddIssueModal => f.write_str("issueTypeAddIssueModal"),
FieldId::SummaryAddIssueModal => f.write_str("summaryAddIssueModal"),
FieldId::DescriptionAddIssueModal => f.write_str("descriptionAddIssueModal"),
FieldId::ReporterAddIssueModal => f.write_str("reporterAddIssueModal"),
FieldId::AssigneesAddIssueModal => f.write_str("assigneesAddIssueModal"),
FieldId::IssuePriorityAddIssueModal => f.write_str("issuePriorityAddIssueModal"),
FieldId::TitleIssueEditModal => f.write_str("titleIssueEditModal"),
FieldId::DescriptionIssueEditModal => f.write_str("descriptionIssueEditModal"),
FieldId::StatusIssueEditModal => f.write_str("statusIssueEditModal"),
FieldId::AssigneesIssueEditModal => f.write_str("assigneesIssueEditModal"),
FieldId::ReporterIssueEditModal => f.write_str("reporterIssueEditModal"),
FieldId::PriorityIssueEditModal => f.write_str("priorityIssueEditModal"),
FieldId::EstimateIssueEditModal => f.write_str("estimateIssueEditModal"),
FieldId::TimeSpendIssueEditModal => f.write_str("timeSpendIssueEditModal"),
FieldId::TimeRemainingIssueEditModal => f.write_str("timeRemainingIssueEditModal"),
}
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum FieldChange {
LinkCopied(FieldId, bool),
TabChanged(FieldId, TabMode),
ToggleCreateComment(FieldId, bool),
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum Msg {
NoOp,
GlobalKeyDown {
key: String,
shift: bool,
ctrl: bool,
alt: bool,
},
// Auth Token
AuthTokenStored,
@ -93,7 +113,6 @@ pub enum Msg {
StyledSelectChanged(FieldId, StyledSelectChange),
ChangePage(model::Page),
CurrentProjectResult(FetchObject<String>),
InternalFailure(String),
ToggleAboutTooltip,
@ -127,6 +146,9 @@ pub enum Msg {
}
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if msg == Msg::NoOp {
return;
}
if cfg!(debug_assertions) {
log!(msg);
}
@ -227,6 +249,28 @@ pub fn render() {
5000,
);
if let Some(body) = seed::html_document().body() {
use wasm_bindgen::JsCast;
let body = body.dyn_ref::<web_sys::HtmlBodyElement>().unwrap().clone();
let key_up_closure =
wasm_bindgen::closure::Closure::wrap(Box::new(|event: web_sys::KeyboardEvent| {
if let Some(Ok(app)) = unsafe { APP.as_mut().map(|app| app.write()) } {
let msg = Msg::GlobalKeyDown {
key: event.key(),
shift: event.shift_key(),
ctrl: event.ctrl_key(),
alt: event.alt_key(),
};
app.update(msg);
}
})
as Box<dyn Fn(web_sys::KeyboardEvent)>);
body.add_event_listener_with_callback("keyup", key_up_closure.as_ref().unchecked_ref())
.unwrap();
key_up_closure.forget();
}
let app = seed::App::builder(update, view)
.routes(routes)
.build_and_start();

View File

@ -1,21 +1,20 @@
use seed::{prelude::*, *};
use jirs_data::{IssuePriority, IssueType, User};
use jirs_data::{IssuePriority, IssueType, ToVec};
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::StyledIcon;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::styled_select::StyledSelect;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::ToNode;
use crate::{FieldId, Msg};
use crate::{AddIssueModalFieldId, FieldId, Msg};
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let modal = model.modals.iter_mut().find(|modal| match modal {
@ -58,16 +57,16 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
orders.skip().send_msg(Msg::ModalDropped);
}
Msg::InputChanged(FieldId::DescriptionAddIssueModal, value) => {
Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Description), value) => {
modal.description = Some(value.clone());
}
Msg::InputChanged(FieldId::SummaryAddIssueModal, value) => {
Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Summary), value) => {
modal.title = value.clone();
}
// IssueTypeAddIssueModal
Msg::StyledSelectChanged(
FieldId::IssueTypeAddIssueModal,
FieldId::AddIssueModal(AddIssueModalFieldId::IssueType),
StyledSelectChange::Changed(id),
) => {
modal.issue_type = (*id).into();
@ -75,7 +74,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
// ReporterAddIssueModal
Msg::StyledSelectChanged(
FieldId::ReporterAddIssueModal,
FieldId::AddIssueModal(AddIssueModalFieldId::Reporter),
StyledSelectChange::Changed(id),
) => {
modal.reporter_id = Some(*id as i32);
@ -83,7 +82,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
// AssigneesAddIssueModal
Msg::StyledSelectChanged(
FieldId::AssigneesAddIssueModal,
FieldId::AddIssueModal(AddIssueModalFieldId::Assignees),
StyledSelectChange::Changed(id),
) => {
let id = *id as i32;
@ -92,7 +91,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
}
}
Msg::StyledSelectChanged(
FieldId::AssigneesAddIssueModal,
FieldId::AddIssueModal(AddIssueModalFieldId::Assignees),
StyledSelectChange::RemoveMulti(id),
) => {
let id = *id as i32;
@ -107,7 +106,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
// IssuePriorityAddIssueModal
Msg::StyledSelectChanged(
FieldId::IssuePriorityAddIssueModal,
FieldId::AddIssueModal(AddIssueModalFieldId::Priority),
StyledSelectChange::Changed(id),
) => {
modal.priority = (*id).into();
@ -118,18 +117,19 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
}
pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build(FieldId::IssueTypeAddIssueModal)
let select_type = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::IssueType))
.name("type")
.normal()
.text_filter(modal.type_state.text_filter.as_str())
.opened(modal.type_state.opened)
.valid(true)
.options(vec![
IssueTypeOption(IssueType::Story),
IssueTypeOption(IssueType::Task),
IssueTypeOption(IssueType::Bug),
])
.selected(vec![IssueTypeOption(modal.issue_type.clone())])
.options(
IssueType::ordered()
.iter()
.map(|t| t.to_select_child().name("type"))
.collect(),
)
.selected(vec![modal.issue_type.to_select_child().name("type")])
.build()
.into_node();
let issue_type_field = StyledField::build()
@ -139,7 +139,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.into_node();
let short_summary = StyledInput::build(FieldId::SummaryAddIssueModal)
let short_summary = StyledInput::build(FieldId::AddIssueModal(AddIssueModalFieldId::Summary))
.valid(true)
.build()
.into_node();
@ -152,7 +152,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let description = StyledTextarea::build()
.height(110)
.build(FieldId::DescriptionAddIssueModal)
.build(FieldId::AddIssueModal(AddIssueModalFieldId::Description))
.into_node();
let description_field = StyledField::build()
.label("Description")
@ -165,18 +165,24 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.reporter_id
.or_else(|| model.user.as_ref().map(|u| u.id))
.unwrap_or_default();
let reporter = StyledSelect::build(FieldId::ReporterAddIssueModal)
let reporter = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Reporter))
.normal()
.text_filter(modal.reporter_state.text_filter.as_str())
.opened(modal.reporter_state.opened)
.options(model.users.iter().map(|u| UserOption(u)).collect())
.options(
model
.users
.iter()
.map(|u| u.to_select_child().name("reporter"))
.collect(),
)
.selected(
model
.users
.iter()
.filter_map(|user| {
if user.id == reporter_id {
Some(UserOption(user))
Some(user.to_select_child().name("reporter"))
} else {
None
}
@ -193,19 +199,25 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.into_node();
let assignees = StyledSelect::build(FieldId::AssigneesAddIssueModal)
let assignees = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Assignees))
.normal()
.multi()
.text_filter(modal.assignees_state.text_filter.as_str())
.opened(modal.assignees_state.opened)
.options(model.users.iter().map(|u| UserOption(u)).collect())
.options(
model
.users
.iter()
.map(|u| u.to_select_child().name("assignees"))
.collect(),
)
.selected(
model
.users
.iter()
.filter_map(|user| {
if modal.user_ids.contains(&user.id) {
Some(UserOption(user))
Some(user.to_select_child().name("assignees"))
} else {
None
}
@ -222,22 +234,22 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.into_node();
let select_priority = StyledSelect::build(FieldId::IssuePriorityAddIssueModal)
.name("priority")
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
.opened(modal.priority_state.opened)
.valid(true)
.options(vec![
IssuePriorityOption(IssuePriority::Highest),
IssuePriorityOption(IssuePriority::High),
IssuePriorityOption(IssuePriority::Medium),
IssuePriorityOption(IssuePriority::Low),
IssuePriorityOption(IssuePriority::Lowest),
])
.selected(vec![IssuePriorityOption(modal.priority.clone())])
.build()
.into_node();
let select_priority =
StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Priority))
.name("priority")
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
.opened(modal.priority_state.opened)
.valid(true)
.options(
IssuePriority::ordered()
.iter()
.map(|p| p.to_select_child().name("priority"))
.collect(),
)
.selected(vec![modal.priority.to_select_child().name("priority")])
.build()
.into_node();
let issue_priority_field = StyledField::build()
.label("Issue Type")
.tip("Priority in relation to other issues.")
@ -292,139 +304,3 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.into_node()
}
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeOption(pub IssueType);
impl crate::shared::styled_select::SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon")
.build()
.into_node();
div![
attrs![At::Class => "type optionItem"],
icon,
div![attrs![At::Class => "typeLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => "selectItem"],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct IssuePriorityOption(IssuePriority);
impl crate::shared::styled_select::SelectOption for IssuePriorityOption {
fn into_option(self) -> Node<Msg> {
let name = format!("{}", self.0);
let icon = StyledIcon::build(self.0.into())
.add_class("issuePriorityIcon")
.size(18)
.build()
.into_node();
div![
attrs![At::Class => format!("priority optionItem {}", name)],
icon,
div![attrs![At::Class => "priorityLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = format!("{}", self.0);
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => format!("selectItem priority {}", name)],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct UserOption<'opt>(pub &'opt User);
impl<'opt> UserOption<'opt> {
fn avatar_node(&self) -> Node<Msg> {
let user = self.0;
StyledAvatar::build()
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
.size(20)
.name(user.name.as_str())
.build()
.into_node()
}
}
impl<'opt> crate::shared::styled_select::SelectOption for UserOption<'opt> {
fn into_option(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "user optionItem"],
styled_avatar,
div![attrs![At::Class => "typeLabel optionLabel"], user.name]
]
}
fn into_value(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "selectItem"],
styled_avatar,
div![attrs![At::Class => "selectItemLabel"], user.name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0.name.contains(text_filter)
}
fn to_value(&self) -> u32 {
self.0.id as u32
}
}

View File

@ -8,12 +8,13 @@ use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_editor::StyledEditor;
use crate::shared::styled_field::StyledField;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_icon::Icon;
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange};
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_select_child::ToStyledSelectChild;
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::ToNode;
use crate::{FieldChange, FieldId, IssueId, Msg};
use crate::{EditIssueModalFieldId, FieldChange, FieldId, Msg};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
@ -31,35 +32,35 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.payload = issue.clone().into();
}
Msg::StyledSelectChanged(
FieldId::IssueTypeEditModalTop,
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_type = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::StatusIssueEditModal,
FieldId::EditIssueModal(EditIssueModalFieldId::Status),
StyledSelectChange::Changed(value),
) => {
modal.payload.status = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::ReporterIssueEditModal,
FieldId::EditIssueModal(EditIssueModalFieldId::Reporter),
StyledSelectChange::Changed(value),
) => {
modal.payload.reporter_id = *value as i32;
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal,
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
StyledSelectChange::Changed(value),
) => {
modal.payload.user_ids.push(*value as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal,
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
StyledSelectChange::RemoveMulti(value),
) => {
let mut old = vec![];
@ -73,61 +74,68 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::PriorityIssueEditModal,
FieldId::EditIssueModal(EditIssueModalFieldId::Priority),
StyledSelectChange::Changed(value),
) => {
modal.payload.priority = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::InputChanged(FieldId::TitleIssueEditModal, value) => {
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Title), value) => {
modal.payload.title = value.clone();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => {
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Description), value) => {
modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone());
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => {
Msg::ModalChanged(FieldChange::TabChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Description),
mode,
)) => {
modal.description_editor_mode = mode.clone();
}
Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
flag,
)) => {
modal.creating_comment = *flag;
}
Msg::GlobalKeyDown { key, .. } if key.as_str() == "m" && !modal.creating_comment => {
orders
.skip()
.send_msg(Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
true,
)));
}
_ => (),
}
}
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
div![
attrs![At::Class => "issueDetails"],
top_modal_row(model, modal),
div![
attrs![At::Class => "content"],
left_modal_column(model, modal),
right_modal_column(model, modal),
],
]
}
fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal {
id,
link_copied,
payload,
top_type_state,
status_state,
reporter_state: _,
assignees_state: _,
priority_state: _,
description_editor_mode,
link_copied,
..
} = modal;
let issue_id = id.clone();
let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
.dropdown_width(150)
.name("type")
.text_filter(top_type_state.text_filter.as_str())
.opened(top_type_state.opened)
.valid(true)
.options(
IssueType::ordered()
.into_iter()
.map(|t| IssueTypeTopOption(issue_id, t))
.collect(),
)
.selected(vec![IssueTypeTopOption(
issue_id,
payload.issue_type.clone(),
)])
.build()
.into_node();
let issue_id = id.clone();
let click_handler = mouse_ev(Ev::Click, move |_| {
use wasm_bindgen::JsCast;
@ -177,26 +185,135 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
// left
let issue_type_select =
StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::IssueType))
.dropdown_width(150)
.name("type")
.text_filter(top_type_state.text_filter.as_str())
.opened(top_type_state.opened)
.valid(true)
.options(
IssueType::ordered()
.into_iter()
.map(|t| t.to_select_child().name("type"))
.collect(),
)
.selected(vec![{
let id = modal.id.clone();
let issue_type = &payload.issue_type;
issue_type
.to_select_child()
.name("type")
.text(format!("{} - {}", issue_type, id))
}])
.build()
.into_node();
div![
attrs![At::Class => "topActions"],
issue_type_select,
div![
attrs![At::Class => "topActionsRight"],
copy_button,
delete_button,
close_button
],
]
}
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal {
payload,
description_editor_mode,
creating_comment,
..
} = modal;
let title = StyledTextarea::build()
.value(payload.title.as_str())
.add_class("textarea")
.max_height(48)
.height(0)
.build(FieldId::TitleIssueEditModal)
.build(FieldId::EditIssueModal(EditIssueModalFieldId::Title))
.into_node();
let description_text = payload.description.as_ref().cloned().unwrap_or_default();
let description = StyledEditor::build(FieldId::DescriptionIssueEditModal)
.text(description_text)
.mode(description_editor_mode.clone())
.update_on(Ev::Change)
.build()
.into_node();
let description =
StyledEditor::build(FieldId::EditIssueModal(EditIssueModalFieldId::Description))
.text(description_text)
.mode(description_editor_mode.clone())
.update_on(Ev::Change)
.build()
.into_node();
let description_field = StyledField::build().input(description).build().into_node();
// right
let status = StyledSelect::build(FieldId::StatusIssueEditModal)
let user_avatar = StyledAvatar::build()
.add_class("userAvatar")
.size(32)
.avatar_url(
model
.user
.as_ref()
.and_then(|u| u.avatar_url.clone())
.unwrap_or_default(),
)
.build()
.into_node();
let create_comment = if *creating_comment {
let text_area = StyledTextarea::build()
.build(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody))
.into_node();
div![text_area]
} else {
let creating_comment = *creating_comment;
let handler = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
!creating_comment,
))
});
div![class!["fakeTextArea"], handler]
};
div![
class!["left"],
title,
description_field,
div![
class!["comments"],
div![class!["title"], "Comments"],
div![
class!["create"],
user_avatar,
div![
class!["right"],
create_comment,
div![
class!["proTip"],
strong![class!["strong"], "Pro tip: "],
"press ",
span![class!["tipLetter"], "M"],
" to comment"
]
]
]
],
]
}
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal {
payload,
status_state,
reporter_state,
assignees_state,
priority_state,
..
} = modal;
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Status))
.name("status")
.opened(status_state.opened)
.normal()
@ -204,10 +321,10 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.options(
IssueStatus::ordered()
.into_iter()
.map(|opt| IssueStatusOption(issue_id, opt))
.map(|opt| opt.to_select_child().name("status"))
.collect(),
)
.selected(vec![IssueStatusOption(issue_id, payload.status.clone())])
.selected(vec![payload.status.to_select_child().name("status")])
.valid(true)
.build()
.into_node();
@ -217,19 +334,25 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let assignees = StyledSelect::build(FieldId::AssigneesIssueEditModal)
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Assignees))
.name("assignees")
.opened(modal.assignees_state.opened)
.normal()
.opened(assignees_state.opened)
.empty()
.multi()
.text_filter(modal.assignees_state.text_filter.as_str())
.options(model.users.iter().map(|user| UserOption(user)).collect())
.text_filter(assignees_state.text_filter.as_str())
.options(
model
.users
.iter()
.map(|user| user.to_select_child().name("assignees"))
.collect(),
)
.selected(
model
.users
.iter()
.filter(|user| payload.user_ids.contains(&user.id))
.map(|user| UserOption(user))
.map(|user| user.to_select_child().name("assignees"))
.collect(),
)
.build()
@ -240,18 +363,24 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let reporter = StyledSelect::build(FieldId::ReporterIssueEditModal)
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Reporter))
.name("reporter")
.opened(modal.reporter_state.opened)
.normal()
.text_filter(modal.reporter_state.text_filter.as_str())
.options(model.users.iter().map(|user| UserOption(user)).collect())
.opened(reporter_state.opened)
.empty()
.text_filter(reporter_state.text_filter.as_str())
.options(
model
.users
.iter()
.map(|user| user.to_select_child().name("reporter"))
.collect(),
)
.selected(
model
.users
.iter()
.filter(|user| payload.reporter_id == user.id)
.map(|user| UserOption(user))
.map(|user| user.to_select_child().name("reporter"))
.collect(),
)
.build()
@ -262,18 +391,18 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let priority = StyledSelect::build(FieldId::PriorityIssueEditModal)
.name("assignees")
.opened(modal.priority_state.opened)
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Priority))
.name("priority")
.opened(priority_state.opened)
.empty()
.text_filter(priority_state.text_filter.as_str())
.options(
IssuePriority::ordered()
.into_iter()
.map(|p| IssuePriorityOption(p))
.map(|p| p.to_select_child().name("priority"))
.collect(),
)
.selected(vec![IssuePriorityOption(payload.priority.clone())])
.selected(vec![payload.priority.to_select_child().name("priority")])
.build()
.into_node();
let priority_field = StyledField::build()
@ -282,7 +411,7 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build()
.into_node();
let estimate = StyledInput::build(FieldId::EstimateIssueEditModal)
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
.valid(true)
.build()
.into_node();
@ -293,246 +422,11 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.into_node();
div![
attrs![At::Class => "issueDetails"],
div![
attrs![At::Class => "topActions"],
issue_type_select,
div![
attrs![At::Class => "topActionsRight"],
copy_button,
delete_button,
close_button
],
],
div![
attrs![At::Class => "content"],
div![
attrs![At::Class => "left"],
title,
description_field,
div![attrs![At::Class => "comments"]],
],
div![
attrs![At::Class => "right"],
status_field,
assignees_field,
reporter_field,
priority_field,
estimate_field
],
],
attrs![At::Class => "right"],
status_field,
assignees_field,
reporter_field,
priority_field,
estimate_field
]
}
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeTopOption(pub IssueId, pub IssueType);
impl SelectOption for IssueTypeTopOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
let icon = StyledIcon::build(self.1.into())
.add_class("issueTypeIcon".to_string())
.build()
.into_node();
div![
attrs![At::Class => "optionItem"],
icon,
div![attrs![At::Class => "optionLabel typeLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let issue_id = self.0;
let name = self.1.to_label().to_owned();
StyledButton::build()
.empty()
.children(vec![span![format!("{}-{}", name, issue_id)]])
.icon(StyledIcon::build(self.1.into()).build())
.build()
.into_node()
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.1
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}
/////
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueStatusOption(pub IssueId, pub IssueStatus);
impl SelectOption for IssueStatusOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
div![
attrs![At::Class => "type optionItem"],
div![attrs![At::Class => "typeLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
div![
attrs![At::Class => "selectItem"],
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
format!("{}", self.0)
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeOption(pub IssueType);
impl SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon")
.build()
.into_node();
div![
attrs![At::Class => "type optionItem"],
icon,
div![attrs![At::Class => "typeLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => "selectItem"],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct IssuePriorityOption(IssuePriority);
impl SelectOption for IssuePriorityOption {
fn into_option(self) -> Node<Msg> {
let name = format!("{}", self.0);
let icon = StyledIcon::build(self.0.into())
.add_class("issuePriorityIcon")
.size(18)
.build()
.into_node();
div![
attrs![At::Class => format!("priority optionItem {}", name)],
icon,
div![attrs![At::Class => "priorityLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = format!("{}", self.0);
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => format!("selectItem priority {}", name)],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct UserOption<'opt>(pub &'opt User);
impl<'opt> UserOption<'opt> {
fn avatar_node(&self) -> Node<Msg> {
let user = self.0;
StyledAvatar::build()
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
.size(20)
.name(user.name.as_str())
.build()
.into_node()
}
}
impl<'opt> SelectOption for UserOption<'opt> {
fn into_option(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "user optionItem"],
styled_avatar,
div![attrs![At::Class => "typeLabel optionLabel"], user.name]
]
}
fn into_value(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "selectItem"],
styled_avatar,
div![attrs![At::Class => "selectItemLabel"], user.name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0.name.contains(text_filter)
}
fn to_value(&self) -> u32 {
self.0.id as u32
}
}

View File

@ -1,12 +1,10 @@
use seed::{prelude::*, *};
use jirs_data::{UpdateIssuePayload, WsMsg};
use jirs_data::WsMsg;
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_editor::Mode;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::styled_select::StyledSelectState;
use crate::shared::{find_issue, ToNode};
use crate::{model, FieldChange, FieldId, Msg};
@ -91,34 +89,7 @@ fn push_edit_modal(issue_id: &i32, model: &mut Model) {
Some(issue) => issue,
_ => return,
};
ModalType::EditIssue(
*issue_id,
EditIssueModal {
id: *issue_id,
link_copied: false,
payload: UpdateIssuePayload {
title: issue.title.clone(),
issue_type: issue.issue_type.clone(),
status: issue.status.clone(),
priority: issue.priority.clone(),
list_position: issue.list_position.clone(),
description: issue.description.clone(),
description_text: issue.description_text.clone(),
estimate: issue.estimate.clone(),
time_spent: issue.time_spent.clone(),
time_remaining: issue.time_remaining.clone(),
project_id: issue.project_id.clone(),
reporter_id: issue.reporter_id.clone(),
user_ids: issue.user_ids.clone(),
},
top_type_state: StyledSelectState::new(FieldId::IssueTypeEditModalTop),
status_state: StyledSelectState::new(FieldId::StatusIssueEditModal),
reporter_state: StyledSelectState::new(FieldId::ReporterIssueEditModal),
assignees_state: StyledSelectState::new(FieldId::AssigneesIssueEditModal),
priority_state: StyledSelectState::new(FieldId::PriorityIssueEditModal),
description_editor_mode: Mode::Editor,
},
)
ModalType::EditIssue(*issue_id, EditIssueModal::new(issue))
};
model.modals.push(modal);
}

View File

@ -7,9 +7,7 @@ use jirs_data::*;
use crate::shared::styled_editor::Mode;
use crate::shared::styled_select::StyledSelectState;
use crate::{FieldId, IssueId, UserId, HOST_URL};
pub type ProjectId = i32;
use crate::{AddIssueModalFieldId, EditIssueModalFieldId, FieldId, HOST_URL};
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum ModalType {
@ -30,6 +28,48 @@ pub struct EditIssueModal {
pub priority_state: StyledSelectState,
pub description_editor_mode: Mode,
pub creating_comment: bool,
}
impl EditIssueModal {
pub fn new(issue: &Issue) -> Self {
Self {
id: issue.id,
link_copied: false,
payload: UpdateIssuePayload {
title: issue.title.clone(),
issue_type: issue.issue_type.clone(),
status: issue.status.clone(),
priority: issue.priority.clone(),
list_position: issue.list_position.clone(),
description: issue.description.clone(),
description_text: issue.description_text.clone(),
estimate: issue.estimate.clone(),
time_spent: issue.time_spent.clone(),
time_remaining: issue.time_remaining.clone(),
project_id: issue.project_id.clone(),
reporter_id: issue.reporter_id.clone(),
user_ids: issue.user_ids.clone(),
},
top_type_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::IssueType,
)),
status_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::Status,
)),
reporter_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::Reporter,
)),
assignees_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::Assignees,
)),
priority_state: StyledSelectState::new(FieldId::EditIssueModal(
EditIssueModalFieldId::Priority,
)),
description_editor_mode: Mode::Editor,
creating_comment: false,
}
}
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
@ -69,10 +109,18 @@ impl Default for AddIssueModal {
project_id: Default::default(),
user_ids: Default::default(),
reporter_id: Default::default(),
type_state: StyledSelectState::new(FieldId::IssueTypeAddIssueModal),
reporter_state: StyledSelectState::new(FieldId::ReporterAddIssueModal),
assignees_state: StyledSelectState::new(FieldId::AssigneesAddIssueModal),
priority_state: StyledSelectState::new(FieldId::IssuePriorityAddIssueModal),
type_state: StyledSelectState::new(FieldId::AddIssueModal(
AddIssueModalFieldId::IssueType,
)),
reporter_state: StyledSelectState::new(FieldId::AddIssueModal(
AddIssueModalFieldId::Reporter,
)),
assignees_state: StyledSelectState::new(FieldId::AddIssueModal(
AddIssueModalFieldId::Assignees,
)),
priority_state: StyledSelectState::new(FieldId::AddIssueModal(
AddIssueModalFieldId::Priority,
)),
}
}
}

View File

@ -10,7 +10,7 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{FieldId, Msg};
use crate::{EditIssueModalFieldId, FieldId, Msg};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
match msg {
@ -47,7 +47,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
}
Msg::StyledSelectChanged(
FieldId::IssueTypeEditModalTop,
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
StyledSelectChange::Text(text),
) => {
let modal = model
@ -289,13 +289,12 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
.iter()
.filter(|user| issue.user_ids.contains(&user.id))
.map(|user| {
StyledAvatar {
avatar_url: user.avatar_url.clone(),
size: 24,
name: user.name.clone(),
on_click: None,
}
.into_node()
StyledAvatar::build()
.size(24)
.name(user.name.as_str())
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
.build()
.into_node()
})
.collect();

View File

@ -1,10 +1,10 @@
use seed::{prelude::*, *};
use wasm_bindgen::JsCast;
use jirs_data::Issue;
use jirs_data::*;
use crate::model::Model;
use crate::{IssueId, Msg};
use crate::Msg;
pub mod aside;
pub mod navbar_left;
@ -18,6 +18,7 @@ pub mod styled_icon;
pub mod styled_input;
pub mod styled_modal;
pub mod styled_select;
pub mod styled_select_child;
pub mod styled_textarea;
pub mod styled_tooltip;

View File

@ -4,10 +4,11 @@ use crate::shared::ToNode;
use crate::Msg;
pub struct StyledAvatar {
pub avatar_url: Option<String>,
pub size: u32,
pub name: String,
pub on_click: Option<EventHandler<Msg>>,
avatar_url: Option<String>,
size: u32,
name: String,
on_click: Option<EventHandler<Msg>>,
class_list: Vec<String>,
}
impl Default for StyledAvatar {
@ -17,6 +18,7 @@ impl Default for StyledAvatar {
size: 32,
name: "".to_string(),
on_click: None,
class_list: vec![],
}
}
}
@ -28,6 +30,7 @@ impl StyledAvatar {
size: None,
name: "".to_string(),
on_click: None,
class_list: vec![],
}
}
}
@ -39,10 +42,11 @@ impl ToNode for StyledAvatar {
}
pub struct StyledAvatarBuilder {
pub avatar_url: Option<String>,
pub size: Option<u32>,
pub name: String,
pub on_click: Option<EventHandler<Msg>>,
avatar_url: Option<String>,
size: Option<u32>,
name: String,
on_click: Option<EventHandler<Msg>>,
class_list: Vec<String>,
}
impl StyledAvatarBuilder {
@ -50,7 +54,10 @@ impl StyledAvatarBuilder {
where
S: Into<String>,
{
self.avatar_url = Some(avatar_url.into());
let url = avatar_url.into();
if !url.is_empty() {
self.avatar_url = Some(url);
}
self
}
@ -72,12 +79,21 @@ impl StyledAvatarBuilder {
self
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn build(self) -> StyledAvatar {
StyledAvatar {
avatar_url: self.avatar_url,
size: self.size.unwrap_or(32),
name: self.name,
on_click: self.on_click,
class_list: self.class_list,
}
}
}
@ -88,7 +104,15 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
size,
name,
on_click,
mut class_list,
} = values;
class_list.push("styledAvatar".to_string());
match avatar_url {
Some(_) => class_list.push("image".to_string()),
_ => class_list.push("letter".to_string()),
};
let shared_style = format!("width: {size}px; height: {size}px", size = size);
let handler = match on_click {
None => vec![],
@ -96,11 +120,11 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
};
match avatar_url {
Some(url) => div![
attrs![At::Class => "styledAvatar image", At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)],
attrs![At::Class => class_list.join(" "), At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)],
handler,
],
_ => div![
attrs![At::Class => "styledAvatar letter", At::Style => shared_style],
attrs![At::Class => class_list.join(" "), At::Style => shared_style],
span![name],
handler
],

View File

@ -28,7 +28,7 @@ impl StyledEditor {
StyledEditorBuilder {
id,
text: String::new(),
mode: Mode::Editor,
mode: Mode::View,
update_event: None,
}
}

View File

@ -74,12 +74,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
let mut handlers = vec![];
let input_handler = input_ev(Ev::KeyUp, move |value| Msg::InputChanged(id, value));
handlers.push(input_handler);
handlers.push(input_ev(Ev::KeyUp, move |value| {
Msg::InputChanged(id, value)
}));
div![
attrs!(At::Class => wrapper_class_list.join(" ")),
icon,
keyboard_ev(Ev::KeyUp, |ev| {
ev.stop_propagation();
Msg::NoOp
}),
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers],
]
}

View File

@ -1,6 +1,7 @@
use seed::{prelude::*, *};
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_select_child::*;
use crate::shared::ToNode;
use crate::{FieldId, Msg};
@ -33,16 +34,6 @@ impl std::fmt::Display for Variant {
}
}
pub trait SelectOption {
fn into_option(self) -> Node<Msg>;
fn into_value(self) -> Node<Msg>;
fn match_text_filter(&self, text_filter: &str) -> bool;
fn to_value(&self) -> u32;
}
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
pub struct StyledSelectState {
pub field_id: FieldId,
@ -79,10 +70,7 @@ impl StyledSelectState {
}
}
pub struct StyledSelect<Child>
where
Child: SelectOption + PartialEq,
{
pub struct StyledSelect {
id: FieldId,
variant: Variant,
dropdown_width: Option<usize>,
@ -90,26 +78,20 @@ where
valid: bool,
is_multi: bool,
allow_clear: bool,
options: Vec<Child>,
selected: Vec<Child>,
options: Vec<StyledSelectChildBuilder>,
selected: Vec<StyledSelectChildBuilder>,
text_filter: String,
opened: bool,
}
impl<Child> ToNode for StyledSelect<Child>
where
Child: SelectOption + PartialEq,
{
impl ToNode for StyledSelect {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
impl<Child> StyledSelect<Child>
where
Child: SelectOption + PartialEq,
{
pub fn build(id: FieldId) -> StyledSelectBuilder<Child> {
impl StyledSelect {
pub fn build(id: FieldId) -> StyledSelectBuilder {
StyledSelectBuilder {
id,
variant: None,
@ -127,10 +109,7 @@ where
}
#[derive(Debug)]
pub struct StyledSelectBuilder<Child>
where
Child: SelectOption + PartialEq,
{
pub struct StyledSelectBuilder {
id: FieldId,
variant: Option<Variant>,
dropdown_width: Option<Option<usize>>,
@ -138,17 +117,14 @@ where
valid: Option<bool>,
is_multi: Option<bool>,
allow_clear: Option<bool>,
options: Option<Vec<Child>>,
selected: Option<Vec<Child>>,
options: Option<Vec<StyledSelectChildBuilder>>,
selected: Option<Vec<StyledSelectChildBuilder>>,
text_filter: Option<String>,
opened: Option<bool>,
}
impl<Child> StyledSelectBuilder<Child>
where
Child: SelectOption + PartialEq,
{
pub fn build(self) -> StyledSelect<Child> {
impl StyledSelectBuilder {
pub fn build(self) -> StyledSelect {
StyledSelect {
id: self.id,
variant: self.variant.unwrap_or_default(),
@ -195,12 +171,12 @@ where
self
}
pub fn options(mut self, options: Vec<Child>) -> Self {
pub fn options(mut self, options: Vec<StyledSelectChildBuilder>) -> Self {
self.options = Some(options);
self
}
pub fn selected(mut self, selected: Vec<Child>) -> Self {
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder>) -> Self {
self.selected = Some(selected);
self
}
@ -210,16 +186,18 @@ where
self
}
pub fn empty(mut self) -> Self {
self.variant = Some(Variant::Empty);
self
}
pub fn multi(mut self) -> Self {
self.is_multi = Some(true);
self
}
}
pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg>
where
Child: SelectOption + PartialEq,
{
pub fn render(values: StyledSelect) -> Node<Msg> {
let StyledSelect {
id,
variant,
@ -247,6 +225,7 @@ where
let dropdown_style = dropdown_width
.map(|n| format!("width: {}px;", n))
.unwrap_or_else(|| format!("width: 100%;"));
let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)];
if !valid {
select_class.push("invalid".to_string());
@ -262,9 +241,11 @@ where
let children: Vec<Node<Msg>> = options
.into_iter()
.filter(|o| !selected.contains(&o) && o.match_text_filter(text_filter.as_str()))
.filter(|o| !selected.contains(&o) && o.match_text(text_filter.as_str()))
.map(|child| {
let value = child.to_value();
let child = child.build(DisplayType::SelectOption);
let value = child.value();
let node = child.into_node();
let field_id = id.clone();
let on_change = mouse_ev(Ev::Click, move |_| {
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(value))
@ -273,7 +254,7 @@ where
attrs![At::Class => "option"],
on_change,
visibility_handler.clone(),
child.into_option()
node
]
})
.collect();
@ -320,12 +301,16 @@ where
} else {
selected
.into_iter()
.map(|m| render_value(m.into_value()))
.map(|m| render_value(m.build(DisplayType::SelectValue).into_node()))
.collect()
};
seed::div![
attrs![At::Class => select_class.join(" ")],
attrs![At::Class => select_class.join(" "), At::Style => dropdown_style.as_str()],
keyboard_ev(Ev::KeyUp, |ev| {
ev.stop_propagation();
Msg::NoOp
}),
div![
attrs![At::Class => format!("valueContainer {}", variant)],
visibility_handler,
@ -333,7 +318,8 @@ where
chevron_down,
],
div![
attrs![At::Class => "dropDown", At::Style => dropdown_style],
class!["dropDown"],
attrs![At::Style => dropdown_style.as_str()],
text_input,
clear_icon,
option_list
@ -346,13 +332,12 @@ fn render_value(mut content: Node<Msg>) -> Node<Msg> {
content
}
fn into_multi_value<Opt>(opt: Opt, field_id: FieldId) -> Node<Msg>
where
Opt: SelectOption,
{
fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node<Msg> {
let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node();
let value = opt.to_value();
let mut opt = opt.into_value();
let child = opt.build(DisplayType::SelectValue);
let value = child.value();
let mut opt = child.into_node();
opt.add_class("value");
opt.add_child(close_icon);

View File

@ -0,0 +1,240 @@
use seed::{prelude::*, *};
use crate::shared::styled_select::Variant;
use crate::shared::ToNode;
use crate::Msg;
pub trait ToStyledSelectChild {
fn to_select_child(&self) -> StyledSelectChildBuilder;
}
pub enum DisplayType {
SelectOption,
SelectValue,
}
pub struct StyledSelectChild {
name: Option<String>,
icon: Option<Node<Msg>>,
text: Option<String>,
display_type: DisplayType,
value: u32,
class_list: Vec<String>,
variant: Variant,
}
impl StyledSelectChild {
pub fn build() -> StyledSelectChildBuilder {
StyledSelectChildBuilder {
icon: None,
text: None,
name: None,
value: 0,
class_list: vec![],
variant: Default::default(),
}
}
#[inline]
pub fn value(&self) -> u32 {
self.value
}
}
impl ToNode for StyledSelectChild {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
#[derive(Debug)]
pub struct StyledSelectChildBuilder {
icon: Option<Node<Msg>>,
text: Option<String>,
name: Option<String>,
value: u32,
class_list: Vec<String>,
variant: Variant,
}
impl PartialEq for StyledSelectChildBuilder {
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}
impl StyledSelectChildBuilder {
pub fn icon(mut self, icon: Node<Msg>) -> Self {
self.icon = Some(icon);
self
}
pub fn text<S>(mut self, text: S) -> Self
where
S: Into<String>,
{
self.text = Some(text.into());
self
}
pub fn name<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.name = Some(name.into());
self
}
pub fn value(mut self, value: u32) -> Self {
self.value = value;
self
}
pub fn match_text(&self, text: &str) -> bool {
self.text
.as_ref()
.map(|t| t.contains(text))
.unwrap_or_default()
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn build(self, display_type: DisplayType) -> StyledSelectChild {
StyledSelectChild {
name: self.name,
icon: self.icon,
text: self.text,
display_type,
value: self.value,
class_list: self.class_list,
variant: self.variant,
}
}
}
pub fn render(values: StyledSelectChild) -> Node<Msg> {
let StyledSelectChild {
name,
icon,
text,
display_type,
value: _,
mut class_list,
variant,
} = values;
class_list.push(format!("{}", variant));
let label_class = match display_type {
DisplayType::SelectOption => vec![
"optionLabel".to_string(),
variant.to_string(),
name.as_ref().cloned().unwrap_or_default(),
name.as_ref()
.map(|s| format!("{}Label", s))
.unwrap_or_default(),
class_list.join(" "),
],
DisplayType::SelectValue => vec![
"selectItemLabel".to_string(),
variant.to_string(),
name.as_ref().cloned().unwrap_or_default(),
name.as_ref()
.map(|s| format!("{}Label", s))
.unwrap_or_default(),
class_list.join(" "),
],
}
.join(" ");
let wrapper_class = match display_type {
DisplayType::SelectOption => vec![
"optionItem".to_string(),
name.as_ref().cloned().unwrap_or_default(),
class_list.join(" "),
],
DisplayType::SelectValue => vec![
"selectItem".to_string(),
name.as_ref().cloned().unwrap_or_default(),
class_list.join(" "),
],
}
.join(" ");
let icon_node = match icon {
Some(icon) => icon,
_ => empty![],
};
let label_node = match text {
Some(text) => div![class![label_class.as_str()], text],
_ => empty![],
};
div![class![wrapper_class.as_str()], icon_node, label_node]
}
impl ToStyledSelectChild for jirs_data::User {
fn to_select_child(&self) -> StyledSelectChildBuilder {
let avatar = crate::shared::styled_avatar::StyledAvatar::build()
.avatar_url(self.avatar_url.as_ref().cloned().unwrap_or_default())
.size(20)
.name(self.name.as_str())
.build()
.into_node();
StyledSelectChild::build()
.value(self.id as u32)
.icon(avatar)
.text(self.name.as_str())
}
}
impl ToStyledSelectChild for jirs_data::IssuePriority {
fn to_select_child(&self) -> StyledSelectChildBuilder {
let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
.add_class(self.to_string())
.build()
.into_node();
let text = self.to_string();
StyledSelectChild::build()
.icon(icon)
.value(self.clone().into())
.text(text)
.add_class(format!("{}", self))
}
}
impl ToStyledSelectChild for jirs_data::IssueStatus {
fn to_select_child(&self) -> StyledSelectChildBuilder {
let text = self.to_label();
StyledSelectChild::build()
.value(self.clone().into())
.add_class(text.clone())
.text(text)
}
}
impl ToStyledSelectChild for jirs_data::IssueType {
fn to_select_child(&self) -> StyledSelectChildBuilder {
let name = self.to_label().to_owned();
let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
.add_class(name.as_str())
.build()
.into_node();
StyledSelectChild::build()
.add_class(name.as_str())
.text(name)
.icon(type_icon)
.value(self.clone().into())
}
}

View File

@ -140,6 +140,10 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
Msg::InputChanged(id, value)
});
handlers.push(text_input_handler);
handlers.push(keyboard_ev(Ev::KeyUp, |ev| {
ev.stop_propagation();
Msg::NoOp
}));
class_list.push("textAreaInput".to_string());

View File

@ -2,7 +2,6 @@ use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::Model;
use crate::IssueId;
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
model.project_page.dragged_issue_id = Some(issue_id);

View File

@ -163,19 +163,6 @@ impl IssueStatus {
IssueStatus::Done => "Done",
}
}
pub fn to_payload(&self) -> &str {
match self {
IssueStatus::Backlog => "backlog",
IssueStatus::Selected => "selected",
IssueStatus::InProgress => "in_progress",
IssueStatus::Done => "done",
}
}
pub fn match_name(&self, name: &str) -> bool {
self.to_payload() == name
}
}
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]

View File

@ -1,13 +1,13 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import api from '../../../../../shared/utils/api';
import toast from '../../../../../shared/utils/toast';
import api from '../../../../../shared/utils/api';
import toast from '../../../../../shared/utils/toast';
import { fetchCurrentUser } from "../../../../../actions/users";
import BodyForm from '../BodyForm';
import ProTip from './ProTip';
import BodyForm from '../BodyForm';
import ProTip from './ProTip';
import { Create, FakeTextarea, Right, UserAvatar } from './Styles';
class ProjectBoardIssueDetailsCommentsCreate extends React.Component {
@ -26,8 +26,7 @@ class ProjectBoardIssueDetailsCommentsCreate extends React.Component {
handleCommentCreate = async () => {
try {
this.setCreatingTrue();
const response = await api.post(`/comments`, { body: this.state.body, issueId: this.props.issueId });
console.log(response);
await api.post(`/comments`, { body: this.state.body, issueId: this.props.issueId });
await this.props.fetchIssue();
this.setState({ isCreating: false, isFormOpen: false, body: '' });
} catch (error) {