Add modal to add issue
This commit is contained in:
parent
a0f1b90d9d
commit
780a0c498a
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -283,6 +283,21 @@ dependencies = [
|
|||||||
"url 2.1.1",
|
"url 2.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-actors"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1bd41bd66c4e9b5274cec87aac30168e63d64e96fd19db38edef6b46ba2982"
|
||||||
|
dependencies = [
|
||||||
|
"actix",
|
||||||
|
"actix-codec",
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"bytes",
|
||||||
|
"futures 0.3.4",
|
||||||
|
"pin-project",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -425,6 +440,16 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bincode"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -1038,10 +1063,12 @@ version = "0.1.0"
|
|||||||
name = "jirs-client"
|
name = "jirs-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures 0.1.29",
|
"futures 0.1.29",
|
||||||
"jirs-data",
|
"jirs-data",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
"seed",
|
"seed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1070,7 +1097,9 @@ dependencies = [
|
|||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-service",
|
"actix-service",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"actix-web-actors",
|
||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
|
"bincode",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
@ -18,11 +18,15 @@ jirs-data = { path = "../jirs-data" }
|
|||||||
seed = { version = "*" }
|
seed = { version = "*" }
|
||||||
serde = "*"
|
serde = "*"
|
||||||
serde_json = "*"
|
serde_json = "*"
|
||||||
|
bincode = "1.2.1"
|
||||||
chrono = { version = "*", features = [ "serde" ] }
|
chrono = { version = "*", features = [ "serde" ] }
|
||||||
uuid = { version = "*", features = [ "serde" ] }
|
uuid = { version = "*", features = [ "serde" ] }
|
||||||
wasm-bindgen = "*"
|
wasm-bindgen = "*"
|
||||||
js-sys = "*"
|
|
||||||
futures = "^0.1.26"
|
futures = "^0.1.26"
|
||||||
|
lazy_static = "*"
|
||||||
|
|
||||||
|
[dependencies.js-sys]
|
||||||
|
js-sys = "*"
|
||||||
|
|
||||||
[dependencies.web-sys]
|
[dependencies.web-sys]
|
||||||
version = "*"
|
version = "*"
|
||||||
@ -34,5 +38,9 @@ features = [
|
|||||||
"DomRect",
|
"DomRect",
|
||||||
"HtmlDocument",
|
"HtmlDocument",
|
||||||
"Document",
|
"Document",
|
||||||
"Selection"
|
"Selection",
|
||||||
|
"CssStyleDeclaration",
|
||||||
|
"WebSocket",
|
||||||
|
"BinaryType",
|
||||||
|
"Blob",
|
||||||
]
|
]
|
||||||
|
@ -62,3 +62,31 @@
|
|||||||
.styledField > * {
|
.styledField > * {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.styledTextArea {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.styledTextArea > textarea {
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px 9px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--borderLightest);
|
||||||
|
color: var(--textDarkest);
|
||||||
|
background: var(--backgroundLightest);
|
||||||
|
font-family: var(--font-regular);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 15px
|
||||||
|
}
|
||||||
|
|
||||||
|
.styledTextArea > textarea:focus {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--borderInputFocus);
|
||||||
|
box-shadow: 0 0 0 1px var(--borderInputFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
.styledTextArea > textarea.invalid:focus {
|
||||||
|
border: 1px solid var(--danger);
|
||||||
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
use seed::Method;
|
use seed::Method;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use jirs_data::UpdateIssuePayload;
|
use jirs_data::{UpdateIssuePayload, WsMsg};
|
||||||
|
|
||||||
use crate::shared::host_client;
|
use crate::shared::host_client;
|
||||||
use crate::Msg;
|
use crate::Msg;
|
||||||
|
use seed::prelude::Closure;
|
||||||
|
use std::sync::Once;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
pub async fn fetch_current_project(host_url: String) -> Result<Msg, Msg> {
|
pub async fn fetch_current_project(host_url: String) -> Result<Msg, Msg> {
|
||||||
match host_client(host_url, "/project") {
|
match host_client(host_url, "/project") {
|
||||||
@ -49,3 +53,93 @@ pub async fn delete_issue(host_url: String, id: i32) -> Result<Msg, Msg> {
|
|||||||
Err(e) => return Ok(Msg::InternalFailure(e)),
|
Err(e) => return Ok(Msg::InternalFailure(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct WebSocket {
|
||||||
|
ws: web_sys::WebSocket,
|
||||||
|
queue: Vec<WsMsg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WebSocket {
|
||||||
|
fn default() -> WebSocket {
|
||||||
|
use js_sys::*;
|
||||||
|
use seed::prelude::*;
|
||||||
|
use web_sys::*;
|
||||||
|
|
||||||
|
let native = web_sys::WebSocket::new("ws://localhost:5000/ws/").unwrap();
|
||||||
|
native.set_binary_type(web_sys::BinaryType::Arraybuffer);
|
||||||
|
|
||||||
|
let onmessage_callback =
|
||||||
|
Closure::wrap(Box::new(move |e: MessageEvent| {}) as Box<dyn FnMut(MessageEvent)>);
|
||||||
|
native.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
|
||||||
|
onmessage_callback.forget();
|
||||||
|
|
||||||
|
// let onerror_callback = Closure::wrap(Box::new(move |e: ErrorEvent| {
|
||||||
|
// seed::log!("error event: {:?}", e);
|
||||||
|
// }) as Box<dyn FnMut(ErrorEvent)>);
|
||||||
|
// native.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
|
||||||
|
// onerror_callback.forget();
|
||||||
|
|
||||||
|
let cloned_ws = native.clone();
|
||||||
|
let onopen_callback = Closure::wrap(Box::new(move |_| {
|
||||||
|
seed::log!("socket opened");
|
||||||
|
match cloned_ws.send_with_str("ping") {
|
||||||
|
Ok(_) => seed::log!("message successfully sent"),
|
||||||
|
Err(err) => seed::log!("error sending message: {:?}", err),
|
||||||
|
}
|
||||||
|
}) as Box<dyn FnMut(JsValue)>);
|
||||||
|
native.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
|
||||||
|
onopen_callback.forget();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
ws: native,
|
||||||
|
queue: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebSocket {
|
||||||
|
pub fn send_with_u8_array(&self, buffer: &[u8]) {
|
||||||
|
use seed::*;
|
||||||
|
self.ws
|
||||||
|
.send_with_u8_array(buffer)
|
||||||
|
.unwrap_or_else(|e| error!(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&mut self) {
|
||||||
|
use bincode;
|
||||||
|
for msg in self.queue.iter() {
|
||||||
|
let encoded: Vec<u8> = bincode::serialize(msg).unwrap();
|
||||||
|
self.send_with_u8_array(encoded.as_slice());
|
||||||
|
}
|
||||||
|
self.queue.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static INIT_WS: Once = Once::new();
|
||||||
|
static mut WS: Option<WebSocket> = None;
|
||||||
|
|
||||||
|
pub fn ws() -> &'static mut WebSocket {
|
||||||
|
unsafe {
|
||||||
|
INIT_WS.call_once(|| WS = Some(WebSocket::default()));
|
||||||
|
|
||||||
|
let ws_ping = Box::new(|| match WS.as_mut().map(|ws| ws.ws.ready_state()) {
|
||||||
|
Some(0) => {}
|
||||||
|
Some(1) => {
|
||||||
|
ws_send(WsMsg::Ping);
|
||||||
|
WS.as_mut().unwrap().send();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
WS = Some(WebSocket::default());
|
||||||
|
}
|
||||||
|
}) as Box<dyn Fn()>;
|
||||||
|
seed::set_interval(ws_ping, 10_000);
|
||||||
|
|
||||||
|
WS.as_mut().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub fn ws_received() {}
|
||||||
|
//
|
||||||
|
pub fn ws_send(msg: jirs_data::WsMsg) {
|
||||||
|
ws().queue.push(msg);
|
||||||
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
use seed::fetch::FetchObject;
|
use seed::fetch::FetchObject;
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
use jirs_data::IssueStatus;
|
use jirs_data::IssueStatus;
|
||||||
|
|
||||||
use crate::model::{ModalType, Page};
|
use crate::api::ws;
|
||||||
|
use crate::model::{ModalType, Model, Page};
|
||||||
use crate::shared::styled_select::StyledSelectChange;
|
use crate::shared::styled_select::StyledSelectChange;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
@ -22,9 +26,16 @@ pub type AvatarFilterActive = bool;
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum FieldId {
|
pub enum FieldId {
|
||||||
|
// edit issue
|
||||||
IssueTypeEditModalTop,
|
IssueTypeEditModalTop,
|
||||||
|
// project boards
|
||||||
|
TextFilterBoard,
|
||||||
|
//
|
||||||
CopyButtonLabel,
|
CopyButtonLabel,
|
||||||
|
// add issue
|
||||||
IssueTypeAddIssueModal,
|
IssueTypeAddIssueModal,
|
||||||
|
SummaryAddIssueModal,
|
||||||
|
DescriptionAddIssueModal,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -55,6 +66,9 @@ pub enum Msg {
|
|||||||
IssueDragStopped(IssueId),
|
IssueDragStopped(IssueId),
|
||||||
IssueDropZone(IssueStatus),
|
IssueDropZone(IssueStatus),
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
InputChanged(FieldId, String),
|
||||||
|
|
||||||
// issues
|
// issues
|
||||||
IssueUpdateResult(FetchObject<String>),
|
IssueUpdateResult(FetchObject<String>),
|
||||||
IssueDeleteResult(FetchObject<String>),
|
IssueDeleteResult(FetchObject<String>),
|
||||||
@ -128,7 +142,16 @@ pub fn set_host_url(url: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn after_mount(_url: Url, _orders: &mut impl Orders<Msg>) -> AfterMount<Model> {
|
||||||
|
ws();
|
||||||
|
let model = Model::default();
|
||||||
|
AfterMount::new(model).url_handling(UrlHandling::None)
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn render() {
|
pub fn render() {
|
||||||
App::builder(update, view).routes(routes).build_and_start();
|
App::builder(update, view)
|
||||||
|
.routes(routes)
|
||||||
|
.after_mount(after_mount)
|
||||||
|
.build_and_start();
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,11 @@ use crate::model::{AddIssueModal, Model};
|
|||||||
use crate::shared::styled_button::StyledButton;
|
use crate::shared::styled_button::StyledButton;
|
||||||
use crate::shared::styled_field::StyledField;
|
use crate::shared::styled_field::StyledField;
|
||||||
use crate::shared::styled_form::StyledForm;
|
use crate::shared::styled_form::StyledForm;
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
use crate::shared::styled_icon::StyledIcon;
|
||||||
use crate::shared::styled_input::StyledInput;
|
use crate::shared::styled_input::StyledInput;
|
||||||
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
|
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
|
||||||
use crate::shared::styled_select::StyledSelect;
|
use crate::shared::styled_select::StyledSelect;
|
||||||
|
use crate::shared::styled_textarea::StyledTextarea;
|
||||||
use crate::shared::ToNode;
|
use crate::shared::ToNode;
|
||||||
use crate::{FieldId, Msg};
|
use crate::{FieldId, Msg};
|
||||||
|
|
||||||
@ -35,8 +36,7 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let short_summary = StyledInput::build()
|
let short_summary = StyledInput::build(FieldId::SummaryAddIssueModal)
|
||||||
.id("issue-short-summary")
|
|
||||||
.valid(true)
|
.valid(true)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
@ -47,6 +47,17 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
|
let description = StyledTextarea::build()
|
||||||
|
.height(110)
|
||||||
|
.build(FieldId::DescriptionAddIssueModal)
|
||||||
|
.into_node();
|
||||||
|
let description_field = StyledField::build()
|
||||||
|
.label("Description")
|
||||||
|
.tip("Describe the issue in as much detail as you'd like.")
|
||||||
|
.input(description)
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
|
||||||
let submit = StyledButton::build()
|
let submit = StyledButton::build()
|
||||||
.primary()
|
.primary()
|
||||||
.text("Create Issue")
|
.text("Create Issue")
|
||||||
@ -66,6 +77,7 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
.add_field(issue_type_field)
|
.add_field(issue_type_field)
|
||||||
.add_field(crate::shared::divider())
|
.add_field(crate::shared::divider())
|
||||||
.add_field(short_summary_field)
|
.add_field(short_summary_field)
|
||||||
|
.add_field(description_field)
|
||||||
.add_field(actions)
|
.add_field(actions)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
@ -94,7 +94,7 @@ pub struct ProjectPage {
|
|||||||
pub dragged_issue_id: Option<IssueId>,
|
pub dragged_issue_id: Option<IssueId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
pub access_token: Option<Uuid>,
|
pub access_token: Option<Uuid>,
|
||||||
pub user: Option<User>,
|
pub user: Option<User>,
|
||||||
|
@ -4,11 +4,11 @@ use jirs_data::*;
|
|||||||
|
|
||||||
use crate::model::{Model, Page};
|
use crate::model::{Model, Page};
|
||||||
use crate::shared::styled_avatar::StyledAvatar;
|
use crate::shared::styled_avatar::StyledAvatar;
|
||||||
use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant};
|
use crate::shared::styled_button::StyledButton;
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||||
use crate::shared::styled_input::StyledInput;
|
use crate::shared::styled_input::StyledInput;
|
||||||
use crate::shared::{drag_ev, inner_layout, ToNode};
|
use crate::shared::{drag_ev, inner_layout, ToNode};
|
||||||
use crate::Msg;
|
use crate::{FieldId, Msg};
|
||||||
|
|
||||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
@ -172,11 +172,11 @@ fn header() -> Node<Msg> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn project_board_filters(model: &Model) -> Node<Msg> {
|
fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||||
let search_input = StyledInput::build()
|
let search_input = StyledInput::build(FieldId::TextFilterBoard)
|
||||||
.icon(Icon::Search)
|
.icon(Icon::Search)
|
||||||
.id("searchInput")
|
|
||||||
.valid(true)
|
.valid(true)
|
||||||
.on_change(input_ev(Ev::Change, |value| {
|
.on_change(input_ev(Ev::Change, |value| {
|
||||||
|
crate::api::ws_send(WsMsg::Ping);
|
||||||
Msg::ProjectTextFilterChanged(value)
|
Msg::ProjectTextFilterChanged(value)
|
||||||
}))
|
}))
|
||||||
.build()
|
.build()
|
||||||
|
@ -17,6 +17,7 @@ pub mod styled_icon;
|
|||||||
pub mod styled_input;
|
pub mod styled_input;
|
||||||
pub mod styled_modal;
|
pub mod styled_modal;
|
||||||
pub mod styled_select;
|
pub mod styled_select;
|
||||||
|
pub mod styled_textarea;
|
||||||
pub mod styled_tooltip;
|
pub mod styled_tooltip;
|
||||||
|
|
||||||
pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> {
|
pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> {
|
||||||
|
@ -2,39 +2,36 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||||
use crate::shared::ToNode;
|
use crate::shared::ToNode;
|
||||||
use crate::Msg;
|
use crate::{FieldId, Msg};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StyledInput {
|
pub struct StyledInput {
|
||||||
id: Option<String>,
|
id: FieldId,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
valid: bool,
|
valid: bool,
|
||||||
on_change: Option<EventHandler<Msg>>,
|
on_change: Option<EventHandler<Msg>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyledInput {
|
impl StyledInput {
|
||||||
pub fn build() -> StyledInputBuilder {
|
pub fn build(id: FieldId) -> StyledInputBuilder {
|
||||||
StyledInputBuilder::default()
|
StyledInputBuilder {
|
||||||
|
id,
|
||||||
|
icon: None,
|
||||||
|
valid: None,
|
||||||
|
on_change: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StyledInputBuilder {
|
pub struct StyledInputBuilder {
|
||||||
id: Option<String>,
|
id: FieldId,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
valid: Option<bool>,
|
valid: Option<bool>,
|
||||||
on_change: Option<EventHandler<Msg>>,
|
on_change: Option<EventHandler<Msg>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyledInputBuilder {
|
impl StyledInputBuilder {
|
||||||
pub fn id<S>(mut self, id: S) -> Self
|
|
||||||
where
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
self.id = Some(id.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn icon(mut self, icon: Icon) -> Self {
|
pub fn icon(mut self, icon: Icon) -> Self {
|
||||||
self.icon = Some(icon);
|
self.icon = Some(icon);
|
||||||
self
|
self
|
||||||
@ -89,15 +86,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
|||||||
_ => empty![],
|
_ => empty![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_node = match on_change {
|
let mut handlers = vec![];
|
||||||
Some(on_change) => seed::input![attrs![At::Class => input_class_list.join(" ")], on_change],
|
|
||||||
_ => seed::input![attrs![At::Class => input_class_list.join(" ")]],
|
if let Some(handler) = on_change {
|
||||||
};
|
handlers.push(handler);
|
||||||
|
}
|
||||||
|
let input_handler = input_ev(Ev::KeyPress, move |value| Msg::InputChanged(id, value));
|
||||||
|
handlers.push(input_handler);
|
||||||
|
|
||||||
div![
|
div![
|
||||||
id![id.unwrap_or_default()],
|
|
||||||
attrs!(At::Class => wrapper_class_list.join(" ")),
|
attrs!(At::Class => wrapper_class_list.join(" ")),
|
||||||
icon,
|
icon,
|
||||||
input_node,
|
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
use crate::shared::styled_button::StyledButton;
|
|
||||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||||
use crate::shared::ToNode;
|
use crate::shared::ToNode;
|
||||||
use crate::{FieldId, Msg};
|
use crate::{FieldId, Msg};
|
||||||
@ -51,7 +50,6 @@ where
|
|||||||
variant: Variant,
|
variant: Variant,
|
||||||
dropdown_width: Option<usize>,
|
dropdown_width: Option<usize>,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
placeholder: Option<String>,
|
|
||||||
valid: bool,
|
valid: bool,
|
||||||
is_multi: bool,
|
is_multi: bool,
|
||||||
allow_clear: bool,
|
allow_clear: bool,
|
||||||
@ -80,7 +78,6 @@ where
|
|||||||
variant: None,
|
variant: None,
|
||||||
dropdown_width: None,
|
dropdown_width: None,
|
||||||
name: None,
|
name: None,
|
||||||
placeholder: None,
|
|
||||||
valid: None,
|
valid: None,
|
||||||
is_multi: None,
|
is_multi: None,
|
||||||
allow_clear: None,
|
allow_clear: None,
|
||||||
@ -101,7 +98,6 @@ where
|
|||||||
variant: Option<Variant>,
|
variant: Option<Variant>,
|
||||||
dropdown_width: Option<Option<usize>>,
|
dropdown_width: Option<Option<usize>>,
|
||||||
name: Option<Option<String>>,
|
name: Option<Option<String>>,
|
||||||
placeholder: Option<Option<String>>,
|
|
||||||
valid: Option<bool>,
|
valid: Option<bool>,
|
||||||
is_multi: Option<bool>,
|
is_multi: Option<bool>,
|
||||||
allow_clear: Option<bool>,
|
allow_clear: Option<bool>,
|
||||||
@ -121,7 +117,6 @@ where
|
|||||||
variant: self.variant.unwrap_or_default(),
|
variant: self.variant.unwrap_or_default(),
|
||||||
dropdown_width: self.dropdown_width.unwrap_or_default(),
|
dropdown_width: self.dropdown_width.unwrap_or_default(),
|
||||||
name: self.name.unwrap_or_default(),
|
name: self.name.unwrap_or_default(),
|
||||||
placeholder: self.placeholder.unwrap_or_default(),
|
|
||||||
valid: self.valid.unwrap_or(true),
|
valid: self.valid.unwrap_or(true),
|
||||||
is_multi: self.is_multi.unwrap_or_default(),
|
is_multi: self.is_multi.unwrap_or_default(),
|
||||||
allow_clear: self.allow_clear.unwrap_or_default(),
|
allow_clear: self.allow_clear.unwrap_or_default(),
|
||||||
@ -188,7 +183,6 @@ where
|
|||||||
variant,
|
variant,
|
||||||
dropdown_width,
|
dropdown_width,
|
||||||
name,
|
name,
|
||||||
placeholder: _,
|
|
||||||
valid,
|
valid,
|
||||||
is_multi,
|
is_multi,
|
||||||
allow_clear,
|
allow_clear,
|
||||||
|
116
jirs-client/src/shared/styled_textarea.rs
Normal file
116
jirs-client/src/shared/styled_textarea.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
use crate::shared::ToNode;
|
||||||
|
use crate::{FieldId, Msg};
|
||||||
|
use seed::{prelude::*, *};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct StyledTextarea {
|
||||||
|
id: FieldId,
|
||||||
|
height: usize,
|
||||||
|
on_change: Option<EventHandler<Msg>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToNode for StyledTextarea {
|
||||||
|
fn into_node(self) -> Node<Msg> {
|
||||||
|
render(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledTextarea {
|
||||||
|
pub fn build() -> StyledTextareaBuilder {
|
||||||
|
StyledTextareaBuilder::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct StyledTextareaBuilder {
|
||||||
|
height: Option<usize>,
|
||||||
|
on_change: Option<EventHandler<Msg>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledTextareaBuilder {
|
||||||
|
pub fn height(mut self, height: usize) -> Self {
|
||||||
|
self.height = Some(height);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_change(mut self, on_change: EventHandler<Msg>) -> Self {
|
||||||
|
self.on_change = Some(on_change);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self, id: FieldId) -> StyledTextarea {
|
||||||
|
StyledTextarea {
|
||||||
|
id,
|
||||||
|
height: self.height.unwrap_or(110),
|
||||||
|
on_change: self.on_change,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FONT_SIZE: f64 = 15f64;
|
||||||
|
const LINE_HEIGHT: f64 = 1.4285;
|
||||||
|
const LETTER_HEIGHT: f64 = FONT_SIZE * LINE_HEIGHT;
|
||||||
|
const PADDING_TOP_BOTTOM: f64 = 17f64;
|
||||||
|
const BORDER_TOP_BOTTOM: f64 = 2f64;
|
||||||
|
const ADDITIONAL_HEIGHT: f64 = PADDING_TOP_BOTTOM + BORDER_TOP_BOTTOM;
|
||||||
|
|
||||||
|
// height = `calc( (${$0.value.split("\n").length}px * ( 15 * 1.4285 )) + 17px + 2px)`
|
||||||
|
// where:
|
||||||
|
// * 15 is font-size
|
||||||
|
// * 1.4285 is line-height
|
||||||
|
// * 17 is padding top + bottom
|
||||||
|
// * 2 is border top + bottom
|
||||||
|
pub fn render(values: StyledTextarea) -> Node<Msg> {
|
||||||
|
let StyledTextarea {
|
||||||
|
id,
|
||||||
|
height,
|
||||||
|
on_change,
|
||||||
|
} = values;
|
||||||
|
let mut style_list = vec![];
|
||||||
|
style_list.push(format!("min-height: {}px", height));
|
||||||
|
|
||||||
|
let mut handlers = vec![];
|
||||||
|
if let Some(handler) = on_change {
|
||||||
|
handlers.push(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resize_handler = ev(Ev::KeyPress, move |event| {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
|
let target = match event.target() {
|
||||||
|
Some(el) => el,
|
||||||
|
_ => return Msg::NoOp,
|
||||||
|
};
|
||||||
|
let text_area = target.dyn_ref::<web_sys::HtmlTextAreaElement>().unwrap();
|
||||||
|
let value: String = text_area.value();
|
||||||
|
let len = value.lines().count() as f64;
|
||||||
|
|
||||||
|
let calc_height = (len * LETTER_HEIGHT) + ADDITIONAL_HEIGHT;
|
||||||
|
let height = if calc_height + ADDITIONAL_HEIGHT < height as f64 {
|
||||||
|
height as f64
|
||||||
|
} else {
|
||||||
|
calc_height + ADDITIONAL_HEIGHT
|
||||||
|
};
|
||||||
|
|
||||||
|
text_area
|
||||||
|
.style()
|
||||||
|
.set_css_text(format!("height: {height}px", height = height).as_str());
|
||||||
|
Msg::NoOp
|
||||||
|
});
|
||||||
|
handlers.push(resize_handler);
|
||||||
|
let text_input_handler = input_ev(Ev::KeyPress, move |value| Msg::InputChanged(id, value));
|
||||||
|
handlers.push(text_input_handler);
|
||||||
|
|
||||||
|
div![
|
||||||
|
attrs![At::Class => "styledTextArea"],
|
||||||
|
div![attrs![At::Class => "textAreaHeading"]],
|
||||||
|
textarea![
|
||||||
|
attrs![
|
||||||
|
At::Class => "textAreaInput";
|
||||||
|
At::ContentEditable => "true";
|
||||||
|
At::Style => style_list.join(";");
|
||||||
|
],
|
||||||
|
handlers,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
@ -386,3 +386,9 @@ pub struct UpdateProjectPayload {
|
|||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub category: Option<String>,
|
pub category: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
|
||||||
|
pub enum WsMsg {
|
||||||
|
Ping,
|
||||||
|
Pong,
|
||||||
|
}
|
||||||
|
@ -19,6 +19,8 @@ actix-web = { version = "*" }
|
|||||||
actix-cors = { version = "*" }
|
actix-cors = { version = "*" }
|
||||||
actix-service = { version = "*" }
|
actix-service = { version = "*" }
|
||||||
actix-rt = "1"
|
actix-rt = "1"
|
||||||
|
actix-web-actors = "*"
|
||||||
|
|
||||||
dotenv = { version = "*" }
|
dotenv = { version = "*" }
|
||||||
byteorder = "1.0"
|
byteorder = "1.0"
|
||||||
chrono = { version = "0.4", features = [ "serde" ] }
|
chrono = { version = "0.4", features = [ "serde" ] }
|
||||||
@ -26,6 +28,7 @@ libc = { version = "0.2.0" }
|
|||||||
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
||||||
quickcheck = { version = "0.4" }
|
quickcheck = { version = "0.4" }
|
||||||
serde_json = { version = ">=0.8.0, <2.0" }
|
serde_json = { version = ">=0.8.0, <2.0" }
|
||||||
|
bincode = "1.2.1"
|
||||||
time = { version = "0.1" }
|
time = { version = "0.1" }
|
||||||
url = { version = "2.1.0" }
|
url = { version = "2.1.0" }
|
||||||
percent-encoding = { version = "2.1.0" }
|
percent-encoding = { version = "2.1.0" }
|
||||||
|
@ -12,6 +12,7 @@ pub mod middleware;
|
|||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod schema;
|
pub mod schema;
|
||||||
|
pub mod ws;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> Result<(), String> {
|
async fn main() -> Result<(), String> {
|
||||||
@ -30,6 +31,7 @@ async fn main() -> Result<(), String> {
|
|||||||
.wrap(Cors::default())
|
.wrap(Cors::default())
|
||||||
.data(db_addr.clone())
|
.data(db_addr.clone())
|
||||||
.data(crate::db::build_pool())
|
.data(crate::db::build_pool())
|
||||||
|
.service(crate::ws::index)
|
||||||
.service(
|
.service(
|
||||||
web::scope("/issues")
|
web::scope("/issues")
|
||||||
.service(crate::routes::issues::project_issues)
|
.service(crate::routes::issues::project_issues)
|
||||||
|
27
jirs-server/src/ws/mod.rs
Normal file
27
jirs-server/src/ws/mod.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use actix::{Actor, StreamHandler};
|
||||||
|
use actix_web::{get, web, Error, HttpRequest, HttpResponse};
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
|
||||||
|
struct MyWs;
|
||||||
|
|
||||||
|
impl Actor for MyWs {
|
||||||
|
type Context = ws::WebsocketContext<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
|
||||||
|
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||||
|
match msg {
|
||||||
|
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||||
|
Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||||
|
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/ws/")]
|
||||||
|
pub async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
let resp = ws::start(MyWs {}, &req, stream);
|
||||||
|
println!("{:?}", resp);
|
||||||
|
resp
|
||||||
|
}
|
@ -4,22 +4,6 @@ import TextareaAutoSize from 'react-textarea-autosize';
|
|||||||
|
|
||||||
import { StyledTextarea } from './Styles';
|
import { StyledTextarea } from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
invalid: PropTypes.bool,
|
|
||||||
minRows: PropTypes.number,
|
|
||||||
value: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
className: undefined,
|
|
||||||
invalid: false,
|
|
||||||
minRows: 2,
|
|
||||||
value: undefined,
|
|
||||||
onChange: () => {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps }, ref) => (
|
const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps }, ref) => (
|
||||||
<StyledTextarea className={className} invalid={invalid}>
|
<StyledTextarea className={className} invalid={invalid}>
|
||||||
<TextareaAutoSize
|
<TextareaAutoSize
|
||||||
@ -30,7 +14,20 @@ const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps },
|
|||||||
</StyledTextarea>
|
</StyledTextarea>
|
||||||
));
|
));
|
||||||
|
|
||||||
Textarea.propTypes = propTypes;
|
Textarea.propTypes = {
|
||||||
Textarea.defaultProps = defaultProps;
|
className: PropTypes.string,
|
||||||
|
invalid: PropTypes.bool,
|
||||||
|
minRows: PropTypes.number,
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
Textarea.defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
invalid: false,
|
||||||
|
minRows: 2,
|
||||||
|
value: undefined,
|
||||||
|
onChange: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
export default Textarea;
|
export default Textarea;
|
||||||
|
Loading…
Reference in New Issue
Block a user