Add modal to add issue

This commit is contained in:
Adrian Wozniak 2020-04-04 17:42:02 +02:00
parent e082444a7c
commit 8950277149
27 changed files with 2667 additions and 178 deletions

View File

@ -1,3 +1,4 @@
pkg pkg
node_modules node_modules
dist dist
.yarn-error.log

View File

@ -42,6 +42,7 @@ aside#navbar-left .item {
transition: color 0.1s; transition: color 0.1s;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
display: block;
} }
aside#navbar-left .item:hover { aside#navbar-left .item:hover {

View File

@ -0,0 +1,12 @@
.styledForm {
display: block;
}
.styledForm > .formElement {
padding: 25px 40px 35px;
}
.styledForm > .formElement > .formHeading {
padding-bottom: 15px;
font-size: 21px;
}

View File

@ -1,17 +1,18 @@
@import "css/normalize.css"; @import "./css/normalize.css";
@import "css/fonts.css"; @import "./css/fonts.css";
@import "css/variables.css"; @import "./css/variables.css";
@import "css/global.css"; @import "./css/global.css";
@import "css/sidebar.css"; @import "./css/sidebar.css";
@import "css/aside.css"; @import "./css/aside.css";
@import "css/styledIcon.css"; @import "./css/styledIcon.css";
@import "css/shared.css"; @import "./css/shared.css";
@import "css/styledTooltip.css"; @import "./css/styledTooltip.css";
@import "css/styledAvatar.css"; @import "./css/styledAvatar.css";
@import "css/styledSelect.css"; @import "./css/styledSelect.css";
@import "css/styledButton.css"; @import "./css/styledButton.css";
@import "css/styledInput.css"; @import "./css/styledInput.css";
@import "css/styledModal.css"; @import "./css/styledModal.css";
@import "css/app.css"; @import "./css/styledForm.css";
@import "css/issue.css"; @import "./css/app.css";
@import "css/project.css"; @import "./css/issue.css";
@import "./css/project.css";

View File

@ -2,16 +2,28 @@
"devDependencies": { "devDependencies": {
"@swc/core": "^1.1.37", "@swc/core": "^1.1.37",
"@wasm-tool/wasm-pack-plugin": "^1.2.0", "@wasm-tool/wasm-pack-plugin": "^1.2.0",
"autoprefixer": "^9.7.5",
"css-loader": "^3.4.2", "css-loader": "^3.4.2",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"extract-text-webpack-plugin": "2.1.2",
"file-loader": "^6.0.0",
"glob": "^7.1.6",
"html-webpack-plugin": "^4.0.3", "html-webpack-plugin": "^4.0.3",
"mini-css-extract-plugin": "^0.9.0",
"optipng": "^2.1.0",
"postcss-loader": "^3.0.0",
"style-loader": "^1.1.3", "style-loader": "^1.1.3",
"sugarss": "^2.0.0",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"swc-loader": "^0.1.8", "swc-loader": "^0.1.8",
"webpack": "^4.42.1", "webpack": "^4.42.1",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3" "webpack-dev-server": "^3.10.3"
}, },
"scripts": { "scripts": {
"start": "webpack-dev-server" "start": "webpack-dev-server",
"build": "./scripts/build"
} }
} }

View File

@ -0,0 +1,7 @@
module.exports = {
parser: 'sugarss',
plugins: [
require('autoprefixer')({}),
require('cssnano'),
],
};

16
jirs-client/scripts/build Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
export NODE_ENV=production
rm -Rf dist
mkdir -p dist
cp -R ./dev/* ./dist
yarn svgo -r -o ./dist/ -f ./static
yarn svgo -r -o ./dist/ -f ./js
yarn svgo -r -o ./dist/ -f ./dev
for f in $(ls {js,static,dev}/*.png); do
yarn optipng -dir ./dist -o7 ${f}
done
NODE_ENV=production RUST_LOG=error yarn webpack

View File

@ -36,3 +36,16 @@ pub async fn update_issue(
Err(e) => return Ok(Msg::InternalFailure(e)), Err(e) => return Ok(Msg::InternalFailure(e)),
} }
} }
pub async fn delete_issue(host_url: String, id: i32) -> Result<Msg, Msg> {
match host_client(host_url, format!("/issues/{id}", id = id).as_str()) {
Ok(client) => {
client
.method(Method::Delete)
.header("Content-Type", "application/json")
.fetch_string(Msg::IssueDeleteResult)
.await
}
Err(e) => return Ok(Msg::InternalFailure(e)),
}
}

View File

@ -24,6 +24,7 @@ pub type AvatarFilterActive = bool;
pub enum FieldId { pub enum FieldId {
IssueTypeEditModalTop, IssueTypeEditModalTop,
CopyButtonLabel, CopyButtonLabel,
IssueTypeAddIssueModal,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -56,6 +57,7 @@ pub enum Msg {
// issues // issues
IssueUpdateResult(FetchObject<String>), IssueUpdateResult(FetchObject<String>),
IssueDeleteResult(FetchObject<String>),
DeleteIssue(IssueId), DeleteIssue(IssueId),
// modals // modals
@ -77,7 +79,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
crate::shared::update(&msg, model, orders); crate::shared::update(&msg, model, orders);
crate::modal::update(&msg, model, orders); crate::modal::update(&msg, model, orders);
match model.page { match model.page {
Page::Project => project::update(msg, model, orders), Page::Project | Page::AddIssue => project::update(msg, model, orders),
Page::EditIssue(_id) => project::update(msg, model, orders), Page::EditIssue(_id) => project::update(msg, model, orders),
Page::ProjectSettings => project_settings::update(msg, model, orders), Page::ProjectSettings => project_settings::update(msg, model, orders),
Page::Login => login::update(msg, model, orders), Page::Login => login::update(msg, model, orders),
@ -90,7 +92,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
fn view(model: &model::Model) -> Node<Msg> { fn view(model: &model::Model) -> Node<Msg> {
match model.page { match model.page {
Page::Project => project::view(model), Page::Project | Page::AddIssue => project::view(model),
Page::EditIssue(_id) => project::view(model), Page::EditIssue(_id) => project::view(model),
Page::ProjectSettings => project_settings::view(model), Page::ProjectSettings => project_settings::view(model),
Page::Login => login::view(model), Page::Login => login::view(model),
@ -109,6 +111,7 @@ fn routes(url: Url) -> Option<Msg> {
Some(Ok(id)) => Some(Msg::ChangePage(model::Page::EditIssue(id))), Some(Ok(id)) => Some(Msg::ChangePage(model::Page::EditIssue(id))),
_ => None, _ => None,
}, },
"add-issue" => Some(Msg::ChangePage(Page::AddIssue)),
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)), "project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::Login)), "login" => Some(Msg::ChangePage(model::Page::Login)),
"register" => Some(Msg::ChangePage(model::Page::Register)), "register" => Some(Msg::ChangePage(model::Page::Register)),

View File

@ -0,0 +1,84 @@
use crate::model::{AddIssueModal, Model};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::styled_select::StyledSelect;
use crate::shared::ToNode;
use crate::{FieldId, Msg};
use jirs_data::IssueType;
use seed::{prelude::*, *};
pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build(FieldId::IssueTypeAddIssueModal)
.name("type")
.normal()
.text_filter(modal.type_select_filter.as_str())
.opened(modal.type_select_opened)
.valid(true)
.options(vec![
IssueTypeOption(IssueType::Story),
IssueTypeOption(IssueType::Task),
IssueTypeOption(IssueType::Bug),
])
.selected(vec![IssueTypeOption(modal.issue_type.clone())])
.build()
.into_node();
let form = StyledForm::build()
.heading("Create issue")
.add_field(select_type)
.build()
.into_node();
StyledModal::build()
.width(800)
.variant(ModalVariant::Center)
.children(vec![form])
.build()
.into_node()
}
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeOption(pub IssueType);
impl crate::shared::styled_select::SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon".to_string())
.build()
.into_node();
div![
attrs![At::Class => "type"],
icon,
div![attrs![At::Class => "typeLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
let chevron_icon = StyledIcon::build(Icon::ChevronDown).build().into_node();
div![attrs![At::Class => "option"], type_icon, name, chevron_icon]
// StyledButton::build()
// .secondary()
// .children(vec![span![format!("{}", name)]])
// .icon(StyledIcon::build(self.0.into()).build())
// .build()
// .into_node()
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}

View File

@ -26,6 +26,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.title("Are you sure you want to delete this issue?") .title("Are you sure you want to delete this issue?")
.message("Once you delete, it's gone for good.") .message("Once you delete, it's gone for good.")
.confirm_text("Delete issue") .confirm_text("Delete issue")
.cancel_text("Cancel")
.on_confirm(handle_issue_delete) .on_confirm(handle_issue_delete)
.build() .build()
.into_node() .into_node()

View File

@ -5,74 +5,26 @@ use jirs_data::{Issue, IssueType};
use crate::model::{EditIssueModal, ModalType, Model}; use crate::model::{EditIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_select::{StyledSelect, Variant as SelectVariant}; use crate::shared::styled_select::StyledSelect;
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::{FieldChange, FieldId, IssueId, Msg}; use crate::{FieldChange, FieldId, IssueId, Msg};
#[derive(PartialOrd, PartialEq, Debug)]
struct IssueTypeOption(IssueId, IssueType);
impl crate::shared::styled_select::SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
let icon = StyledIcon::build(self.1.into())
.add_class("issueTypeIcon".to_string())
.build()
.into_node();
div![
attrs![At::Class => "type"],
icon,
div![attrs![At::Class => "typeLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let issue_id = self.0;
let name = self.1.to_label().to_owned();
StyledButton::build()
.empty()
.children(vec![span![format!("{}-{}", name, issue_id)]])
.icon(StyledIcon::build(self.1.into()).build())
.build()
.into_node()
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.1
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}
pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg> { pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg> {
let issue_id = issue.id; let issue_id = issue.id;
let issue_type_select = StyledSelect { let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
id: FieldId::IssueTypeEditModalTop, .dropdown_width(150)
variant: SelectVariant::Empty, .name("type")
dropdown_width: Some(150), .text_filter(modal.top_select_filter.as_str())
name: Some("type".to_string()), .opened(modal.top_select_opened)
placeholder: None, .valid(true)
text_filter: modal.top_select_filter.clone(), .options(vec![
opened: modal.top_select_opened,
valid: true,
is_multi: false,
allow_clear: false,
options: vec![
IssueTypeOption(issue_id, IssueType::Story), IssueTypeOption(issue_id, IssueType::Story),
IssueTypeOption(issue_id, IssueType::Task), IssueTypeOption(issue_id, IssueType::Task),
IssueTypeOption(issue_id, IssueType::Bug), IssueTypeOption(issue_id, IssueType::Bug),
], ])
selected: vec![IssueTypeOption(issue_id, modal.value.clone())], .selected(vec![IssueTypeOption(issue_id, modal.value.clone())])
} .build()
.into_node(); .into_node();
let click_handler = mouse_ev(Ev::Click, move |_| { let click_handler = mouse_ev(Ev::Click, move |_| {
@ -147,3 +99,46 @@ pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg>
], ],
] ]
} }
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeOption(pub IssueId, pub IssueType);
impl crate::shared::styled_select::SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
let icon = StyledIcon::build(self.1.into())
.add_class("issueTypeIcon".to_string())
.build()
.into_node();
div![
attrs![At::Class => "type"],
icon,
div![attrs![At::Class => "typeLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let issue_id = self.0;
let name = self.1.to_label().to_owned();
StyledButton::build()
.empty()
.children(vec![span![format!("{}-{}", name, issue_id)]])
.icon(StyledIcon::build(self.1.into()).build())
.build()
.into_node()
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.1
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}

View File

@ -3,18 +3,23 @@ use seed::{prelude::*, *};
use jirs_data::{Issue, IssueType, UpdateIssuePayload}; use jirs_data::{Issue, IssueType, UpdateIssuePayload};
use crate::api::update_issue; use crate::api::update_issue;
use crate::model::{EditIssueModal, ModalType, Page}; use crate::model::{AddIssueModal, EditIssueModal, ModalType, Page};
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
use crate::shared::styled_select::StyledSelectChange; use crate::shared::styled_select::StyledSelectChange;
use crate::shared::{find_issue, ToNode}; use crate::shared::{find_issue, ToNode};
use crate::{model, FieldChange, FieldId, Msg}; use crate::{model, FieldChange, FieldId, Msg};
mod add_issue;
mod confirm_delete_issue; mod confirm_delete_issue;
mod issue_details; mod issue_details;
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::ModalDropped => match model.modals.pop() { Msg::ModalDropped => match model.modals.pop() {
Some(ModalType::EditIssue(..)) => {
seed::push_route(vec!["board"]);
orders.send_msg(Msg::ChangePage(Page::Project));
}
_ => (), _ => (),
}, },
@ -45,6 +50,11 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
}, },
)); ));
} }
Msg::ChangePage(Page::AddIssue) => {
let mut modal = AddIssueModal::default();
modal.project_id = model.project.as_ref().map(|p| p.id).unwrap_or_default();
model.modals.push(ModalType::AddIssue(modal));
}
Msg::StyledSelectChanged(FieldId::IssueTypeEditModalTop, change) => { Msg::StyledSelectChanged(FieldId::IssueTypeEditModalTop, change) => {
match (change, model.modals.last_mut()) { match (change, model.modals.last_mut()) {
@ -125,7 +135,7 @@ pub fn view(model: &model::Model) -> Node<Msg> {
} }
} }
ModalType::DeleteIssueConfirm(_id) => confirm_delete_issue::view(model), ModalType::DeleteIssueConfirm(_id) => confirm_delete_issue::view(model),
_ => empty![], ModalType::AddIssue(modal) => add_issue::view(model, modal),
}) })
.collect(); .collect();
section![id!["modals"], modals] section![id!["modals"], modals]

View File

@ -11,6 +11,7 @@ pub type ProjectId = i32;
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType { pub enum ModalType {
AddIssue(AddIssueModal),
EditIssue(IssueId, EditIssueModal), EditIssue(IssueId, EditIssueModal),
DeleteIssueConfirm(IssueId), DeleteIssueConfirm(IssueId),
} }
@ -24,10 +25,31 @@ pub struct EditIssueModal {
pub link_copied: bool, pub link_copied: bool,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialOrd, PartialEq)]
pub struct AddIssueModal {
pub title: String,
#[serde(rename = "type")]
pub issue_type: IssueType,
pub status: IssueStatus,
pub priority: IssuePriority,
pub description: Option<String>,
pub description_text: Option<String>,
pub estimate: Option<i32>,
pub time_spent: Option<i32>,
pub time_remaining: Option<i32>,
pub project_id: i32,
pub user_ids: Vec<i32>,
// modal fields
pub type_select_filter: String,
pub type_select_opened: bool,
}
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)]
pub enum Page { pub enum Page {
Project, Project,
EditIssue(IssueId), EditIssue(IssueId),
AddIssue,
ProjectSettings, ProjectSettings,
Login, Login,
Register, Register,
@ -38,6 +60,7 @@ impl Page {
match self { match self {
Page::Project => "/board".to_string(), Page::Project => "/board".to_string(),
Page::EditIssue(id) => format!("/issues/{id}", id = id), Page::EditIssue(id) => format!("/issues/{id}", id = id),
Page::AddIssue => format!("/add-issues"),
Page::ProjectSettings => "/project-settings".to_string(), Page::ProjectSettings => "/project-settings".to_string(),
Page::Login => "/login".to_string(), Page::Login => "/login".to_string(),
Page::Register => "/register".to_string(), Page::Register => "/register".to_string(),

View File

@ -12,15 +12,9 @@ use crate::Msg;
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::ChangePage(Page::Project) => { Msg::ChangePage(Page::Project)
orders | Msg::ChangePage(Page::AddIssue)
.skip() | Msg::ChangePage(Page::EditIssue(..)) => {
.perform_cmd(crate::api::fetch_current_project(model.host_url.clone()));
orders
.skip()
.perform_cmd(crate::api::fetch_current_user(model.host_url.clone()));
}
Msg::ChangePage(Page::EditIssue(_issue_id)) => {
orders orders
.skip() .skip()
.perform_cmd(crate::api::fetch_current_project(model.host_url.clone())); .perform_cmd(crate::api::fetch_current_project(model.host_url.clone()));
@ -119,6 +113,11 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
Msg::IssueUpdateResult(fetched) => { Msg::IssueUpdateResult(fetched) => {
crate::api_handlers::update_issue_response(&fetched, model); crate::api_handlers::update_issue_response(&fetched, model);
} }
Msg::DeleteIssue(issue_id) => {
orders
.skip()
.perform_cmd(crate::api::delete_issue(model.host_url.clone(), issue_id));
}
_ => (), _ => (),
} }
} }

View File

@ -11,6 +11,7 @@ pub mod navbar_left;
pub mod styled_avatar; pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;
pub mod styled_confirm_modal; pub mod styled_confirm_modal;
pub mod styled_form;
pub mod styled_icon; pub mod styled_icon;
pub mod styled_input; pub mod styled_input;
pub mod styled_modal; pub mod styled_modal;

View File

@ -18,7 +18,11 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
div![attrs![At::Class => "styledLogo"], logo_svg] div![attrs![At::Class => "styledLogo"], logo_svg]
], ],
navbar_left_item(model, "Search issues", Icon::Search), navbar_left_item(model, "Search issues", Icon::Search),
navbar_left_item(model, "Create Issue", Icon::Plus), a![
attrs![At::Class => "item"; At::Href=> "/add-issue"; ],
i![attrs![At::Class => format!("styledIcon {}", Icon::Plus)]],
span![attrs![At::Class => "itemText"], "Create Issue"]
],
div![ div![
attrs![At::Class => "bottom"], attrs![At::Class => "bottom"],
about_tooltip(model, navbar_left_item(model, "About", Icon::Help)), about_tooltip(model, navbar_left_item(model, "About", Icon::Help)),

View File

@ -46,13 +46,13 @@ impl StyledButtonBuilder {
self.variant(Variant::Primary) self.variant(Variant::Primary)
} }
pub fn success(self) -> Self { // pub fn success(self) -> Self {
self.variant(Variant::Success) // self.variant(Variant::Success)
} // }
pub fn danger(self) -> Self { // pub fn danger(self) -> Self {
self.variant(Variant::Danger) // self.variant(Variant::Danger)
} // }
pub fn secondary(self) -> Self { pub fn secondary(self) -> Self {
self.variant(Variant::Secondary) self.variant(Variant::Secondary)
@ -62,15 +62,15 @@ impl StyledButtonBuilder {
self.variant(Variant::Empty) self.variant(Variant::Empty)
} }
pub fn disabled(mut self, value: bool) -> Self { // pub fn disabled(mut self, value: bool) -> Self {
self.disabled = Some(value); // self.disabled = Some(value);
self // self
} // }
pub fn active(mut self, value: bool) -> Self { // pub fn active(mut self, value: bool) -> Self {
self.active = Some(value); // self.active = Some(value);
self // self
} // }
pub fn text(mut self, value: String) -> Self { pub fn text(mut self, value: String) -> Self {
self.text = Some(Some(value)); self.text = Some(Some(value));

View File

@ -11,14 +11,8 @@ const MESSAGE: &str = "Are you sure you want to continue with this action?";
const CONFIRM_TEXT: &str = "Confirm"; const CONFIRM_TEXT: &str = "Confirm";
const CANCEL_TEXT: &str = "Cancel"; const CANCEL_TEXT: &str = "Cancel";
#[derive(Debug)]
pub enum Variant {
Primary,
}
#[derive(Debug)] #[derive(Debug)]
pub struct StyledConfirmModal { pub struct StyledConfirmModal {
pub variant: Variant,
pub title: String, pub title: String,
pub message: String, pub message: String,
pub confirm_text: String, pub confirm_text: String,
@ -40,7 +34,6 @@ impl ToNode for StyledConfirmModal {
#[derive(Default)] #[derive(Default)]
pub struct StyledConfirmModalBuilder { pub struct StyledConfirmModalBuilder {
variant: Option<Variant>,
title: Option<String>, title: Option<String>,
message: Option<String>, message: Option<String>,
confirm_text: Option<String>, confirm_text: Option<String>,
@ -49,11 +42,6 @@ pub struct StyledConfirmModalBuilder {
} }
impl StyledConfirmModalBuilder { impl StyledConfirmModalBuilder {
pub fn variant(mut self, variant: Variant) -> Self {
self.variant = Some(variant);
self
}
pub fn title<S>(mut self, title: S) -> Self pub fn title<S>(mut self, title: S) -> Self
where where
S: Into<String>, S: Into<String>,
@ -93,7 +81,6 @@ impl StyledConfirmModalBuilder {
pub fn build(self) -> StyledConfirmModal { pub fn build(self) -> StyledConfirmModal {
StyledConfirmModal { StyledConfirmModal {
variant: self.variant.unwrap_or_else(|| Variant::Primary),
title: self.title.unwrap_or_else(|| TITLE.to_string()), title: self.title.unwrap_or_else(|| TITLE.to_string()),
message: self.message.unwrap_or_else(|| MESSAGE.to_string()), message: self.message.unwrap_or_else(|| MESSAGE.to_string()),
confirm_text: self confirm_text: self
@ -107,7 +94,6 @@ impl StyledConfirmModalBuilder {
pub fn render(values: StyledConfirmModal) -> Node<Msg> { pub fn render(values: StyledConfirmModal) -> Node<Msg> {
let StyledConfirmModal { let StyledConfirmModal {
variant,
title, title,
message, message,
confirm_text, confirm_text,

View File

@ -0,0 +1,61 @@
use crate::shared::ToNode;
use crate::Msg;
use seed::{prelude::*, *};
#[derive(Debug, Clone)]
pub struct StyledForm {
heading: String,
fields: Vec<Node<Msg>>,
}
impl StyledForm {
pub fn build() -> StyledFormBuilder {
StyledFormBuilder::default()
}
}
impl ToNode for StyledForm {
fn into_node(self) -> Node<Msg> {
render(self)
}
}
#[derive(Debug, Default)]
pub struct StyledFormBuilder {
fields: Vec<Node<Msg>>,
heading: String,
}
impl StyledFormBuilder {
pub fn add_field(mut self, node: Node<Msg>) -> Self {
self.fields.push(node);
self
}
pub fn heading<S>(mut self, heading: S) -> Self
where
S: Into<String>,
{
self.heading = heading.into();
self
}
pub fn build(self) -> StyledForm {
StyledForm {
heading: self.heading,
fields: self.fields,
}
}
}
pub fn render(values: StyledForm) -> Node<Msg> {
let StyledForm { heading, fields } = values;
div![
attrs![At::Class => "styledForm"],
div![
attrs![At::Class => "formElement"],
div![attrs![At::Class => "heading"], heading],
fields
],
]
}

View File

@ -68,10 +68,10 @@ impl StyledModalBuilder {
self self
} }
pub fn with_icon(mut self, with_icon: bool) -> Self { // pub fn with_icon(mut self, with_icon: bool) -> Self {
self.with_icon = Some(with_icon); // self.with_icon = Some(with_icon);
self // self
} // }
pub fn children(mut self, children: Vec<Node<Msg>>) -> Self { pub fn children(mut self, children: Vec<Node<Msg>>) -> Self {
self.children = Some(children); self.children = Some(children);

View File

@ -1,5 +1,6 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
@ -17,6 +18,12 @@ pub enum Variant {
Normal, Normal,
} }
impl Default for Variant {
fn default() -> Self {
Variant::Empty
}
}
impl std::fmt::Display for Variant { impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
@ -40,18 +47,18 @@ pub struct StyledSelect<Child>
where where
Child: SelectOption + PartialEq, Child: SelectOption + PartialEq,
{ {
pub id: FieldId, id: FieldId,
pub variant: Variant, variant: Variant,
pub dropdown_width: Option<usize>, dropdown_width: Option<usize>,
pub name: Option<String>, name: Option<String>,
pub placeholder: Option<String>, placeholder: Option<String>,
pub valid: bool, valid: bool,
pub is_multi: bool, is_multi: bool,
pub allow_clear: bool, allow_clear: bool,
pub options: Vec<Child>, options: Vec<Child>,
pub selected: Vec<Child>, selected: Vec<Child>,
pub text_filter: String, text_filter: String,
pub opened: bool, opened: bool,
} }
impl<Child> ToNode for StyledSelect<Child> impl<Child> ToNode for StyledSelect<Child>
@ -63,6 +70,115 @@ where
} }
} }
impl<Child> StyledSelect<Child>
where
Child: SelectOption + PartialEq,
{
pub fn build(id: FieldId) -> StyledSelectBuilder<Child> {
StyledSelectBuilder {
id,
variant: None,
dropdown_width: None,
name: None,
placeholder: None,
valid: None,
is_multi: None,
allow_clear: None,
options: None,
selected: None,
text_filter: None,
opened: None,
}
}
}
#[derive(Debug)]
pub struct StyledSelectBuilder<Child>
where
Child: SelectOption + PartialEq,
{
id: FieldId,
variant: Option<Variant>,
dropdown_width: Option<Option<usize>>,
name: Option<Option<String>>,
placeholder: Option<Option<String>>,
valid: Option<bool>,
is_multi: Option<bool>,
allow_clear: Option<bool>,
options: Option<Vec<Child>>,
selected: Option<Vec<Child>>,
text_filter: Option<String>,
opened: Option<bool>,
}
impl<Child> StyledSelectBuilder<Child>
where
Child: SelectOption + PartialEq,
{
pub fn build(self) -> StyledSelect<Child> {
StyledSelect {
id: self.id,
variant: self.variant.unwrap_or_default(),
dropdown_width: self.dropdown_width.unwrap_or_default(),
name: self.name.unwrap_or_default(),
placeholder: self.placeholder.unwrap_or_default(),
valid: self.valid.unwrap_or(true),
is_multi: self.is_multi.unwrap_or_default(),
allow_clear: self.allow_clear.unwrap_or_default(),
options: self.options.unwrap_or_default(),
selected: self.selected.unwrap_or_default(),
text_filter: self.text_filter.unwrap_or_default(),
opened: self.opened.unwrap_or_default(),
}
}
pub fn dropdown_width(mut self, dropdown_width: usize) -> Self {
self.dropdown_width = Some(Some(dropdown_width));
self
}
pub fn name<S>(mut self, name: S) -> Self
where
S: Into<String>,
{
self.name = Some(Some(name.into()));
self
}
pub fn text_filter<S>(mut self, text_filter: S) -> Self
where
S: Into<String>,
{
self.text_filter = Some(text_filter.into());
self
}
pub fn opened(mut self, opened: bool) -> Self {
self.opened = Some(opened);
self
}
pub fn valid(mut self, valid: bool) -> Self {
self.valid = Some(valid);
self
}
pub fn options(mut self, options: Vec<Child>) -> Self {
self.options = Some(options);
self
}
pub fn selected(mut self, selected: Vec<Child>) -> Self {
self.selected = Some(selected);
self
}
pub fn normal(mut self) -> Self {
self.variant = Some(Variant::Normal);
self
}
}
pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg> pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg>
where where
Child: SelectOption + PartialEq, Child: SelectOption + PartialEq,

View File

@ -0,0 +1,5 @@
plugins:
- removeTitle: true
- convertPathData: true
- convertColors:
shorthex: true

View File

@ -4,6 +4,10 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const webpack = require('webpack'); const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
process.env.RUST_LOG = 'info';
dotenv.config(); dotenv.config();
module.exports = { module.exports = {
@ -32,8 +36,27 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.css$/i, test: /\.css$/i,
use: ['style-loader', 'css-loader'], use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
// options: { importLoaders: 1 }
}, },
// 'postcss-loader'
],
},
{
test: /\.svg$/,
use: [
{ loader: 'file-loader' },
{
loader: 'svgo-loader',
options: {
externalConfig: "svgo-config.yml"
}
}
]
}
], ],
}, },
plugins: [ plugins: [
@ -51,5 +74,10 @@ module.exports = {
'JIRS_SERVER_PORT', 'JIRS_SERVER_PORT',
'JIRS_SERVER_BIND', 'JIRS_SERVER_BIND',
]), ]),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: true,
}),
] ]
}; };

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,12 @@ pub enum IssueType {
Story, Story,
} }
impl Default for IssueType {
fn default() -> Self {
IssueType::Task
}
}
impl IssueType { impl IssueType {
pub fn to_label(&self) -> &str { pub fn to_label(&self) -> &str {
match self { match self {
@ -74,6 +80,12 @@ pub enum IssueStatus {
Done, Done,
} }
impl Default for IssueStatus {
fn default() -> Self {
IssueStatus::Backlog
}
}
impl FromStr for IssueStatus { impl FromStr for IssueStatus {
type Err = String; type Err = String;
@ -138,6 +150,12 @@ impl FromStr for IssuePriority {
} }
} }
impl Default for IssuePriority {
fn default() -> Self {
IssuePriority::Medium
}
}
impl std::fmt::Display for IssuePriority { impl std::fmt::Display for IssuePriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {

View File

@ -35,7 +35,6 @@ class ProjectIssueCreate extends React.Component {
...this.state.form, ...this.state.form,
status: IssueStatus.BACKLOG, status: IssueStatus.BACKLOG,
projectId: project.id, projectId: project.id,
// userIds: values.userIds,
}); });
await fetchProject(); await fetchProject();
toast.success('Issue has been successfully created.'); toast.success('Issue has been successfully created.');