Set epic as issue group
This commit is contained in:
parent
316d315bea
commit
331acd4574
13
README.md
13
README.md
@ -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
|
||||
|
46
jirs-client/src/modal/issues.rs
Normal file
46
jirs-client/src/modal/issues.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
@ -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,
|
||||
]
|
||||
}
|
@ -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>) {
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
""
|
||||
]
|
||||
}
|
||||
|
@ -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 field_id = id.clone();
|
||||
let on_text = input_ev(Ev::KeyUp, move |value| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value))
|
||||
});
|
||||
let on_text = {
|
||||
let field_id = id.clone();
|
||||
input_ev(Ev::KeyUp, move |value| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value))
|
||||
})
|
||||
};
|
||||
|
||||
let field_id = id.clone();
|
||||
let visibility_handler = mouse_ev(Ev::Click, move |_| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened))
|
||||
});
|
||||
let on_handler = {
|
||||
let field_id = id.clone();
|
||||
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
|
||||
]
|
||||
]
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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!(
|
||||
|
@ -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 {
|
||||
|
@ -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>,
|
||||
|
@ -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);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user