Start display messages

This commit is contained in:
Adrian Woźniak 2020-05-20 21:44:58 +02:00
parent 92e129115a
commit 2d55c5f143
19 changed files with 373 additions and 108 deletions

View File

@ -1,8 +1,9 @@
use crate::FieldId;
use jirs_data::{IssueId, IssueStatusId};
use seed::prelude::WebSocketMessage;
use jirs_data::{IssueId, IssueStatusId, WsMsg};
use crate::shared::styled_editor::Mode as TabMode;
use seed::prelude::WebSocketMessage;
use crate::FieldId;
#[derive(Clone, Debug, PartialEq)]
pub enum FieldChange {
@ -59,10 +60,11 @@ pub enum PageChanged {
#[derive(Debug)]
pub enum WebSocketChanged {
WsMsg(jirs_data::WsMsg),
WsMsg(WsMsg),
WebSocketMessage(WebSocketMessage),
WebSocketMessageLoaded(Vec<u8>),
WebSocketOpened,
WebSocketClosed,
SendPing,
Bounced(WsMsg),
}

View File

@ -7,8 +7,9 @@ use jirs_data::*;
use crate::model::{ModalType, Model, Page};
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{go_to_board, go_to_login};
use crate::ws::{open_socket, read_incoming, send_ws_msg};
use crate::shared::styled_tooltip::Variant as StyledTooltip;
use crate::shared::{go_to_board, go_to_login, styled_tooltip};
use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
mod changes;
mod fields;
@ -40,7 +41,7 @@ pub enum Msg {
StyledSelectChanged(FieldId, StyledSelectChange),
InternalFailure(String),
ToggleAboutTooltip,
ToggleTooltip(StyledTooltip),
// Auth Token
AuthTokenStored,
@ -100,12 +101,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
Msg::WebSocketChange(change) => {
match change {
WebSocketChanged::WebSocketOpened => {
authorize_or_redirect(model);
send_ws_msg(WsMsg::Ping, model.ws.as_ref());
flush_queue(model, orders);
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
authorize_or_redirect(model, orders);
return;
}
WebSocketChanged::SendPing => {
send_ws_msg(WsMsg::Ping, model.ws.as_ref());
send_ws_msg(WsMsg::Ping, model.ws.as_ref(), orders);
return;
}
WebSocketChanged::WebSocketMessage(incoming) => {
@ -135,6 +137,11 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
WebSocketChanged::WebSocketClosed => {
open_socket(model, orders);
}
WebSocketChanged::Bounced(ws_msg) => {
model.ws_queue.push(ws_msg);
open_socket(model, orders);
return;
}
};
Msg::WebSocketChange(change)
}
@ -147,23 +154,24 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match &msg {
Msg::AuthTokenStored => {
go_to_board();
orders.skip().send_msg(Msg::ChangePage(Page::Project));
authorize_or_redirect(model);
go_to_board(orders);
return;
}
Msg::AuthTokenErased => {
go_to_login();
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
authorize_or_redirect(model);
go_to_login(orders);
return;
}
Msg::ChangePage(page) => {
model.page = *page;
}
Msg::ToggleAboutTooltip => {
model.about_tooltip_visible = !model.about_tooltip_visible;
}
Msg::ToggleTooltip(variant) => match variant {
styled_tooltip::Variant::About => {
model.about_tooltip_visible = !model.about_tooltip_visible;
}
styled_tooltip::Variant::Messages => {
model.messages_tooltip_visible = !model.messages_tooltip_visible;
}
},
_ => (),
}
crate::modal::update(&msg, model, orders);
@ -250,7 +258,6 @@ fn after_mount(url: Url, orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
WS_URL = "".to_string();
}
model.page = resolve_page(url).unwrap_or_else(|| Page::Project);
log!(model);
open_socket(&mut model, orders);
AfterMount::new(model).url_handling(UrlHandling::PassToRoutes)
}
@ -279,17 +286,17 @@ fn window_events(_model: &Model) -> Vec<EventHandler<Msg>> {
}
#[inline]
fn authorize_or_redirect(model: &Model) {
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());
send_ws_msg(WsMsg::AuthorizeRequest(token), model.ws.as_ref(), orders);
}
Err(..) => {
let pathname = seed::document().location().unwrap().pathname().unwrap();
match pathname.as_str() {
"/login" | "/register" | "/invite" => {}
_ => {
go_to_login();
go_to_login(orders);
}
};
}

View File

@ -53,6 +53,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
send_ws_msg(
jirs_data::WsMsg::IssueCreateRequest(payload),
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {

View File

@ -22,6 +22,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
crate::ws::send_ws_msg(
WsMsg::IssueStatusDelete(*issue_status_id),
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDeleted(_))) => {

View File

@ -50,6 +50,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::IssueType(modal.payload.issue_type),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -64,6 +65,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::I32(modal.payload.issue_status_id),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -78,6 +80,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::I32(modal.payload.reporter_id),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -92,6 +95,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -113,6 +117,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::VecI32(modal.payload.user_ids.clone()),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -127,6 +132,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::IssuePriority(modal.payload.priority),
),
model.ws.as_ref(),
orders,
);
}
Msg::StrInputChanged(
@ -141,6 +147,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::String(modal.payload.title.clone()),
),
model.ws.as_ref(),
orders,
);
}
Msg::StrInputChanged(
@ -163,6 +170,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
),
),
model.ws.as_ref(),
orders,
);
}
// TimeSpent
@ -178,6 +186,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.time_spent),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -192,6 +201,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.time_spent),
),
model.ws.as_ref(),
orders,
);
}
// Time Remaining
@ -207,6 +217,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.time_remaining),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -222,6 +233,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.time_remaining),
),
model.ws.as_ref(),
orders,
);
}
// Estimate
@ -237,6 +249,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.estimate),
),
model.ws.as_ref(),
orders,
);
}
Msg::StyledSelectChanged(
@ -251,6 +264,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
PayloadVariant::OptionI32(modal.payload.estimate),
),
model.ws.as_ref(),
orders,
);
}
Msg::ModalChanged(FieldChange::TabChanged(
@ -290,7 +304,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
issue_id: modal.id,
}),
};
send_ws_msg(msg, model.ws.as_ref());
send_ws_msg(msg, model.ws.as_ref(), orders);
orders
.skip()
.send_msg(Msg::ModalChanged(FieldChange::ToggleCommentForm(
@ -314,7 +328,11 @@ 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());
send_ws_msg(
WsMsg::CommentDeleteRequest(*comment_id),
model.ws.as_ref(),
orders,
);
orders.skip().send_msg(Msg::ModalDropped);
}

View File

@ -19,8 +19,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
match msg {
Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
go_to_board();
orders.send_msg(Msg::ChangePage(Page::Project));
go_to_board(orders);
}
_ => (),
},
@ -40,14 +39,14 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::ProjectIssuesLoaded(_issues))) => {
match model.page {
Page::EditIssue(issue_id) if model.modals.is_empty() => {
push_edit_modal(issue_id, model)
push_edit_modal(issue_id, model, orders)
}
_ => (),
}
}
Msg::ChangePage(Page::EditIssue(issue_id)) => {
push_edit_modal(*issue_id, model);
push_edit_modal(*issue_id, model, orders);
}
Msg::ChangePage(Page::AddIssue) => {
@ -102,7 +101,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
section![id!["modals"], modals]
}
fn push_edit_modal(issue_id: i32, model: &mut Model) {
fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Msg>) {
let time_tracking_type = model
.project
.as_ref()
@ -118,6 +117,10 @@ fn push_edit_modal(issue_id: i32, model: &mut Model) {
Box::new(EditIssueModal::new(issue, time_tracking_type)),
)
};
send_ws_msg(WsMsg::IssueCommentsRequest(issue_id), model.ws.as_ref());
send_ws_msg(
WsMsg::IssueCommentsRequest(issue_id),
model.ws.as_ref(),
orders,
);
model.modals.push(modal);
}

View File

@ -434,10 +434,12 @@ pub enum PageContent {
#[derive(Debug)]
pub struct Model {
pub ws: Option<WebSocket>,
pub ws_queue: Vec<WsMsg>,
pub host_url: String,
pub ws_url: String,
pub access_token: Option<Uuid>,
pub about_tooltip_visible: bool,
pub messages_tooltip_visible: bool,
// mapped
pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>,
@ -467,6 +469,7 @@ impl Model {
pub fn new(host_url: String, ws_url: String) -> Self {
Self {
ws: None,
ws_queue: vec![],
access_token: None,
user: None,
issue_form: None,
@ -483,6 +486,7 @@ impl Model {
project: None,
comments: vec![],
about_tooltip_visible: false,
messages_tooltip_visible: false,
issue_statuses: vec![],
messages: vec![Message {
id: 0,

View File

@ -14,15 +14,14 @@ use crate::ws::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>) {
let user = match model.user {
Some(ref user) => user,
_ => return,
};
match msg {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)))
| Msg::ChangePage(Page::Profile) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref(), orders);
let user = match model.user {
Some(ref user) => user,
_ => return,
};
model.page_content = PageContent::Profile(Box::new(ProfilePage::new(user)));
}
_ => (),
@ -71,6 +70,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
profile_page.name.value.clone(),
),
model.ws.as_ref(),
orders,
);
}
_ => (),

View File

@ -10,7 +10,7 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{inner_layout, ToNode};
use crate::ws::send_ws_msg;
use crate::ws::{enqueue_ws_msg, 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>) {
@ -37,10 +37,16 @@ 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(..)) => {
send_ws_msg(jirs_data::WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::ProjectIssuesRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::ProjectUsersRequest, model.ws.as_ref());
send_ws_msg(jirs_data::WsMsg::IssueStatusesRequest, model.ws.as_ref());
enqueue_ws_msg(
vec![
jirs_data::WsMsg::ProjectRequest,
jirs_data::WsMsg::ProjectIssuesRequest,
jirs_data::WsMsg::ProjectUsersRequest,
jirs_data::WsMsg::IssueStatusesRequest,
],
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => {
let mut old: Vec<Issue> = vec![];
@ -108,7 +114,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
crate::ws::issue::drag_started(issue_id, model)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStopped(_))) => {
crate::ws::issue::sync(model);
crate::ws::issue::sync(model, orders);
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::ExchangePosition(
issue_bellow_id,
@ -117,7 +123,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
crate::ws::issue::change_status(status, model)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDropZone(_status))) => {
crate::ws::issue::sync(model)
crate::ws::issue::sync(model, orders)
}
Msg::PageChanged(PageChanged::Board(BoardPageChange::DragLeave(_id))) => {
project_page.issue_drag.clear_last();
@ -126,6 +132,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
send_ws_msg(
jirs_data::WsMsg::IssueDeleteRequest(issue_id),
model.ws.as_ref(),
orders,
);
}
_ => (),

View File

@ -1,3 +1,5 @@
use std::collections::HashSet;
use seed::{prelude::*, *};
use wasm_bindgen::__rt::std::collections::HashMap;
@ -18,7 +20,7 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::{inner_layout, ToChild, ToNode};
use crate::ws::send_ws_msg;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::FieldChange::TabChanged;
use crate::{
model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, WebSocketChanged,
@ -389,9 +391,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg {
Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
enqueue_ws_msg(
vec![
WsMsg::ProjectRequest,
WsMsg::IssueStatusesRequest,
WsMsg::ProjectIssuesRequest,
],
model.ws.as_ref(),
orders,
);
}
WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model);
@ -409,9 +417,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model);
if model.user.is_some() {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
enqueue_ws_msg(
vec![
WsMsg::ProjectRequest,
WsMsg::IssueStatusesRequest,
WsMsg::ProjectIssuesRequest,
],
model.ws.as_ref(),
orders,
);
}
}
_ => (),
@ -465,6 +479,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
time_tracking: Some(page.time_tracking.value.into()),
}),
model.ws.as_ref(),
orders,
);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
@ -475,7 +490,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
_issue_status_id,
))) => {
sync(model);
sync(model, orders);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(
_issue_status_id,
@ -486,7 +501,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id,
))) => {
sync(model);
sync(model, orders);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(
id,
@ -503,6 +518,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
send_ws_msg(
WsMsg::IssueStatusUpdate(id, name.to_string(), pos),
model.ws.as_ref(),
orders,
);
}
}
@ -525,7 +541,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
let name = page.name.value.clone();
let position = model.issue_statuses.len();
let ws_msg = WsMsg::IssueStatusCreate(name, position as i32);
send_ws_msg(ws_msg, model.ws.as_ref());
send_ws_msg(ws_msg, model.ws.as_ref(), orders);
}
_ => (),
}
@ -581,20 +597,25 @@ fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
page.column_drag.last_id = Some(bellow_id);
}
fn sync(model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
let dirty = match &mut model.page_content {
PageContent::ProjectSettings(page) => {
let mut old = HashSet::new();
std::mem::swap(&mut old, &mut page.column_drag.dirty);
old
}
_ => return error!("bad content type"),
};
for id in page.column_drag.dirty.iter() {
for id in dirty {
let IssueStatus { name, position, .. } =
match model.issue_statuses.iter().find(|is| is.id == *id) {
match model.issue_statuses.iter().find(|is| is.id == id) {
Some(is) => is,
_ => continue,
};
send_ws_msg(
WsMsg::IssueStatusUpdate(*id, name.clone(), *position),
WsMsg::IssueStatusUpdate(id, name.clone(), *position),
model.ws.as_ref(),
orders,
);
}
}

View File

@ -1,9 +1,11 @@
use seed::{prelude::*, *};
use std::str::FromStr;
use seed::{prelude::*, *};
use jirs_data::*;
use crate::model::Model;
use crate::model::Page;
use crate::Msg;
pub mod aside;
@ -33,12 +35,14 @@ pub trait ToChild {
fn to_child(&self) -> Self::Builder;
}
pub fn go_to_board() {
pub fn go_to_board(orders: &mut impl Orders<Msg>) {
go_to("/board");
orders.skip().send_msg(Msg::ChangePage(Page::Project));
}
pub fn go_to_login() {
pub fn go_to_login(orders: &mut impl Orders<Msg>) {
go_to("/login");
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
}
pub fn go_to(url: &str) {

View File

@ -1,5 +1,7 @@
use seed::{prelude::*, *};
use jirs_data::Message;
use crate::model::Model;
use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton;
@ -42,49 +44,102 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
let messages = if model.messages.is_empty() {
empty![]
} else {
navbar_left_item("Messages", Icon::Message, None)
navbar_left_item(
"Messages",
Icon::Message,
None,
Some(mouse_ev(Ev::Click, |ev| {
ev.prevent_default();
Msg::ToggleTooltip(styled_tooltip::Variant::Messages)
})),
)
};
vec![
about_tooltip_popup(model),
messages_tooltip_popup(model),
aside![
id!["navbar-left"],
a![
attrs![At::Class => "logoLink", At::Href => "/"],
div![attrs![At::Class => "styledLogo"], logo_svg]
class!["logoLink"],
attrs![At::Href => "/"],
div![class!["styledLogo"], logo_svg]
],
navbar_left_item("Search issues", Icon::Search, None),
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue")),
navbar_left_item("Search issues", Icon::Search, None, None),
navbar_left_item("Create Issue", Icon::Plus, Some("/add-issue"), None),
div![
class!["bottom"],
navbar_left_item("Profile", user_icon, Some("/profile")),
navbar_left_item("Profile", user_icon, Some("/profile"), None),
messages,
about_tooltip(model, navbar_left_item("About", Icon::Help, None)),
about_tooltip(model, navbar_left_item("About", Icon::Help, None, None)),
],
],
]
}
fn navbar_left_item<I>(text: &str, icon: I, href: Option<&str>) -> Node<Msg>
fn navbar_left_item<I>(
text: &str,
icon: I,
href: Option<&str>,
on_click: Option<EventHandler<Msg>>,
) -> Node<Msg>
where
I: IntoNavItemIcon,
{
let styled_icon = icon.into_nav_item_icon();
let href = href.unwrap_or_else(|| "#");
a![
class!["item"],
attrs![At::Href => href],
styled_icon,
span![class!["itemText"], text]
span![class!["itemText"], text],
on_click,
]
}
pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> {
div![
attrs![At::Class => "aboutTooltip"],
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
children
]
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::About))
});
div![class!["aboutTooltip"], on_click, children]
}
fn messages_tooltip_popup(model: &Model) -> Node<Msg> {
let on_click: EventHandler<Msg> = ev(Ev::Click, move |_| {
Some(Msg::ToggleTooltip(styled_tooltip::Variant::Messages))
});
let messages: Vec<Node<Msg>> = model
.messages
.iter()
.map(|message| {
let Message {
id: _,
receiver_id: _,
sender_id: _,
summary,
description,
message_type,
hyper_link,
created_at: _,
updated_at: _,
} = message;
div![
class!["message"],
class![message_type.as_str()],
div![class!["summary"], summary],
div![class!["description"], description],
div![class!["hyperlink"], hyper_link],
]
})
.collect();
let body = div![on_click, class!["messagesList"], messages];
styled_tooltip::StyledTooltip::build()
.visible(model.messages_tooltip_visible)
.messages_tooltip()
.add_child(body)
.build()
.into_node()
}
fn about_tooltip_popup(model: &Model) -> Node<Msg> {
@ -100,23 +155,23 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
.build()
.into_node();
styled_tooltip::StyledTooltip {
visible: model.about_tooltip_visible,
class_name: "aboutTooltipPopup".to_string(),
children: div![
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
attrs![At::Class => "feedbackDropdown"],
let on_click = mouse_ev(Ev::Click, |_| {
Msg::ToggleTooltip(styled_tooltip::Variant::About)
});
let body = div![
on_click,
class!["feedbackDropdown"],
div![
attrs![At::Class => "feedbackImageCont"],
class!["feedbackImageCont"],
img![attrs![At::Src => "/feedback.png"]],
class!["feedbackImage"],
],
div![
attrs![At::Class => "feedbackParagraph"],
class!["feedbackParagraph"],
"This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end."
],
div![
attrs![At::Class => "feedbackParagraph"],
class!["feedbackParagraph"],
"Read more on my website or reach out via ",
a![
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
@ -140,6 +195,13 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
],
github_repo
]
],
}.into_node()
];
styled_tooltip::StyledTooltip::build()
.visible(model.about_tooltip_visible)
.about_tooltip()
.add_class("aboutTooltipPopup")
.add_child(body)
.build()
.into_node()
}

View File

@ -3,10 +3,32 @@ use seed::{prelude::*, *};
use crate::shared::ToNode;
use crate::Msg;
#[derive(Debug, Copy, Clone)]
pub enum Variant {
About,
Messages,
}
impl Default for Variant {
fn default() -> Self {
Variant::Messages
}
}
impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Variant::About => f.write_str("about"),
Variant::Messages => f.write_str("messages"),
}
}
}
pub struct StyledTooltip {
pub visible: bool,
pub class_name: String,
pub children: Node<Msg>,
visible: bool,
class_name: String,
children: Vec<Node<Msg>>,
variant: Variant,
}
impl ToNode for StyledTooltip {
@ -15,15 +37,69 @@ impl ToNode for StyledTooltip {
}
}
impl StyledTooltip {
pub fn build() -> StyledTooltipBuilder {
StyledTooltipBuilder::default()
}
}
#[derive(Default)]
pub struct StyledTooltipBuilder {
visible: bool,
class_list: Vec<String>,
children: Vec<Node<Msg>>,
variant: Variant,
}
impl StyledTooltipBuilder {
pub fn visible(mut self, b: bool) -> Self {
self.visible = b;
self
}
pub fn add_class<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.class_list.push(name.into());
self
}
pub fn add_child(mut self, child: Node<Msg>) -> Self {
self.children.push(child);
self
}
pub fn about_tooltip(mut self) -> Self {
self.variant = Variant::About;
self
}
pub fn messages_tooltip(mut self) -> Self {
self.variant = Variant::Messages;
self
}
pub fn build(self) -> StyledTooltip {
StyledTooltip {
visible: self.visible,
class_name: self.class_list.join(" "),
children: self.children,
variant: self.variant,
}
}
}
pub fn render(values: StyledTooltip) -> Node<Msg> {
let StyledTooltip {
visible,
class_name,
children,
variant,
} = values;
if visible {
div![
attrs![At::Class => format!("styledTooltip {}", class_name)],
attrs![At::Class => format!("styledTooltip {} {}", class_name, variant)],
children
]
} else {

View File

@ -52,6 +52,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
send_ws_msg(
WsMsg::AuthenticateRequest(page.email.clone(), page.username.clone()),
model.ws.as_ref(),
orders,
);
}
Msg::BindClientRequest => {
@ -62,7 +63,7 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
return;
}
};
send_ws_msg(WsMsg::BindTokenCheck(bind_token), model.ws.as_ref());
send_ws_msg(WsMsg::BindTokenCheck(bind_token), model.ws.as_ref(), orders);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthenticateSuccess) => {

View File

@ -14,7 +14,7 @@ use crate::validations::is_email;
use crate::ws::send_ws_msg;
use crate::{model, FieldId, Msg, WebSocketChanged};
pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>) {
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::SignUp {
return;
}
@ -45,6 +45,7 @@ pub fn update(msg: Msg, model: &mut model::Model, _orders: &mut impl Orders<Msg>
send_ws_msg(
WsMsg::SignUpRequest(page.email.clone(), page.username.clone()),
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(change) => match change {

View File

@ -10,7 +10,7 @@ use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::*;
use crate::shared::{inner_layout, ToChild, ToNode};
use crate::validations::is_email;
use crate::ws::send_ws_msg;
use crate::ws::{enqueue_ws_msg, send_ws_msg};
use crate::{FieldId, Msg, PageChanged, UsersPageChange, WebSocketChanged};
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
@ -28,13 +28,19 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::ChangePage(Page::Users) if model.user.is_some() => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
send_ws_msg(WsMsg::InvitedUsersRequest, model.ws.as_ref());
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}
Msg::WebSocketChange(change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(Ok(_))) if model.user.is_some() => {
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref());
send_ws_msg(WsMsg::InvitedUsersRequest, model.ws.as_ref());
enqueue_ws_msg(
vec![WsMsg::InvitationListRequest, WsMsg::InvitedUsersRequest],
model.ws.as_ref(),
orders,
);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUsersLoaded(users)) => {
page.invited_users = users;
@ -51,7 +57,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());
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
}
WebSocketChanged::WsMsg(WsMsg::InvitedUserRemoveSuccess(email)) => {
let mut old = vec![];
@ -63,7 +69,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());
send_ws_msg(WsMsg::InvitationListRequest, model.ws.as_ref(), orders);
page.form_state = InvitationFormState::Succeed;
}
WebSocketChanged::WsMsg(WsMsg::InvitationSendFailure) => {
@ -102,16 +108,22 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
email: page.email.clone(),
},
model.ws.as_ref(),
orders,
);
}
Msg::InviteRevokeRequest(invitation_id) => {
send_ws_msg(
WsMsg::InvitationRevokeRequest(invitation_id),
model.ws.as_ref(),
orders,
);
}
Msg::InvitedUserRemove(email) => {
send_ws_msg(WsMsg::InvitedUserRemoveRequest(email), model.ws.as_ref());
send_ws_msg(
WsMsg::InvitedUserRemoveRequest(email),
model.ws.as_ref(),
orders,
);
}
_ => (),
}

View File

@ -1,9 +1,11 @@
use seed::prelude::Orders;
use seed::*;
use jirs_data::*;
use crate::model::{Model, PageContent};
use crate::ws::send_ws_msg;
use crate::Msg;
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
let project_page = match &mut model.page_content {
@ -76,7 +78,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
project_page.issue_drag.last_id = Some(issue_bellow_id);
}
pub fn sync(model: &mut Model) {
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
// log!("------------------------------------------------------------------");
// log!("| SYNC |");
// log!("------------------------------------------------------------------");
@ -97,6 +99,7 @@ pub fn sync(model: &mut Model) {
PayloadVariant::I32(issue.issue_status_id),
),
model.ws.as_ref(),
orders,
);
send_ws_msg(
WsMsg::IssueUpdateRequest(
@ -105,6 +108,7 @@ pub fn sync(model: &mut Model) {
PayloadVariant::I32(issue.list_position),
),
model.ws.as_ref(),
orders,
);
}
project_page.issue_drag.clear();

View File

@ -3,15 +3,41 @@ use seed::prelude::*;
use jirs_data::WsMsg;
use crate::model::*;
use crate::shared::write_auth_token;
use crate::shared::{go_to_board, write_auth_token};
use crate::{Msg, WebSocketChanged};
pub mod issue;
pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>) {
pub fn flush_queue(model: &mut Model, orders: &mut impl Orders<Msg>) {
use seed::browser::web_socket::State;
match model.ws.as_ref() {
Some(ws) if ws.state() != State::Open => return,
None => return,
_ => (),
};
let mut old = vec![];
std::mem::swap(&mut model.ws_queue, &mut old);
for msg in old {
send_ws_msg(msg, model.ws.as_ref(), orders);
}
}
pub fn enqueue_ws_msg(v: Vec<WsMsg>, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
for msg in v {
send_ws_msg(msg, ws.clone(), orders);
}
}
pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>, orders: &mut impl Orders<Msg>) {
use seed::browser::web_socket::State;
let ws = match ws {
Some(ws) => ws,
_ => return,
Some(ws) if ws.state() == State::Open => ws,
_ => {
orders
.skip()
.send_msg(Msg::WebSocketChange(WebSocketChanged::Bounced(msg)));
return;
}
};
let binary = bincode::serialize(&msg).unwrap();
ws.send_bytes(binary.as_slice())
@ -19,6 +45,16 @@ pub fn send_ws_msg(msg: WsMsg, ws: Option<&WebSocket>) {
}
pub fn open_socket(model: &mut Model, orders: &mut impl Orders<Msg>) {
use seed::browser::web_socket::State;
use seed::{prelude::*, *};
log!(model.ws.as_ref().map(|ws| ws.state()));
match model.ws.as_ref() {
Some(ws) if ws.state() != State::Closed => {
return;
}
_ => (),
};
if model.host_url.is_empty() {
return;
}
@ -43,6 +79,7 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
// auth
WsMsg::AuthorizeLoaded(Ok(user)) => {
model.user = Some(user.clone());
go_to_board(orders);
}
WsMsg::AuthorizeExpired => {
if let Ok(msg) = write_auth_token(None) {

View File

@ -736,4 +736,8 @@ pub enum WsMsg {
// messages
Message(Message),
MessagesRequest,
MessagesResponse(Vec<Message>),
MessageMarkSeen(MessageId),
MessageMarkedSeen(MessageId),
}