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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.2.1"
|
||||
@ -425,6 +440,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "bitflags"
|
||||
version = "1.2.1"
|
||||
@ -1038,10 +1063,12 @@ version = "0.1.0"
|
||||
name = "jirs-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"futures 0.1.29",
|
||||
"jirs-data",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"seed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1070,7 +1097,9 @@ dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-web",
|
||||
"actix-web-actors",
|
||||
"bigdecimal",
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
|
@ -18,11 +18,15 @@ jirs-data = { path = "../jirs-data" }
|
||||
seed = { version = "*" }
|
||||
serde = "*"
|
||||
serde_json = "*"
|
||||
bincode = "1.2.1"
|
||||
chrono = { version = "*", features = [ "serde" ] }
|
||||
uuid = { version = "*", features = [ "serde" ] }
|
||||
wasm-bindgen = "*"
|
||||
js-sys = "*"
|
||||
futures = "^0.1.26"
|
||||
lazy_static = "*"
|
||||
|
||||
[dependencies.js-sys]
|
||||
js-sys = "*"
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "*"
|
||||
@ -34,5 +38,9 @@ features = [
|
||||
"DomRect",
|
||||
"HtmlDocument",
|
||||
"Document",
|
||||
"Selection"
|
||||
"Selection",
|
||||
"CssStyleDeclaration",
|
||||
"WebSocket",
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
]
|
||||
|
@ -62,3 +62,31 @@
|
||||
.styledField > * {
|
||||
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 wasm_bindgen::prelude::*;
|
||||
|
||||
use jirs_data::UpdateIssuePayload;
|
||||
use jirs_data::{UpdateIssuePayload, WsMsg};
|
||||
|
||||
use crate::shared::host_client;
|
||||
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> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
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::{prelude::*, *};
|
||||
|
||||
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;
|
||||
|
||||
mod api;
|
||||
@ -22,9 +26,16 @@ pub type AvatarFilterActive = bool;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FieldId {
|
||||
// edit issue
|
||||
IssueTypeEditModalTop,
|
||||
// project boards
|
||||
TextFilterBoard,
|
||||
//
|
||||
CopyButtonLabel,
|
||||
// add issue
|
||||
IssueTypeAddIssueModal,
|
||||
SummaryAddIssueModal,
|
||||
DescriptionAddIssueModal,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -55,6 +66,9 @@ pub enum Msg {
|
||||
IssueDragStopped(IssueId),
|
||||
IssueDropZone(IssueStatus),
|
||||
|
||||
// inputs
|
||||
InputChanged(FieldId, String),
|
||||
|
||||
// issues
|
||||
IssueUpdateResult(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]
|
||||
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_field::StyledField;
|
||||
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_modal::{StyledModal, Variant as ModalVariant};
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
@ -35,8 +36,7 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let short_summary = StyledInput::build()
|
||||
.id("issue-short-summary")
|
||||
let short_summary = StyledInput::build(FieldId::SummaryAddIssueModal)
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
@ -47,6 +47,17 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.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()
|
||||
.primary()
|
||||
.text("Create Issue")
|
||||
@ -66,6 +77,7 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.add_field(issue_type_field)
|
||||
.add_field(crate::shared::divider())
|
||||
.add_field(short_summary_field)
|
||||
.add_field(description_field)
|
||||
.add_field(actions)
|
||||
.build()
|
||||
.into_node();
|
||||
|
@ -94,7 +94,7 @@ pub struct ProjectPage {
|
||||
pub dragged_issue_id: Option<IssueId>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
pub access_token: Option<Uuid>,
|
||||
pub user: Option<User>,
|
||||
|
@ -4,11 +4,11 @@ use jirs_data::*;
|
||||
|
||||
use crate::model::{Model, Page};
|
||||
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_input::StyledInput;
|
||||
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>) {
|
||||
match msg {
|
||||
@ -172,11 +172,11 @@ fn header() -> 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)
|
||||
.id("searchInput")
|
||||
.valid(true)
|
||||
.on_change(input_ev(Ev::Change, |value| {
|
||||
crate::api::ws_send(WsMsg::Ping);
|
||||
Msg::ProjectTextFilterChanged(value)
|
||||
}))
|
||||
.build()
|
||||
|
@ -17,6 +17,7 @@ pub mod styled_icon;
|
||||
pub mod styled_input;
|
||||
pub mod styled_modal;
|
||||
pub mod styled_select;
|
||||
pub mod styled_textarea;
|
||||
pub mod styled_tooltip;
|
||||
|
||||
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::ToNode;
|
||||
use crate::Msg;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledInput {
|
||||
id: Option<String>,
|
||||
id: FieldId,
|
||||
icon: Option<Icon>,
|
||||
valid: bool,
|
||||
on_change: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl StyledInput {
|
||||
pub fn build() -> StyledInputBuilder {
|
||||
StyledInputBuilder::default()
|
||||
pub fn build(id: FieldId) -> StyledInputBuilder {
|
||||
StyledInputBuilder {
|
||||
id,
|
||||
icon: None,
|
||||
valid: None,
|
||||
on_change: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct StyledInputBuilder {
|
||||
id: Option<String>,
|
||||
id: FieldId,
|
||||
icon: Option<Icon>,
|
||||
valid: Option<bool>,
|
||||
on_change: Option<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
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 {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
@ -89,15 +86,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
let input_node = match on_change {
|
||||
Some(on_change) => seed::input![attrs![At::Class => input_class_list.join(" ")], on_change],
|
||||
_ => seed::input![attrs![At::Class => input_class_list.join(" ")]],
|
||||
};
|
||||
let mut handlers = vec![];
|
||||
|
||||
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![
|
||||
id![id.unwrap_or_default()],
|
||||
attrs!(At::Class => wrapper_class_list.join(" ")),
|
||||
icon,
|
||||
input_node,
|
||||
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers],
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldId, Msg};
|
||||
@ -51,7 +50,6 @@ where
|
||||
variant: Variant,
|
||||
dropdown_width: Option<usize>,
|
||||
name: Option<String>,
|
||||
placeholder: Option<String>,
|
||||
valid: bool,
|
||||
is_multi: bool,
|
||||
allow_clear: bool,
|
||||
@ -80,7 +78,6 @@ where
|
||||
variant: None,
|
||||
dropdown_width: None,
|
||||
name: None,
|
||||
placeholder: None,
|
||||
valid: None,
|
||||
is_multi: None,
|
||||
allow_clear: None,
|
||||
@ -101,7 +98,6 @@ where
|
||||
variant: Option<Variant>,
|
||||
dropdown_width: Option<Option<usize>>,
|
||||
name: Option<Option<String>>,
|
||||
placeholder: Option<Option<String>>,
|
||||
valid: Option<bool>,
|
||||
is_multi: Option<bool>,
|
||||
allow_clear: Option<bool>,
|
||||
@ -121,7 +117,6 @@ where
|
||||
variant: self.variant.unwrap_or_default(),
|
||||
dropdown_width: self.dropdown_width.unwrap_or_default(),
|
||||
name: self.name.unwrap_or_default(),
|
||||
placeholder: self.placeholder.unwrap_or_default(),
|
||||
valid: self.valid.unwrap_or(true),
|
||||
is_multi: self.is_multi.unwrap_or_default(),
|
||||
allow_clear: self.allow_clear.unwrap_or_default(),
|
||||
@ -188,7 +183,6 @@ where
|
||||
variant,
|
||||
dropdown_width,
|
||||
name,
|
||||
placeholder: _,
|
||||
valid,
|
||||
is_multi,
|
||||
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 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-service = { version = "*" }
|
||||
actix-rt = "1"
|
||||
actix-web-actors = "*"
|
||||
|
||||
dotenv = { version = "*" }
|
||||
byteorder = "1.0"
|
||||
chrono = { version = "0.4", features = [ "serde" ] }
|
||||
@ -26,6 +28,7 @@ libc = { version = "0.2.0" }
|
||||
pq-sys = { version = ">=0.3.0, <0.5.0" }
|
||||
quickcheck = { version = "0.4" }
|
||||
serde_json = { version = ">=0.8.0, <2.0" }
|
||||
bincode = "1.2.1"
|
||||
time = { version = "0.1" }
|
||||
url = { version = "2.1.0" }
|
||||
percent-encoding = { version = "2.1.0" }
|
||||
|
@ -12,6 +12,7 @@ pub mod middleware;
|
||||
pub mod models;
|
||||
pub mod routes;
|
||||
pub mod schema;
|
||||
pub mod ws;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> Result<(), String> {
|
||||
@ -30,6 +31,7 @@ async fn main() -> Result<(), String> {
|
||||
.wrap(Cors::default())
|
||||
.data(db_addr.clone())
|
||||
.data(crate::db::build_pool())
|
||||
.service(crate::ws::index)
|
||||
.service(
|
||||
web::scope("/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';
|
||||
|
||||
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) => (
|
||||
<StyledTextarea className={className} invalid={invalid}>
|
||||
<TextareaAutoSize
|
||||
@ -30,7 +14,20 @@ const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps },
|
||||
</StyledTextarea>
|
||||
));
|
||||
|
||||
Textarea.propTypes = propTypes;
|
||||
Textarea.defaultProps = defaultProps;
|
||||
Textarea.propTypes = {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user