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
|
|
|
|
2020-04-12 13:21:47 +02: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-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;
|
2020-04-02 19:32:40 +02:00
|
|
|
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 shared;
|
2020-04-17 16:28:02 +02:00
|
|
|
mod sign_in;
|
|
|
|
mod sign_up;
|
|
|
|
mod validations;
|
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;
|
2020-04-12 13:21:47 +02:00
|
|
|
pub type AppType = App<Msg, Model, Node<Msg>>;
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
2020-04-17 14:10:05 +02:00
|
|
|
pub enum EditIssueModalSection {
|
|
|
|
Issue(IssueFieldId),
|
|
|
|
Comment(CommentFieldId),
|
2020-04-14 23:10:58 +02:00
|
|
|
}
|
|
|
|
|
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 {
|
2020-04-17 16:28:02 +02:00
|
|
|
SignIn(SignInFieldId),
|
|
|
|
SignUp(SignUpFieldId),
|
2020-04-12 13:21:47 +02:00
|
|
|
// issue
|
2020-04-17 14:10:05 +02:00
|
|
|
AddIssueModal(IssueFieldId),
|
|
|
|
EditIssueModal(EditIssueModalSection),
|
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
|
|
|
|
2020-04-17 14:10:05 +02:00
|
|
|
ProjectSettings(ProjectFieldId),
|
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 {
|
2020-04-12 13:21:47 +02:00
|
|
|
FieldId::EditIssueModal(sub) => match sub {
|
2020-04-17 14:10:05 +02:00
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Type) => {
|
|
|
|
f.write_str("issueTypeEditModalTop")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Title) => {
|
|
|
|
f.write_str("titleIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Description) => {
|
|
|
|
f.write_str("descriptionIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Status) => {
|
|
|
|
f.write_str("statusIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Assignees) => {
|
|
|
|
f.write_str("assigneesIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Reporter) => {
|
|
|
|
f.write_str("reporterIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Priority) => {
|
|
|
|
f.write_str("priorityIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::Estimate) => {
|
|
|
|
f.write_str("estimateIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::TimeSpend) => {
|
|
|
|
f.write_str("timeSpendIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::TimeRemaining) => {
|
|
|
|
f.write_str("timeRemainingIssueEditModal")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Comment(CommentFieldId::Body) => {
|
|
|
|
f.write_str("editIssue-commentBody")
|
|
|
|
}
|
|
|
|
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
|
|
|
|
f.write_str("editIssue-listPosition")
|
|
|
|
}
|
2020-04-12 13:21:47 +02:00
|
|
|
},
|
|
|
|
FieldId::AddIssueModal(sub) => match sub {
|
2020-04-17 14:10:05 +02:00
|
|
|
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
|
|
|
|
IssueFieldId::Title => f.write_str("summaryAddIssueModal"),
|
|
|
|
IssueFieldId::Description => f.write_str("descriptionAddIssueModal"),
|
|
|
|
IssueFieldId::Reporter => f.write_str("reporterAddIssueModal"),
|
|
|
|
IssueFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
|
|
|
|
IssueFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
|
|
|
|
IssueFieldId::Status => f.write_str("addIssueModal-status"),
|
|
|
|
IssueFieldId::Estimate => f.write_str("addIssueModal-estimate"),
|
|
|
|
IssueFieldId::TimeSpend => f.write_str("addIssueModal-timeSpend"),
|
|
|
|
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
|
|
|
|
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
|
2020-04-12 13:21:47 +02:00
|
|
|
},
|
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 {
|
2020-04-17 14:10:05 +02:00
|
|
|
ProjectFieldId::Name => f.write_str("projectSettings-name"),
|
|
|
|
ProjectFieldId::Url => f.write_str("projectSettings-url"),
|
|
|
|
ProjectFieldId::Description => f.write_str("projectSettings-description"),
|
|
|
|
ProjectFieldId::Category => f.write_str("projectSettings-category"),
|
2020-04-13 16:29:26 +02:00
|
|
|
},
|
2020-04-17 16:28:02 +02:00
|
|
|
FieldId::SignIn(sub) => match sub {
|
|
|
|
SignInFieldId::Email => f.write_str("login-email"),
|
|
|
|
SignInFieldId::Username => f.write_str("login-username"),
|
|
|
|
SignInFieldId::Token => f.write_str("login-token"),
|
|
|
|
},
|
|
|
|
FieldId::SignUp(sub) => match sub {
|
|
|
|
SignUpFieldId::Username => f.write_str("signUp-email"),
|
|
|
|
SignUpFieldId::Email => f.write_str("signUp-username"),
|
2020-04-14 23:10:58 +02:00
|
|
|
},
|
2020-04-06 08:38:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-12 13:21:47 +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
|
|
|
}
|
|
|
|
|
2020-04-12 13:21:47 +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,
|
2020-04-12 13:21:47 +02:00
|
|
|
GlobalKeyDown {
|
|
|
|
key: String,
|
|
|
|
shift: bool,
|
|
|
|
ctrl: bool,
|
|
|
|
alt: bool,
|
|
|
|
},
|
2020-04-06 14:25:52 +02:00
|
|
|
|
|
|
|
// Auth Token
|
|
|
|
AuthTokenStored,
|
|
|
|
AuthTokenErased,
|
2020-04-15 20:28:07 +02:00
|
|
|
SignInRequest,
|
2020-04-16 16:11:19 +02:00
|
|
|
BindClientRequest,
|
2020-04-06 14:25:52 +02:00
|
|
|
|
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,
|
2020-03-31 08:56:46 +02:00
|
|
|
|
|
|
|
// 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-16 22:20:30 +02:00
|
|
|
DragLeave(IssueId),
|
2020-04-09 08:56:12 +02:00
|
|
|
ExchangePosition(IssueId),
|
2020-04-09 11:10:52 +02:00
|
|
|
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,
|
2020-04-03 23:43:29 +02:00
|
|
|
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>) {
|
2020-04-12 13:21:47 +02:00
|
|
|
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 {
|
2020-04-14 23:10:58 +02:00
|
|
|
Msg::AuthTokenStored => {
|
2020-04-16 16:11:19 +02:00
|
|
|
seed::push_route(vec!["dashboard"]);
|
|
|
|
orders.skip().send_msg(Msg::ChangePage(Page::Project));
|
2020-04-16 20:55:03 +02:00
|
|
|
authorize_or_redirect();
|
2020-04-14 23:10:58 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
Msg::AuthTokenErased => {
|
2020-04-16 16:11:19 +02:00
|
|
|
seed::push_route(vec!["login"]);
|
2020-04-17 16:28:02 +02:00
|
|
|
orders.skip().send_msg(Msg::ChangePage(Page::SignIn));
|
2020-04-16 20:55:03 +02:00
|
|
|
authorize_or_redirect();
|
2020-04-14 23:10:58 +02:00
|
|
|
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
|
|
|
}
|
2020-04-13 19:55:21 +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);
|
2020-04-02 19:32:40 +02:00
|
|
|
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),
|
2020-04-17 16:28:02 +02:00
|
|
|
Page::SignIn => sign_in::update(msg, model, orders),
|
|
|
|
Page::SignUp => sign_up::update(msg, model, orders),
|
2020-03-30 14:26:25 +02:00
|
|
|
}
|
|
|
|
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),
|
2020-04-17 16:28:02 +02:00
|
|
|
Page::SignIn => sign_in::view(model),
|
|
|
|
Page::SignUp => sign_up::view(model),
|
2020-03-30 14:26:25 +02:00
|
|
|
}
|
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)),
|
2020-04-17 16:28:02 +02:00
|
|
|
"login" => Some(Msg::ChangePage(model::Page::SignIn)),
|
|
|
|
"register" => Some(Msg::ChangePage(model::Page::SignUp)),
|
2020-03-30 14:26:25 +02:00
|
|
|
_ => 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() {
|
2020-04-14 23:10:58 +02:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
|
2020-04-12 13:21:47 +02:00
|
|
|
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
|
|
|
|
2020-04-14 23:10:58 +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
|
|
|
}
|
2020-04-14 23:10:58 +02:00
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn authorize_or_redirect() {
|
|
|
|
match crate::shared::read_auth_token() {
|
2020-04-17 14:10:05 +02:00
|
|
|
Ok(token) => {
|
|
|
|
send_ws_msg(WsMsg::AuthorizeRequest(token));
|
2020-04-14 23:10:58 +02:00
|
|
|
}
|
|
|
|
Err(..) => {
|
|
|
|
let pathname = seed::document().location().unwrap().pathname().unwrap();
|
|
|
|
match pathname.as_str() {
|
|
|
|
"/login" | "/register" => {}
|
|
|
|
_ => {
|
|
|
|
seed::push_route(vec!["login"]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|