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;
}
.styledButton:not(.onlyIcon) {
.styledButton:not(.iconOnly) {
padding: 0 12px;
}
.styledButton.onlyIcon {
.styledButton.iconOnly {
padding: 0 9px;
}

View File

@ -25,13 +25,23 @@
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 {
background: #fff;
border: 1px solid var(--borderInputFocus);
box-shadow: 0 0 0 1px var(--borderInputFocus);
}
.styledEditor > .navbar.active {
.styledEditor > .navbar {
border-color: var(--borderInputFocus);
}
@ -48,14 +58,15 @@
display: none;
}
.styledEditor > input.editorRadio:checked ~ .styledTextArea {
display: block;
}
.styledEditor > .view {
grid-area: view;
display: none;
min-height: 40px;
padding-top: 15px;
}
.styledEditor > input.editorRadio:checked ~ .styledTextArea {
display: block;
}
.styledEditor > input.viewRadio:checked ~ .view {

View File

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

View File

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

View File

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

View File

@ -1,13 +1,15 @@
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::shared::styled_avatar::StyledAvatar;
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_editor::StyledEditor;
use crate::shared::styled_field::StyledField;
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_textarea::StyledTextarea;
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);
match msg {
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
modal.payload = issue.clone().into();
}
Msg::StyledSelectChanged(
FieldId::IssueTypeEditModalTop,
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_type = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::StatusIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.status = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::ReporterIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.reporter_id = *value as i32;
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal,
StyledSelectChange::Changed(value),
) => {
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(
FieldId::PriorityIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.priority = (*value).into();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::InputChanged(FieldId::TitleIssueEditModal, value) => {
modal.payload.title = value.clone();
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => {
modal.payload.description = 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)) => {
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)
.text(description_text)
.mode(description_editor_mode.clone())
.update_on(Ev::Change)
.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()
.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![
attrs![At::Class => "issueDetails"],
div![
@ -233,7 +312,14 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
description_field,
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 jirs_data::UpdateIssuePayload;
use jirs_data::*;
use crate::api::send_ws_msg;
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
use crate::shared::styled_editor::Mode;
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());
}
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() => {
push_edit_modal(&issue_id, model)
}

View File

@ -20,6 +20,7 @@ pub struct StyledEditor {
id: FieldId,
text: String,
mode: Mode,
update_event: Ev,
}
impl StyledEditor {
@ -28,6 +29,7 @@ impl StyledEditor {
id,
text: String::new(),
mode: Mode::Editor,
update_event: None,
}
}
}
@ -37,6 +39,7 @@ pub struct StyledEditorBuilder {
id: FieldId,
text: String,
mode: Mode,
update_event: Option<Ev>,
}
impl StyledEditorBuilder {
@ -58,8 +61,14 @@ impl StyledEditorBuilder {
id: self.id,
text: self.text,
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 {
@ -69,7 +78,12 @@ impl ToNode for StyledEditor {
}
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 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()
.height(40)
.update_on(update_event)
.value(text.as_str())
.build(id.clone())
.into_node();
@ -129,11 +144,13 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
Mode::View => (
seed::input![
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![
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![
attrs![At::Class => "styledEditor"],
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",
on_editor_clicked
],
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",
on_view_clicked
],
editor_radio_node,
text_area,
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 {
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));

View File

@ -10,6 +10,7 @@ pub struct StyledTextarea {
max_height: usize,
value: String,
class_list: Vec<String>,
update_event: Ev,
}
impl ToNode for StyledTextarea {
@ -31,6 +32,7 @@ pub struct StyledTextareaBuilder {
on_change: Option<EventHandler<Msg>>,
value: String,
class_list: Vec<String>,
update_event: Option<Ev>,
}
impl StyledTextareaBuilder {
@ -64,6 +66,11 @@ impl StyledTextareaBuilder {
self
}
pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev);
self
}
#[inline]
pub fn build(self, id: FieldId) -> StyledTextarea {
StyledTextarea {
@ -72,6 +79,7 @@ impl StyledTextareaBuilder {
height: self.height.unwrap_or(110),
class_list: self.class_list,
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,
value,
mut class_list,
update_event,
} = values;
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 {
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 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
};
let min_height = get_min_height(value.as_str(), height as f64);
text_area
.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
});
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);
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>,
}
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)]
pub struct CreateCommentPayload {
pub user_id: Option<i32>,

View File

@ -1,5 +1,11 @@
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 issues(
title,

View File

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