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 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
### 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 jirs_data::{EpicId, IssueFieldId, WsMsg};
use jirs_data::{IssuePriority, ToVec};
use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm;
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::{StyledSelectChild, StyledSelectChildBuilder};
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{ToChild, ToNode};
use crate::ws::send_ws_msg;
use crate::{FieldId, Msg, WebSocketChanged};
use crate::{
modal::issues::epic_field,
model::{AddIssueModal, IssueModal, ModalType, Model},
shared::{
styled_button::StyledButton,
styled_field::StyledField,
styled_form::StyledForm,
styled_input::StyledInput,
styled_modal::{StyledModal, Variant as ModalVariant},
styled_select::StyledSelect,
styled_select::StyledSelectChange,
styled_select_child::{StyledSelectChild, StyledSelectChildBuilder},
styled_textarea::StyledTextarea,
ToChild, ToNode,
},
ws::send_ws_msg,
FieldId, Msg, WebSocketChanged,
};
#[derive(Copy, Clone)]
enum Type {
@ -114,12 +118,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
_ => return,
};
modal.title_state.update(msg);
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);
modal.update_states(msg, orders);
match msg {
Msg::AddEpic => {
@ -182,15 +181,15 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
FieldId::AddIssueModal(IssueFieldId::Reporter),
StyledSelectChange::Changed(id),
) => {
modal.reporter_id = Some(*id as i32);
modal.reporter_id = id.map(|n| n as UserId);
}
// AssigneesAddIssueModal
Msg::StyledSelectChanged(
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) {
modal.user_ids.push(id);
}
@ -212,7 +211,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
// IssuePriorityAddIssueModal
Msg::StyledSelectChanged(
FieldId::AddIssueModal(IssueFieldId::Priority),
StyledSelectChange::Changed(id),
StyledSelectChange::Changed(Some(id)),
) => {
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 assignees_field = assignees_field(model, 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)
.add_field(description_field)
@ -456,38 +455,6 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
.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> {
let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title))
.state(&modal.title_state)

View File

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

View File

@ -2,19 +2,24 @@ use seed::{prelude::*, *};
use jirs_data::{TimeTracking, WsMsg};
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_confirm_modal::StyledConfirmModal;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::{find_issue, go_to_board, ToNode};
use crate::ws::send_ws_msg;
use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged};
use crate::{
modal::issues::*,
model::{self, AddIssueModal, EditIssueModal, ModalType, Model, Page},
shared::{
find_issue, go_to_board,
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;
#[cfg(debug_assertions)]
mod debug_modal;
mod delete_issue_status;
mod issue_details;
pub mod issues;
pub mod time_tracking;
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 chrono::{prelude::*, NaiveDate};
use seed::app::Orders;
use seed::browser::web_socket::WebSocket;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -15,7 +16,14 @@ use crate::shared::styled_image_input::StyledImageInputState;
use crate::shared::styled_input::StyledInputState;
use crate::shared::styled_rte::StyledRteState;
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)]
pub enum ModalType {
@ -61,6 +69,7 @@ pub struct EditIssueModal {
pub reporter_state: StyledSelectState,
pub assignees_state: StyledSelectState,
pub priority_state: StyledSelectState,
pub epic_state: StyledSelectState,
pub estimate: StyledInputState,
pub estimate_select: StyledSelectState,
@ -75,6 +84,31 @@ pub struct EditIssueModal {
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 {
pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self {
Self {
@ -115,6 +149,14 @@ impl EditIssueModal {
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
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(
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
value_for_time_tracking(&issue.estimate, &time_tracking_type),
@ -175,6 +217,25 @@ pub struct AddIssueModal {
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 {
fn default() -> Self {
Self {

View File

@ -1,7 +1,7 @@
use seed::prelude::{Method, Orders, Request};
use web_sys::FormData;
use jirs_data::{UsersFieldId, WsMsg};
use jirs_data::{ProjectId, UsersFieldId, WsMsg};
use crate::model::{Model, Page, PageContent, ProfilePage};
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(
FieldId::Profile(UsersFieldId::CurrentProject),
StyledSelectChange::Changed(id),
StyledSelectChange::Changed(Some(id)),
) => {
if let Some(up) = model
.user_projects
.iter()
.find(|up| up.project_id == id as i32)
.find(|up| up.project_id == id as ProjectId)
{
send_ws_msg(
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(
FieldId::ProjectSettings(ProjectFieldId::Category),
StyledSelectChange::Changed(value),
StyledSelectChange::Changed(Some(value)),
) => {
let category = value.into();
page.payload.category = Some(category);

View File

@ -242,6 +242,7 @@ pub struct StyledIconBuilder {
size: Option<i32>,
class_list: Vec<String>,
style_list: Vec<String>,
on_click: Option<EventHandler<Msg>>,
}
impl StyledIconBuilder {
@ -266,12 +267,18 @@ impl StyledIconBuilder {
self
}
pub fn on_click(mut self, on_click: EventHandler<Msg>) -> Self {
self.on_click = Some(on_click);
self
}
pub fn build(self) -> StyledIcon {
StyledIcon {
icon: self.icon,
size: self.size,
class_list: self.class_list,
style_list: self.style_list,
on_click: self.on_click,
}
}
}
@ -281,6 +288,7 @@ pub struct StyledIcon {
size: Option<i32>,
class_list: Vec<String>,
style_list: Vec<String>,
on_click: Option<EventHandler<Msg>>,
}
impl StyledIcon {
@ -290,6 +298,7 @@ impl StyledIcon {
size: None,
class_list: vec![],
style_list: vec![],
on_click: None,
}
}
}
@ -306,6 +315,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
size,
mut class_list,
mut style_list,
on_click,
} = values;
if let Some(s) = icon.to_color() {
@ -320,6 +330,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
i![
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 {
Text(String),
DropDownVisibility(bool),
Changed(u32),
Changed(Option<u32>),
RemoveMulti(u32),
}
@ -75,11 +75,16 @@ impl StyledSelectState {
{
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 =>
{
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))
if field_id == &self.field_id =>
{
@ -104,11 +109,11 @@ pub struct StyledSelect {
name: Option<String>,
valid: bool,
is_multi: bool,
allow_clear: bool,
options: Vec<StyledSelectChildBuilder>,
selected: Vec<StyledSelectChildBuilder>,
text_filter: String,
opened: bool,
clearable: bool,
}
impl ToNode for StyledSelect {
@ -126,11 +131,11 @@ impl StyledSelect {
name: None,
valid: None,
is_multi: None,
allow_clear: None,
options: None,
selected: None,
text_filter: None,
opened: None,
clearable: false,
}
}
}
@ -143,11 +148,11 @@ pub struct StyledSelectBuilder {
name: Option<String>,
valid: Option<bool>,
is_multi: Option<bool>,
allow_clear: Option<bool>,
options: Option<Vec<StyledSelectChildBuilder>>,
selected: Option<Vec<StyledSelectChildBuilder>>,
text_filter: Option<String>,
opened: Option<bool>,
clearable: bool,
}
impl StyledSelectBuilder {
@ -159,11 +164,11 @@ impl StyledSelectBuilder {
name: self.name,
valid: self.valid.unwrap_or(true),
is_multi: self.is_multi.unwrap_or_default(),
allow_clear: self.allow_clear.unwrap_or_default(),
options: self.options.unwrap_or_default(),
selected: self.selected.unwrap_or_default(),
text_filter: self.text_filter.unwrap_or_default(),
opened: self.opened.unwrap_or_default(),
clearable: self.clearable,
}
}
@ -227,6 +232,11 @@ impl StyledSelectBuilder {
self.is_multi = Some(true);
self
}
pub fn clearable(mut self) -> Self {
self.clearable = true;
self
}
}
pub fn render(values: StyledSelect) -> Node<Msg> {
@ -237,22 +247,26 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
name,
valid,
is_multi,
allow_clear,
options,
selected,
text_filter,
opened,
clearable,
} = values;
let on_text = {
let field_id = id.clone();
let on_text = input_ev(Ev::KeyUp, move |value| {
input_ev(Ev::KeyUp, move |value| {
Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value))
});
})
};
let on_handler = {
let field_id = id.clone();
let visibility_handler = mouse_ev(Ev::Click, move |_| {
mouse_ev(Ev::Click, move |_| {
Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened))
});
})
};
let dropdown_style = dropdown_width
.map(|n| format!("width: {}px;", n))
@ -263,7 +277,19 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
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)
.add_class("chevronIcon")
.build()
@ -281,12 +307,12 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
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))
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value)))
});
div![
attrs![At::Class => "option"],
on_change,
visibility_handler.clone(),
on_handler.clone(),
node
]
})
@ -307,11 +333,6 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
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()) {
(false, _) => empty![],
(_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"],
@ -347,15 +368,14 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
}),
div![
attrs![At::Class => format!("valueContainer {}", variant)],
visibility_handler,
on_handler,
value,
chevron_down,
action_icon,
],
div![
class!["dropDown"],
C!["dropDown"],
attrs![At::Style => dropdown_style.as_str()],
text_input,
clear_icon,
option_list
]
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,9 +6,7 @@ use serde::{Deserialize, Serialize};
use jirs_data::{IssuePriority, IssueStatusId, IssueType};
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
use crate::models::Issue;
use crate::{db::DbExecutor, errors::ServiceErrors, models::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 reporter_id: Option<i32>,
pub issue_status_id: Option<i32>,
pub epic_id: Option<Option<i32>>,
}
impl Message for UpdateIssue {
@ -127,6 +126,7 @@ impl Handler<UpdateIssue> for DbExecutor {
.map(|project_id| dsl::project_id.eq(project_id)),
msg.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()),
));
debug!(

View File

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

View File

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

View File

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