Most of fields. Send on blur

This commit is contained in:
Adrian Wozniak 2020-04-10 22:33:07 +02:00
parent 767d247647
commit b334927829
13 changed files with 205 additions and 39 deletions

View File

@ -23,11 +23,11 @@
cursor: default; cursor: default;
} }
.styledButton:not(.onlyIcon) { .styledButton:not(.iconOnly) {
padding: 0 12px; padding: 0 12px;
} }
.styledButton.onlyIcon { .styledButton.iconOnly {
padding: 0 9px; padding: 0 9px;
} }

View File

@ -25,13 +25,23 @@
font-size: 14.5px; font-size: 14.5px;
} }
.styledEditor > .navbar:not(:hover) {
border-color: var(--backgroundLightest);
background-color: var(--borderLight);
}
.styledEditor > .navbar.activeTab {
background-color: var(--backgroundLightest);
border-color: var(--borderLight);
}
.styledEditor > .navbar:hover { .styledEditor > .navbar:hover {
background: #fff; background: #fff;
border: 1px solid var(--borderInputFocus); border: 1px solid var(--borderInputFocus);
box-shadow: 0 0 0 1px var(--borderInputFocus); box-shadow: 0 0 0 1px var(--borderInputFocus);
} }
.styledEditor > .navbar.active { .styledEditor > .navbar {
border-color: var(--borderInputFocus); border-color: var(--borderInputFocus);
} }
@ -48,14 +58,15 @@
display: none; display: none;
} }
.styledEditor > input.editorRadio:checked ~ .styledTextArea {
display: block;
}
.styledEditor > .view { .styledEditor > .view {
grid-area: view; grid-area: view;
display: none; display: none;
min-height: 40px; min-height: 40px;
padding-top: 15px;
}
.styledEditor > input.editorRadio:checked ~ .styledTextArea {
display: block;
} }
.styledEditor > input.viewRadio:checked ~ .view { .styledEditor > input.viewRadio:checked ~ .view {

View File

@ -11,7 +11,8 @@
font-size: 21px; font-size: 21px;
} }
.styledForm > .formElement .selectItem { /*.styledForm > .formElement*/
.selectItem {
display: flex; display: flex;
align-items: center; align-items: center;
margin-right: 15px; margin-right: 15px;

View File

@ -2,6 +2,7 @@ i.styledIcon {
color: var(--primary); color: var(--primary);
display: inline-block; display: inline-block;
font-size: 16px; font-size: 16px;
line-height: 1;
} }
i.styledIcon.left { i.styledIcon.left {

View File

@ -13,7 +13,8 @@
background: var(--backgroundLightest); background: var(--backgroundLightest);
font-family: var(--font-regular); font-family: var(--font-regular);
font-weight: normal; font-weight: normal;
font-size: 15px font-size: 15px;
resize: vertical;
} }
.styledTextArea > textarea:focus { .styledTextArea > textarea:focus {

View File

@ -1,13 +1,15 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::{IssuePriority, IssueStatus, IssueType, ToVec, User}; use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{EditIssueModal, ModalType, Model}; use crate::model::{EditIssueModal, ModalType, Model};
use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_editor::StyledEditor; use crate::shared::styled_editor::StyledEditor;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange}; use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange};
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::ToNode; use crate::shared::ToNode;
@ -25,42 +27,66 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
modal.priority_state.update(msg, orders); modal.priority_state.update(msg, orders);
match msg { match msg {
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
modal.payload = issue.clone().into();
}
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::IssueTypeEditModalTop, FieldId::IssueTypeEditModalTop,
StyledSelectChange::Changed(value), StyledSelectChange::Changed(value),
) => { ) => {
modal.payload.issue_type = (*value).into(); modal.payload.issue_type = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::StatusIssueEditModal, FieldId::StatusIssueEditModal,
StyledSelectChange::Changed(value), StyledSelectChange::Changed(value),
) => { ) => {
modal.payload.status = (*value).into(); modal.payload.status = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::ReporterIssueEditModal, FieldId::ReporterIssueEditModal,
StyledSelectChange::Changed(value), StyledSelectChange::Changed(value),
) => { ) => {
modal.payload.reporter_id = *value as i32; modal.payload.reporter_id = *value as i32;
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal, FieldId::AssigneesIssueEditModal,
StyledSelectChange::Changed(value), StyledSelectChange::Changed(value),
) => { ) => {
modal.payload.user_ids.push(*value as i32); modal.payload.user_ids.push(*value as i32);
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal,
StyledSelectChange::RemoveMulti(value),
) => {
let mut old = vec![];
std::mem::swap(&mut old, &mut modal.payload.user_ids);
let dropped = *value as i32;
for id in old.into_iter() {
if id != dropped {
modal.payload.user_ids.push(id);
}
}
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::StyledSelectChanged( Msg::StyledSelectChanged(
FieldId::PriorityIssueEditModal, FieldId::PriorityIssueEditModal,
StyledSelectChange::Changed(value), StyledSelectChange::Changed(value),
) => { ) => {
modal.payload.priority = (*value).into(); modal.payload.priority = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::InputChanged(FieldId::TitleIssueEditModal, value) => { Msg::InputChanged(FieldId::TitleIssueEditModal, value) => {
modal.payload.title = value.clone(); modal.payload.title = value.clone();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => { Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => {
modal.payload.description = Some(value.clone()); modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone()); modal.payload.description_text = Some(value.clone());
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
} }
Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => { Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => {
modal.description_editor_mode = mode.clone(); modal.description_editor_mode = mode.clone();
@ -164,6 +190,7 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let description = StyledEditor::build(FieldId::DescriptionIssueEditModal) let description = StyledEditor::build(FieldId::DescriptionIssueEditModal)
.text(description_text) .text(description_text)
.mode(description_editor_mode.clone()) .mode(description_editor_mode.clone())
.update_on(Ev::Change)
.build() .build()
.into_node(); .into_node();
let description_field = StyledField::build().input(description).build().into_node(); let description_field = StyledField::build().input(description).build().into_node();
@ -213,6 +240,58 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let reporter = StyledSelect::build(FieldId::ReporterIssueEditModal)
.name("reporter")
.opened(modal.reporter_state.opened)
.normal()
.text_filter(modal.reporter_state.text_filter.as_str())
.options(model.users.iter().map(|user| UserOption(user)).collect())
.selected(
model
.users
.iter()
.filter(|user| payload.reporter_id == user.id)
.map(|user| UserOption(user))
.collect(),
)
.build()
.into_node();
let reporter_field = StyledField::build()
.input(reporter)
.label("Reporter")
.build()
.into_node();
let priority = StyledSelect::build(FieldId::PriorityIssueEditModal)
.name("assignees")
.opened(modal.priority_state.opened)
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
.options(
IssuePriority::ordered()
.into_iter()
.map(|p| IssuePriorityOption(p))
.collect(),
)
.selected(vec![IssuePriorityOption(payload.priority.clone())])
.build()
.into_node();
let priority_field = StyledField::build()
.input(priority)
.label("Priority")
.build()
.into_node();
let estimate = StyledInput::build(FieldId::EstimateIssueEditModal)
.valid(true)
.build()
.into_node();
let estimate_field = StyledField::build()
.input(estimate)
.label("Original Estimate (hours)")
.build()
.into_node();
div![ div![
attrs![At::Class => "issueDetails"], attrs![At::Class => "issueDetails"],
div![ div![
@ -233,7 +312,14 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
description_field, description_field,
div![attrs![At::Class => "comments"]], div![attrs![At::Class => "comments"]],
], ],
div![attrs![At::Class => "right"], status_field, assignees_field,], div![
attrs![At::Class => "right"],
status_field,
assignees_field,
reporter_field,
priority_field,
estimate_field
],
], ],
] ]
} }

View File

@ -1,9 +1,7 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::UpdateIssuePayload; use jirs_data::UpdateIssuePayload;
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page}; use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_editor::Mode; use crate::shared::styled_editor::Mode;
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
@ -37,7 +35,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
model.modals.push(modal_type.clone()); model.modals.push(modal_type.clone());
} }
Msg::WsMsg(jirs_data::WsMsg::ProjectIssuesLoaded(issues)) => match model.page.clone() { Msg::WsMsg(jirs_data::WsMsg::ProjectIssuesLoaded(_issues)) => match model.page.clone() {
Page::EditIssue(issue_id) if model.modals.is_empty() => { Page::EditIssue(issue_id) if model.modals.is_empty() => {
push_edit_modal(&issue_id, model) push_edit_modal(&issue_id, model)
} }

View File

@ -20,6 +20,7 @@ pub struct StyledEditor {
id: FieldId, id: FieldId,
text: String, text: String,
mode: Mode, mode: Mode,
update_event: Ev,
} }
impl StyledEditor { impl StyledEditor {
@ -28,6 +29,7 @@ impl StyledEditor {
id, id,
text: String::new(), text: String::new(),
mode: Mode::Editor, mode: Mode::Editor,
update_event: None,
} }
} }
} }
@ -37,6 +39,7 @@ pub struct StyledEditorBuilder {
id: FieldId, id: FieldId,
text: String, text: String,
mode: Mode, mode: Mode,
update_event: Option<Ev>,
} }
impl StyledEditorBuilder { impl StyledEditorBuilder {
@ -58,8 +61,14 @@ impl StyledEditorBuilder {
id: self.id, id: self.id,
text: self.text, text: self.text,
mode: self.mode, mode: self.mode,
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
} }
} }
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
} }
impl ToNode for StyledEditor { impl ToNode for StyledEditor {
@ -69,7 +78,12 @@ impl ToNode for StyledEditor {
} }
pub fn render(values: StyledEditor) -> Node<Msg> { pub fn render(values: StyledEditor) -> Node<Msg> {
let StyledEditor { id, text, mode } = values; let StyledEditor {
id,
text,
mode,
update_event,
} = values;
let field_id = id.clone(); let field_id = id.clone();
let on_editor_clicked = mouse_ev(Ev::Click, move |ev| { let on_editor_clicked = mouse_ev(Ev::Click, move |ev| {
@ -89,6 +103,7 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
let text_area = StyledTextarea::build() let text_area = StyledTextarea::build()
.height(40) .height(40)
.update_on(update_event)
.value(text.as_str()) .value(text.as_str())
.build(id.clone()) .build(id.clone())
.into_node(); .into_node();
@ -129,11 +144,13 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
Mode::View => ( Mode::View => (
seed::input![ seed::input![
id![editor_id.as_str()], id![editor_id.as_str()],
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio";], class!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str();],
], ],
seed::input![ seed::input![
id![view_id.as_str()], id![view_id.as_str()],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio"; At::Checked => true], class!["viewRadio"],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
], ],
), ),
}; };
@ -141,18 +158,28 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
div![ div![
attrs![At::Class => "styledEditor"], attrs![At::Class => "styledEditor"],
label![ label![
attrs![At::Class => "navbar editorTab", At::For => editor_id.as_str()], if mode == Mode::Editor {
class!["navbar editorTab activeTab"]
} else {
class!["navbar editorTab"]
},
attrs![At::For => editor_id.as_str()],
"Editor", "Editor",
on_editor_clicked on_editor_clicked
], ],
label![ label![
attrs![At::Class => "navbar viewTab"; At::For => view_id.as_str()], if mode == Mode::View {
class!["navbar viewTab activeTab"]
} else {
class!["navbar viewTab"]
},
attrs![At::For => view_id.as_str()],
"View", "View",
on_view_clicked on_view_clicked
], ],
editor_radio_node, editor_radio_node,
text_area, text_area,
view_radio_node, view_radio_node,
div![attrs![At::Class => "view"], parsed_node] div![attrs![At::Class => "view"], parsed_node],
] ]
} }

View File

@ -206,7 +206,7 @@ pub fn render(values: StyledIcon) -> Node<Msg> {
} }
if let Some(size) = size { if let Some(size) = size {
style_list.push(format!("width: {s}px; height: {s}px", s = size)); style_list.push(format!("font-size: {s}px", s = size));
} }
class_list.push(format!("styledIcon {}", icon)); class_list.push(format!("styledIcon {}", icon));

View File

@ -10,6 +10,7 @@ pub struct StyledTextarea {
max_height: usize, max_height: usize,
value: String, value: String,
class_list: Vec<String>, class_list: Vec<String>,
update_event: Ev,
} }
impl ToNode for StyledTextarea { impl ToNode for StyledTextarea {
@ -31,6 +32,7 @@ pub struct StyledTextareaBuilder {
on_change: Option<EventHandler<Msg>>, on_change: Option<EventHandler<Msg>>,
value: String, value: String,
class_list: Vec<String>, class_list: Vec<String>,
update_event: Option<Ev>,
} }
impl StyledTextareaBuilder { impl StyledTextareaBuilder {
@ -64,6 +66,11 @@ impl StyledTextareaBuilder {
self self
} }
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
#[inline] #[inline]
pub fn build(self, id: FieldId) -> StyledTextarea { pub fn build(self, id: FieldId) -> StyledTextarea {
StyledTextarea { StyledTextarea {
@ -72,6 +79,7 @@ impl StyledTextareaBuilder {
height: self.height.unwrap_or(110), height: self.height.unwrap_or(110),
class_list: self.class_list, class_list: self.class_list,
max_height: self.max_height.unwrap_or_default(), max_height: self.max_height.unwrap_or_default(),
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
} }
} }
} }
@ -96,11 +104,15 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
max_height, max_height,
value, value,
mut class_list, mut class_list,
update_event,
} = values; } = values;
let mut style_list = vec![]; let mut style_list = vec![];
if height > 0 {
style_list.push(format!("min-height: {}px", height)); let min_height = get_min_height(value.as_str(), height as f64);
if min_height > 0f64 {
style_list.push(format!("min-height: {}px", min_height));
} }
if max_height > 0 { if max_height > 0 {
style_list.push(format!("max-height: {}px", max_height)); style_list.push(format!("max-height: {}px", max_height));
} }
@ -116,22 +128,17 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
}; };
let text_area = target.dyn_ref::<web_sys::HtmlTextAreaElement>().unwrap(); let text_area = target.dyn_ref::<web_sys::HtmlTextAreaElement>().unwrap();
let value: String = text_area.value(); let value: String = text_area.value();
let len = value.lines().count() as f64; let min_height = get_min_height(value.as_str(), height 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 text_area
.style() .style()
.set_css_text(format!("height: {height}px", height = height).as_str()); .set_css_text(format!("height: {min_height}px", min_height = min_height).as_str());
Msg::NoOp Msg::NoOp
}); });
handlers.push(resize_handler); handlers.push(resize_handler);
let text_input_handler = input_ev(Ev::KeyUp, move |value| Msg::InputChanged(id, value)); let text_input_handler = input_ev(update_event.clone(), move |value| {
Msg::InputChanged(id, value)
});
handlers.push(text_input_handler); handlers.push(text_input_handler);
class_list.push("textAreaInput".to_string()); class_list.push("textAreaInput".to_string());
@ -150,3 +157,15 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
] ]
] ]
} }
fn get_min_height(value: &str, min_height: f64) -> f64 {
let len = value.lines().count() as f64;
if value.chars().last() == Some('\n') {}
let calc_height = (len * LETTER_HEIGHT) + ADDITIONAL_HEIGHT;
if calc_height + ADDITIONAL_HEIGHT < min_height {
min_height
} else {
calc_height + ADDITIONAL_HEIGHT
}
}

View File

@ -401,6 +401,26 @@ pub struct UpdateIssuePayload {
pub user_ids: Vec<i32>, pub user_ids: Vec<i32>,
} }
impl From<Issue> for UpdateIssuePayload {
fn from(issue: Issue) -> Self {
Self {
title: issue.title,
issue_type: issue.issue_type,
status: issue.status,
priority: issue.priority,
list_position: issue.list_position,
description: issue.description,
description_text: issue.description_text,
estimate: issue.estimate,
time_spent: issue.time_spent,
time_remaining: issue.time_remaining,
project_id: issue.project_id,
reporter_id: issue.reporter_id,
user_ids: issue.user_ids,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateCommentPayload { pub struct CreateCommentPayload {
pub user_id: Option<i32>, pub user_id: Option<i32>,

View File

@ -1,5 +1,11 @@
insert into projects (name) values ('initial'), ('second'), ('third'); insert into projects (name) values ('initial'), ('second'), ('third');
insert into users (project_id, email, name, avatar_url) values (1, 'john@example.com', 'John Doe', 'http://cdn.onlinewebfonts.com/svg/img_553934.png'), (1, 'kate@exampe.com', 'Kate Snow', 'http://www.asthmamd.org/images/icon_user_6.png'); insert into users (project_id, email, name, avatar_url) values (
1, 'john@example.com', 'John Doe', 'http://cdn.onlinewebfonts.com/svg/img_553934.png
), (
1, 'kate@exampe.com', 'Kate Snow', 'http://www.asthmamd.org/images/icon_user_6.png
), (
1, 'mike@example.com', 'Mike Keningham', 'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png'
);
insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() );
insert into issues( insert into issues(
title, title,

View File

@ -37,7 +37,7 @@ impl WebSocketActor {
use futures::executor::block_on; use futures::executor::block_on;
if msg != WsMsg::Ping && msg != WsMsg::Pong { if msg != WsMsg::Ping && msg != WsMsg::Pong {
info!("(2)incoming message: {:?}", msg); info!("incoming message: {:?}", msg);
} }
let msg = match msg { let msg = match msg {
@ -196,10 +196,6 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WebSocketActor {
Ok(m) => m, Ok(m) => m,
_ => return, _ => return,
}; };
if msg != WsMsg::Ping && msg != WsMsg::Pong {
info!("(1)incoming message: {:?}", msg);
}
let _x = 1;
match self.handle_ws_msg(msg) { match self.handle_ws_msg(msg) {
Ok(Some(msg)) => ctx.send_msg(msg), Ok(Some(msg)) => ctx.send_msg(msg),
Err(e) => ctx.send_msg(e), Err(e) => ctx.send_msg(e),