Add epic, display epics select. Filter epics

This commit is contained in:
Adrian Woźniak 2020-08-11 22:15:56 +02:00
parent 02a2ecdd01
commit 38654fca85
29 changed files with 1002 additions and 482 deletions

View File

@ -68,6 +68,7 @@ impl std::fmt::Display for FieldId {
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
f.write_str("editIssue-listPosition")
}
EditIssueModalSection::Issue(IssueFieldId::Epic) => f.write_str("editIssue-epic"),
},
FieldId::AddIssueModal(sub) => match sub {
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
@ -81,6 +82,7 @@ impl std::fmt::Display for FieldId {
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
IssueFieldId::Epic => f.write_str("addIssueModal-epic"),
},
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),

View File

@ -1,3 +1,5 @@
#![feature(or_patterns)]
use seed::{prelude::*, *};
use web_sys::File;
@ -80,6 +82,11 @@ pub enum Msg {
AddIssue,
DeleteIssue(IssueId),
// epics
AddEpic,
DeleteEpic,
UpdateEpic,
// issue statuses
DeleteIssueStatus(IssueStatusId),
@ -130,19 +137,18 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
ws::update(ws_msg, model, orders);
}
WebSocketChanged::WebSocketMessageLoaded(v) => {
if let Ok(m) = bincode::deserialize(v.as_slice()) {
match m {
WsMsg::Ping | WsMsg::Pong => {
orders.perform_cmd(cmds::timeout(1000, || {
Msg::WebSocketChange(WebSocketChanged::SendPing)
}));
}
_ => {
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::WsMsg(m)));
}
match bincode::deserialize(v.as_slice()) {
Ok(WsMsg::Ping | WsMsg::Pong) => {
orders.perform_cmd(cmds::timeout(1000, || {
Msg::WebSocketChange(WebSocketChanged::SendPing)
}));
}
Ok(m) => {
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::WsMsg(m)));
}
_ => (),
};
return;
}
@ -309,7 +315,7 @@ fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) {
let pathname = seed::document().location().unwrap().pathname().unwrap();
match crate::shared::read_auth_token() {
Ok(token) => {
send_ws_msg(WsMsg::AuthorizeRequest(token), model.ws.as_ref(), orders);
send_ws_msg(WsMsg::AuthorizeLoad(token), model.ws.as_ref(), orders);
}
Err(..) => {
match pathname.as_str() {

View File

@ -1,7 +1,7 @@
use seed::{prelude::*, *};
use jirs_data::{IssueFieldId, WsMsg};
use jirs_data::{IssuePriority, IssueType, ToVec};
use jirs_data::{EpicId, IssueFieldId, WsMsg};
use jirs_data::{IssuePriority, ToVec};
use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton;
@ -11,11 +11,99 @@ 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};
#[derive(Copy, Clone)]
enum Type {
Task,
Bug,
Story,
Epic,
}
impl From<u32> for Type {
fn from(n: u32) -> Self {
match n {
0 => Type::Task,
1 => Type::Bug,
2 => Type::Story,
3 => Type::Epic,
_ => Type::Task,
}
}
}
impl Type {
fn ordered<'l>() -> &'l [Type] {
use Type::*;
&[Task, Bug, Story, Epic]
}
fn submit_label(&self) -> &str {
use Type::*;
match self {
Epic => "Create epic",
Bug | Task | Story => "Create issue",
}
}
fn form_label(&self) -> &str {
use Type::*;
match self {
Epic => "Create epic",
Bug | Task | Story => "Create issue",
}
}
fn submit_action(&self) -> Msg {
use Type::*;
match self {
Epic => Msg::AddEpic,
Bug | Task | Story => Msg::AddIssue,
}
}
}
impl ToChild for Type {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> Self::Builder {
let name = match self {
Type::Task => "Task",
Type::Bug => "Bug",
Type::Story => "Story",
Type::Epic => "Epic",
};
let value = match self {
Type::Task => 0,
Type::Bug => 1,
Type::Story => 2,
Type::Epic => 3,
};
let icon = match self {
Type::Task => crate::shared::styled_icon::Icon::Task,
Type::Bug => crate::shared::styled_icon::Icon::Bug,
Type::Story => crate::shared::styled_icon::Icon::Story,
Type::Epic => crate::shared::styled_icon::Icon::Epic,
};
let type_icon = crate::shared::styled_icon::StyledIcon::build(icon)
.add_class(name)
.build()
.into_node();
StyledSelectChild::build()
.add_class(name)
.text(name)
.icon(type_icon)
.value(value)
}
}
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
let modal = model.modals.iter_mut().find(|modal| match modal {
ModalType::AddIssue(..) => true,
@ -31,33 +119,50 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
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 {
Msg::AddIssue => {
let user_id = model.user.as_ref().map(|u| u.id).unwrap_or_default();
let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
let payload = jirs_data::CreateIssuePayload {
title: modal.title_state.value.clone(),
issue_type: modal.issue_type,
issue_status_id: modal.issue_status_id,
priority: modal.priority,
description: modal.description.clone(),
description_text: modal.description_text.clone(),
estimate: modal.estimate,
time_spent: modal.time_spent,
time_remaining: modal.time_remaining,
project_id: modal.project_id.unwrap_or(project_id),
user_ids: modal.user_ids.clone(),
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
epic_id: modal.epic_id,
};
Msg::AddEpic => {
send_ws_msg(
jirs_data::WsMsg::IssueCreateRequest(payload),
WsMsg::EpicCreate(modal.title_state.value.clone()),
model.ws.as_ref(),
orders,
);
}
Msg::AddIssue => {
let user_id = model.user.as_ref().map(|u| u.id).unwrap_or_default();
let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
let type_value = modal.type_state.values.get(0).cloned().unwrap_or_default();
match type_value {
0 | 1 | 2 => {
let issue_type = type_value.into();
let payload = jirs_data::CreateIssuePayload {
title: modal.title_state.value.clone(),
issue_type,
issue_status_id: modal.issue_status_id,
priority: modal.priority,
description: modal.description.clone(),
description_text: modal.description_text.clone(),
estimate: modal.estimate,
time_spent: modal.time_spent,
time_remaining: modal.time_remaining,
project_id: modal.project_id.unwrap_or(project_id),
user_ids: modal.user_ids.clone(),
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
epic_id: modal.epic_id,
};
send_ws_msg(
jirs_data::WsMsg::IssueCreate(payload),
model.ws.as_ref(),
orders,
);
}
_ => {
//
}
};
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {
model.issues.push(issue.clone());
orders.skip().send_msg(Msg::ModalDropped);
@ -67,14 +172,6 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
modal.description = Some(value.clone());
}
// IssueTypeAddIssueModal
Msg::StyledSelectChanged(
FieldId::AddIssueModal(IssueFieldId::Type),
StyledSelectChange::Changed(id),
) => {
modal.issue_type = (*id).into();
}
// ReporterAddIssueModal
Msg::StyledSelectChanged(
FieldId::AddIssueModal(IssueFieldId::Reporter),
@ -120,6 +217,85 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
}
pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let issue_type = modal
.type_state
.values
.get(0)
.cloned()
.map(|n| Type::from(n))
.unwrap_or_else(|| Type::Task);
let issue_type_field = issue_type_field(modal);
let form = StyledForm::build()
.heading(issue_type.form_label())
.add_field(issue_type_field)
.add_field(crate::shared::divider());
let form = match issue_type {
Type::Epic => {
let name_field = name_field(modal);
form.add_field(name_field)
}
Type::Task | Type::Story | Type::Bug => {
let short_summary_field = short_summary_field(modal);
let description_field = description_field();
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);
form.add_field(short_summary_field)
.add_field(description_field)
.add_field(reporter_field)
.add_field(assignees_field)
.add_field(issue_priority_field)
.try_field(epic_field)
}
};
let submit = {
let issue_type = issue_type.clone();
StyledButton::build()
.primary()
.text(issue_type.submit_label())
.add_class("action")
.add_class("submit")
.add_class("actionButton")
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
ev.prevent_default();
Some(issue_type.submit_action())
}))
.build()
.into_node()
};
let cancel = StyledButton::build()
.empty()
.add_class("action")
.add_class("cancel")
.add_class("actionButton")
.text("Cancel")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.stop_propagation();
ev.prevent_default();
Some(Msg::ModalDropped)
}))
.build()
.into_node();
let actions = div![attrs![At::Class => "actions"], submit, cancel];
let form = form.add_field(actions).build().into_node();
StyledModal::build()
.width(800)
.add_class("add-issue")
.variant(ModalVariant::Center)
.children(vec![form])
.build()
.into_node()
}
fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Type))
.name("type")
.normal()
@ -127,44 +303,54 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.opened(modal.type_state.opened)
.valid(true)
.options(
IssueType::ordered()
Type::ordered()
.iter()
.map(|t| t.to_child().name("type"))
.collect(),
)
.selected(vec![modal.issue_type.to_child().name("type")])
.selected(vec![Type::from(
modal.type_state.values.get(0).cloned().unwrap_or_default(),
)
.to_child()
.name("type")])
.build()
.into_node();
let issue_type_field = StyledField::build()
StyledField::build()
.label("Issue Type")
.tip("Start typing to get a list of possible matches.")
.input(select_type)
.build()
.into_node();
.into_node()
}
fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> {
let short_summary = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title))
.state(&modal.title_state)
.build()
.into_node();
let short_summary_field = StyledField::build()
StyledField::build()
.label("Short Summary")
.tip("Concisely summarize the issue in one or two sentences.")
.input(short_summary)
.build()
.into_node();
.into_node()
}
fn description_field() -> Node<Msg> {
let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description))
.height(110)
.add_class("textarea")
.build()
.into_node();
let description_field = StyledField::build()
StyledField::build()
.label("Description")
.tip("Describe the issue in as much detail as you'd like.")
.input(description)
.build()
.into_node();
.into_node()
}
fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let reporter_id = modal
.reporter_id
.or_else(|| model.user.as_ref().map(|u| u.id))
@ -196,13 +382,15 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.valid(true)
.build()
.into_node();
let reporter_field = StyledField::build()
StyledField::build()
.input(reporter)
.label("Reporter")
.tip("")
.build()
.into_node();
.into_node()
}
fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let assignees = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Assignees))
.normal()
.multi()
@ -231,13 +419,15 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.valid(true)
.build()
.into_node();
let assignees_field = StyledField::build()
StyledField::build()
.input(assignees)
.label("Assignees")
.tip("")
.build()
.into_node();
.into_node()
}
fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
let select_priority = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Priority))
.name("priority")
.normal()
@ -253,59 +443,55 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.selected(vec![modal.priority.to_child().name("priority")])
.build()
.into_node();
let issue_priority_field = StyledField::build()
StyledField::build()
.label("Issue Type")
.tip("Priority in relation to other issues.")
.input(select_priority)
.build()
.into_node();
.into_node()
}
let submit = StyledButton::build()
.primary()
.text("Create Issue")
.add_class("action")
.add_class("submit")
.add_class("ActionButton")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.stop_propagation();
ev.prevent_default();
Some(Msg::AddIssue)
}))
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)
.build()
.into_node();
let cancel = StyledButton::build()
.empty()
.add_class("action")
.add_class("cancel")
.add_class("actionButton")
.text("Cancel")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.stop_propagation();
ev.prevent_default();
Some(Msg::ModalDropped)
}))
.build()
.into_node();
let actions = div![attrs![At::Class => "actions"], submit, cancel];
let form = StyledForm::build()
.heading("Create issue")
.add_field(issue_type_field)
.add_field(crate::shared::divider())
.add_field(short_summary_field)
.add_field(description_field)
.add_field(reporter_field)
.add_field(assignees_field)
.add_field(issue_priority_field)
.add_field(actions)
.build()
.into_node();
StyledModal::build()
.width(800)
.add_class("add-issue")
.variant(ModalVariant::Center)
.children(vec![form])
StyledField::build()
.label("Epic name")
.tip("Describe upcoming feature.")
.input(name)
.build()
.into_node()
}

View File

@ -44,7 +44,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.issue_type = (*value).into();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Type,
PayloadVariant::IssueType(modal.payload.issue_type),
@ -59,7 +59,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.issue_status_id = *value as IssueStatusId;
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(modal.payload.issue_status_id),
@ -74,7 +74,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.reporter_id = *value as i32;
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Reporter,
PayloadVariant::I32(modal.payload.reporter_id),
@ -89,7 +89,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.user_ids.push(*value as i32);
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
@ -111,7 +111,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Assignees,
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
@ -126,7 +126,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.priority = (*value).into();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Priority,
PayloadVariant::IssuePriority(modal.payload.priority),
@ -141,7 +141,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.title = value.clone();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Title,
PayloadVariant::String(modal.payload.title.clone()),
@ -157,7 +157,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone());
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Description,
PayloadVariant::String(
@ -180,7 +180,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.time_spent = modal.time_spent.represent_f64_as_i32();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
@ -195,7 +195,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.time_spent = modal.time_spent_select.values.get(0).map(|n| *n as i32);
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::TimeSpent,
PayloadVariant::OptionI32(modal.payload.time_spent),
@ -211,7 +211,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.time_remaining = modal.time_remaining.represent_f64_as_i32();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
@ -227,7 +227,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.payload.time_remaining =
modal.time_remaining_select.values.get(0).map(|n| *n as i32);
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::TimeRemaining,
PayloadVariant::OptionI32(modal.payload.time_remaining),
@ -243,7 +243,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.estimate = modal.estimate.represent_f64_as_i32();
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
@ -258,7 +258,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
) => {
modal.payload.estimate = modal.estimate_select.values.get(0).map(|n| *n as i32);
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
modal.id,
IssueFieldId::Estimate,
PayloadVariant::OptionI32(modal.payload.estimate),
@ -294,11 +294,11 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::SaveComment => {
let msg = match modal.comment_form.id {
Some(id) => WsMsg::UpdateComment(UpdateCommentPayload {
Some(id) => WsMsg::CommentUpdate(UpdateCommentPayload {
id,
body: modal.comment_form.body.clone(),
}),
_ => WsMsg::CreateComment(CreateCommentPayload {
_ => WsMsg::CommentCreate(CreateCommentPayload {
user_id: None,
body: modal.comment_form.body.clone(),
issue_id: modal.id,
@ -328,11 +328,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.comment_form.creating = true;
}
Msg::DeleteComment(comment_id) => {
send_ws_msg(
WsMsg::CommentDeleteRequest(*comment_id),
model.ws.as_ref(),
orders,
);
send_ws_msg(WsMsg::CommentDelete(*comment_id), model.ws.as_ref(), orders);
orders.skip().send_msg(Msg::ModalDropped);
}

View File

@ -132,7 +132,7 @@ fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Ms
)
};
send_ws_msg(
WsMsg::IssueCommentsRequest(issue_id),
WsMsg::IssueCommentsLoad(issue_id),
model.ws.as_ref(),
orders,
);

View File

@ -154,7 +154,6 @@ impl EditIssueModal {
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct AddIssueModal {
pub issue_type: IssueType,
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
@ -173,12 +172,12 @@ pub struct AddIssueModal {
pub reporter_state: StyledSelectState,
pub assignees_state: StyledSelectState,
pub priority_state: StyledSelectState,
pub epic_state: StyledSelectState,
}
impl Default for AddIssueModal {
fn default() -> Self {
Self {
issue_type: Default::default(),
priority: Default::default(),
description: Default::default(),
description_text: Default::default(),
@ -204,6 +203,7 @@ impl Default for AddIssueModal {
FieldId::AddIssueModal(IssueFieldId::Priority),
vec![],
),
epic_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Epic), vec![]),
}
}
}
@ -509,6 +509,7 @@ pub struct Model {
pub messages: Vec<Message>,
pub user_projects: Vec<UserProject>,
pub projects: Vec<Project>,
pub epics: Vec<Epic>,
}
impl Model {
@ -538,6 +539,7 @@ impl Model {
messages: vec![],
user_projects: vec![],
projects: vec![],
epics: vec![],
}
}

View File

@ -5,14 +5,14 @@ use jirs_data::{UsersFieldId, WsMsg};
use crate::model::{Model, Page, PageContent, ProfilePage};
use crate::shared::styled_select::StyledSelectChange;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::ws::{board_load, send_ws_msg};
use crate::{FieldId, Msg, PageChanged, ProfilePageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Profile) => {
init_load(model, orders);
board_load(model, orders);
build_page_content(model);
}
_ => (),
@ -87,13 +87,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
}
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
return;
}
enqueue_ws_msg(vec![WsMsg::ProjectIssuesRequest], model.ws.as_ref(), orders);
}
fn build_page_content(model: &mut Model) {
let user = match model.user {
Some(ref user) => user,

View File

@ -4,7 +4,7 @@ use jirs_data::{Issue, IssueFieldId, WsMsg};
use crate::model::{ModalType, Model, Page, PageContent, ProjectPage};
use crate::shared::styled_select::StyledSelectChange;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::ws::{board_load, send_ws_msg};
use crate::{BoardPageChange, EditIssueModalSection, FieldId, Msg, PageChanged, WebSocketChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
@ -32,7 +32,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
| Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => {
init_load(model, orders);
board_load(model, orders);
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
let mut old: Vec<Issue> = vec![];
@ -116,7 +116,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
}
Msg::DeleteIssue(issue_id) => {
send_ws_msg(
jirs_data::WsMsg::IssueDeleteRequest(issue_id),
jirs_data::WsMsg::IssueDelete(issue_id),
model.ws.as_ref(),
orders,
);
@ -125,14 +125,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
}
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![WsMsg::ProjectIssuesRequest, WsMsg::IssueStatusesRequest],
model.ws.as_ref(),
orders,
);
}
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Project(Box::new(ProjectPage::default()));
}

View File

@ -7,7 +7,7 @@ use jirs_data::{IssueStatus, IssueStatusId, ProjectFieldId, UpdateProjectPayload
use crate::model::{Model, Page, PageContent, ProjectSettingsPage};
use crate::shared::styled_select::StyledSelectChange;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::ws::{board_load, send_ws_msg};
use crate::FieldChange::TabChanged;
use crate::{FieldId, Msg, PageChanged, ProjectPageChange, WebSocketChanged};
@ -22,7 +22,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
init_load(model, orders);
board_load(model, orders);
}
WebSocketChanged::WsMsg(WsMsg::IssueStatusCreated(_)) => {
match &mut model.page_content {
@ -37,7 +37,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model);
if model.user.is_some() {
init_load(model, orders);
board_load(model, orders);
}
}
_ => (),
@ -83,7 +83,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
ProjectPageChange::SubmitProjectSettingsForm,
)) => {
send_ws_msg(
WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
WsMsg::ProjectUpdateLoad(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
@ -160,14 +160,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![WsMsg::IssueStatusesRequest, WsMsg::ProjectIssuesRequest],
model.ws.as_ref(),
orders,
);
}
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,

View File

@ -4,7 +4,7 @@ use jirs_data::WsMsg;
use crate::changes::{PageChanged, ReportsPageChange};
use crate::model::{Model, Page, PageContent, ReportsPage};
use crate::ws::enqueue_ws_msg;
use crate::ws::board_load;
use crate::{Msg, WebSocketChanged};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
@ -24,7 +24,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
Msg::UserChanged(Some(..))
| Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Reports) => {
init_load(model, orders);
board_load(model, orders);
}
Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DayHovered(v))) => {
page.hovered_day = v;
@ -39,15 +39,3 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Reports(Box::new(ReportsPage::default()))
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() {
return;
}
enqueue_ws_msg(
vec![WsMsg::ProjectIssuesRequest, WsMsg::IssueStatusesRequest],
model.ws.as_ref(),
orders,
);
}

View File

@ -14,8 +14,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
vec![
WsMsg::UserProjectsLoad,
WsMsg::ProjectsLoad,
WsMsg::MessagesRequest,
WsMsg::ProjectUsersRequest,
WsMsg::MessagesLoad,
WsMsg::ProjectUsersLoad,
],
model.ws.as_ref(),
orders,

View File

@ -35,6 +35,13 @@ impl StyledFormBuilder {
self
}
pub fn try_field(mut self, node: Option<Node<Msg>>) -> Self {
if let Some(n) = node {
self.fields.push(n);
}
self
}
pub fn heading<S>(mut self, heading: S) -> Self
where
S: Into<String>,

View File

@ -23,6 +23,7 @@ impl ToString for Variant {
#[derive(Clone, Debug, PartialOrd, PartialEq)]
pub struct StyledInputState {
id: FieldId,
touched: bool,
pub value: String,
}
@ -33,6 +34,7 @@ impl StyledInputState {
{
Self {
id,
touched: false,
value: value.into(),
}
}
@ -53,6 +55,7 @@ impl StyledInputState {
match msg {
Msg::StrInputChanged(field_id, s) if field_id == &self.id => {
self.value = s.clone();
self.touched = true;
}
_ => (),
}
@ -129,7 +132,7 @@ impl StyledInputBuilder {
pub fn state(self, state: &StyledInputState) -> Self {
self.value(state.value.as_str())
.valid(!state.value.is_empty())
.valid(!state.touched || !state.value.is_empty())
}
pub fn add_input_class<S>(mut self, name: S) -> Self

View File

@ -279,6 +279,16 @@ impl ToChild for jirs_data::Project {
}
}
impl ToChild for jirs_data::Epic {
type Builder = StyledSelectChildBuilder;
fn to_child(&self) -> Self::Builder {
StyledSelectChild::build()
.text(self.name.as_str())
.value(self.id as u32)
}
}
impl ToChild for u32 {
type Builder = StyledSelectChildBuilder;

View File

@ -4,7 +4,7 @@ use jirs_data::{InvitationState, UserRole, UsersFieldId, WsMsg};
use crate::model::{InvitationFormState, Model, Page, PageContent, UsersPage};
use crate::shared::styled_select::StyledSelectChange;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::ws::{invitation_load, send_ws_msg};
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -22,11 +22,11 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::ChangePage(Page::Users) if model.user.is_some() => {
init_load(model, orders);
invitation_load(model, orders);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) if model.user.is_some() => {
init_load(model, orders);
invitation_load(model, orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
@ -43,7 +43,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
page.invitations.push(invitation);
}
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
send_ws_msg(WsMsg::InvitationListLoad, model.ws.as_ref(), orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUserRemoveSuccess(removed_id)) => {
let mut old = vec![];
@ -55,7 +55,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendSuccess) => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
send_ws_msg(WsMsg::InvitationListLoad, model.ws.as_ref(), orders);
page.form_state = InvitationFormState::Succeed;
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendFailure) => {
@ -124,11 +124,3 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Users(Box::new(UsersPage::default()));
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}

View File

@ -0,0 +1,27 @@
use seed::app::Orders;
use jirs_data::WsMsg;
use crate::model::Model;
use crate::ws::enqueue_ws_msg;
use crate::Msg;
pub fn board_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![
WsMsg::IssueStatusesLoad,
WsMsg::ProjectIssuesLoad,
WsMsg::EpicsLoad,
],
model.ws.as_ref(),
orders,
);
}
pub fn invitation_load(model: &mut Model, orders: &mut impl Orders<Msg>) {
enqueue_ws_msg(
vec![WsMsg::InvitationListLoad, WsMsg::InvitedUsersLoad],
model.ws.as_ref(),
orders,
);
}

View File

@ -93,7 +93,7 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
}
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
issue.id,
IssueFieldId::IssueStatusId,
PayloadVariant::I32(issue.issue_status_id),
@ -102,7 +102,7 @@ pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
orders,
);
send_ws_msg(
WsMsg::IssueUpdateRequest(
WsMsg::IssueUpdate(
issue.id,
IssueFieldId::ListPosition,
PayloadVariant::I32(issue.list_position),

View File

@ -1,11 +1,14 @@
use seed::prelude::*;
pub use init_load_sets::*;
use jirs_data::WsMsg;
use crate::model::*;
use crate::shared::{go_to_board, write_auth_token};
use crate::{Msg, WebSocketChanged};
mod init_load_sets;
pub mod issue;
pub fn flush_queue(model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -123,7 +126,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model.issues = v;
}
// issue statuses
WsMsg::IssueStatusesResponse(v) => {
WsMsg::IssueStatusesLoaded(v) => {
model.issue_statuses = v.clone();
model
.issue_statuses
@ -191,6 +194,17 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
v.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
model.comments = v;
}
WsMsg::CommentUpdated(comment) => {
let mut old = vec![];
std::mem::swap(&mut model.comments, &mut old);
for current in old.into_iter() {
if current.id != comment.id {
model.comments.push(current);
} else {
model.comments.push(comment.clone());
}
}
}
WsMsg::CommentDeleted(comment_id) => {
let mut old = vec![];
std::mem::swap(&mut model.comments, &mut old);
@ -225,7 +239,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
}
WsMsg::MessagesResponse(v) => {
WsMsg::MessagesLoaded(v) => {
model.messages = v.clone();
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
}
@ -239,6 +253,37 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
}
model.messages.sort_by(|a, b| a.id.cmp(&b.id));
}
// epics
WsMsg::EpicsLoaded(epics) => {
model.epics = epics.clone();
}
WsMsg::EpicCreated(epic) => {
model.epics.push(epic.clone());
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
}
WsMsg::EpicUpdated(epic) => {
let mut old = vec![];
std::mem::swap(&mut old, &mut model.epics);
for current in old {
if current.id != epic.id {
model.epics.push(current);
} else {
model.epics.push(epic.clone());
}
}
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
}
WsMsg::EpicDeleted(id) => {
let mut old = vec![];
std::mem::swap(&mut old, &mut model.epics);
for current in old {
if current.id != *id {
model.epics.push(current);
}
}
model.epics.sort_by(|a, b| a.id.cmp(&b.id));
}
_ => (),
};
orders.render();

59
jirs-data/src/fields.rs Normal file
View File

@ -0,0 +1,59 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum ProjectFieldId {
Name,
Url,
Description,
Category,
TimeTracking,
IssueStatusName,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum SignInFieldId {
Username,
Email,
Token,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum SignUpFieldId {
Username,
Email,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum UsersFieldId {
Username,
Email,
UserRole,
Avatar,
CurrentProject,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum InviteFieldId {
Token,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum CommentFieldId {
Body,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum IssueFieldId {
Type,
Title,
Description,
ListPosition,
Assignees,
Reporter,
Priority,
Estimate,
TimeSpent,
TimeRemaining,
IssueStatusId,
Epic,
}

View File

@ -7,9 +7,16 @@ use diesel::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use fields::*;
pub use msg::WsMsg;
pub use payloads::*;
#[cfg(feature = "backend")]
pub use sql::*;
mod fields;
mod msg;
mod payloads;
#[cfg(feature = "backend")]
pub mod sql;
@ -29,9 +36,12 @@ pub type InvitationId = i32;
pub type Position = i32;
pub type MessageId = i32;
pub type EpicId = i32;
pub type EmailString = String;
pub type UsernameString = String;
pub type TitleString = String;
pub type NameString = String;
pub type BindToken = Uuid;
pub type InvitationToken = Uuid;
@ -505,23 +515,6 @@ pub struct Token {
pub bind_token: Option<Uuid>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct UpdateIssuePayload {
pub title: String,
pub issue_type: IssueType,
pub priority: IssuePriority,
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub issue_status_id: IssueStatusId,
pub user_ids: Vec<UserId>,
}
#[cfg_attr(feature = "backend", derive(Queryable))]
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct IssueAssignee {
@ -532,26 +525,6 @@ pub struct IssueAssignee {
pub updated_at: NaiveDateTime,
}
impl From<Issue> for UpdateIssuePayload {
fn from(issue: Issue) -> Self {
Self {
title: issue.title,
issue_type: issue.issue_type,
priority: issue.priority,
list_position: issue.list_position,
description: issue.description,
description_text: issue.description_text,
estimate: issue.estimate,
time_spent: issue.time_spent,
time_remaining: issue.time_remaining,
project_id: issue.project_id,
reporter_id: issue.reporter_id,
user_ids: issue.user_ids,
issue_status_id: issue.issue_status_id,
}
}
}
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
#[cfg_attr(feature = "backend", sql_type = "MessageTypeType")]
#[derive(Clone, Copy, Deserialize, Serialize, Debug, PartialOrd, PartialEq, Hash)]
@ -610,222 +583,9 @@ pub struct Message {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Epic {
pub id: EpicId,
pub name: String,
pub name: NameString,
pub user_id: UserId,
pub project_id: ProjectId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateCommentPayload {
pub user_id: Option<UserId>,
pub issue_id: IssueId,
pub body: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct UpdateCommentPayload {
pub id: CommentId,
pub body: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct CreateIssuePayload {
pub title: String,
pub issue_type: IssueType,
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
pub reporter_id: UserId,
pub issue_status_id: IssueStatusId,
pub epic_id: Option<EpicId>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct UpdateProjectPayload {
pub id: ProjectId,
pub name: Option<String>,
pub url: Option<String>,
pub description: Option<String>,
pub category: Option<ProjectCategory>,
pub time_tracking: Option<TimeTracking>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PayloadVariant {
OptionI32(Option<i32>),
VecI32(Vec<i32>),
I32(i32),
String(String),
IssueType(IssueType),
IssuePriority(IssuePriority),
ProjectCategory(ProjectCategory),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum ProjectFieldId {
Name,
Url,
Description,
Category,
TimeTracking,
IssueStatusName,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum SignInFieldId {
Username,
Email,
Token,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum SignUpFieldId {
Username,
Email,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum UsersFieldId {
Username,
Email,
UserRole,
Avatar,
CurrentProject,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum InviteFieldId {
Token,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum CommentFieldId {
Body,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum IssueFieldId {
Type,
Title,
Description,
ListPosition,
Assignees,
Reporter,
Priority,
Estimate,
TimeSpent,
TimeRemaining,
IssueStatusId,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum WsMsg {
Ping,
Pong,
Die,
// auth
AuthorizeRequest(Uuid),
AuthorizeLoaded(Result<User, String>),
AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess,
BindTokenCheck(Uuid),
BindTokenBad,
BindTokenOk(Uuid),
// Sign up
SignUpRequest(EmailString, UsernameString),
SignUpSuccess,
SignUpPairTaken,
// invitations
InvitationListRequest,
InvitationListLoaded(Vec<Invitation>),
//
InvitedUsersRequest,
InvitedUsersLoaded(Vec<User>),
//
InvitationSendRequest {
name: UsernameString,
email: EmailString,
role: UserRole,
},
InvitationSendSuccess,
InvitationSendFailure,
//
InvitationRevokeRequest(InvitationId),
InvitationRevokeSuccess(InvitationId),
//
InvitationAcceptRequest(InvitationToken),
InvitationAcceptSuccess(BindToken),
InvitationAcceptFailure(InvitationToken),
//
InvitationRejectRequest(InvitationToken),
InvitationRejectSuccess,
InvitationRejectFailure(InvitationToken),
//
InvitedUserRemoveRequest(UserId),
InvitedUserRemoveSuccess(UserId),
// project page
ProjectsLoad,
ProjectsLoaded(Vec<Project>),
ProjectIssuesRequest,
ProjectIssuesLoaded(Vec<Issue>),
ProjectUsersRequest,
ProjectUsersLoaded(Vec<User>),
ProjectUpdateRequest(UpdateProjectPayload),
// issue
IssueUpdateRequest(IssueId, IssueFieldId, PayloadVariant),
IssueUpdated(Issue),
IssueDeleteRequest(IssueId),
IssueDeleted(IssueId),
IssueCreateRequest(CreateIssuePayload),
IssueCreated(Issue),
// issue status
IssueStatusesRequest,
IssueStatusesResponse(Vec<IssueStatus>),
IssueStatusUpdate(IssueStatusId, TitleString, Position),
IssueStatusUpdated(IssueStatus),
IssueStatusCreate(TitleString, Position),
IssueStatusCreated(IssueStatus),
IssueStatusDelete(IssueStatusId),
IssueStatusDeleted(IssueStatusId),
// comments
IssueCommentsRequest(IssueId),
IssueCommentsLoaded(Vec<Comment>),
CreateComment(CreateCommentPayload),
UpdateComment(UpdateCommentPayload),
CommentDeleteRequest(CommentId),
CommentDeleted(CommentId),
// users
AvatarUrlChanged(UserId, String),
ProfileUpdate(EmailString, UsernameString),
ProfileUpdated,
// user projects
UserProjectsLoad,
UserProjectsLoaded(Vec<UserProject>),
UserProjectSetCurrent(UserProjectId),
UserProjectCurrentChanged(UserProject),
// messages
Message(Message),
MessagesRequest,
MessagesResponse(Vec<Message>),
MessageMarkSeen(MessageId),
MessageMarkedSeen(MessageId),
}

127
jirs-data/src/msg.rs Normal file
View File

@ -0,0 +1,127 @@
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{
BindToken, Comment, CommentId, CreateCommentPayload, CreateIssuePayload, EmailString, Epic,
EpicId, Invitation, InvitationId, InvitationToken, Issue, IssueFieldId, IssueId, IssueStatus,
IssueStatusId, Message, MessageId, NameString, PayloadVariant, Position, Project, TitleString,
UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, UserProjectId, UserRole,
UsernameString,
};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum WsMsg {
Ping,
Pong,
Die,
// auth
AuthorizeLoad(Uuid),
AuthorizeLoaded(Result<User, String>),
AuthorizeExpired,
AuthenticateRequest(EmailString, UsernameString),
AuthenticateSuccess,
BindTokenCheck(Uuid),
BindTokenBad,
BindTokenOk(Uuid),
// Sign up
SignUpRequest(EmailString, UsernameString),
SignUpSuccess,
SignUpPairTaken,
// invitations
InvitationListLoad,
InvitationListLoaded(Vec<Invitation>),
//
InvitedUsersLoad,
InvitedUsersLoaded(Vec<User>),
//
InvitationSendRequest {
name: UsernameString,
email: EmailString,
role: UserRole,
},
InvitationSendSuccess,
InvitationSendFailure,
//
InvitationRevokeRequest(InvitationId),
InvitationRevokeSuccess(InvitationId),
//
InvitationAcceptRequest(InvitationToken),
InvitationAcceptSuccess(BindToken),
InvitationAcceptFailure(InvitationToken),
//
InvitationRejectRequest(InvitationToken),
InvitationRejectSuccess,
InvitationRejectFailure(InvitationToken),
//
InvitedUserRemoveRequest(UserId),
InvitedUserRemoveSuccess(UserId),
// project page
ProjectsLoad,
ProjectsLoaded(Vec<Project>),
ProjectIssuesLoad,
ProjectIssuesLoaded(Vec<Issue>),
ProjectUsersLoad,
ProjectUsersLoaded(Vec<User>),
ProjectUpdateLoad(UpdateProjectPayload),
// issue
IssueUpdate(IssueId, IssueFieldId, PayloadVariant),
IssueUpdated(Issue),
IssueDelete(IssueId),
IssueDeleted(IssueId),
IssueCreate(CreateIssuePayload),
IssueCreated(Issue),
// issue status
IssueStatusesLoad,
IssueStatusesLoaded(Vec<IssueStatus>),
IssueStatusUpdate(IssueStatusId, TitleString, Position),
IssueStatusUpdated(IssueStatus),
IssueStatusCreate(TitleString, Position),
IssueStatusCreated(IssueStatus),
IssueStatusDelete(IssueStatusId),
IssueStatusDeleted(IssueStatusId),
// comments
IssueCommentsLoad(IssueId),
IssueCommentsLoaded(Vec<Comment>),
CommentCreate(CreateCommentPayload),
CommentCreated(Comment),
CommentUpdate(UpdateCommentPayload),
CommentUpdated(Comment),
CommentDelete(CommentId),
CommentDeleted(CommentId),
// users
AvatarUrlChanged(UserId, String),
ProfileUpdate(EmailString, UsernameString),
ProfileUpdated,
// user projects
UserProjectsLoad,
UserProjectsLoaded(Vec<UserProject>),
UserProjectSetCurrent(UserProjectId),
UserProjectCurrentChanged(UserProject),
// messages
Message(Message),
MessagesLoad,
MessagesLoaded(Vec<Message>),
MessageMarkSeen(MessageId),
MessageMarkedSeen(MessageId),
// epics
EpicsLoad,
EpicsLoaded(Vec<Epic>),
EpicCreate(NameString),
EpicCreated(Epic),
EpicUpdate(EpicId, NameString),
EpicUpdated(Epic),
EpicDelete(EpicId),
EpicDeleted(EpicId),
}

94
jirs-data/src/payloads.rs Normal file
View File

@ -0,0 +1,94 @@
use serde::{Deserialize, Serialize};
use crate::{
CommentId, EpicId, Issue, IssueId, IssuePriority, IssueStatusId, IssueType, ProjectCategory,
ProjectId, TimeTracking, UserId,
};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateCommentPayload {
pub user_id: Option<UserId>,
pub issue_id: IssueId,
pub body: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct UpdateCommentPayload {
pub id: CommentId,
pub body: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct CreateIssuePayload {
pub title: String,
pub issue_type: IssueType,
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub user_ids: Vec<UserId>,
pub reporter_id: UserId,
pub issue_status_id: IssueStatusId,
pub epic_id: Option<EpicId>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct UpdateProjectPayload {
pub id: ProjectId,
pub name: Option<String>,
pub url: Option<String>,
pub description: Option<String>,
pub category: Option<ProjectCategory>,
pub time_tracking: Option<TimeTracking>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub enum PayloadVariant {
OptionI32(Option<i32>),
VecI32(Vec<i32>),
I32(i32),
String(String),
IssueType(IssueType),
IssuePriority(IssuePriority),
ProjectCategory(ProjectCategory),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
pub struct UpdateIssuePayload {
pub title: String,
pub issue_type: IssueType,
pub priority: IssuePriority,
pub list_position: i32,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: ProjectId,
pub reporter_id: UserId,
pub issue_status_id: IssueStatusId,
pub user_ids: Vec<UserId>,
}
impl From<Issue> for UpdateIssuePayload {
fn from(issue: Issue) -> Self {
Self {
title: issue.title,
issue_type: issue.issue_type,
priority: issue.priority,
list_position: issue.list_position,
description: issue.description,
description_text: issue.description_text,
estimate: issue.estimate,
time_spent: issue.time_spent,
time_remaining: issue.time_remaining,
project_id: issue.project_id,
reporter_id: issue.reporter_id,
user_ids: issue.user_ids,
issue_status_id: issue.issue_status_id,
}
}
}

136
jirs-server/src/db/epics.rs Normal file
View File

@ -0,0 +1,136 @@
use actix::{Handler, Message};
use diesel::pg::Pg;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use jirs_data::Epic;
use crate::db::DbExecutor;
use crate::errors::ServiceErrors;
#[derive(Serialize, Deserialize)]
pub struct LoadEpics {
pub project_id: i32,
}
impl Message for LoadEpics {
type Result = Result<Vec<Epic>, ServiceErrors>;
}
impl Handler<LoadEpics> for DbExecutor {
type Result = Result<Vec<Epic>, ServiceErrors>;
fn handle(&mut self, msg: LoadEpics, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let epics_query = epics.distinct_on(id).filter(project_id.eq(msg.project_id));
debug!("{}", diesel::debug_query::<Pg, _>(&epics_query));
epics_query
.load(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))
}
}
#[derive(Serialize, Deserialize)]
pub struct CreateEpic {
pub user_id: i32,
pub project_id: i32,
pub name: String,
}
impl Message for CreateEpic {
type Result = Result<Epic, ServiceErrors>;
}
impl Handler<CreateEpic> for DbExecutor {
type Result = Result<Epic, ServiceErrors>;
fn handle(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let epic_query = diesel::insert_into(epics).values((
name.eq(msg.name.as_str()),
user_id.eq(msg.user_id),
project_id.eq(msg.project_id),
));
debug!("{}", diesel::debug_query::<Pg, _>(&epic_query));
epic_query
.get_result::<Epic>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))
}
}
#[derive(Serialize, Deserialize)]
pub struct UpdateEpic {
pub epic_id: i32,
pub project_id: i32,
pub name: String,
}
impl Message for UpdateEpic {
type Result = Result<Epic, ServiceErrors>;
}
impl Handler<UpdateEpic> for DbExecutor {
type Result = Result<Epic, ServiceErrors>;
fn handle(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let query = diesel::update(
epics
.filter(project_id.eq(msg.project_id))
.find(msg.epic_id),
)
.set(name.eq(msg.name));
info!("{}", diesel::debug_query::<Pg, _>(&query));
let row: Epic = query
.get_result::<Epic>(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))?;
Ok(row)
}
}
#[derive(Serialize, Deserialize)]
pub struct DeleteEpic {
pub epic_id: i32,
pub user_id: i32,
}
impl Message for DeleteEpic {
type Result = Result<(), ServiceErrors>;
}
impl Handler<DeleteEpic> for DbExecutor {
type Result = Result<(), ServiceErrors>;
fn handle(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> Self::Result {
use crate::schema::epics::dsl::*;
let conn = &self
.pool
.get()
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
let comment_query = diesel::delete(epics.filter(user_id.eq(msg.user_id)).find(msg.epic_id));
debug!("{}", diesel::debug_query::<Pg, _>(&comment_query));
comment_query
.execute(conn)
.map_err(|_| ServiceErrors::RecordNotFound("epics".to_string()))?;
Ok(())
}
}

View File

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
pub mod authorize_user;
pub mod comments;
pub mod epics;
pub mod invitations;
pub mod issue_assignees;
pub mod issue_statuses;

View File

@ -59,10 +59,9 @@ impl WsHandler<CreateCommentPayload> for WebSocketActor {
}
impl WsHandler<UpdateCommentPayload> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateCommentPayload, ctx: &mut Self::Context) -> WsResult {
fn handle_msg(&mut self, msg: UpdateCommentPayload, _ctx: &mut Self::Context) -> WsResult {
use crate::db::comments::UpdateComment;
info!("{:?}", msg);
let user_id = self.require_user()?.id;
let UpdateCommentPayload {
@ -70,12 +69,12 @@ impl WsHandler<UpdateCommentPayload> for WebSocketActor {
body,
} = msg;
let issue_id = match block_on(self.db.send(UpdateComment {
let comment = match block_on(self.db.send(UpdateComment {
comment_id,
user_id,
body,
})) {
Ok(Ok(comment)) => comment.issue_id,
Ok(Ok(comment)) => comment,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
@ -85,9 +84,7 @@ impl WsHandler<UpdateCommentPayload> for WebSocketActor {
return Ok(None);
}
};
if let Some(v) = self.handle_msg(LoadIssueComments { issue_id }, ctx)? {
self.broadcast(&v);
}
self.broadcast(&WsMsg::CommentUpdated(comment));
Ok(None)
}
}

View File

@ -0,0 +1,79 @@
use futures::executor::block_on;
use jirs_data::{EpicId, NameString, UserProject, WsMsg};
use crate::ws::{WebSocketActor, WsHandler, WsResult};
pub struct LoadEpics;
impl WsHandler<LoadEpics> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadEpics, _ctx: &mut Self::Context) -> WsResult {
let project_id = self.require_user_project()?.project_id;
let epics = query_db_or_print!(self, crate::db::epics::LoadEpics { project_id });
Ok(Some(WsMsg::EpicsLoaded(epics)))
}
}
pub struct CreateEpic {
pub name: NameString,
}
impl WsHandler<CreateEpic> for WebSocketActor {
fn handle_msg(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> WsResult {
let CreateEpic { name } = msg;
let UserProject {
user_id,
project_id,
..
} = self.require_user_project()?;
let epic = query_db_or_print!(
self,
crate::db::epics::CreateEpic {
user_id: *user_id,
project_id: *project_id,
name,
}
);
Ok(Some(WsMsg::EpicCreated(epic)))
}
}
pub struct UpdateEpic {
pub epic_id: EpicId,
pub name: NameString,
}
impl WsHandler<UpdateEpic> for WebSocketActor {
fn handle_msg(&mut self, msg: UpdateEpic, _ctx: &mut Self::Context) -> WsResult {
let UpdateEpic { epic_id, name } = msg;
let UserProject { project_id, .. } = self.require_user_project()?;
let epic = query_db_or_print!(
self,
crate::db::epics::UpdateEpic {
project_id: *project_id,
epic_id: epic_id,
name: name.clone(),
}
);
Ok(Some(WsMsg::EpicUpdated(epic)))
}
}
pub struct DeleteEpic {
pub epic_id: EpicId,
}
impl WsHandler<DeleteEpic> for WebSocketActor {
fn handle_msg(&mut self, msg: DeleteEpic, _ctx: &mut Self::Context) -> WsResult {
let DeleteEpic { epic_id } = msg;
let UserProject { user_id, .. } = self.require_user_project()?;
query_db_or_print!(
self,
crate::db::epics::DeleteEpic {
user_id: *user_id,
epic_id: epic_id,
}
);
Ok(Some(WsMsg::EpicDeleted(epic_id)))
}
}

View File

@ -15,7 +15,7 @@ impl WsHandler<LoadIssueStatuses> for WebSocketActor {
self.db
.send(issue_statuses::LoadIssueStatuses { project_id }),
) {
Ok(Ok(v)) => Some(WsMsg::IssueStatusesResponse(v)),
Ok(Ok(v)) => Some(WsMsg::IssueStatusesLoaded(v)),
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);

View File

@ -11,7 +11,7 @@ impl WsHandler<LoadMessages> for WebSocketActor {
fn handle_msg(&mut self, _msg: LoadMessages, _ctx: &mut Self::Context) -> WsResult {
let user_id = self.require_user()?.id;
match block_on(self.db.send(messages::LoadMessages { user_id })) {
Ok(Ok(v)) => Ok(Some(WsMsg::MessagesResponse(v))),
Ok(Ok(v)) => Ok(Some(WsMsg::MessagesLoaded(v))),
Ok(Err(e)) => {
error!("{:?}", e);
Ok(None)

View File

@ -3,29 +3,49 @@ use std::collections::HashMap;
use actix::{
Actor, ActorContext, Addr, AsyncContext, Context, Handler, Message, Recipient, StreamHandler,
};
use actix_web::web::Data;
use actix_web::{get, web, Error, HttpRequest, HttpResponse};
use actix_web::{
get,
web::{self, Data},
Error, HttpRequest, HttpResponse,
};
use actix_web_actors::ws;
use futures::executor::block_on;
use jirs_data::{Project, ProjectId, User, UserId, UserProject, WsMsg};
use crate::db::projects::LoadCurrentProject;
use crate::db::user_projects::CurrentUserProject;
use crate::db::DbExecutor;
use crate::db::{projects::LoadCurrentProject, user_projects::CurrentUserProject, DbExecutor};
use crate::mail::MailExecutor;
use crate::ws::auth::*;
use crate::ws::comments::*;
use crate::ws::invitations::*;
use crate::ws::issue_statuses::*;
use crate::ws::issues::*;
use crate::ws::messages::*;
use crate::ws::projects::*;
use crate::ws::user_projects::{LoadUserProjects, SetCurrentUserProject};
use crate::ws::users::*;
use crate::ws::{
auth::*,
comments::*,
invitations::*,
issue_statuses::*,
issues::*,
messages::*,
projects::*,
user_projects::{LoadUserProjects, SetCurrentUserProject},
users::*,
};
macro_rules! query_db_or_print {
($s:expr,$msg:expr) => {
match block_on($s.db.send($msg)) {
Ok(Ok(r)) => r,
Ok(Err(e)) => {
error!("{:?}", e);
return Ok(None);
}
Err(e) => {
error!("{}", e);
return Ok(None);
}
}
};
}
pub mod auth;
pub mod comments;
pub mod epics;
pub mod invitations;
pub mod issue_statuses;
pub mod issues;
@ -93,7 +113,7 @@ impl WebSocketActor {
WsMsg::Pong => Some(WsMsg::Ping),
// issues
WsMsg::IssueUpdateRequest(id, field_id, payload) => self.handle_msg(
WsMsg::IssueUpdate(id, field_id, payload) => self.handle_msg(
UpdateIssueHandler {
id,
field_id,
@ -101,12 +121,12 @@ impl WebSocketActor {
},
ctx,
)?,
WsMsg::IssueCreateRequest(payload) => self.handle_msg(payload, ctx)?,
WsMsg::IssueDeleteRequest(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
WsMsg::ProjectIssuesRequest => self.handle_msg(LoadIssues, ctx)?,
WsMsg::IssueCreate(payload) => self.handle_msg(payload, ctx)?,
WsMsg::IssueDelete(id) => self.handle_msg(DeleteIssue { id }, ctx)?,
WsMsg::ProjectIssuesLoad => self.handle_msg(LoadIssues, ctx)?,
// issue statuses
WsMsg::IssueStatusesRequest => self.handle_msg(LoadIssueStatuses, ctx)?,
WsMsg::IssueStatusesLoad => self.handle_msg(LoadIssueStatuses, ctx)?,
WsMsg::IssueStatusDelete(issue_status_id) => {
self.handle_msg(DeleteIssueStatus { issue_status_id }, ctx)?
}
@ -124,7 +144,7 @@ impl WebSocketActor {
// projects
WsMsg::ProjectsLoad => self.handle_msg(LoadProjects, ctx)?,
WsMsg::ProjectUpdateRequest(payload) => self.handle_msg(payload, ctx)?,
WsMsg::ProjectUpdateLoad(payload) => self.handle_msg(payload, ctx)?,
// user projects
WsMsg::UserProjectsLoad => self.handle_msg(LoadUserProjects, ctx)?,
@ -136,9 +156,7 @@ impl WebSocketActor {
)?,
// auth
WsMsg::AuthorizeRequest(uuid) => {
self.handle_msg(CheckAuthToken { token: uuid }, ctx)?
}
WsMsg::AuthorizeLoad(uuid) => self.handle_msg(CheckAuthToken { token: uuid }, ctx)?,
WsMsg::BindTokenCheck(uuid) => {
self.handle_msg(CheckBindToken { bind_token: uuid }, ctx)?
}
@ -156,18 +174,18 @@ impl WebSocketActor {
)?,
// users
WsMsg::ProjectUsersRequest => self.handle_msg(LoadProjectUsers, ctx)?,
WsMsg::ProjectUsersLoad => self.handle_msg(LoadProjectUsers, ctx)?,
WsMsg::InvitedUserRemoveRequest(user_id) => {
self.handle_msg(RemoveInvitedUser { user_id }, ctx)?
}
// comments
WsMsg::IssueCommentsRequest(issue_id) => {
WsMsg::IssueCommentsLoad(issue_id) => {
self.handle_msg(LoadIssueComments { issue_id }, ctx)?
}
WsMsg::CreateComment(payload) => self.handle_msg(payload, ctx)?,
WsMsg::UpdateComment(payload) => self.handle_msg(payload, ctx)?,
WsMsg::CommentDeleteRequest(comment_id) => {
WsMsg::CommentCreate(payload) => self.handle_msg(payload, ctx)?,
WsMsg::CommentUpdate(payload) => self.handle_msg(payload, ctx)?,
WsMsg::CommentDelete(comment_id) => {
self.handle_msg(DeleteComment { comment_id }, ctx)?
}
@ -175,12 +193,12 @@ impl WebSocketActor {
WsMsg::InvitationSendRequest { name, email, role } => {
self.handle_msg(CreateInvitation { name, email, role }, ctx)?
}
WsMsg::InvitationListRequest => self.handle_msg(ListInvitation, ctx)?,
WsMsg::InvitationListLoad => self.handle_msg(ListInvitation, ctx)?,
WsMsg::InvitationAcceptRequest(invitation_token) => {
self.handle_msg(AcceptInvitation { invitation_token }, ctx)?
}
WsMsg::InvitationRevokeRequest(id) => self.handle_msg(RevokeInvitation { id }, ctx)?,
WsMsg::InvitedUsersRequest => self.handle_msg(LoadInvitedUsers, ctx)?,
WsMsg::InvitedUsersLoad => self.handle_msg(LoadInvitedUsers, ctx)?,
// users
WsMsg::ProfileUpdate(email, name) => {
@ -188,9 +206,17 @@ impl WebSocketActor {
}
// messages
WsMsg::MessagesRequest => self.handle_msg(LoadMessages, ctx)?,
WsMsg::MessagesLoad => self.handle_msg(LoadMessages, ctx)?,
WsMsg::MessageMarkSeen(id) => self.handle_msg(MarkMessageSeen { id }, ctx)?,
// epics
WsMsg::EpicsLoad => self.handle_msg(epics::LoadEpics, ctx)?,
WsMsg::EpicCreate(name) => self.handle_msg(epics::CreateEpic { name }, ctx)?,
WsMsg::EpicUpdate(epic_id, name) => {
self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)?
}
WsMsg::EpicDelete(epic_id) => self.handle_msg(epics::DeleteEpic { epic_id }, ctx)?,
// else fail
_ => {
error!("No handle for {:?} specified", msg);