Add missing RTE features, add focus after RTE change, fix handle change.

Add Epic as issue type.
This commit is contained in:
Adrian Woźniak 2020-08-10 16:45:30 +02:00
parent 7aadc12c3a
commit ba08c08e35
23 changed files with 826 additions and 598 deletions

916
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -41,6 +41,10 @@ i.styledIcon.story:before {
content: "\ef2d";
}
i.styledIcon.epic:before {
content: '\ef30';
}
i.styledIcon.arrowDown:before {
content: "\ea92";
}

View File

@ -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);

View File

@ -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 */

View File

@ -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

View File

@ -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(),
}
})
},
)]
}

View File

@ -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();

View File

@ -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),

View File

@ -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) => {

View File

@ -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",

View File

@ -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,
}
}
}

View File

@ -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 field_id = id.clone();
input_handlers.push(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| {
event.stop_propagation();
None as Option<Msg>
}));
input_handlers.push(ev(Ev::Click, move |event| {
event.stop_propagation();
None as Option<Msg>
}));
let on_input = {
let field_id = id.clone();
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)
})
};
let on_keyup = {
ev(Ev::KeyUp, move |event| {
event.stop_propagation();
None as Option<Msg>
})
};
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,
],
]

View File

@ -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) => {
Some(ExecCommand::new_with_param("heading", heading.to_string()))
}
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,29 +695,22 @@ 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,
]
.into_iter()
.map(|h| {
let field_id = values.field_id.clone();
let button = StyledButton::build()
.text(h.to_string())
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertHeading(h), field_id))
}))
.empty()
.build()
.into_node();
span![class!["headingOption"], button]
})
.collect();
let options: Vec<Node<Msg>> = HeadingSize::all()
.into_iter()
.map(|h| {
let field_id = values.field_id.clone();
let button = StyledButton::build()
.text(h.to_string())
.on_click(mouse_ev(Ev::Click, move |ev| {
ev.prevent_default();
Some(Msg::Rte(RteMsg::InsertHeading(h), field_id))
}))
.empty()
.build()
.into_node();
span![class!["headingOption"], button]
})
.collect();
let heading_button = span![class!["headingList"], options];
/*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]

View File

@ -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());
});

View File

@ -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"),
}
}
}

View File

@ -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)
}

View File

@ -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";

View File

@ -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";

View File

@ -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,

View File

@ -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,