Add missing RTE features, add focus after RTE change, fix handle change.
Add Epic as issue type.
This commit is contained in:
parent
7aadc12c3a
commit
ba08c08e35
400
Cargo.lock
generated
400
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,15 @@
|
||||
#license = "MPL-2.0"
|
||||
#license-file = "./LICENSE"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0 # Use slightly better optimizations.
|
||||
overflow-checks = false # Disable integer overflow checks.
|
||||
debug = 2
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 's'
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./jirs-cli",
|
||||
|
@ -13,6 +13,10 @@ crate-type = ["cdylib", "rlib"]
|
||||
name = "jirs_client"
|
||||
path = "./src/lib.rs"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 0 # Use slightly better optimizations.
|
||||
overflow-checks = false # Disable integer overflow checks.
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 's'
|
||||
@ -21,20 +25,24 @@ opt-level = 's'
|
||||
jirs-data = { path = "../jirs-data" }
|
||||
seed = { version = "0.7.0" }
|
||||
serde = "*"
|
||||
serde_json = "*"
|
||||
bincode = "1.2.1"
|
||||
chrono = { version = "0.4", features = [ "serde", "wasmbind" ] }
|
||||
uuid = { version = "0.8.1", features = [ "serde" ] }
|
||||
wasm-bindgen = "0.2.60"
|
||||
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
|
||||
uuid = { version = "0.8.1", features = ["serde"] }
|
||||
futures = "^0.1.26"
|
||||
comrak = "*"
|
||||
wee_alloc = "*"
|
||||
|
||||
[dependencies.wasm-bindgen]
|
||||
version = "0.2.66"
|
||||
features = ["enable-interning"]
|
||||
|
||||
[dependencies.js-sys]
|
||||
version = "*"
|
||||
default-features = false
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.22"
|
||||
default-features = false
|
||||
features = [
|
||||
# elements
|
||||
"Window",
|
||||
|
@ -1,13 +1,13 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl http2;
|
||||
# listen 443 ssl http2;
|
||||
server_name jirs.lvh.me;
|
||||
|
||||
charset utf-8;
|
||||
root /home/eraden/code/eraden/jirs/jirs-client/tmp;
|
||||
|
||||
ssl_certificate /home/eraden/code/eraden/jirs/jirs-client/js/nginx-selfsigned.crt;
|
||||
ssl_certificate_key /home/eraden/code/eraden/jirs/jirs-client/js/nginx-selfsigned.key;
|
||||
# ssl_certificate /home/eraden/code/eraden/jirs/jirs-client/js/nginx-selfsigned.crt;
|
||||
# ssl_certificate_key /home/eraden/code/eraden/jirs/jirs-client/js/nginx-selfsigned.key;
|
||||
|
||||
# if ($scheme != "https") {
|
||||
# return 301 https://$host$request_uri;
|
||||
|
@ -41,6 +41,10 @@ i.styledIcon.story:before {
|
||||
content: "\ef2d";
|
||||
}
|
||||
|
||||
i.styledIcon.epic:before {
|
||||
content: '\ef30';
|
||||
}
|
||||
|
||||
i.styledIcon.arrowDown:before {
|
||||
content: "\ea92";
|
||||
}
|
||||
|
@ -11,12 +11,17 @@
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row {
|
||||
padding: 5px 0;
|
||||
padding: 0 0 var(--rte-indent) 0;
|
||||
display: flex;
|
||||
margin: 0 var(--rte-indent);
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row:first-child {
|
||||
padding: var(--rte-indent) 0;
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row > .group {
|
||||
padding: 0 5px 0 0;
|
||||
padding: 0 var(--rte-indent) 0 0;
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row > .group:first-child {
|
||||
@ -30,7 +35,21 @@
|
||||
.styledRte > .bar > .row > .group > .styledRteButton > .styledButton,
|
||||
.styledRte > .bar > .row > .group > .styledButton,
|
||||
.styledRte > .bar > .row > .group > span.headingList {
|
||||
margin: 0 5px;
|
||||
margin-right: var(--rte-indent);
|
||||
font-size: var(--small-font-size);
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row > .group.font > span.headingList > .headingOption {
|
||||
margin-right: var(--rte-indent);
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row > .group > .styledRteButton > .styledButton,
|
||||
.styledRte > .bar > .row > .group > .styledRteButton > .styledButton > .styledIcon,
|
||||
.styledRte > .bar > .row > .group > span.headingList > .headingOption > .styledButton,
|
||||
.styledRte > .bar > .row > .group > span.headingList > .headingOption > .styledButton > span {
|
||||
font-size: var(--small-font-size);
|
||||
line-height: calc(2 * var(--small-font-size));
|
||||
height: calc(2 * var(--small-font-size));
|
||||
}
|
||||
|
||||
.styledRte > .bar > .row > .group > .headingList {
|
||||
@ -64,6 +83,14 @@
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.styledRte > .editorWrapper > .editor ul > li {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
.styledRte > .editorWrapper > .editor ol > li {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.styledRte > .editorWrapper > .editor table {
|
||||
table-layout: fixed;
|
||||
border-spacing: 0;
|
||||
@ -74,13 +101,17 @@
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
padding: var(--rte-indent);
|
||||
border-bottom: 1px solid var(--borderLight);
|
||||
border-right: 1px solid var(--borderLight);
|
||||
border-left: 1px solid var(--borderLight);
|
||||
border-top: 1px solid var(--borderLight);
|
||||
}
|
||||
|
||||
.styledRte > .editorWrapper > .editor *:focus {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
/**********************************************************/
|
||||
/* Table tooltip */
|
||||
/**********************************************************/
|
||||
@ -113,7 +144,7 @@
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
padding: var(--rte-indent);
|
||||
border-bottom: 1px solid var(--borderLight);
|
||||
border-right: 1px solid var(--borderLight);
|
||||
border-left: 1px solid var(--borderLight);
|
||||
|
@ -25,6 +25,7 @@
|
||||
--task: rgb(79, 173, 230); /* blue */
|
||||
--bug: rgb(228, 77, 66); /* red */
|
||||
--story: rgb(101, 186, 67); /* green */
|
||||
--epic: rgb(186, 142, 67); /* gold */
|
||||
}
|
||||
|
||||
:root {
|
||||
@ -66,6 +67,13 @@
|
||||
--font-medium: "CircularStdMedium";
|
||||
--font-bold: "CircularStdBold";
|
||||
--font-black: "CircularStdBlack";
|
||||
--normal-font-size: 1rem;
|
||||
--small-font-size: .8rem;
|
||||
}
|
||||
|
||||
:root /* margin & padding */
|
||||
{
|
||||
--rte-indent: 5px;
|
||||
}
|
||||
|
||||
:root { /* user without avatar */
|
||||
|
@ -6,7 +6,8 @@ rm -Rf tmp
|
||||
mkdir -p tmp
|
||||
mkdir -p target
|
||||
|
||||
wasm-pack build --mode normal --dev --out-name jirs --out-dir ./tmp --target web -- --verbose
|
||||
wasm-pack build --mode force --dev --out-name jirs --out-dir ./tmp --target web -- --verbose
|
||||
|
||||
../target/debug/jirs-css -i ./js/styles.css -O ./tmp/styles.css
|
||||
|
||||
cp -r ./static/* ./tmp
|
||||
|
@ -288,17 +288,18 @@ fn window_events(_model: &Model) -> Vec<EventHandler<Msg>> {
|
||||
.active_element()
|
||||
.map(|el| el.tag_name())
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = match tag_name.to_lowercase().as_str() {
|
||||
"input" | "textarea" => "".to_string(),
|
||||
"" | "input" | "textarea" => return None,
|
||||
_ => event.key(),
|
||||
};
|
||||
|
||||
Msg::GlobalKeyDown {
|
||||
Some(Msg::GlobalKeyDown {
|
||||
key,
|
||||
shift: event.shift_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
}
|
||||
})
|
||||
},
|
||||
)]
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
_ => return,
|
||||
};
|
||||
|
||||
modal.title_state.update(msg);
|
||||
modal.assignees_state.update(msg, orders);
|
||||
modal.reporter_state.update(msg, orders);
|
||||
modal.type_state.update(msg, orders);
|
||||
@ -37,7 +38,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
let project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
|
||||
|
||||
let payload = jirs_data::CreateIssuePayload {
|
||||
title: modal.title.clone(),
|
||||
title: modal.title_state.value.clone(),
|
||||
issue_type: modal.issue_type,
|
||||
issue_status_id: modal.issue_status_id,
|
||||
priority: modal.priority,
|
||||
@ -64,9 +65,6 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Description), value) => {
|
||||
modal.description = Some(value.clone());
|
||||
}
|
||||
Msg::StrInputChanged(FieldId::AddIssueModal(IssueFieldId::Title), value) => {
|
||||
modal.title = value.clone();
|
||||
}
|
||||
|
||||
// IssueTypeAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
@ -144,7 +142,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
let short_summary = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title))
|
||||
.valid(true)
|
||||
.state(&modal.title_state)
|
||||
.build()
|
||||
.into_node();
|
||||
let short_summary_field = StyledField::build()
|
||||
@ -269,7 +267,8 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.add_class("ActionButton")
|
||||
.on_click(mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::AddIssue
|
||||
ev.prevent_default();
|
||||
Some(Msg::AddIssue)
|
||||
}))
|
||||
.build()
|
||||
.into_node();
|
||||
@ -281,7 +280,8 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.text("Cancel")
|
||||
.on_click(mouse_ev(Ev::Click, |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalDropped
|
||||
ev.prevent_default();
|
||||
Some(Msg::ModalDropped)
|
||||
}))
|
||||
.build()
|
||||
.into_node();
|
||||
|
@ -154,7 +154,6 @@ impl EditIssueModal {
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||
pub struct AddIssueModal {
|
||||
pub title: String,
|
||||
pub issue_type: IssueType,
|
||||
pub priority: IssuePriority,
|
||||
pub description: Option<String>,
|
||||
@ -168,6 +167,7 @@ pub struct AddIssueModal {
|
||||
pub issue_status_id: i32,
|
||||
|
||||
// modal fields
|
||||
pub title_state: StyledInputState,
|
||||
pub type_state: StyledSelectState,
|
||||
pub reporter_state: StyledSelectState,
|
||||
pub assignees_state: StyledSelectState,
|
||||
@ -177,7 +177,6 @@ pub struct AddIssueModal {
|
||||
impl Default for AddIssueModal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: Default::default(),
|
||||
issue_type: Default::default(),
|
||||
priority: Default::default(),
|
||||
description: Default::default(),
|
||||
@ -189,6 +188,7 @@ impl Default for AddIssueModal {
|
||||
user_ids: Default::default(),
|
||||
reporter_id: Default::default(),
|
||||
issue_status_id: Default::default(),
|
||||
title_state: StyledInputState::new(FieldId::AddIssueModal(IssueFieldId::Title), ""),
|
||||
type_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Type), vec![]),
|
||||
reporter_state: StyledSelectState::new(
|
||||
FieldId::AddIssueModal(IssueFieldId::Reporter),
|
||||
|
@ -54,7 +54,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
page.project_category_state.update(&msg, orders);
|
||||
page.time_tracking.update(&msg);
|
||||
page.name.update(&msg);
|
||||
page.description_rte.update(&msg);
|
||||
page.description_rte.update(&msg, orders);
|
||||
|
||||
match msg {
|
||||
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
|
||||
|
@ -108,26 +108,7 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let parsed = comrak::markdown_to_html(
|
||||
text.as_str(),
|
||||
&comrak::ComrakOptions {
|
||||
hardbreaks: false,
|
||||
smart: true,
|
||||
github_pre_lang: true,
|
||||
width: 0,
|
||||
default_info_string: None,
|
||||
unsafe_: false,
|
||||
ext_strikethrough: true,
|
||||
ext_tagfilter: true,
|
||||
ext_table: true,
|
||||
ext_autolink: true,
|
||||
ext_tasklist: true,
|
||||
ext_superscript: true,
|
||||
ext_header_ids: None,
|
||||
ext_footnotes: true,
|
||||
ext_description_lists: true,
|
||||
},
|
||||
);
|
||||
let parsed = comrak::markdown_to_html(text.as_str(), &comrak::ComrakOptions::default());
|
||||
let parsed_node = Node::from_html(parsed.as_str());
|
||||
|
||||
let (editor_radio_node, view_radio_node) = match mode {
|
||||
@ -169,9 +150,9 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
|
||||
],
|
||||
label![
|
||||
if mode == Mode::Editor {
|
||||
class!["navbar editorTab activeTab"]
|
||||
C!["navbar editorTab activeTab"]
|
||||
} else {
|
||||
class!["navbar editorTab"]
|
||||
C!["navbar editorTab"]
|
||||
},
|
||||
attrs![At::For => editor_id.as_str()],
|
||||
"Editor",
|
||||
|
@ -8,10 +8,13 @@ use crate::Msg;
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Icon {
|
||||
Bug,
|
||||
Stopwatch,
|
||||
|
||||
Bug,
|
||||
Task,
|
||||
Story,
|
||||
Epic,
|
||||
|
||||
ArrowDown,
|
||||
ArrowLeftCircle,
|
||||
ArrowUp,
|
||||
@ -96,7 +99,7 @@ pub enum Icon {
|
||||
impl Icon {
|
||||
pub fn to_color(self) -> Option<String> {
|
||||
match self {
|
||||
Icon::Bug | Icon::Task | Icon::Story => Some(format!("var(--{})", self)),
|
||||
Icon::Bug | Icon::Task | Icon::Story | Icon::Epic => Some(format!("var(--{})", self)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -192,6 +195,7 @@ impl std::fmt::Display for Icon {
|
||||
Icon::Undo => "undo",
|
||||
Icon::ListingDots => "listing-dots",
|
||||
Icon::ListingNumber => "listing-number",
|
||||
Icon::Epic => "epic",
|
||||
};
|
||||
f.write_str(code)
|
||||
}
|
||||
@ -203,6 +207,7 @@ impl From<IssueType> for Icon {
|
||||
IssueType::Task => Icon::Task,
|
||||
IssueType::Bug => Icon::Bug,
|
||||
IssueType::Story => Icon::Story,
|
||||
IssueType::Epic => Icon::Epic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -196,17 +196,15 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
mut wrapper_class_list,
|
||||
variant,
|
||||
auto_focus,
|
||||
mut input_handlers,
|
||||
input_handlers,
|
||||
} = values;
|
||||
|
||||
wrapper_class_list.push("styledInput".to_string());
|
||||
wrapper_class_list.push(variant.to_string());
|
||||
wrapper_class_list.push(format!("{}", id));
|
||||
if !valid {
|
||||
wrapper_class_list.push("invalid".to_string());
|
||||
}
|
||||
|
||||
input_class_list.push("inputElement".to_string());
|
||||
input_class_list.push(variant.to_string());
|
||||
if icon.is_some() {
|
||||
input_class_list.push("withIcon".to_string());
|
||||
@ -216,27 +214,37 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
Some(icon) => StyledIcon::build(icon).build().into_node(),
|
||||
_ => empty![],
|
||||
};
|
||||
let on_input = {
|
||||
let field_id = id.clone();
|
||||
input_handlers.push(ev(Ev::Input, move |event| {
|
||||
ev(Ev::Input, move |event| {
|
||||
event.stop_propagation();
|
||||
let target = event.target().unwrap();
|
||||
let input = seed::to_input(&target);
|
||||
let value = input.value();
|
||||
Msg::StrInputChanged(field_id, value)
|
||||
}));
|
||||
input_handlers.push(ev(Ev::KeyUp, move |event| {
|
||||
})
|
||||
};
|
||||
let on_keyup = {
|
||||
ev(Ev::KeyUp, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
}));
|
||||
input_handlers.push(ev(Ev::Click, move |event| {
|
||||
})
|
||||
};
|
||||
let on_click = {
|
||||
ev(Ev::Click, move |event| {
|
||||
event.stop_propagation();
|
||||
None as Option<Msg>
|
||||
}));
|
||||
})
|
||||
};
|
||||
|
||||
div![
|
||||
C!["styledInput"],
|
||||
attrs!(At::Class => wrapper_class_list.join(" ")),
|
||||
icon,
|
||||
on_click,
|
||||
on_keyup,
|
||||
seed::input![
|
||||
C!["inputElement"],
|
||||
attrs![
|
||||
At::Id => format!("{}", id),
|
||||
At::Class => input_class_list.join(" "),
|
||||
@ -249,6 +257,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
on_input,
|
||||
input_handlers,
|
||||
],
|
||||
]
|
||||
|
@ -8,6 +8,7 @@ use crate::{FieldId, Msg};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum HeadingSize {
|
||||
Normal,
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
@ -16,20 +17,37 @@ pub enum HeadingSize {
|
||||
H6,
|
||||
}
|
||||
|
||||
impl HeadingSize {
|
||||
fn all() -> Vec<Self> {
|
||||
use HeadingSize::*;
|
||||
|
||||
vec![Normal, H1, H2, H3, H4, H5, H6]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for HeadingSize {
|
||||
fn to_string(&self) -> String {
|
||||
use HeadingSize::*;
|
||||
|
||||
match self {
|
||||
HeadingSize::H1 => "H1",
|
||||
HeadingSize::H2 => "H2",
|
||||
HeadingSize::H3 => "H3",
|
||||
HeadingSize::H4 => "H4",
|
||||
HeadingSize::H5 => "H5",
|
||||
HeadingSize::H6 => "H6",
|
||||
Normal => "Normal",
|
||||
H1 => "H1",
|
||||
H2 => "H2",
|
||||
H3 => "H3",
|
||||
H4 => "H4",
|
||||
H5 => "H5",
|
||||
H6 => "H6",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RteIndentMsg {
|
||||
Increase,
|
||||
Decrease,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RteMsg {
|
||||
Bold,
|
||||
@ -56,6 +74,9 @@ pub enum RteMsg {
|
||||
TableSetColumns(u16),
|
||||
TableSetVisibility(bool),
|
||||
InsertTable { rows: u16, cols: u16 },
|
||||
ChangeIndent(RteIndentMsg),
|
||||
|
||||
RequestFocus(uuid::Uuid),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -101,19 +122,42 @@ impl RteMsg {
|
||||
RteMsg::JustifyLeft => Some(ExecCommand::new("justifyLeft")),
|
||||
RteMsg::JustifyRight => Some(ExecCommand::new("justifyRight")),
|
||||
RteMsg::InsertParagraph => Some(ExecCommand::new("insertParagraph")),
|
||||
RteMsg::InsertHeading(heading) => {
|
||||
RteMsg::InsertHeading(heading) => match heading {
|
||||
HeadingSize::H1
|
||||
| HeadingSize::H2
|
||||
| HeadingSize::H3
|
||||
| HeadingSize::H4
|
||||
| HeadingSize::H5
|
||||
| HeadingSize::H6 => {
|
||||
Some(ExecCommand::new_with_param("heading", heading.to_string()))
|
||||
}
|
||||
HeadingSize::Normal => Some(ExecCommand::new_with_param("formatBlock", "div")),
|
||||
},
|
||||
RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")),
|
||||
RteMsg::InsertOrderedList => Some(ExecCommand::new("insertOrderedList")),
|
||||
RteMsg::RemoveFormat => Some(ExecCommand::new("removeFormat")),
|
||||
RteMsg::Subscript => Some(ExecCommand::new("subscript")),
|
||||
RteMsg::Superscript => Some(ExecCommand::new("superscript")),
|
||||
RteMsg::InsertTable { .. } => None,
|
||||
|
||||
// indent
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")),
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Decrease) => Some(ExecCommand::new("outdent")),
|
||||
|
||||
// outer
|
||||
RteMsg::TableSetColumns(..)
|
||||
| RteMsg::TableSetRows(..)
|
||||
| RteMsg::TableSetVisibility(..) => None,
|
||||
|
||||
RteMsg::RequestFocus(identifier) => {
|
||||
let res = seed::document().query_selector(format!("#{}", identifier).as_str());
|
||||
if let Ok(Some(el)) = res {
|
||||
if let Ok(el) = el.dyn_into::<web_sys::HtmlElement>() {
|
||||
el.focus().is_ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -131,6 +175,7 @@ pub struct StyledRteState {
|
||||
pub field_id: FieldId,
|
||||
pub table_tooltip: StyledRteTableState,
|
||||
range: Option<web_sys::Range>,
|
||||
identifier: uuid::Uuid,
|
||||
}
|
||||
|
||||
impl StyledRteState {
|
||||
@ -144,10 +189,11 @@ impl StyledRteState {
|
||||
cols: 3,
|
||||
},
|
||||
range: None,
|
||||
identifier: uuid::Uuid::new_v4(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: &Msg) {
|
||||
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
let m = match msg {
|
||||
Msg::Rte(m, field) if field == &self.field_id => m,
|
||||
_ => return,
|
||||
@ -166,6 +212,7 @@ impl StyledRteState {
|
||||
if self.restore_range().is_err() {
|
||||
return;
|
||||
}
|
||||
self.schedule_focus(orders);
|
||||
}
|
||||
_ => match m {
|
||||
RteMsg::TableSetRows(n) => {
|
||||
@ -209,6 +256,7 @@ impl StyledRteState {
|
||||
if let Err(e) = r.insert_node(&table) {
|
||||
log!(e);
|
||||
}
|
||||
self.schedule_focus(orders);
|
||||
}
|
||||
_ => log!(m),
|
||||
},
|
||||
@ -241,11 +289,20 @@ impl StyledRteState {
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schedule_focus(&self, orders: &mut impl Orders<Msg>) {
|
||||
let field_id = self.field_id.clone();
|
||||
let identifier = self.identifier.clone();
|
||||
orders.perform_cmd(cmds::timeout(200, move || {
|
||||
Msg::Rte(RteMsg::RequestFocus(identifier), field_id)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledRte {
|
||||
field_id: FieldId,
|
||||
table_tooltip: StyledRteTableState,
|
||||
identifier: Option<uuid::Uuid>,
|
||||
// value: String,
|
||||
}
|
||||
|
||||
@ -259,6 +316,7 @@ impl StyledRte {
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
},
|
||||
identifier: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,12 +331,14 @@ pub struct StyledRteBuilder {
|
||||
field_id: FieldId,
|
||||
value: String,
|
||||
table_tooltip: StyledRteTableState,
|
||||
identifier: Option<uuid::Uuid>,
|
||||
}
|
||||
|
||||
impl StyledRteBuilder {
|
||||
pub fn state(mut self, state: &StyledRteState) -> Self {
|
||||
self.value = state.value.clone();
|
||||
self.table_tooltip = state.table_tooltip.clone();
|
||||
self.identifier = Some(state.identifier.clone());
|
||||
self
|
||||
}
|
||||
|
||||
@ -287,6 +347,7 @@ impl StyledRteBuilder {
|
||||
field_id: self.field_id,
|
||||
// value: self.value,
|
||||
table_tooltip: self.table_tooltip,
|
||||
identifier: self.identifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -373,13 +434,14 @@ pub fn render(values: StyledRte) -> Node<Msg> {
|
||||
ev.stop_propagation();
|
||||
None as Option<Msg>
|
||||
});
|
||||
let id = values.field_id.to_string();
|
||||
|
||||
let id = values.identifier.unwrap_or_default().to_string();
|
||||
|
||||
div![
|
||||
class!["styledRte"],
|
||||
C!["styledRte"],
|
||||
attrs![At::Id => id],
|
||||
div![
|
||||
class!["bar"],
|
||||
C!["bar"],
|
||||
first_row(&values),
|
||||
second_row(&values),
|
||||
// brush_button,
|
||||
@ -392,9 +454,9 @@ pub fn render(values: StyledRte) -> Node<Msg> {
|
||||
// text_width_button,
|
||||
],
|
||||
div![
|
||||
class!["editorWrapper"],
|
||||
C!["editorWrapper"],
|
||||
div![
|
||||
class!["editor"],
|
||||
C!["editor"],
|
||||
attrs![At::ContentEditable => true],
|
||||
capture_event
|
||||
],
|
||||
@ -633,14 +695,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
|
||||
None as Option<Msg>
|
||||
}),
|
||||
);
|
||||
let options: Vec<Node<Msg>> = vec![
|
||||
HeadingSize::H1,
|
||||
HeadingSize::H2,
|
||||
HeadingSize::H3,
|
||||
HeadingSize::H4,
|
||||
HeadingSize::H5,
|
||||
HeadingSize::H6,
|
||||
]
|
||||
let options: Vec<Node<Msg>> = HeadingSize::all()
|
||||
.into_iter()
|
||||
.map(|h| {
|
||||
let field_id = values.field_id.clone();
|
||||
@ -757,20 +812,28 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
|
||||
};
|
||||
|
||||
let indent_outdent = {
|
||||
let field_id = values.field_id.clone();
|
||||
let indent_button = styled_rte_button(
|
||||
"Indent",
|
||||
Icon::Indent,
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
None as Option<Msg>
|
||||
Some(Msg::Rte(
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Increase),
|
||||
field_id,
|
||||
))
|
||||
}),
|
||||
);
|
||||
let field_id = values.field_id.clone();
|
||||
let outdent_button = styled_rte_button(
|
||||
"Outdent",
|
||||
Icon::Outdent,
|
||||
mouse_ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
None as Option<Msg>
|
||||
Some(Msg::Rte(
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Decrease),
|
||||
field_id,
|
||||
))
|
||||
}),
|
||||
);
|
||||
div![class!["group indentOutdent"], indent_button, outdent_button]
|
||||
|
@ -8,3 +8,4 @@ import("/jirs.js").then(async module => {
|
||||
const host_url = `${location.protocol}//${process.env.JIRS_SERVER_BIND}:${process.env.JIRS_SERVER_PORT}`;
|
||||
module.render(host_url, wsUrl());
|
||||
});
|
||||
|
||||
|
@ -41,13 +41,19 @@ pub enum IssueType {
|
||||
Task,
|
||||
Bug,
|
||||
Story,
|
||||
Epic,
|
||||
}
|
||||
|
||||
impl ToVec for IssueType {
|
||||
type Item = IssueType;
|
||||
|
||||
fn ordered() -> Vec<Self> {
|
||||
vec![IssueType::Task, IssueType::Bug, IssueType::Story]
|
||||
vec![
|
||||
IssueType::Task,
|
||||
IssueType::Bug,
|
||||
IssueType::Story,
|
||||
IssueType::Epic,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,6 +69,7 @@ impl IssueType {
|
||||
IssueType::Task => "Task",
|
||||
IssueType::Bug => "Bug",
|
||||
IssueType::Story => "Story",
|
||||
IssueType::Epic => "Epic",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,6 +80,7 @@ impl Into<u32> for IssueType {
|
||||
IssueType::Task => 1,
|
||||
IssueType::Bug => 2,
|
||||
IssueType::Story => 3,
|
||||
IssueType::Epic => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,6 +91,7 @@ impl Into<IssueType> for u32 {
|
||||
1 => IssueType::Task,
|
||||
2 => IssueType::Bug,
|
||||
3 => IssueType::Story,
|
||||
4 => IssueType::Epic,
|
||||
_ => IssueType::Task,
|
||||
}
|
||||
}
|
||||
@ -94,6 +103,7 @@ impl std::fmt::Display for IssueType {
|
||||
IssueType::Task => f.write_str("task"),
|
||||
IssueType::Bug => f.write_str("bug"),
|
||||
IssueType::Story => f.write_str("story"),
|
||||
IssueType::Epic => f.write_str("epic"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ fn issue_type_from_sql(bytes: Option<&[u8]>) -> deserialize::Result<IssueType> {
|
||||
b"task" => Ok(IssueType::Task),
|
||||
b"bug" => Ok(IssueType::Bug),
|
||||
b"story" => Ok(IssueType::Story),
|
||||
b"epic" => Ok(IssueType::Epic),
|
||||
_ => Ok(IssueType::Task),
|
||||
}
|
||||
}
|
||||
@ -77,6 +78,7 @@ impl ToSql<IssueTypeType, Pg> for IssueType {
|
||||
IssueType::Task => out.write_all(b"task")?,
|
||||
IssueType::Story => out.write_all(b"story")?,
|
||||
IssueType::Bug => out.write_all(b"bug")?,
|
||||
IssueType::Epic => out.write_all(b"epic")?,
|
||||
}
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
ALTER TABLE "issues"
|
||||
ALTER COLUMN "issue_type"
|
||||
SET DATA TYPE TEXT
|
||||
USING "issue_type"::TEXT;
|
||||
|
||||
UPDATE "issues"
|
||||
SET "issue_type" = 'task'
|
||||
WHERE "issue_type" = 'epic';
|
||||
|
||||
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
|
||||
CREATE TYPE "IssueTypeType" AS ENUM (
|
||||
'task',
|
||||
'bug',
|
||||
'story'
|
||||
);
|
||||
|
||||
ALTER TABLE "issues"
|
||||
ALTER COLUMN "issue_type"
|
||||
SET DATA TYPE "IssueTypeType"
|
||||
USING "issue_type"::"IssueTypeType";
|
@ -0,0 +1,17 @@
|
||||
ALTER TABLE "issues"
|
||||
ALTER COLUMN "issue_type"
|
||||
SET DATA TYPE TEXT
|
||||
USING "issue_type"::TEXT;
|
||||
|
||||
DROP TYPE IF EXISTS "IssueTypeType" CASCADE;
|
||||
CREATE TYPE "IssueTypeType" AS ENUM (
|
||||
'task',
|
||||
'bug',
|
||||
'story',
|
||||
'epic'
|
||||
);
|
||||
|
||||
ALTER TABLE "issues"
|
||||
ALTER COLUMN "issue_type"
|
||||
SET DATA TYPE "IssueTypeType"
|
||||
USING "issue_type"::"IssueTypeType";
|
@ -226,7 +226,7 @@ impl Message for CreateIssue {
|
||||
impl Handler<CreateIssue> for DbExecutor {
|
||||
type Result = Result<Issue, ServiceErrors>;
|
||||
|
||||
fn handle(&mut self, msg: CreateIssue, _ctx: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: CreateIssue, ctx: &mut Self::Context) -> Self::Result {
|
||||
use crate::schema::issue_assignees::dsl;
|
||||
use crate::schema::issues::dsl::issues;
|
||||
|
||||
@ -241,10 +241,28 @@ impl Handler<CreateIssue> for DbExecutor {
|
||||
.get_result::<i32>(conn)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?;
|
||||
|
||||
info!("{:?}", msg.issue_type);
|
||||
info!("msg.issue_status_id {:?}", msg.issue_status_id);
|
||||
|
||||
let issue_status_id = if msg.issue_status_id == 0 {
|
||||
self.handle(
|
||||
crate::db::issue_statuses::LoadIssueStatuses {
|
||||
project_id: msg.project_id,
|
||||
},
|
||||
ctx,
|
||||
)
|
||||
.map_err(|_| ServiceErrors::DatabaseConnectionLost)?
|
||||
.get(0)
|
||||
.ok_or_else(|| ServiceErrors::DatabaseConnectionLost)?
|
||||
.id
|
||||
} else {
|
||||
msg.issue_status_id
|
||||
};
|
||||
|
||||
let form = crate::models::CreateIssueForm {
|
||||
title: msg.title,
|
||||
issue_type: msg.issue_type,
|
||||
issue_status_id: msg.issue_status_id,
|
||||
issue_status_id,
|
||||
priority: msg.priority,
|
||||
list_position,
|
||||
description: msg.description,
|
||||
|
@ -159,6 +159,53 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::sql::*;
|
||||
|
||||
/// Representation of the `issue_statuses` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_statuses (id) {
|
||||
/// The `id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Varchar`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Varchar,
|
||||
/// The `position` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
position -> Int4,
|
||||
/// The `project_id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `created_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::sql::*;
|
||||
@ -260,53 +307,6 @@ table! {
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::sql::*;
|
||||
|
||||
/// Representation of the `issue_statuses` table.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
issue_statuses (id) {
|
||||
/// The `id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
id -> Int4,
|
||||
/// The `name` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Varchar`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
name -> Varchar,
|
||||
/// The `position` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
position -> Int4,
|
||||
/// The `project_id` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Int4`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
project_id -> Int4,
|
||||
/// The `created_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
created_at -> Timestamp,
|
||||
/// The `updated_at` column of the `issue_statuses` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use jirs_data::sql::*;
|
||||
@ -608,8 +608,8 @@ allow_tables_to_appear_in_same_query!(
|
||||
comments,
|
||||
invitations,
|
||||
issue_assignees,
|
||||
issues,
|
||||
issue_statuses,
|
||||
issues,
|
||||
messages,
|
||||
projects,
|
||||
tokens,
|
||||
|
Loading…
Reference in New Issue
Block a user