Set epic as issue group

This commit is contained in:
Adrian Woźniak 2020-08-12 20:54:00 +02:00
parent 316d315bea
commit 331acd4574
21 changed files with 302 additions and 154 deletions

View File

@ -42,6 +42,19 @@ https://git.sr.ht/~tsumanu/jirs
* Add personal settings to choose MDE (Markdown Editor) or RTE * Add personal settings to choose MDE (Markdown Editor) or RTE
* Add issues and filters * Add issues and filters
##### Work Progress
* [X] Add Epic
* [ ] Edit Epic
* [ ] Delete Epic
* [ ] Epic `starts` and `ends` date
* [X] Grouping by Epic
* [X] Basic Rich Text Editor
* [ ] Insert Code in Rich Text Editor
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE
* [ ] Issues and filters view
* [ ] Issues and filters working filters
## How to run it ## How to run it
### Config files ### Config files

View File

@ -0,0 +1,46 @@
use seed::prelude::Node;
use jirs_data::EpicId;
use crate::{
model::{IssueModal, Model},
shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode},
FieldId, Msg,
};
pub mod add_issue;
pub mod issue_details;
pub fn epic_field<Modal>(model: &Model, modal: &Modal, field_id: FieldId) -> Option<Node<Msg>>
where
Modal: IssueModal,
{
if model.epics.is_empty() {
None
} else {
let selected = modal
.epic_id_value()
.and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId))
.map(|epic| vec![epic.to_child()])
.unwrap_or_default();
let input = StyledSelect::build(field_id)
.name("epic")
.selected(selected)
.options(model.epics.iter().map(|epic| epic.to_child()).collect())
.normal()
.clearable()
.text_filter(modal.epic_state().text_filter.as_str())
.opened(modal.epic_state().opened)
.valid(true)
.build()
.into_node();
Some(
StyledField::build()
.label("Epic")
.tip("Feature group")
.input(input)
.build()
.into_node(),
)
}
}

View File

@ -1,21 +1,25 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{EpicId, IssueFieldId, WsMsg}; use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
use jirs_data::{IssuePriority, ToVec};
use crate::model::{AddIssueModal, ModalType, Model}; use crate::{
use crate::shared::styled_button::StyledButton; modal::issues::epic_field,
use crate::shared::styled_field::StyledField; model::{AddIssueModal, IssueModal, ModalType, Model},
use crate::shared::styled_form::StyledForm; shared::{
use crate::shared::styled_input::StyledInput; styled_button::StyledButton,
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; styled_field::StyledField,
use crate::shared::styled_select::StyledSelect; styled_form::StyledForm,
use crate::shared::styled_select::StyledSelectChange; styled_input::StyledInput,
use crate::shared::styled_select_child::{StyledSelectChild, StyledSelectChildBuilder}; styled_modal::{StyledModal, Variant as ModalVariant},
use crate::shared::styled_textarea::StyledTextarea; styled_select::StyledSelect,
use crate::shared::{ToChild, ToNode}; styled_select::StyledSelectChange,
use crate::ws::send_ws_msg; styled_select_child::{StyledSelectChild, StyledSelectChildBuilder},
use crate::{FieldId, Msg, WebSocketChanged}; styled_textarea::StyledTextarea,
ToChild, ToNode,
},
ws::send_ws_msg,
FieldId, Msg, WebSocketChanged,
};
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
enum Type { enum Type {
@ -114,12 +118,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
_ => return, _ => return,
}; };
modal.title_state.update(msg); modal.update_states(msg, orders);
modal.assignees_state.update(msg, orders);
modal.reporter_state.update(msg, orders);
modal.type_state.update(msg, orders);
modal.priority_state.update(msg, orders);
modal.epic_state.update(msg, orders);
match msg { match msg {
Msg::AddEpic => { Msg::AddEpic => {
@ -182,15 +181,15 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
FieldId::AddIssueModal(IssueFieldId::Reporter), FieldId::AddIssueModal(IssueFieldId::Reporter),
StyledSelectChange::Changed(id), StyledSelectChange::Changed(id),
) => { ) => {
modal.reporter_id = Some(*id as i32); modal.reporter_id = id.map(|n| n as UserId);
} }
// AssigneesAddIssueModal // AssigneesAddIssueModal
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::AddIssueModal(IssueFieldId::Assignees), FieldId::AddIssueModal(IssueFieldId::Assignees),
StyledSelectChange::Changed(id), StyledSelectChange::Changed(Some(id)),
) => { ) => {
let id = *id as i32; let id = *id as UserId;
if !modal.user_ids.contains(&id) { if !modal.user_ids.contains(&id) {
modal.user_ids.push(id); modal.user_ids.push(id);
} }
@ -212,7 +211,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
// IssuePriorityAddIssueModal // IssuePriorityAddIssueModal
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::AddIssueModal(IssueFieldId::Priority), FieldId::AddIssueModal(IssueFieldId::Priority),
StyledSelectChange::Changed(id), StyledSelectChange::Changed(Some(id)),
) => { ) => {
modal.priority = (*id).into(); modal.priority = (*id).into();
} }
@ -247,7 +246,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let reporter_field = reporter_field(model, modal); let reporter_field = reporter_field(model, modal);
let assignees_field = assignees_field(model, modal); let assignees_field = assignees_field(model, modal);
let issue_priority_field = issue_priority_field(modal); let issue_priority_field = issue_priority_field(modal);
let epic_field = epic_field(model, modal); let epic_field = epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::Epic));
form.add_field(short_summary_field) form.add_field(short_summary_field)
.add_field(description_field) .add_field(description_field)
@ -456,38 +455,6 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
.into_node() .into_node()
} }
fn epic_field(model: &Model, modal: &AddIssueModal) -> Option<Node<Msg>> {
if model.epics.is_empty() {
None
} else {
let selected = modal
.epic_state
.values
.get(0)
.and_then(|id| model.epics.iter().find(|epic| epic.id == *id as EpicId))
.map(|epic| vec![epic.to_child()])
.unwrap_or_default();
let input = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Epic))
.name("epic")
.selected(selected)
.options(model.epics.iter().map(|epic| epic.to_child()).collect())
.normal()
.text_filter(modal.epic_state.text_filter.as_str())
.opened(modal.epic_state.opened)
.valid(true)
.build()
.into_node();
Some(
StyledField::build()
.label("Epic")
.tip("Feature group")
.input(input)
.build()
.into_node(),
)
}
}
fn name_field(modal: &AddIssueModal) -> Node<Msg> { fn name_field(modal: &AddIssueModal) -> Node<Msg> {
let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title))
.state(&modal.title_state) .state(&modal.title_state)

View File

@ -2,37 +2,31 @@ use seed::{prelude::*, *};
use jirs_data::*; use jirs_data::*;
use crate::modal::time_tracking::time_tracking_field; use crate::{
use crate::model::{CommentForm, EditIssueModal, ModalType, Model}; modal::{issues::epic_field, time_tracking::time_tracking_field},
use crate::shared::styled_avatar::StyledAvatar; model::{CommentForm, EditIssueModal, IssueModal, ModalType, Model},
use crate::shared::styled_button::StyledButton; shared::{
use crate::shared::styled_editor::StyledEditor; styled_avatar::StyledAvatar,
use crate::shared::styled_field::StyledField; styled_button::StyledButton,
use crate::shared::styled_icon::Icon; styled_editor::StyledEditor,
use crate::shared::styled_input::StyledInput; styled_field::StyledField,
use crate::shared::styled_select::{StyledSelect, StyledSelectChange}; styled_icon::Icon,
use crate::shared::styled_textarea::StyledTextarea; styled_input::StyledInput,
use crate::shared::tracking_widget::tracking_link; styled_select::{StyledSelect, StyledSelectChange},
use crate::shared::{ToChild, ToNode}; styled_textarea::StyledTextarea,
use crate::ws::send_ws_msg; tracking_widget::tracking_link,
use crate::{EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged}; ToChild, ToNode,
},
ws::send_ws_msg,
EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged,
};
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal: &mut EditIssueModal = match model.modals.get_mut(0) { let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
Some(ModalType::EditIssue(_issue_id, modal)) => modal, Some(ModalType::EditIssue(_issue_id, modal)) => modal,
_ => return, _ => return,
}; };
modal.top_type_state.update(msg, orders); modal.update_states(msg, orders);
modal.status_state.update(msg, orders);
modal.reporter_state.update(msg, orders);
modal.assignees_state.update(msg, orders);
modal.priority_state.update(msg, orders);
modal.estimate.update(msg);
modal.estimate_select.update(msg, orders);
modal.time_spent.update(msg);
modal.time_spent_select.update(msg, orders);
modal.time_remaining.update(msg);
modal.time_remaining_select.update(msg, orders);
match msg { match msg {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => { Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
@ -40,7 +34,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
modal.payload.issue_type = (*value).into(); modal.payload.issue_type = (*value).into();
send_ws_msg( send_ws_msg(
@ -55,7 +49,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
modal.payload.issue_status_id = *value as IssueStatusId; modal.payload.issue_status_id = *value as IssueStatusId;
send_ws_msg( send_ws_msg(
@ -70,7 +64,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
modal.payload.reporter_id = *value as i32; modal.payload.reporter_id = *value as i32;
send_ws_msg( send_ws_msg(
@ -85,7 +79,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
modal.payload.user_ids.push(*value as i32); modal.payload.user_ids.push(*value as i32);
send_ws_msg( send_ws_msg(
@ -122,7 +116,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
modal.payload.priority = (*value).into(); modal.payload.priority = (*value).into();
send_ws_msg( send_ws_msg(
@ -267,6 +261,20 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
orders, orders,
); );
} }
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
StyledSelectChange::Changed(v),
) => {
send_ws_msg(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Epic,
PayloadVariant::OptionI32(v.map(|n| n as EpicId).clone()),
),
model.ws.as_ref(),
orders,
);
}
Msg::ModalChanged(FieldChange::TabChanged( Msg::ModalChanged(FieldChange::TabChanged(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
mode, mode,
@ -792,6 +800,13 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
(empty![], empty![]) (empty![], empty![])
}; };
let epic_field = epic_field(
model,
modal,
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
)
.unwrap_or_else(|| empty![]);
div![ div![
attrs![At::Class => "right"], attrs![At::Class => "right"],
status_field, status_field,
@ -800,5 +815,6 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
priority_field, priority_field,
estimate_field, estimate_field,
tracking_field, tracking_field,
epic_field,
] ]
} }

View File

@ -2,19 +2,24 @@ use seed::{prelude::*, *};
use jirs_data::{TimeTracking, WsMsg}; use jirs_data::{TimeTracking, WsMsg};
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page}; use crate::{
use crate::shared::styled_confirm_modal::StyledConfirmModal; modal::issues::*,
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; model::{self, AddIssueModal, EditIssueModal, ModalType, Model, Page},
use crate::shared::{find_issue, go_to_board, ToNode}; shared::{
use crate::ws::send_ws_msg; find_issue, go_to_board,
use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged}; styled_confirm_modal::StyledConfirmModal,
styled_modal::{StyledModal, Variant as ModalVariant},
ToNode,
},
ws::send_ws_msg,
FieldChange, FieldId, Msg, WebSocketChanged,
};
mod add_issue;
mod confirm_delete_issue; mod confirm_delete_issue;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
mod debug_modal; mod debug_modal;
mod delete_issue_status; mod delete_issue_status;
mod issue_details; pub mod issues;
pub mod time_tracking; pub mod time_tracking;
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {

View File

@ -1,6 +1,7 @@
use std::collections::hash_map::HashMap; use std::collections::hash_map::HashMap;
use chrono::{prelude::*, NaiveDate}; use chrono::{prelude::*, NaiveDate};
use seed::app::Orders;
use seed::browser::web_socket::WebSocket; use seed::browser::web_socket::WebSocket;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@ -15,7 +16,14 @@ use crate::shared::styled_image_input::StyledImageInputState;
use crate::shared::styled_input::StyledInputState; use crate::shared::styled_input::StyledInputState;
use crate::shared::styled_rte::StyledRteState; use crate::shared::styled_rte::StyledRteState;
use crate::shared::styled_select::StyledSelectState; use crate::shared::styled_select::StyledSelectState;
use crate::{EditIssueModalSection, FieldId, ProjectFieldId /*HOST_URL*/}; use crate::{EditIssueModalSection, FieldId, Msg, ProjectFieldId};
pub trait IssueModal {
fn epic_id_value(&self) -> Option<u32>;
fn epic_state(&self) -> &StyledSelectState;
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>);
}
#[derive(Clone, Debug, PartialOrd, PartialEq)] #[derive(Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType { pub enum ModalType {
@ -61,6 +69,7 @@ pub struct EditIssueModal {
pub reporter_state: StyledSelectState, pub reporter_state: StyledSelectState,
pub assignees_state: StyledSelectState, pub assignees_state: StyledSelectState,
pub priority_state: StyledSelectState, pub priority_state: StyledSelectState,
pub epic_state: StyledSelectState,
pub estimate: StyledInputState, pub estimate: StyledInputState,
pub estimate_select: StyledSelectState, pub estimate_select: StyledSelectState,
@ -75,6 +84,31 @@ pub struct EditIssueModal {
pub comment_form: CommentForm, pub comment_form: CommentForm,
} }
impl IssueModal for EditIssueModal {
fn epic_id_value(&self) -> Option<u32> {
self.epic_state.values.get(0).cloned()
}
fn epic_state(&self) -> &StyledSelectState {
&self.epic_state
}
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.top_type_state.update(msg, orders);
self.status_state.update(msg, orders);
self.reporter_state.update(msg, orders);
self.assignees_state.update(msg, orders);
self.priority_state.update(msg, orders);
self.estimate.update(msg);
self.estimate_select.update(msg, orders);
self.time_spent.update(msg);
self.time_spent_select.update(msg, orders);
self.time_remaining.update(msg);
self.time_remaining_select.update(msg, orders);
self.epic_state.update(msg, orders);
}
}
impl EditIssueModal { impl EditIssueModal {
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self { pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self {
Self { Self {
@ -115,6 +149,14 @@ impl EditIssueModal {
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
vec![issue.priority.into()], vec![issue.priority.into()],
), ),
epic_state: StyledSelectState::new(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
issue
.epic_id
.as_ref()
.map(|id| vec![*id as u32])
.unwrap_or_default(),
),
estimate: StyledInputState::new( estimate: StyledInputState::new(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)), FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value_for_time_tracking(&issue.estimate, &time_tracking_type), value_for_time_tracking(&issue.estimate, &time_tracking_type),
@ -175,6 +217,25 @@ pub struct AddIssueModal {
pub epic_state: StyledSelectState, pub epic_state: StyledSelectState,
} }
impl IssueModal for AddIssueModal {
fn epic_id_value(&self) -> Option<u32> {
self.epic_state.values.get(0).cloned()
}
fn epic_state(&self) -> &StyledSelectState {
&self.epic_state
}
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.title_state.update(msg);
self.assignees_state.update(msg, orders);
self.reporter_state.update(msg, orders);
self.type_state.update(msg, orders);
self.priority_state.update(msg, orders);
self.epic_state.update(msg, orders);
}
}
impl Default for AddIssueModal { impl Default for AddIssueModal {
fn default() -> Self { fn default() -> Self {
Self { Self {

View File

@ -1,7 +1,7 @@
use seed::prelude::{Method, Orders, Request}; use seed::prelude::{Method, Orders, Request};
use web_sys::FormData; use web_sys::FormData;
use jirs_data::{UsersFieldId, WsMsg}; use jirs_data::{ProjectId, UsersFieldId, WsMsg};
use crate::model::{Model, Page, PageContent, ProfilePage}; use crate::model::{Model, Page, PageContent, ProfilePage};
use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select::StyledSelectChange;
@ -69,12 +69,12 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::Profile(UsersFieldId::CurrentProject), FieldId::Profile(UsersFieldId::CurrentProject),
StyledSelectChange::Changed(id), StyledSelectChange::Changed(Some(id)),
) => { ) => {
if let Some(up) = model if let Some(up) = model
.user_projects .user_projects
.iter() .iter()
.find(|up| up.project_id == id as i32) .find(|up| up.project_id == id as ProjectId)
{ {
send_ws_msg( send_ws_msg(
WsMsg::UserProjectSetCurrent(up.id), WsMsg::UserProjectSetCurrent(up.id),

View File

@ -68,7 +68,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::ProjectSettings(ProjectFieldId::Category), FieldId::ProjectSettings(ProjectFieldId::Category),
StyledSelectChange::Changed(value), StyledSelectChange::Changed(Some(value)),
) => { ) => {
let category = value.into(); let category = value.into();
page.payload.category = Some(category); page.payload.category = Some(category);

View File

@ -242,6 +242,7 @@ pub struct StyledIconBuilder {
size: Option<i32>, size: Option<i32>,
class_list: Vec<String>, class_list: Vec<String>,
style_list: Vec<String>, style_list: Vec<String>,
on_click: Option<EventHandler<Msg>>,
} }
impl StyledIconBuilder { impl StyledIconBuilder {
@ -266,12 +267,18 @@ impl StyledIconBuilder {
self self
} }
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
self.on_click = Some(on_click);
self
}
pub fn build(self) -> StyledIcon { pub fn build(self) -> StyledIcon {
StyledIcon { StyledIcon {
icon: self.icon, icon: self.icon,
size: self.size, size: self.size,
class_list: self.class_list, class_list: self.class_list,
style_list: self.style_list, style_list: self.style_list,
on_click: self.on_click,
} }
} }
} }
@ -281,6 +288,7 @@ pub struct StyledIcon {
size: Option<i32>, size: Option<i32>,
class_list: Vec<String>, class_list: Vec<String>,
style_list: Vec<String>, style_list: Vec<String>,
on_click: Option<EventHandler<Msg>>,
} }
impl StyledIcon { impl StyledIcon {
@ -290,6 +298,7 @@ impl StyledIcon {
size: None, size: None,
class_list: vec![], class_list: vec![],
style_list: vec![], style_list: vec![],
on_click: None,
} }
} }
} }
@ -306,6 +315,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
size, size,
mut class_list, mut class_list,
mut style_list, mut style_list,
on_click,
} = values; } = values;
if let Some(s) = icon.to_color() { if let Some(s) = icon.to_color() {
@ -320,6 +330,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
i![ i![
attrs![At::Class => class_list.join(" "), At::Style => style_list.join(";")], attrs![At::Class => class_list.join(" "), At::Style => style_list.join(";")],
on_click,
"" ""
] ]
} }

View File

@ -9,7 +9,7 @@ use crate::{FieldId, Msg};
pub enum StyledSelectChange { pub enum StyledSelectChange {
Text(String), Text(String),
DropDownVisibility(bool), DropDownVisibility(bool),
Changed(u32), Changed(Option<u32>),
RemoveMulti(u32), RemoveMulti(u32),
} }
@ -75,11 +75,16 @@ impl StyledSelectState {
{ {
self.text_filter = text.clone(); self.text_filter = text.clone();
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(v)) Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(v)))
if field_id == &self.field_id => if field_id == &self.field_id =>
{ {
self.values = vec![*v]; self.values = vec![*v];
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None))
if field_id == &self.field_id =>
{
self.values.clear();
}
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v)) Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v))
if field_id == &self.field_id => if field_id == &self.field_id =>
{ {
@ -104,11 +109,11 @@ pub struct StyledSelect {
name: Option<String>, name: Option<String>,
valid: bool, valid: bool,
is_multi: bool, is_multi: bool,
allow_clear: bool,
options: Vec<StyledSelectChildBuilder>, options: Vec<StyledSelectChildBuilder>,
selected: Vec<StyledSelectChildBuilder>, selected: Vec<StyledSelectChildBuilder>,
text_filter: String, text_filter: String,
opened: bool, opened: bool,
clearable: bool,
} }
impl ToNode for StyledSelect { impl ToNode for StyledSelect {
@ -126,11 +131,11 @@ impl StyledSelect {
name: None, name: None,
valid: None, valid: None,
is_multi: None, is_multi: None,
allow_clear: None,
options: None, options: None,
selected: None, selected: None,
text_filter: None, text_filter: None,
opened: None, opened: None,
clearable: false,
} }
} }
} }
@ -143,11 +148,11 @@ pub struct StyledSelectBuilder {
name: Option<String>, name: Option<String>,
valid: Option<bool>, valid: Option<bool>,
is_multi: Option<bool>, is_multi: Option<bool>,
allow_clear: Option<bool>,
options: Option<Vec<StyledSelectChildBuilder>>, options: Option<Vec<StyledSelectChildBuilder>>,
selected: Option<Vec<StyledSelectChildBuilder>>, selected: Option<Vec<StyledSelectChildBuilder>>,
text_filter: Option<String>, text_filter: Option<String>,
opened: Option<bool>, opened: Option<bool>,
clearable: bool,
} }
impl StyledSelectBuilder { impl StyledSelectBuilder {
@ -159,11 +164,11 @@ impl StyledSelectBuilder {
name: self.name, name: self.name,
valid: self.valid.unwrap_or(true), valid: self.valid.unwrap_or(true),
is_multi: self.is_multi.unwrap_or_default(), is_multi: self.is_multi.unwrap_or_default(),
allow_clear: self.allow_clear.unwrap_or_default(),
options: self.options.unwrap_or_default(), options: self.options.unwrap_or_default(),
selected: self.selected.unwrap_or_default(), selected: self.selected.unwrap_or_default(),
text_filter: self.text_filter.unwrap_or_default(), text_filter: self.text_filter.unwrap_or_default(),
opened: self.opened.unwrap_or_default(), opened: self.opened.unwrap_or_default(),
clearable: self.clearable,
} }
} }
@ -227,6 +232,11 @@ impl StyledSelectBuilder {
self.is_multi = Some(true); self.is_multi = Some(true);
self self
} }
pub fn clearable(mut self) -> Self {
self.clearable = true;
self
}
} }
pub fn render(values: StyledSelect) -> Node<Msg> { pub fn render(values: StyledSelect) -> Node<Msg> {
@ -237,22 +247,26 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
name, name,
valid, valid,
is_multi, is_multi,
allow_clear,
options, options,
selected, selected,
text_filter, text_filter,
opened, opened,
clearable,
} = values; } = values;
let field_id = id.clone(); let on_text = {
let on_text = input_ev(Ev::KeyUp, move |value| { let field_id = id.clone();
Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value)) input_ev(Ev::KeyUp, move |value| {
}); Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value))
})
};
let field_id = id.clone(); let on_handler = {
let visibility_handler = mouse_ev(Ev::Click, move |_| { let field_id = id.clone();
Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened)) mouse_ev(Ev::Click, move |_| {
}); Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened))
})
};
let dropdown_style = dropdown_width let dropdown_style = dropdown_width
.map(|n| format!("width: {}px;", n)) .map(|n| format!("width: {}px;", n))
@ -263,7 +277,19 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
select_class.push("invalid".to_string()); select_class.push("invalid".to_string());
} }
let chevron_down = if (selected.is_empty() || !is_multi) && variant != Variant::Empty { let action_icon = if clearable && !selected.is_empty() {
let field_id = id.clone();
let on_click = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None))
});
StyledIcon::build(Icon::Close)
.add_class("chevronIcon")
.on_click(on_click)
.build()
.into_node()
} else if (selected.is_empty() || !is_multi) && variant != Variant::Empty {
StyledIcon::build(Icon::ChevronDown) StyledIcon::build(Icon::ChevronDown)
.add_class("chevronIcon") .add_class("chevronIcon")
.build() .build()
@ -281,12 +307,12 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
let node = child.into_node(); let node = child.into_node();
let field_id = id.clone(); let field_id = id.clone();
let on_change = mouse_ev(Ev::Click, move |_| { let on_change = mouse_ev(Ev::Click, move |_| {
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(value)) Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value)))
}); });
div![ div![
attrs![At::Class => "option"], attrs![At::Class => "option"],
on_change, on_change,
visibility_handler.clone(), on_handler.clone(),
node node
] ]
}) })
@ -307,11 +333,6 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
empty![] empty![]
}; };
let clear_icon = match (opened, allow_clear) {
(true, true) => StyledIcon::build(Icon::Close).build().into_node(),
_ => empty![],
};
let option_list = match (opened, children.is_empty()) { let option_list = match (opened, children.is_empty()) {
(false, _) => empty![], (false, _) => empty![],
(_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"], (_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"],
@ -347,15 +368,14 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
}), }),
div![ div![
attrs![At::Class => format!("valueContainer {}", variant)], attrs![At::Class => format!("valueContainer {}", variant)],
visibility_handler, on_handler,
value, value,
chevron_down, action_icon,
], ],
div![ div![
class!["dropDown"], C!["dropDown"],
attrs![At::Style => dropdown_style.as_str()], attrs![At::Style => dropdown_style.as_str()],
text_input, text_input,
clear_icon,
option_list option_list
] ]
] ]

View File

@ -74,7 +74,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::Users(UsersFieldId::UserRole), FieldId::Users(UsersFieldId::UserRole),
StyledSelectChange::Changed(role), StyledSelectChange::Changed(Some(role)),
) => { ) => {
page.user_role = role.into(); page.user_role = role.into();
} }

View File

@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize};
use jirs_data::{Token, User}; use jirs_data::{Token, User};
use crate::db::{DbExecutor, DbPool, SyncQuery}; use crate::{
use crate::errors::ServiceErrors; db::{DbExecutor, DbPool, SyncQuery},
errors::ServiceErrors,
};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AuthorizeUser { pub struct AuthorizeUser {

View File

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
use jirs_data::Comment; use jirs_data::Comment;
use crate::db::DbExecutor; use crate::{db::DbExecutor, errors::ServiceErrors};
use crate::errors::ServiceErrors;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadIssueComments { pub struct LoadIssueComments {

View File

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
use jirs_data::Epic; use jirs_data::Epic;
use crate::db::DbExecutor; use crate::{db::DbExecutor, errors::ServiceErrors};
use crate::errors::ServiceErrors;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadEpics { pub struct LoadEpics {

View File

@ -8,10 +8,14 @@ use jirs_data::{
User, UserId, UserRole, UsernameString, User, UserId, UserRole, UsernameString,
}; };
use crate::db::tokens::CreateBindToken; use crate::{
use crate::db::users::{LookupUser, Register}; db::{
use crate::db::DbExecutor; tokens::CreateBindToken,
use crate::errors::ServiceErrors; users::{LookupUser, Register},
DbExecutor,
},
errors::ServiceErrors,
};
pub struct ListInvitation { pub struct ListInvitation {
pub user_id: UserId, pub user_id: UserId,

View File

@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize};
use jirs_data::IssueAssignee; use jirs_data::IssueAssignee;
use crate::db::DbExecutor; use crate::{db::DbExecutor, errors::ServiceErrors};
use crate::errors::ServiceErrors;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadAssignees { pub struct LoadAssignees {

View File

@ -4,8 +4,7 @@ use diesel::prelude::*;
use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString}; use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString};
use crate::db::DbExecutor; use crate::{db::DbExecutor, errors::ServiceErrors};
use crate::errors::ServiceErrors;
pub struct LoadIssueStatuses { pub struct LoadIssueStatuses {
pub project_id: ProjectId, pub project_id: ProjectId,

View File

@ -6,9 +6,7 @@ use serde::{Deserialize, Serialize};
use jirs_data::{IssuePriority, IssueStatusId, IssueType}; use jirs_data::{IssuePriority, IssueStatusId, IssueType};
use crate::db::DbExecutor; use crate::{db::DbExecutor, errors::ServiceErrors, models::Issue};
use crate::errors::ServiceErrors;
use crate::models::Issue;
const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue"; const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue";
@ -88,6 +86,7 @@ pub struct UpdateIssue {
pub user_ids: Option<Vec<i32>>, pub user_ids: Option<Vec<i32>>,
pub reporter_id: Option<i32>, pub reporter_id: Option<i32>,
pub issue_status_id: Option<i32>, pub issue_status_id: Option<i32>,
pub epic_id: Option<Option<i32>>,
} }
impl Message for UpdateIssue { impl Message for UpdateIssue {
@ -127,6 +126,7 @@ impl Handler<UpdateIssue> for DbExecutor {
.map(|project_id| dsl::project_id.eq(project_id)), .map(|project_id| dsl::project_id.eq(project_id)),
msg.reporter_id msg.reporter_id
.map(|reporter_id| dsl::reporter_id.eq(reporter_id)), .map(|reporter_id| dsl::reporter_id.eq(reporter_id)),
msg.epic_id.map(|epic_id| dsl::epic_id.eq(epic_id)),
dsl::updated_at.eq(chrono::Utc::now().naive_utc()), dsl::updated_at.eq(chrono::Utc::now().naive_utc()),
)); ));
debug!( debug!(

View File

@ -3,9 +3,13 @@ use diesel::prelude::*;
use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId}; use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId};
use crate::db::users::{FindUser, LookupUser}; use crate::{
use crate::db::DbExecutor; db::{
use crate::errors::ServiceErrors; users::{FindUser, LookupUser},
DbExecutor,
},
errors::ServiceErrors,
};
#[derive(Debug)] #[derive(Debug)]
pub struct LoadMessages { pub struct LoadMessages {

View File

@ -3,7 +3,7 @@ use diesel::pg::Pg;
use diesel::prelude::*; use diesel::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use jirs_data::{Project, ProjectCategory, TimeTracking, UserId}; use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId};
use crate::db::DbExecutor; use crate::db::DbExecutor;
use crate::errors::ServiceErrors; use crate::errors::ServiceErrors;
@ -11,7 +11,7 @@ use crate::schema::projects::all_columns;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct LoadCurrentProject { pub struct LoadCurrentProject {
pub project_id: i32, pub project_id: ProjectId,
} }
impl Message for LoadCurrentProject { impl Message for LoadCurrentProject {
@ -43,7 +43,7 @@ impl Handler<LoadCurrentProject> for DbExecutor {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct CreateProject { pub struct CreateProject {
pub name: String, pub name: NameString,
pub url: Option<String>, pub url: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub category: Option<ProjectCategory>, pub category: Option<ProjectCategory>,
@ -82,8 +82,8 @@ impl Handler<CreateProject> for DbExecutor {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct UpdateProject { pub struct UpdateProject {
pub project_id: i32, pub project_id: ProjectId,
pub name: Option<String>, pub name: Option<NameString>,
pub url: Option<String>, pub url: Option<String>,
pub description: Option<String>, pub description: Option<String>,
pub category: Option<ProjectCategory>, pub category: Option<ProjectCategory>,

View File

@ -60,6 +60,9 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => { (IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
msg.time_remaining = o; msg.time_remaining = o;
} }
(IssueFieldId::Epic, PayloadVariant::OptionI32(o)) => {
msg.epic_id = Some(o);
}
_ => (), _ => (),
}; };