bitque/jirs-client/src/lib.rs

339 lines
9.9 KiB
Rust
Raw Normal View History

2020-04-06 08:38:08 +02:00
use std::sync::RwLock;
2020-04-05 15:15:09 +02:00
2020-03-30 08:16:26 +02:00
use seed::{prelude::*, *};
2020-03-27 12:17:27 +01:00
use jirs_data::*;
2020-03-31 22:05:18 +02:00
2020-04-06 22:59:33 +02:00
use crate::api::send_ws_msg;
2020-04-05 15:15:09 +02:00
use crate::model::{ModalType, Model, Page};
2020-04-06 22:59:33 +02:00
use crate::shared::read_auth_token;
2020-04-10 08:09:40 +02:00
use crate::shared::styled_editor::Mode as TabMode;
2020-04-02 08:45:43 +02:00
use crate::shared::styled_select::StyledSelectChange;
2020-03-30 23:19:00 +02:00
2020-03-30 14:26:25 +02:00
mod api;
mod login;
mod modal;
2020-03-30 08:16:26 +02:00
mod model;
2020-03-30 14:26:25 +02:00
mod project;
mod project_settings;
mod register;
mod shared;
2020-04-06 08:38:08 +02:00
mod ws;
2020-03-30 08:16:26 +02:00
2020-03-31 11:11:06 +02:00
pub type AvatarFilterActive = bool;
pub type AppType = App<Msg, Model, Node<Msg>>;
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum EditIssueModalFieldId {
IssueType,
Title,
Description,
Status,
Assignees,
Reporter,
Priority,
Estimate,
TimeSpend,
TimeRemaining,
// comment
CommentBody,
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum AddIssueModalFieldId {
IssueType,
Summary,
Description,
Reporter,
Assignees,
Priority,
}
2020-03-31 11:11:06 +02:00
2020-04-13 16:29:26 +02:00
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum ProjectSettingsFieldId {
Name,
Url,
Description,
Category,
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum LoginFieldId {
Email,
}
2020-04-08 16:14:59 +02:00
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
2020-04-02 08:45:43 +02:00
pub enum FieldId {
Login(LoginFieldId),
// issue
AddIssueModal(AddIssueModalFieldId),
EditIssueModal(EditIssueModalFieldId),
2020-04-05 15:15:09 +02:00
// project boards
TextFilterBoard,
2020-04-03 16:15:56 +02:00
CopyButtonLabel,
2020-04-13 16:29:26 +02:00
ProjectSettings(ProjectSettingsFieldId),
2020-04-03 16:15:56 +02:00
}
2020-04-06 08:38:08 +02:00
impl std::fmt::Display for FieldId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FieldId::EditIssueModal(sub) => match sub {
EditIssueModalFieldId::IssueType => f.write_str("issueTypeEditModalTop"),
EditIssueModalFieldId::Title => f.write_str("titleIssueEditModal"),
EditIssueModalFieldId::Description => f.write_str("descriptionIssueEditModal"),
EditIssueModalFieldId::Status => f.write_str("statusIssueEditModal"),
EditIssueModalFieldId::Assignees => f.write_str("assigneesIssueEditModal"),
EditIssueModalFieldId::Reporter => f.write_str("reporterIssueEditModal"),
EditIssueModalFieldId::Priority => f.write_str("priorityIssueEditModal"),
EditIssueModalFieldId::Estimate => f.write_str("estimateIssueEditModal"),
EditIssueModalFieldId::TimeSpend => f.write_str("timeSpendIssueEditModal"),
EditIssueModalFieldId::TimeRemaining => f.write_str("timeRemainingIssueEditModal"),
EditIssueModalFieldId::CommentBody => f.write_str("editIssue-commentBody"),
},
FieldId::AddIssueModal(sub) => match sub {
AddIssueModalFieldId::IssueType => f.write_str("issueTypeAddIssueModal"),
AddIssueModalFieldId::Summary => f.write_str("summaryAddIssueModal"),
AddIssueModalFieldId::Description => f.write_str("descriptionAddIssueModal"),
AddIssueModalFieldId::Reporter => f.write_str("reporterAddIssueModal"),
AddIssueModalFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
AddIssueModalFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
},
2020-04-06 08:38:08 +02:00
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
2020-04-13 16:29:26 +02:00
FieldId::ProjectSettings(sub) => match sub {
ProjectSettingsFieldId::Name => f.write_str("projectSettings-name"),
ProjectSettingsFieldId::Url => f.write_str("projectSettings-url"),
ProjectSettingsFieldId::Description => f.write_str("projectSettings-description"),
ProjectSettingsFieldId::Category => f.write_str("projectSettings-category"),
},
FieldId::Login(sub) => match sub {
LoginFieldId::Email => f.write_str("login-email"),
},
2020-04-06 08:38:08 +02:00
}
}
}
#[derive(Clone, Debug, PartialEq)]
2020-04-03 16:15:56 +02:00
pub enum FieldChange {
LinkCopied(FieldId, bool),
2020-04-10 08:09:40 +02:00
TabChanged(FieldId, TabMode),
2020-04-13 10:56:42 +02:00
ToggleCommentForm(FieldId, bool),
EditComment(FieldId, i32),
2020-04-02 08:45:43 +02:00
}
#[derive(Clone, Debug, PartialEq)]
2020-03-30 14:26:25 +02:00
pub enum Msg {
2020-03-31 22:05:18 +02:00
NoOp,
GlobalKeyDown {
key: String,
shift: bool,
ctrl: bool,
alt: bool,
},
// Auth Token
AuthTokenStored,
AuthTokenErased,
2020-04-02 08:45:43 +02:00
StyledSelectChanged(FieldId, StyledSelectChange),
2020-03-30 14:26:25 +02:00
ChangePage(model::Page),
InternalFailure(String),
2020-03-30 23:19:00 +02:00
ToggleAboutTooltip,
// project
2020-03-31 11:11:06 +02:00
ProjectAvatarFilterChanged(UserId, AvatarFilterActive),
ProjectToggleOnlyMy,
2020-03-31 11:36:39 +02:00
ProjectToggleRecentlyUpdated,
2020-03-31 14:28:30 +02:00
ProjectClearFilters,
2020-04-14 16:20:05 +02:00
ProjectSaveChanges,
2020-03-31 22:05:18 +02:00
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
2020-04-09 08:56:12 +02:00
ExchangePosition(IssueId),
IssueDragOverStatus(IssueStatus),
2020-03-31 22:05:18 +02:00
IssueDropZone(IssueStatus),
2020-04-09 08:56:12 +02:00
UnlockDragOver,
2020-03-31 22:05:18 +02:00
2020-04-05 15:15:09 +02:00
// inputs
InputChanged(FieldId, String),
2020-03-31 22:05:18 +02:00
// issues
2020-04-08 16:14:59 +02:00
AddIssue,
DeleteIssue(IssueId),
2020-04-01 18:30:01 +02:00
2020-04-13 10:56:42 +02:00
// comments
SaveComment,
DeleteComment(CommentId),
2020-04-01 18:30:01 +02:00
// modals
2020-04-14 16:20:05 +02:00
ModalOpened(Box<ModalType>),
2020-04-03 16:15:56 +02:00
ModalDropped,
ModalChanged(FieldChange),
2020-04-06 08:38:08 +02:00
WsMsg(jirs_data::WsMsg),
2020-03-29 19:56:55 +02:00
}
2020-03-30 14:26:25 +02:00
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if msg == Msg::NoOp {
return;
}
2020-03-30 14:26:25 +02:00
if cfg!(debug_assertions) {
log!(msg);
}
2020-04-06 08:38:08 +02:00
match &msg {
Msg::AuthTokenStored => {
seed::push_route(vec!["/dashboard"]);
return;
}
Msg::AuthTokenErased => {
seed::push_route(vec!["/login"]);
return;
}
2020-03-30 14:26:25 +02:00
Msg::ChangePage(page) => {
2020-04-14 16:20:05 +02:00
model.page = *page;
2020-03-30 14:26:25 +02:00
}
Msg::ToggleAboutTooltip => {
model.about_tooltip_visible = !model.about_tooltip_visible;
}
2020-03-30 14:26:25 +02:00
_ => (),
}
2020-04-06 08:38:08 +02:00
crate::ws::update(&msg, model, orders);
crate::modal::update(&msg, model, orders);
2020-03-30 14:26:25 +02:00
match model.page {
2020-04-14 16:20:05 +02:00
Page::Project | Page::AddIssue | Page::EditIssue(..) => project::update(msg, model, orders),
2020-03-30 14:26:25 +02:00
Page::ProjectSettings => project_settings::update(msg, model, orders),
Page::Login => login::update(msg, model, orders),
Page::Register => register::update(msg, model, orders),
}
if cfg!(debug_assertions) {
2020-03-31 22:05:18 +02:00
// debug!(model);
2020-03-29 19:56:55 +02:00
}
2020-03-30 08:16:26 +02:00
}
2020-03-29 19:56:55 +02:00
2020-03-30 14:26:25 +02:00
fn view(model: &model::Model) -> Node<Msg> {
match model.page {
2020-04-04 17:42:02 +02:00
Page::Project | Page::AddIssue => project::view(model),
2020-04-01 10:36:05 +02:00
Page::EditIssue(_id) => project::view(model),
2020-03-30 14:26:25 +02:00
Page::ProjectSettings => project_settings::view(model),
Page::Login => login::view(model),
Page::Register => register::view(model),
}
2020-03-30 08:16:26 +02:00
}
2020-03-29 19:56:55 +02:00
2020-03-30 08:16:26 +02:00
fn routes(url: Url) -> Option<Msg> {
if url.path.is_empty() {
2020-03-30 14:26:25 +02:00
return Some(Msg::ChangePage(model::Page::Project));
2020-03-30 08:16:26 +02:00
}
2020-03-29 19:56:55 +02:00
2020-03-30 08:16:26 +02:00
match url.path[0].as_ref() {
2020-03-30 14:26:25 +02:00
"board" => Some(Msg::ChangePage(model::Page::Project)),
2020-04-01 10:36:05 +02:00
"issues" => match url.path.get(1).as_ref().map(|s| s.parse::<i32>()) {
Some(Ok(id)) => Some(Msg::ChangePage(model::Page::EditIssue(id))),
_ => None,
},
2020-04-04 17:42:02 +02:00
"add-issue" => Some(Msg::ChangePage(Page::AddIssue)),
2020-03-30 14:26:25 +02:00
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::Login)),
"register" => Some(Msg::ChangePage(model::Page::Register)),
_ => Some(Msg::ChangePage(model::Page::Project)),
}
}
pub static mut HOST_URL: String = String::new();
2020-04-06 08:38:08 +02:00
pub static mut APP: Option<RwLock<App<Msg, Model, Node<Msg>>>> = None;
2020-03-30 14:26:25 +02:00
#[wasm_bindgen]
pub fn set_host_url(url: String) {
unsafe {
HOST_URL = url;
2020-03-29 19:56:55 +02:00
}
}
2020-04-06 08:38:08 +02:00
#[wasm_bindgen]
pub fn handle_ws_message(value: &wasm_bindgen::JsValue) {
let a = js_sys::Uint8Array::new(value);
let mut v = Vec::new();
for idx in 0..a.length() {
v.push(a.get_index(idx));
}
2020-04-14 16:20:05 +02:00
if let Ok(msg) = bincode::deserialize(v.as_slice()) {
ws::handle(msg);
2020-04-06 08:38:08 +02:00
};
}
2020-04-06 22:59:33 +02:00
#[wasm_bindgen]
pub fn reconnected() {
authorize_or_redirect();
2020-04-06 22:59:33 +02:00
}
2020-04-06 08:38:08 +02:00
#[wasm_bindgen]
extern "C" {
pub fn send_bin_code(data: wasm_bindgen::JsValue);
2020-04-05 15:15:09 +02:00
}
2020-03-30 14:26:25 +02:00
#[wasm_bindgen]
2020-03-30 08:16:26 +02:00
pub fn render() {
2020-04-06 08:38:08 +02:00
seed::set_interval(
Box::new(|| {
let binary = bincode::serialize(&jirs_data::WsMsg::Ping).unwrap();
let data = JsValue::from_serde(&binary).unwrap();
send_bin_code(data);
}) as Box<dyn Fn()>,
5000,
);
if let Some(body) = seed::html_document().body() {
use wasm_bindgen::JsCast;
let body = body.dyn_ref::<web_sys::HtmlBodyElement>().unwrap().clone();
let key_up_closure =
wasm_bindgen::closure::Closure::wrap(Box::new(|event: web_sys::KeyboardEvent| {
if let Some(Ok(app)) = unsafe { APP.as_mut().map(|app| app.write()) } {
let msg = Msg::GlobalKeyDown {
key: event.key(),
shift: event.shift_key(),
ctrl: event.ctrl_key(),
alt: event.alt_key(),
};
app.update(msg);
}
})
as Box<dyn Fn(web_sys::KeyboardEvent)>);
body.add_event_listener_with_callback("keyup", key_up_closure.as_ref().unchecked_ref())
.unwrap();
key_up_closure.forget();
}
2020-04-06 08:38:08 +02:00
let app = seed::App::builder(update, view)
2020-04-05 15:15:09 +02:00
.routes(routes)
.build_and_start();
2020-04-06 08:38:08 +02:00
authorize_or_redirect();
2020-04-06 22:59:33 +02:00
2020-04-06 08:38:08 +02:00
let cell_app = std::sync::RwLock::new(app);
unsafe {
APP = Some(cell_app);
};
2020-03-29 19:56:55 +02:00
}
#[inline]
fn authorize_or_redirect() {
match crate::shared::read_auth_token() {
Ok(uuid) => {
send_ws_msg(WsMsg::AuthorizeRequest(uuid));
}
Err(..) => {
let pathname = seed::document().location().unwrap().pathname().unwrap();
match pathname.as_str() {
"/login" | "/register" => {}
_ => {
seed::push_route(vec!["login"]);
}
};
}
};
}