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
node_modules
dist
.yarn-error.log

View File

@ -42,6 +42,7 @@ aside#navbar-left .item {
transition: color 0.1s;
cursor: pointer;
user-select: none;
display: block;
}
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/fonts.css";
@import "css/variables.css";
@import "css/global.css";
@import "css/sidebar.css";
@import "css/aside.css";
@import "css/styledIcon.css";
@import "css/shared.css";
@import "css/styledTooltip.css";
@import "css/styledAvatar.css";
@import "css/styledSelect.css";
@import "css/styledButton.css";
@import "css/styledInput.css";
@import "css/styledModal.css";
@import "css/app.css";
@import "css/issue.css";
@import "css/project.css";
@import "./css/normalize.css";
@import "./css/fonts.css";
@import "./css/variables.css";
@import "./css/global.css";
@import "./css/sidebar.css";
@import "./css/aside.css";
@import "./css/styledIcon.css";
@import "./css/shared.css";
@import "./css/styledTooltip.css";
@import "./css/styledAvatar.css";
@import "./css/styledSelect.css";
@import "./css/styledButton.css";
@import "./css/styledInput.css";
@import "./css/styledModal.css";
@import "./css/styledForm.css";
@import "./css/app.css";
@import "./css/issue.css";
@import "./css/project.css";

View File

@ -2,16 +2,28 @@
"devDependencies": {
"@swc/core": "^1.1.37",
"@wasm-tool/wasm-pack-plugin": "^1.2.0",
"autoprefixer": "^9.7.5",
"css-loader": "^3.4.2",
"cssnano": "^4.1.10",
"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",
"mini-css-extract-plugin": "^0.9.0",
"optipng": "^2.1.0",
"postcss-loader": "^3.0.0",
"style-loader": "^1.1.3",
"sugarss": "^2.0.0",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"swc-loader": "^0.1.8",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
},
"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)),
}
}
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 {
IssueTypeEditModalTop,
CopyButtonLabel,
IssueTypeAddIssueModal,
}
#[derive(Clone, Debug)]
@ -56,6 +57,7 @@ pub enum Msg {
// issues
IssueUpdateResult(FetchObject<String>),
IssueDeleteResult(FetchObject<String>),
DeleteIssue(IssueId),
// 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::modal::update(&msg, model, orders);
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::ProjectSettings => project_settings::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> {
match model.page {
Page::Project => project::view(model),
Page::Project | Page::AddIssue => project::view(model),
Page::EditIssue(_id) => project::view(model),
Page::ProjectSettings => project_settings::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))),
_ => None,
},
"add-issue" => Some(Msg::ChangePage(Page::AddIssue)),
"project-settings" => Some(Msg::ChangePage(model::Page::ProjectSettings)),
"login" => Some(Msg::ChangePage(model::Page::Login)),
"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?")
.message("Once you delete, it's gone for good.")
.confirm_text("Delete issue")
.cancel_text("Cancel")
.on_confirm(handle_issue_delete)
.build()
.into_node()

View File

@ -5,75 +5,27 @@ use jirs_data::{Issue, IssueType};
use crate::model::{EditIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton;
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::{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> {
let issue_id = issue.id;
let issue_type_select = StyledSelect {
id: FieldId::IssueTypeEditModalTop,
variant: SelectVariant::Empty,
dropdown_width: Some(150),
name: Some("type".to_string()),
placeholder: None,
text_filter: modal.top_select_filter.clone(),
opened: modal.top_select_opened,
valid: true,
is_multi: false,
allow_clear: false,
options: vec![
let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
.dropdown_width(150)
.name("type")
.text_filter(modal.top_select_filter.as_str())
.opened(modal.top_select_opened)
.valid(true)
.options(vec![
IssueTypeOption(issue_id, IssueType::Story),
IssueTypeOption(issue_id, IssueType::Task),
IssueTypeOption(issue_id, IssueType::Bug),
],
selected: vec![IssueTypeOption(issue_id, modal.value.clone())],
}
.into_node();
])
.selected(vec![IssueTypeOption(issue_id, modal.value.clone())])
.build()
.into_node();
let click_handler = mouse_ev(Ev::Click, move |_| {
use wasm_bindgen::JsCast;
@ -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 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_select::StyledSelectChange;
use crate::shared::{find_issue, ToNode};
use crate::{model, FieldChange, FieldId, Msg};
mod add_issue;
mod confirm_delete_issue;
mod issue_details;
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
match msg {
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) => {
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),
_ => empty![],
ModalType::AddIssue(modal) => add_issue::view(model, modal),
})
.collect();
section![id!["modals"], modals]

View File

@ -11,6 +11,7 @@ pub type ProjectId = i32;
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq)]
pub enum ModalType {
AddIssue(AddIssueModal),
EditIssue(IssueId, EditIssueModal),
DeleteIssueConfirm(IssueId),
}
@ -24,10 +25,31 @@ pub struct EditIssueModal {
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)]
pub enum Page {
Project,
EditIssue(IssueId),
AddIssue,
ProjectSettings,
Login,
Register,
@ -38,6 +60,7 @@ impl Page {
match self {
Page::Project => "/board".to_string(),
Page::EditIssue(id) => format!("/issues/{id}", id = id),
Page::AddIssue => format!("/add-issues"),
Page::ProjectSettings => "/project-settings".to_string(),
Page::Login => "/login".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>) {
match msg {
Msg::ChangePage(Page::Project) => {
orders
.skip()
.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)) => {
Msg::ChangePage(Page::Project)
| Msg::ChangePage(Page::AddIssue)
| Msg::ChangePage(Page::EditIssue(..)) => {
orders
.skip()
.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) => {
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_button;
pub mod styled_confirm_modal;
pub mod styled_form;
pub mod styled_icon;
pub mod styled_input;
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]
],
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![
attrs![At::Class => "bottom"],
about_tooltip(model, navbar_left_item(model, "About", Icon::Help)),

View File

@ -46,13 +46,13 @@ impl StyledButtonBuilder {
self.variant(Variant::Primary)
}
pub fn success(self) -> Self {
self.variant(Variant::Success)
}
// pub fn success(self) -> Self {
// self.variant(Variant::Success)
// }
pub fn danger(self) -> Self {
self.variant(Variant::Danger)
}
// pub fn danger(self) -> Self {
// self.variant(Variant::Danger)
// }
pub fn secondary(self) -> Self {
self.variant(Variant::Secondary)
@ -62,15 +62,15 @@ impl StyledButtonBuilder {
self.variant(Variant::Empty)
}
pub fn disabled(mut self, value: bool) -> Self {
self.disabled = Some(value);
self
}
// pub fn disabled(mut self, value: bool) -> Self {
// self.disabled = Some(value);
// self
// }
pub fn active(mut self, value: bool) -> Self {
self.active = Some(value);
self
}
// pub fn active(mut self, value: bool) -> Self {
// self.active = Some(value);
// self
// }
pub fn text(mut self, value: String) -> Self {
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 CANCEL_TEXT: &str = "Cancel";
#[derive(Debug)]
pub enum Variant {
Primary,
}
#[derive(Debug)]
pub struct StyledConfirmModal {
pub variant: Variant,
pub title: String,
pub message: String,
pub confirm_text: String,
@ -40,7 +34,6 @@ impl ToNode for StyledConfirmModal {
#[derive(Default)]
pub struct StyledConfirmModalBuilder {
variant: Option<Variant>,
title: Option<String>,
message: Option<String>,
confirm_text: Option<String>,
@ -49,11 +42,6 @@ pub struct 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
where
S: Into<String>,
@ -93,7 +81,6 @@ impl StyledConfirmModalBuilder {
pub fn build(self) -> StyledConfirmModal {
StyledConfirmModal {
variant: self.variant.unwrap_or_else(|| Variant::Primary),
title: self.title.unwrap_or_else(|| TITLE.to_string()),
message: self.message.unwrap_or_else(|| MESSAGE.to_string()),
confirm_text: self
@ -107,7 +94,6 @@ impl StyledConfirmModalBuilder {
pub fn render(values: StyledConfirmModal) -> Node<Msg> {
let StyledConfirmModal {
variant,
title,
message,
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
}
pub fn with_icon(mut self, with_icon: bool) -> Self {
self.with_icon = Some(with_icon);
self
}
// pub fn with_icon(mut self, with_icon: bool) -> Self {
// self.with_icon = Some(with_icon);
// self
// }
pub fn children(mut self, children: Vec<Node<Msg>>) -> Self {
self.children = Some(children);

View File

@ -1,5 +1,6 @@
use seed::{prelude::*, *};
use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::ToNode;
use crate::{FieldId, Msg};
@ -17,6 +18,12 @@ pub enum Variant {
Normal,
}
impl Default for Variant {
fn default() -> Self {
Variant::Empty
}
}
impl std::fmt::Display for Variant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -40,18 +47,18 @@ pub struct StyledSelect<Child>
where
Child: SelectOption + PartialEq,
{
pub id: FieldId,
pub variant: Variant,
pub dropdown_width: Option<usize>,
pub name: Option<String>,
pub placeholder: Option<String>,
pub valid: bool,
pub is_multi: bool,
pub allow_clear: bool,
pub options: Vec<Child>,
pub selected: Vec<Child>,
pub text_filter: String,
pub opened: bool,
id: FieldId,
variant: Variant,
dropdown_width: Option<usize>,
name: Option<String>,
placeholder: Option<String>,
valid: bool,
is_multi: bool,
allow_clear: bool,
options: Vec<Child>,
selected: Vec<Child>,
text_filter: String,
opened: bool,
}
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>
where
Child: SelectOption + PartialEq,

View File

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

View File

@ -4,39 +4,62 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const dotenv = require('dotenv');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
process.env.RUST_LOG = 'info';
dotenv.config();
module.exports = {
entry: path.resolve(__dirname, 'js', 'index.js'),
output: {
filename: '[name].js',
path: path.resolve(__dirname, process.env.NODE_ENV === 'production' ? 'dist' : 'dev'),
entry: path.resolve(__dirname, 'js', 'index.js'),
output: {
filename: '[name].js',
path: path.resolve(__dirname, process.env.NODE_ENV === 'production' ? 'dist' : 'dev'),
publicPath: '/',
},
devtool: 'source-map',
devtool: 'source-map',
devServer: {
contentBase: path.join(__dirname, 'dev'),
contentBase: path.join(__dirname, 'dev'),
historyApiFallback: true,
hot: true,
port: process.env.JIRS_CLIENT_PORT || 6000,
host: process.env.JIRS_CLIENT_BIND || '0.0.0.0',
allowedHosts: [
hot: true,
port: process.env.JIRS_CLIENT_PORT || 6000,
host: process.env.JIRS_CLIENT_BIND || '0.0.0.0',
allowedHosts: [
'localhost:6000',
'localhost:8000',
],
headers: {
headers: {
'Access-Control-Allow-Origin': '*',
}
},
module: {
module: {
rules: [
{
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: [
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname),
}),
@ -51,5 +74,10 @@ module.exports = {
'JIRS_SERVER_PORT',
'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,
}
impl Default for IssueType {
fn default() -> Self {
IssueType::Task
}
}
impl IssueType {
pub fn to_label(&self) -> &str {
match self {
@ -74,6 +80,12 @@ pub enum IssueStatus {
Done,
}
impl Default for IssueStatus {
fn default() -> Self {
IssueStatus::Backlog
}
}
impl FromStr for IssueStatus {
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View File

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