Add modal to add issue
This commit is contained in:
parent
e082444a7c
commit
8950277149
1
jirs-client/.gitignore
vendored
1
jirs-client/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
pkg
|
||||
node_modules
|
||||
dist
|
||||
.yarn-error.log
|
||||
|
@ -42,6 +42,7 @@ aside#navbar-left .item {
|
||||
transition: color 0.1s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
aside#navbar-left .item:hover {
|
||||
|
12
jirs-client/js/css/styledForm.css
Normal file
12
jirs-client/js/css/styledForm.css
Normal file
@ -0,0 +1,12 @@
|
||||
.styledForm {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.styledForm > .formElement {
|
||||
padding: 25px 40px 35px;
|
||||
}
|
||||
|
||||
.styledForm > .formElement > .formHeading {
|
||||
padding-bottom: 15px;
|
||||
font-size: 21px;
|
||||
}
|
@ -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";
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
7
jirs-client/postcss.config.js
Normal file
7
jirs-client/postcss.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
parser: 'sugarss',
|
||||
plugins: [
|
||||
require('autoprefixer')({}),
|
||||
require('cssnano'),
|
||||
],
|
||||
};
|
16
jirs-client/scripts/build
Executable file
16
jirs-client/scripts/build
Executable 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
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
|
84
jirs-client/src/modal/add_issue.rs
Normal file
84
jirs-client/src/modal/add_issue.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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(),
|
||||
|
@ -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));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)),
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
61
jirs-client/src/shared/styled_form.rs
Normal file
61
jirs-client/src/shared/styled_form.rs
Normal 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
|
||||
],
|
||||
]
|
||||
}
|
@ -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);
|
||||
|
@ -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,
|
||||
|
5
jirs-client/svgo-config.yml
Normal file
5
jirs-client/svgo-config.yml
Normal file
@ -0,0 +1,5 @@
|
||||
plugins:
|
||||
- removeTitle: true
|
||||
- convertPathData: true
|
||||
- convertColors:
|
||||
shorthex: true
|
@ -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
@ -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 {
|
||||
|
@ -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.');
|
||||
|
Loading…
Reference in New Issue
Block a user