Most of fields. Send on blur
This commit is contained in:
parent
767d247647
commit
b334927829
@ -23,11 +23,11 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.styledButton:not(.onlyIcon) {
|
||||
.styledButton:not(.iconOnly) {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.styledButton.onlyIcon {
|
||||
.styledButton.iconOnly {
|
||||
padding: 0 9px;
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -11,7 +11,8 @@
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.styledForm > .formElement .selectItem {
|
||||
/*.styledForm > .formElement*/
|
||||
.selectItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
|
@ -2,6 +2,7 @@ i.styledIcon {
|
||||
color: var(--primary);
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
i.styledIcon.left {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
],
|
||||
],
|
||||
]
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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],
|
||||
]
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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>,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user