Add comments section. Refactor Field ids
This commit is contained in:
parent
1663d9a485
commit
7262411676
@ -31,18 +31,26 @@ version = "*"
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.22"
|
||||
features = [
|
||||
# elements
|
||||
"Window",
|
||||
"DataTransfer",
|
||||
"DragEvent",
|
||||
"HtmlDivElement",
|
||||
"DomRect",
|
||||
"HtmlDocument",
|
||||
"Document",
|
||||
"HtmlBodyElement",
|
||||
# types
|
||||
"DataTransfer",
|
||||
"DomRect",
|
||||
"Selection",
|
||||
"CssStyleDeclaration",
|
||||
"WebSocket",
|
||||
"BinaryType",
|
||||
"Blob",
|
||||
"MessageEvent",
|
||||
"AddEventListenerOptions",
|
||||
# events
|
||||
"EventTarget",
|
||||
"ErrorEvent",
|
||||
"MessageEvent",
|
||||
"KeyEvent",
|
||||
"KeyboardEvent",
|
||||
"DragEvent",
|
||||
]
|
||||
|
@ -34,6 +34,71 @@
|
||||
background: var(--backgroundLight);
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .title {
|
||||
font-family: var(--font-medium);
|
||||
font-weight: normal;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create {
|
||||
position: relative;
|
||||
margin-top: 25px;
|
||||
font-size: 15px
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .userAvatar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right {
|
||||
padding-left: 44px;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right > .fakeTextArea {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--borderLightest);
|
||||
color: var(--textLight);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right > .fakeTextArea:hover {
|
||||
border: 1px solid var(--borderLight);
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right > .proTip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
color: var(--textMedium);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right > .proTip > .strong {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.issueDetails > .content > .left > .comments > .create > .right > .proTip > .tipLetter {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
color: var(--textDarkest);
|
||||
background: var(--backgroundMedium);
|
||||
font-family: var(--font-bold);
|
||||
font-weight: normal;
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.issueDetails > .content > .right {
|
||||
width: 35%;
|
||||
padding-top: 5px;
|
||||
|
@ -197,58 +197,3 @@
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem > .styledIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem > .optionLabel {
|
||||
padding: 0 5px 0 7px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* issue priority */
|
||||
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority > .optionLabel {
|
||||
padding: 0 5px 0 7px;
|
||||
font-size: 15px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority > .selectItemLabel {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority > .styledIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority.highest > .styledIcon,
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority.highest > .styledIcon {
|
||||
color: var(--highest);
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority.high > .styledIcon,
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority.high > .styledIcon {
|
||||
color: var(--high);
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority.medium > .styledIcon,
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority.medium > .styledIcon {
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority.low > .styledIcon,
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority.low > .styledIcon {
|
||||
color: var(--low);
|
||||
}
|
||||
|
||||
.styledSelect > .valueContainer > .selectItem.priority.lowest > .styledIcon,
|
||||
.styledSelect > .dropDown > .options > .option > .optionItem.priority.lowest > .styledIcon {
|
||||
color: var(--lowest);
|
||||
}
|
||||
|
67
jirs-client/js/css/styledSelectChild.css
Normal file
67
jirs-client/js/css/styledSelectChild.css
Normal file
@ -0,0 +1,67 @@
|
||||
.selectItem {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.selectItem.priority.highest > .styledIcon,
|
||||
.optionItem.priority.highest > .styledIcon {
|
||||
color: var(--highest);
|
||||
}
|
||||
|
||||
.selectItem.priority.high > .styledIcon,
|
||||
.optionItem.priority.high > .styledIcon {
|
||||
color: var(--high);
|
||||
}
|
||||
|
||||
.selectItem.priority.medium > .styledIcon,
|
||||
.optionItem.priority.medium > .styledIcon {
|
||||
color: var(--medium);
|
||||
}
|
||||
|
||||
.selectItem.priority.low > .styledIcon,
|
||||
.optionItem.priority.low > .styledIcon {
|
||||
color: var(--low);
|
||||
}
|
||||
|
||||
.selectItem.priority.lowest > .styledIcon,
|
||||
.optionItem.priority.lowest > .styledIcon {
|
||||
color: var(--lowest);
|
||||
}
|
||||
|
||||
.selectItem.priority > .selectItemLabel {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.optionItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.optionItem > .styledIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.selectItem > .selectItemLabel,
|
||||
.optionItem > .optionLabel {
|
||||
padding: 0 5px 0 7px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.optionItem.priority > .optionLabel {
|
||||
padding: 0 5px 0 7px;
|
||||
font-size: 15px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.optionItem.priority > .styledIcon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* edit issue */
|
||||
.topActions > .styledSelect > .valueContainer,
|
||||
.topActions > .styledSelect > .valueContainer > .selectItem {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.topActions .selectItem, .topActions .optionItem {
|
||||
padding: 0 12px;
|
||||
}
|
@ -9,6 +9,7 @@
|
||||
@import "./css/styledTooltip.css";
|
||||
@import "./css/styledAvatar.css";
|
||||
@import "./css/styledSelect.css";
|
||||
@import "./css/styledSelectChild.css";
|
||||
@import "./css/styledButton.css";
|
||||
@import "./css/styledInput.css";
|
||||
@import "./css/styledModal.css";
|
||||
|
@ -1,9 +1,8 @@
|
||||
use std::sync::RwLock;
|
||||
|
||||
use seed::fetch::FetchObject;
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{IssueStatus, WsMsg};
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{ModalType, Model, Page};
|
||||
@ -21,70 +20,91 @@ mod register;
|
||||
mod shared;
|
||||
mod ws;
|
||||
|
||||
pub type UserId = i32;
|
||||
pub type IssueId = i32;
|
||||
pub type AvatarFilterActive = bool;
|
||||
pub type AppType = App<Msg, Model, Node<Msg>>;
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum EditIssueModalFieldId {
|
||||
IssueType,
|
||||
Title,
|
||||
Description,
|
||||
Status,
|
||||
Assignees,
|
||||
Reporter,
|
||||
Priority,
|
||||
Estimate,
|
||||
TimeSpend,
|
||||
TimeRemaining,
|
||||
// comment
|
||||
CommentBody,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum AddIssueModalFieldId {
|
||||
IssueType,
|
||||
Summary,
|
||||
Description,
|
||||
Reporter,
|
||||
Assignees,
|
||||
Priority,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum FieldId {
|
||||
// edit issue
|
||||
IssueTypeEditModalTop,
|
||||
TitleIssueEditModal,
|
||||
DescriptionIssueEditModal,
|
||||
StatusIssueEditModal,
|
||||
AssigneesIssueEditModal,
|
||||
ReporterIssueEditModal,
|
||||
PriorityIssueEditModal,
|
||||
EstimateIssueEditModal,
|
||||
TimeSpendIssueEditModal,
|
||||
TimeRemainingIssueEditModal,
|
||||
// issue
|
||||
AddIssueModal(AddIssueModalFieldId),
|
||||
EditIssueModal(EditIssueModalFieldId),
|
||||
// project boards
|
||||
TextFilterBoard,
|
||||
//
|
||||
CopyButtonLabel,
|
||||
// add issue
|
||||
IssueTypeAddIssueModal,
|
||||
SummaryAddIssueModal,
|
||||
DescriptionAddIssueModal,
|
||||
ReporterAddIssueModal,
|
||||
AssigneesAddIssueModal,
|
||||
IssuePriorityAddIssueModal,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FieldId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FieldId::IssueTypeEditModalTop => f.write_str("issueTypeEditModalTop"),
|
||||
FieldId::EditIssueModal(sub) => match sub {
|
||||
EditIssueModalFieldId::IssueType => f.write_str("issueTypeEditModalTop"),
|
||||
EditIssueModalFieldId::Title => f.write_str("titleIssueEditModal"),
|
||||
EditIssueModalFieldId::Description => f.write_str("descriptionIssueEditModal"),
|
||||
EditIssueModalFieldId::Status => f.write_str("statusIssueEditModal"),
|
||||
EditIssueModalFieldId::Assignees => f.write_str("assigneesIssueEditModal"),
|
||||
EditIssueModalFieldId::Reporter => f.write_str("reporterIssueEditModal"),
|
||||
EditIssueModalFieldId::Priority => f.write_str("priorityIssueEditModal"),
|
||||
EditIssueModalFieldId::Estimate => f.write_str("estimateIssueEditModal"),
|
||||
EditIssueModalFieldId::TimeSpend => f.write_str("timeSpendIssueEditModal"),
|
||||
EditIssueModalFieldId::TimeRemaining => f.write_str("timeRemainingIssueEditModal"),
|
||||
EditIssueModalFieldId::CommentBody => f.write_str("editIssue-commentBody"),
|
||||
},
|
||||
FieldId::AddIssueModal(sub) => match sub {
|
||||
AddIssueModalFieldId::IssueType => f.write_str("issueTypeAddIssueModal"),
|
||||
AddIssueModalFieldId::Summary => f.write_str("summaryAddIssueModal"),
|
||||
AddIssueModalFieldId::Description => f.write_str("descriptionAddIssueModal"),
|
||||
AddIssueModalFieldId::Reporter => f.write_str("reporterAddIssueModal"),
|
||||
AddIssueModalFieldId::Assignees => f.write_str("assigneesAddIssueModal"),
|
||||
AddIssueModalFieldId::Priority => f.write_str("issuePriorityAddIssueModal"),
|
||||
},
|
||||
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
|
||||
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
|
||||
FieldId::IssueTypeAddIssueModal => f.write_str("issueTypeAddIssueModal"),
|
||||
FieldId::SummaryAddIssueModal => f.write_str("summaryAddIssueModal"),
|
||||
FieldId::DescriptionAddIssueModal => f.write_str("descriptionAddIssueModal"),
|
||||
FieldId::ReporterAddIssueModal => f.write_str("reporterAddIssueModal"),
|
||||
FieldId::AssigneesAddIssueModal => f.write_str("assigneesAddIssueModal"),
|
||||
FieldId::IssuePriorityAddIssueModal => f.write_str("issuePriorityAddIssueModal"),
|
||||
FieldId::TitleIssueEditModal => f.write_str("titleIssueEditModal"),
|
||||
FieldId::DescriptionIssueEditModal => f.write_str("descriptionIssueEditModal"),
|
||||
FieldId::StatusIssueEditModal => f.write_str("statusIssueEditModal"),
|
||||
FieldId::AssigneesIssueEditModal => f.write_str("assigneesIssueEditModal"),
|
||||
FieldId::ReporterIssueEditModal => f.write_str("reporterIssueEditModal"),
|
||||
FieldId::PriorityIssueEditModal => f.write_str("priorityIssueEditModal"),
|
||||
FieldId::EstimateIssueEditModal => f.write_str("estimateIssueEditModal"),
|
||||
FieldId::TimeSpendIssueEditModal => f.write_str("timeSpendIssueEditModal"),
|
||||
FieldId::TimeRemainingIssueEditModal => f.write_str("timeRemainingIssueEditModal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FieldChange {
|
||||
LinkCopied(FieldId, bool),
|
||||
TabChanged(FieldId, TabMode),
|
||||
ToggleCreateComment(FieldId, bool),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Msg {
|
||||
NoOp,
|
||||
GlobalKeyDown {
|
||||
key: String,
|
||||
shift: bool,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
},
|
||||
|
||||
// Auth Token
|
||||
AuthTokenStored,
|
||||
@ -93,7 +113,6 @@ pub enum Msg {
|
||||
StyledSelectChanged(FieldId, StyledSelectChange),
|
||||
|
||||
ChangePage(model::Page),
|
||||
CurrentProjectResult(FetchObject<String>),
|
||||
InternalFailure(String),
|
||||
ToggleAboutTooltip,
|
||||
|
||||
@ -127,6 +146,9 @@ pub enum Msg {
|
||||
}
|
||||
|
||||
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
if msg == Msg::NoOp {
|
||||
return;
|
||||
}
|
||||
if cfg!(debug_assertions) {
|
||||
log!(msg);
|
||||
}
|
||||
@ -227,6 +249,28 @@ pub fn render() {
|
||||
5000,
|
||||
);
|
||||
|
||||
if let Some(body) = seed::html_document().body() {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let body = body.dyn_ref::<web_sys::HtmlBodyElement>().unwrap().clone();
|
||||
let key_up_closure =
|
||||
wasm_bindgen::closure::Closure::wrap(Box::new(|event: web_sys::KeyboardEvent| {
|
||||
if let Some(Ok(app)) = unsafe { APP.as_mut().map(|app| app.write()) } {
|
||||
let msg = Msg::GlobalKeyDown {
|
||||
key: event.key(),
|
||||
shift: event.shift_key(),
|
||||
ctrl: event.ctrl_key(),
|
||||
alt: event.alt_key(),
|
||||
};
|
||||
app.update(msg);
|
||||
}
|
||||
})
|
||||
as Box<dyn Fn(web_sys::KeyboardEvent)>);
|
||||
body.add_event_listener_with_callback("keyup", key_up_closure.as_ref().unchecked_ref())
|
||||
.unwrap();
|
||||
key_up_closure.forget();
|
||||
}
|
||||
|
||||
let app = seed::App::builder(update, view)
|
||||
.routes(routes)
|
||||
.build_and_start();
|
||||
|
@ -1,21 +1,20 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{IssuePriority, IssueType, User};
|
||||
use jirs_data::{IssuePriority, IssueType, ToVec};
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{AddIssueModal, ModalType, Model};
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_icon::StyledIcon;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::styled_select::StyledSelectChange;
|
||||
use crate::shared::styled_select_child::ToStyledSelectChild;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldId, Msg};
|
||||
use crate::{AddIssueModalFieldId, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
let modal = model.modals.iter_mut().find(|modal| match modal {
|
||||
@ -58,16 +57,16 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::InputChanged(FieldId::DescriptionAddIssueModal, value) => {
|
||||
Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Description), value) => {
|
||||
modal.description = Some(value.clone());
|
||||
}
|
||||
Msg::InputChanged(FieldId::SummaryAddIssueModal, value) => {
|
||||
Msg::InputChanged(FieldId::AddIssueModal(AddIssueModalFieldId::Summary), value) => {
|
||||
modal.title = value.clone();
|
||||
}
|
||||
|
||||
// IssueTypeAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::IssueTypeAddIssueModal,
|
||||
FieldId::AddIssueModal(AddIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Changed(id),
|
||||
) => {
|
||||
modal.issue_type = (*id).into();
|
||||
@ -75,7 +74,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
|
||||
// ReporterAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::ReporterAddIssueModal,
|
||||
FieldId::AddIssueModal(AddIssueModalFieldId::Reporter),
|
||||
StyledSelectChange::Changed(id),
|
||||
) => {
|
||||
modal.reporter_id = Some(*id as i32);
|
||||
@ -83,7 +82,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
|
||||
// AssigneesAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AssigneesAddIssueModal,
|
||||
FieldId::AddIssueModal(AddIssueModalFieldId::Assignees),
|
||||
StyledSelectChange::Changed(id),
|
||||
) => {
|
||||
let id = *id as i32;
|
||||
@ -92,7 +91,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
}
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AssigneesAddIssueModal,
|
||||
FieldId::AddIssueModal(AddIssueModalFieldId::Assignees),
|
||||
StyledSelectChange::RemoveMulti(id),
|
||||
) => {
|
||||
let id = *id as i32;
|
||||
@ -107,7 +106,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
|
||||
// IssuePriorityAddIssueModal
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::IssuePriorityAddIssueModal,
|
||||
FieldId::AddIssueModal(AddIssueModalFieldId::Priority),
|
||||
StyledSelectChange::Changed(id),
|
||||
) => {
|
||||
modal.priority = (*id).into();
|
||||
@ -118,18 +117,19 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
||||
}
|
||||
|
||||
pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
let select_type = StyledSelect::build(FieldId::IssueTypeAddIssueModal)
|
||||
let select_type = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::IssueType))
|
||||
.name("type")
|
||||
.normal()
|
||||
.text_filter(modal.type_state.text_filter.as_str())
|
||||
.opened(modal.type_state.opened)
|
||||
.valid(true)
|
||||
.options(vec![
|
||||
IssueTypeOption(IssueType::Story),
|
||||
IssueTypeOption(IssueType::Task),
|
||||
IssueTypeOption(IssueType::Bug),
|
||||
])
|
||||
.selected(vec![IssueTypeOption(modal.issue_type.clone())])
|
||||
.options(
|
||||
IssueType::ordered()
|
||||
.iter()
|
||||
.map(|t| t.to_select_child().name("type"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![modal.issue_type.to_select_child().name("type")])
|
||||
.build()
|
||||
.into_node();
|
||||
let issue_type_field = StyledField::build()
|
||||
@ -139,7 +139,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let short_summary = StyledInput::build(FieldId::SummaryAddIssueModal)
|
||||
let short_summary = StyledInput::build(FieldId::AddIssueModal(AddIssueModalFieldId::Summary))
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
@ -152,7 +152,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
|
||||
let description = StyledTextarea::build()
|
||||
.height(110)
|
||||
.build(FieldId::DescriptionAddIssueModal)
|
||||
.build(FieldId::AddIssueModal(AddIssueModalFieldId::Description))
|
||||
.into_node();
|
||||
let description_field = StyledField::build()
|
||||
.label("Description")
|
||||
@ -165,18 +165,24 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.reporter_id
|
||||
.or_else(|| model.user.as_ref().map(|u| u.id))
|
||||
.unwrap_or_default();
|
||||
let reporter = StyledSelect::build(FieldId::ReporterAddIssueModal)
|
||||
let reporter = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Reporter))
|
||||
.normal()
|
||||
.text_filter(modal.reporter_state.text_filter.as_str())
|
||||
.opened(modal.reporter_state.opened)
|
||||
.options(model.users.iter().map(|u| UserOption(u)).collect())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|u| u.to_select_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter_map(|user| {
|
||||
if user.id == reporter_id {
|
||||
Some(UserOption(user))
|
||||
Some(user.to_select_child().name("reporter"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -193,19 +199,25 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let assignees = StyledSelect::build(FieldId::AssigneesAddIssueModal)
|
||||
let assignees = StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Assignees))
|
||||
.normal()
|
||||
.multi()
|
||||
.text_filter(modal.assignees_state.text_filter.as_str())
|
||||
.opened(modal.assignees_state.opened)
|
||||
.options(model.users.iter().map(|u| UserOption(u)).collect())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|u| u.to_select_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter_map(|user| {
|
||||
if modal.user_ids.contains(&user.id) {
|
||||
Some(UserOption(user))
|
||||
Some(user.to_select_child().name("assignees"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -222,22 +234,22 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let select_priority = StyledSelect::build(FieldId::IssuePriorityAddIssueModal)
|
||||
.name("priority")
|
||||
.normal()
|
||||
.text_filter(modal.priority_state.text_filter.as_str())
|
||||
.opened(modal.priority_state.opened)
|
||||
.valid(true)
|
||||
.options(vec![
|
||||
IssuePriorityOption(IssuePriority::Highest),
|
||||
IssuePriorityOption(IssuePriority::High),
|
||||
IssuePriorityOption(IssuePriority::Medium),
|
||||
IssuePriorityOption(IssuePriority::Low),
|
||||
IssuePriorityOption(IssuePriority::Lowest),
|
||||
])
|
||||
.selected(vec![IssuePriorityOption(modal.priority.clone())])
|
||||
.build()
|
||||
.into_node();
|
||||
let select_priority =
|
||||
StyledSelect::build(FieldId::AddIssueModal(AddIssueModalFieldId::Priority))
|
||||
.name("priority")
|
||||
.normal()
|
||||
.text_filter(modal.priority_state.text_filter.as_str())
|
||||
.opened(modal.priority_state.opened)
|
||||
.valid(true)
|
||||
.options(
|
||||
IssuePriority::ordered()
|
||||
.iter()
|
||||
.map(|p| p.to_select_child().name("priority"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![modal.priority.to_select_child().name("priority")])
|
||||
.build()
|
||||
.into_node();
|
||||
let issue_priority_field = StyledField::build()
|
||||
.label("Issue Type")
|
||||
.tip("Priority in relation to other issues.")
|
||||
@ -292,139 +304,3 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
||||
.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")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "type optionItem"],
|
||||
icon,
|
||||
div![attrs![At::Class => "typeLabel optionLabel"], 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();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "selectItem"],
|
||||
type_icon,
|
||||
div![attrs![At::Class => "selectItemLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct IssuePriorityOption(IssuePriority);
|
||||
|
||||
impl crate::shared::styled_select::SelectOption for IssuePriorityOption {
|
||||
fn into_option(self) -> Node<Msg> {
|
||||
let name = format!("{}", self.0);
|
||||
|
||||
let icon = StyledIcon::build(self.0.into())
|
||||
.add_class("issuePriorityIcon")
|
||||
.size(18)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => format!("priority optionItem {}", name)],
|
||||
icon,
|
||||
div![attrs![At::Class => "priorityLabel optionLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
fn into_value(self) -> Node<Msg> {
|
||||
let name = format!("{}", self.0);
|
||||
|
||||
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => format!("selectItem priority {}", name)],
|
||||
type_icon,
|
||||
div![attrs![At::Class => "selectItemLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct UserOption<'opt>(pub &'opt User);
|
||||
|
||||
impl<'opt> UserOption<'opt> {
|
||||
fn avatar_node(&self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
StyledAvatar::build()
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.size(20)
|
||||
.name(user.name.as_str())
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'opt> crate::shared::styled_select::SelectOption for UserOption<'opt> {
|
||||
fn into_option(self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
|
||||
let styled_avatar = self.avatar_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "user optionItem"],
|
||||
styled_avatar,
|
||||
div![attrs![At::Class => "typeLabel optionLabel"], user.name]
|
||||
]
|
||||
}
|
||||
|
||||
fn into_value(self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
|
||||
let styled_avatar = self.avatar_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "selectItem"],
|
||||
styled_avatar,
|
||||
div![attrs![At::Class => "selectItemLabel"], user.name]
|
||||
]
|
||||
}
|
||||
|
||||
fn match_text_filter(&self, text_filter: &str) -> bool {
|
||||
self.0.name.contains(text_filter)
|
||||
}
|
||||
|
||||
fn to_value(&self) -> u32 {
|
||||
self.0.id as u32
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,13 @@ use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_editor::StyledEditor;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_icon::Icon;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange};
|
||||
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
|
||||
use crate::shared::styled_select_child::ToStyledSelectChild;
|
||||
use crate::shared::styled_textarea::StyledTextarea;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldChange, FieldId, IssueId, Msg};
|
||||
use crate::{EditIssueModalFieldId, FieldChange, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
|
||||
@ -31,35 +32,35 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
modal.payload = issue.clone().into();
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::IssueTypeEditModalTop,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
modal.payload.issue_type = (*value).into();
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::StatusIssueEditModal,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Status),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
modal.payload.status = (*value).into();
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::ReporterIssueEditModal,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Reporter),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
modal.payload.reporter_id = *value as i32;
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AssigneesIssueEditModal,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
modal.payload.user_ids.push(*value as i32);
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::AssigneesIssueEditModal,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
|
||||
StyledSelectChange::RemoveMulti(value),
|
||||
) => {
|
||||
let mut old = vec![];
|
||||
@ -73,61 +74,68 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::PriorityIssueEditModal,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Priority),
|
||||
StyledSelectChange::Changed(value),
|
||||
) => {
|
||||
modal.payload.priority = (*value).into();
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::InputChanged(FieldId::TitleIssueEditModal, value) => {
|
||||
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Title), value) => {
|
||||
modal.payload.title = value.clone();
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => {
|
||||
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Description), value) => {
|
||||
modal.payload.description = Some(value.clone());
|
||||
modal.payload.description_text = Some(value.clone());
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => {
|
||||
Msg::ModalChanged(FieldChange::TabChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::Description),
|
||||
mode,
|
||||
)) => {
|
||||
modal.description_editor_mode = mode.clone();
|
||||
}
|
||||
Msg::ModalChanged(FieldChange::ToggleCreateComment(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
|
||||
flag,
|
||||
)) => {
|
||||
modal.creating_comment = *flag;
|
||||
}
|
||||
Msg::GlobalKeyDown { key, .. } if key.as_str() == "m" && !modal.creating_comment => {
|
||||
orders
|
||||
.skip()
|
||||
.send_msg(Msg::ModalChanged(FieldChange::ToggleCreateComment(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
|
||||
true,
|
||||
)));
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
div![
|
||||
attrs![At::Class => "issueDetails"],
|
||||
top_modal_row(model, modal),
|
||||
div![
|
||||
attrs![At::Class => "content"],
|
||||
left_modal_column(model, modal),
|
||||
right_modal_column(model, modal),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
id,
|
||||
link_copied,
|
||||
payload,
|
||||
top_type_state,
|
||||
status_state,
|
||||
reporter_state: _,
|
||||
assignees_state: _,
|
||||
priority_state: _,
|
||||
description_editor_mode,
|
||||
link_copied,
|
||||
..
|
||||
} = modal;
|
||||
let issue_id = id.clone();
|
||||
|
||||
let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
|
||||
.dropdown_width(150)
|
||||
.name("type")
|
||||
.text_filter(top_type_state.text_filter.as_str())
|
||||
.opened(top_type_state.opened)
|
||||
.valid(true)
|
||||
.options(
|
||||
IssueType::ordered()
|
||||
.into_iter()
|
||||
.map(|t| IssueTypeTopOption(issue_id, t))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![IssueTypeTopOption(
|
||||
issue_id,
|
||||
payload.issue_type.clone(),
|
||||
)])
|
||||
.build()
|
||||
.into_node();
|
||||
let issue_id = id.clone();
|
||||
|
||||
let click_handler = mouse_ev(Ev::Click, move |_| {
|
||||
use wasm_bindgen::JsCast;
|
||||
@ -177,26 +185,135 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
// left
|
||||
let issue_type_select =
|
||||
StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::IssueType))
|
||||
.dropdown_width(150)
|
||||
.name("type")
|
||||
.text_filter(top_type_state.text_filter.as_str())
|
||||
.opened(top_type_state.opened)
|
||||
.valid(true)
|
||||
.options(
|
||||
IssueType::ordered()
|
||||
.into_iter()
|
||||
.map(|t| t.to_select_child().name("type"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![{
|
||||
let id = modal.id.clone();
|
||||
let issue_type = &payload.issue_type;
|
||||
issue_type
|
||||
.to_select_child()
|
||||
.name("type")
|
||||
.text(format!("{} - {}", issue_type, id))
|
||||
}])
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "topActions"],
|
||||
issue_type_select,
|
||||
div![
|
||||
attrs![At::Class => "topActionsRight"],
|
||||
copy_button,
|
||||
delete_button,
|
||||
close_button
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
description_editor_mode,
|
||||
creating_comment,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let title = StyledTextarea::build()
|
||||
.value(payload.title.as_str())
|
||||
.add_class("textarea")
|
||||
.max_height(48)
|
||||
.height(0)
|
||||
.build(FieldId::TitleIssueEditModal)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalFieldId::Title))
|
||||
.into_node();
|
||||
|
||||
let description_text = payload.description.as_ref().cloned().unwrap_or_default();
|
||||
let description = StyledEditor::build(FieldId::DescriptionIssueEditModal)
|
||||
.text(description_text)
|
||||
.mode(description_editor_mode.clone())
|
||||
.update_on(Ev::Change)
|
||||
.build()
|
||||
.into_node();
|
||||
let description =
|
||||
StyledEditor::build(FieldId::EditIssueModal(EditIssueModalFieldId::Description))
|
||||
.text(description_text)
|
||||
.mode(description_editor_mode.clone())
|
||||
.update_on(Ev::Change)
|
||||
.build()
|
||||
.into_node();
|
||||
let description_field = StyledField::build().input(description).build().into_node();
|
||||
|
||||
// right
|
||||
let status = StyledSelect::build(FieldId::StatusIssueEditModal)
|
||||
let user_avatar = StyledAvatar::build()
|
||||
.add_class("userAvatar")
|
||||
.size(32)
|
||||
.avatar_url(
|
||||
model
|
||||
.user
|
||||
.as_ref()
|
||||
.and_then(|u| u.avatar_url.clone())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let create_comment = if *creating_comment {
|
||||
let text_area = StyledTextarea::build()
|
||||
.build(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody))
|
||||
.into_node();
|
||||
div![text_area]
|
||||
} else {
|
||||
let creating_comment = *creating_comment;
|
||||
let handler = mouse_ev(Ev::Click, move |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::ModalChanged(FieldChange::ToggleCreateComment(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
|
||||
!creating_comment,
|
||||
))
|
||||
});
|
||||
div![class!["fakeTextArea"], handler]
|
||||
};
|
||||
|
||||
div![
|
||||
class!["left"],
|
||||
title,
|
||||
description_field,
|
||||
div![
|
||||
class!["comments"],
|
||||
div![class!["title"], "Comments"],
|
||||
div![
|
||||
class!["create"],
|
||||
user_avatar,
|
||||
div![
|
||||
class!["right"],
|
||||
create_comment,
|
||||
div![
|
||||
class!["proTip"],
|
||||
strong![class!["strong"], "Pro tip: "],
|
||||
"press ",
|
||||
span![class!["tipLetter"], "M"],
|
||||
" to comment"
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
let EditIssueModal {
|
||||
payload,
|
||||
status_state,
|
||||
reporter_state,
|
||||
assignees_state,
|
||||
priority_state,
|
||||
..
|
||||
} = modal;
|
||||
|
||||
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Status))
|
||||
.name("status")
|
||||
.opened(status_state.opened)
|
||||
.normal()
|
||||
@ -204,10 +321,10 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.options(
|
||||
IssueStatus::ordered()
|
||||
.into_iter()
|
||||
.map(|opt| IssueStatusOption(issue_id, opt))
|
||||
.map(|opt| opt.to_select_child().name("status"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![IssueStatusOption(issue_id, payload.status.clone())])
|
||||
.selected(vec![payload.status.to_select_child().name("status")])
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
@ -217,19 +334,25 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let assignees = StyledSelect::build(FieldId::AssigneesIssueEditModal)
|
||||
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Assignees))
|
||||
.name("assignees")
|
||||
.opened(modal.assignees_state.opened)
|
||||
.normal()
|
||||
.opened(assignees_state.opened)
|
||||
.empty()
|
||||
.multi()
|
||||
.text_filter(modal.assignees_state.text_filter.as_str())
|
||||
.options(model.users.iter().map(|user| UserOption(user)).collect())
|
||||
.text_filter(assignees_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_select_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.user_ids.contains(&user.id))
|
||||
.map(|user| UserOption(user))
|
||||
.map(|user| user.to_select_child().name("assignees"))
|
||||
.collect(),
|
||||
)
|
||||
.build()
|
||||
@ -240,18 +363,24 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let reporter = StyledSelect::build(FieldId::ReporterIssueEditModal)
|
||||
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Reporter))
|
||||
.name("reporter")
|
||||
.opened(modal.reporter_state.opened)
|
||||
.normal()
|
||||
.text_filter(modal.reporter_state.text_filter.as_str())
|
||||
.options(model.users.iter().map(|user| UserOption(user)).collect())
|
||||
.opened(reporter_state.opened)
|
||||
.empty()
|
||||
.text_filter(reporter_state.text_filter.as_str())
|
||||
.options(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.map(|user| user.to_select_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(
|
||||
model
|
||||
.users
|
||||
.iter()
|
||||
.filter(|user| payload.reporter_id == user.id)
|
||||
.map(|user| UserOption(user))
|
||||
.map(|user| user.to_select_child().name("reporter"))
|
||||
.collect(),
|
||||
)
|
||||
.build()
|
||||
@ -262,18 +391,18 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let priority = StyledSelect::build(FieldId::PriorityIssueEditModal)
|
||||
.name("assignees")
|
||||
.opened(modal.priority_state.opened)
|
||||
.normal()
|
||||
.text_filter(modal.priority_state.text_filter.as_str())
|
||||
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Priority))
|
||||
.name("priority")
|
||||
.opened(priority_state.opened)
|
||||
.empty()
|
||||
.text_filter(priority_state.text_filter.as_str())
|
||||
.options(
|
||||
IssuePriority::ordered()
|
||||
.into_iter()
|
||||
.map(|p| IssuePriorityOption(p))
|
||||
.map(|p| p.to_select_child().name("priority"))
|
||||
.collect(),
|
||||
)
|
||||
.selected(vec![IssuePriorityOption(payload.priority.clone())])
|
||||
.selected(vec![payload.priority.to_select_child().name("priority")])
|
||||
.build()
|
||||
.into_node();
|
||||
let priority_field = StyledField::build()
|
||||
@ -282,7 +411,7 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let estimate = StyledInput::build(FieldId::EstimateIssueEditModal)
|
||||
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
|
||||
.valid(true)
|
||||
.build()
|
||||
.into_node();
|
||||
@ -293,246 +422,11 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "issueDetails"],
|
||||
div![
|
||||
attrs![At::Class => "topActions"],
|
||||
issue_type_select,
|
||||
div![
|
||||
attrs![At::Class => "topActionsRight"],
|
||||
copy_button,
|
||||
delete_button,
|
||||
close_button
|
||||
],
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "content"],
|
||||
div![
|
||||
attrs![At::Class => "left"],
|
||||
title,
|
||||
description_field,
|
||||
div![attrs![At::Class => "comments"]],
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "right"],
|
||||
status_field,
|
||||
assignees_field,
|
||||
reporter_field,
|
||||
priority_field,
|
||||
estimate_field
|
||||
],
|
||||
],
|
||||
attrs![At::Class => "right"],
|
||||
status_field,
|
||||
assignees_field,
|
||||
reporter_field,
|
||||
priority_field,
|
||||
estimate_field
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
pub struct IssueTypeTopOption(pub IssueId, pub IssueType);
|
||||
|
||||
impl SelectOption for IssueTypeTopOption {
|
||||
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 => "optionItem"],
|
||||
icon,
|
||||
div![attrs![At::Class => "optionLabel 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()
|
||||
}
|
||||
}
|
||||
|
||||
/////
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
pub struct IssueStatusOption(pub IssueId, pub IssueStatus);
|
||||
|
||||
impl SelectOption for IssueStatusOption {
|
||||
fn into_option(self) -> Node<Msg> {
|
||||
let name = self.1.to_label().to_owned();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "type optionItem"],
|
||||
div![attrs![At::Class => "typeLabel optionLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
fn into_value(self) -> Node<Msg> {
|
||||
let name = self.1.to_label().to_owned();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "selectItem"],
|
||||
div![attrs![At::Class => "selectItemLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
fn match_text_filter(&self, text_filter: &str) -> bool {
|
||||
format!("{}", self.0)
|
||||
.to_lowercase()
|
||||
.contains(&text_filter.to_lowercase())
|
||||
}
|
||||
|
||||
fn to_value(&self) -> u32 {
|
||||
self.1.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
pub struct IssueTypeOption(pub IssueType);
|
||||
|
||||
impl 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")
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "type optionItem"],
|
||||
icon,
|
||||
div![attrs![At::Class => "typeLabel optionLabel"], 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();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "selectItem"],
|
||||
type_icon,
|
||||
div![attrs![At::Class => "selectItemLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct IssuePriorityOption(IssuePriority);
|
||||
|
||||
impl SelectOption for IssuePriorityOption {
|
||||
fn into_option(self) -> Node<Msg> {
|
||||
let name = format!("{}", self.0);
|
||||
|
||||
let icon = StyledIcon::build(self.0.into())
|
||||
.add_class("issuePriorityIcon")
|
||||
.size(18)
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => format!("priority optionItem {}", name)],
|
||||
icon,
|
||||
div![attrs![At::Class => "priorityLabel optionLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
fn into_value(self) -> Node<Msg> {
|
||||
let name = format!("{}", self.0);
|
||||
|
||||
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => format!("selectItem priority {}", name)],
|
||||
type_icon,
|
||||
div![attrs![At::Class => "selectItemLabel"], name]
|
||||
]
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct UserOption<'opt>(pub &'opt User);
|
||||
|
||||
impl<'opt> UserOption<'opt> {
|
||||
fn avatar_node(&self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
StyledAvatar::build()
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.size(20)
|
||||
.name(user.name.as_str())
|
||||
.build()
|
||||
.into_node()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'opt> SelectOption for UserOption<'opt> {
|
||||
fn into_option(self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
|
||||
let styled_avatar = self.avatar_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "user optionItem"],
|
||||
styled_avatar,
|
||||
div![attrs![At::Class => "typeLabel optionLabel"], user.name]
|
||||
]
|
||||
}
|
||||
|
||||
fn into_value(self) -> Node<Msg> {
|
||||
let user = self.0;
|
||||
|
||||
let styled_avatar = self.avatar_node();
|
||||
|
||||
div![
|
||||
attrs![At::Class => "selectItem"],
|
||||
styled_avatar,
|
||||
div![attrs![At::Class => "selectItemLabel"], user.name]
|
||||
]
|
||||
}
|
||||
|
||||
fn match_text_filter(&self, text_filter: &str) -> bool {
|
||||
self.0.name.contains(text_filter)
|
||||
}
|
||||
|
||||
fn to_value(&self) -> u32 {
|
||||
self.0.id as u32
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,10 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use jirs_data::{UpdateIssuePayload, WsMsg};
|
||||
use jirs_data::WsMsg;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page};
|
||||
use crate::shared::styled_editor::Mode;
|
||||
use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant};
|
||||
use crate::shared::styled_select::StyledSelectState;
|
||||
use crate::shared::{find_issue, ToNode};
|
||||
use crate::{model, FieldChange, FieldId, Msg};
|
||||
|
||||
@ -91,34 +89,7 @@ fn push_edit_modal(issue_id: &i32, model: &mut Model) {
|
||||
Some(issue) => issue,
|
||||
_ => return,
|
||||
};
|
||||
ModalType::EditIssue(
|
||||
*issue_id,
|
||||
EditIssueModal {
|
||||
id: *issue_id,
|
||||
link_copied: false,
|
||||
payload: UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type.clone(),
|
||||
status: issue.status.clone(),
|
||||
priority: issue.priority.clone(),
|
||||
list_position: issue.list_position.clone(),
|
||||
description: issue.description.clone(),
|
||||
description_text: issue.description_text.clone(),
|
||||
estimate: issue.estimate.clone(),
|
||||
time_spent: issue.time_spent.clone(),
|
||||
time_remaining: issue.time_remaining.clone(),
|
||||
project_id: issue.project_id.clone(),
|
||||
reporter_id: issue.reporter_id.clone(),
|
||||
user_ids: issue.user_ids.clone(),
|
||||
},
|
||||
top_type_state: StyledSelectState::new(FieldId::IssueTypeEditModalTop),
|
||||
status_state: StyledSelectState::new(FieldId::StatusIssueEditModal),
|
||||
reporter_state: StyledSelectState::new(FieldId::ReporterIssueEditModal),
|
||||
assignees_state: StyledSelectState::new(FieldId::AssigneesIssueEditModal),
|
||||
priority_state: StyledSelectState::new(FieldId::PriorityIssueEditModal),
|
||||
description_editor_mode: Mode::Editor,
|
||||
},
|
||||
)
|
||||
ModalType::EditIssue(*issue_id, EditIssueModal::new(issue))
|
||||
};
|
||||
model.modals.push(modal);
|
||||
}
|
||||
|
@ -7,9 +7,7 @@ use jirs_data::*;
|
||||
|
||||
use crate::shared::styled_editor::Mode;
|
||||
use crate::shared::styled_select::StyledSelectState;
|
||||
use crate::{FieldId, IssueId, UserId, HOST_URL};
|
||||
|
||||
pub type ProjectId = i32;
|
||||
use crate::{AddIssueModalFieldId, EditIssueModalFieldId, FieldId, HOST_URL};
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
pub enum ModalType {
|
||||
@ -30,6 +28,48 @@ pub struct EditIssueModal {
|
||||
pub priority_state: StyledSelectState,
|
||||
|
||||
pub description_editor_mode: Mode,
|
||||
pub creating_comment: bool,
|
||||
}
|
||||
|
||||
impl EditIssueModal {
|
||||
pub fn new(issue: &Issue) -> Self {
|
||||
Self {
|
||||
id: issue.id,
|
||||
link_copied: false,
|
||||
payload: UpdateIssuePayload {
|
||||
title: issue.title.clone(),
|
||||
issue_type: issue.issue_type.clone(),
|
||||
status: issue.status.clone(),
|
||||
priority: issue.priority.clone(),
|
||||
list_position: issue.list_position.clone(),
|
||||
description: issue.description.clone(),
|
||||
description_text: issue.description_text.clone(),
|
||||
estimate: issue.estimate.clone(),
|
||||
time_spent: issue.time_spent.clone(),
|
||||
time_remaining: issue.time_remaining.clone(),
|
||||
project_id: issue.project_id.clone(),
|
||||
reporter_id: issue.reporter_id.clone(),
|
||||
user_ids: issue.user_ids.clone(),
|
||||
},
|
||||
top_type_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::IssueType,
|
||||
)),
|
||||
status_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::Status,
|
||||
)),
|
||||
reporter_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::Reporter,
|
||||
)),
|
||||
assignees_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::Assignees,
|
||||
)),
|
||||
priority_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::Priority,
|
||||
)),
|
||||
description_editor_mode: Mode::Editor,
|
||||
creating_comment: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||
@ -69,10 +109,18 @@ impl Default for AddIssueModal {
|
||||
project_id: Default::default(),
|
||||
user_ids: Default::default(),
|
||||
reporter_id: Default::default(),
|
||||
type_state: StyledSelectState::new(FieldId::IssueTypeAddIssueModal),
|
||||
reporter_state: StyledSelectState::new(FieldId::ReporterAddIssueModal),
|
||||
assignees_state: StyledSelectState::new(FieldId::AssigneesAddIssueModal),
|
||||
priority_state: StyledSelectState::new(FieldId::IssuePriorityAddIssueModal),
|
||||
type_state: StyledSelectState::new(FieldId::AddIssueModal(
|
||||
AddIssueModalFieldId::IssueType,
|
||||
)),
|
||||
reporter_state: StyledSelectState::new(FieldId::AddIssueModal(
|
||||
AddIssueModalFieldId::Reporter,
|
||||
)),
|
||||
assignees_state: StyledSelectState::new(FieldId::AddIssueModal(
|
||||
AddIssueModalFieldId::Assignees,
|
||||
)),
|
||||
priority_state: StyledSelectState::new(FieldId::AddIssueModal(
|
||||
AddIssueModalFieldId::Priority,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::StyledSelectChange;
|
||||
use crate::shared::{drag_ev, inner_layout, ToNode};
|
||||
use crate::{FieldId, Msg};
|
||||
use crate::{EditIssueModalFieldId, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
@ -47,7 +47,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::IssueTypeEditModalTop,
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Text(text),
|
||||
) => {
|
||||
let modal = model
|
||||
@ -289,13 +289,12 @@ fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||
.iter()
|
||||
.filter(|user| issue.user_ids.contains(&user.id))
|
||||
.map(|user| {
|
||||
StyledAvatar {
|
||||
avatar_url: user.avatar_url.clone(),
|
||||
size: 24,
|
||||
name: user.name.clone(),
|
||||
on_click: None,
|
||||
}
|
||||
.into_node()
|
||||
StyledAvatar::build()
|
||||
.size(24)
|
||||
.name(user.name.as_str())
|
||||
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.build()
|
||||
.into_node()
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
use seed::{prelude::*, *};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use jirs_data::Issue;
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::{IssueId, Msg};
|
||||
use crate::Msg;
|
||||
|
||||
pub mod aside;
|
||||
pub mod navbar_left;
|
||||
@ -18,6 +18,7 @@ pub mod styled_icon;
|
||||
pub mod styled_input;
|
||||
pub mod styled_modal;
|
||||
pub mod styled_select;
|
||||
pub mod styled_select_child;
|
||||
pub mod styled_textarea;
|
||||
pub mod styled_tooltip;
|
||||
|
||||
|
@ -4,10 +4,11 @@ use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
|
||||
pub struct StyledAvatar {
|
||||
pub avatar_url: Option<String>,
|
||||
pub size: u32,
|
||||
pub name: String,
|
||||
pub on_click: Option<EventHandler<Msg>>,
|
||||
avatar_url: Option<String>,
|
||||
size: u32,
|
||||
name: String,
|
||||
on_click: Option<EventHandler<Msg>>,
|
||||
class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for StyledAvatar {
|
||||
@ -17,6 +18,7 @@ impl Default for StyledAvatar {
|
||||
size: 32,
|
||||
name: "".to_string(),
|
||||
on_click: None,
|
||||
class_list: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -28,6 +30,7 @@ impl StyledAvatar {
|
||||
size: None,
|
||||
name: "".to_string(),
|
||||
on_click: None,
|
||||
class_list: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,10 +42,11 @@ impl ToNode for StyledAvatar {
|
||||
}
|
||||
|
||||
pub struct StyledAvatarBuilder {
|
||||
pub avatar_url: Option<String>,
|
||||
pub size: Option<u32>,
|
||||
pub name: String,
|
||||
pub on_click: Option<EventHandler<Msg>>,
|
||||
avatar_url: Option<String>,
|
||||
size: Option<u32>,
|
||||
name: String,
|
||||
on_click: Option<EventHandler<Msg>>,
|
||||
class_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl StyledAvatarBuilder {
|
||||
@ -50,7 +54,10 @@ impl StyledAvatarBuilder {
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.avatar_url = Some(avatar_url.into());
|
||||
let url = avatar_url.into();
|
||||
if !url.is_empty() {
|
||||
self.avatar_url = Some(url);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@ -72,12 +79,21 @@ impl StyledAvatarBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_class<S>(mut self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.class_list.push(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledAvatar {
|
||||
StyledAvatar {
|
||||
avatar_url: self.avatar_url,
|
||||
size: self.size.unwrap_or(32),
|
||||
name: self.name,
|
||||
on_click: self.on_click,
|
||||
class_list: self.class_list,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -88,7 +104,15 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
||||
size,
|
||||
name,
|
||||
on_click,
|
||||
mut class_list,
|
||||
} = values;
|
||||
|
||||
class_list.push("styledAvatar".to_string());
|
||||
match avatar_url {
|
||||
Some(_) => class_list.push("image".to_string()),
|
||||
_ => class_list.push("letter".to_string()),
|
||||
};
|
||||
|
||||
let shared_style = format!("width: {size}px; height: {size}px", size = size);
|
||||
let handler = match on_click {
|
||||
None => vec![],
|
||||
@ -96,11 +120,11 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
||||
};
|
||||
match avatar_url {
|
||||
Some(url) => div![
|
||||
attrs![At::Class => "styledAvatar image", At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)],
|
||||
attrs![At::Class => class_list.join(" "), At::Style => format!("{shared}; background-image: url({url});", shared = shared_style, url = url)],
|
||||
handler,
|
||||
],
|
||||
_ => div![
|
||||
attrs![At::Class => "styledAvatar letter", At::Style => shared_style],
|
||||
attrs![At::Class => class_list.join(" "), At::Style => shared_style],
|
||||
span![name],
|
||||
handler
|
||||
],
|
||||
|
@ -28,7 +28,7 @@ impl StyledEditor {
|
||||
StyledEditorBuilder {
|
||||
id,
|
||||
text: String::new(),
|
||||
mode: Mode::Editor,
|
||||
mode: Mode::View,
|
||||
update_event: None,
|
||||
}
|
||||
}
|
||||
|
@ -74,12 +74,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
|
||||
let mut handlers = vec![];
|
||||
|
||||
let input_handler = input_ev(Ev::KeyUp, move |value| Msg::InputChanged(id, value));
|
||||
handlers.push(input_handler);
|
||||
handlers.push(input_ev(Ev::KeyUp, move |value| {
|
||||
Msg::InputChanged(id, value)
|
||||
}));
|
||||
|
||||
div![
|
||||
attrs!(At::Class => wrapper_class_list.join(" ")),
|
||||
icon,
|
||||
keyboard_ev(Ev::KeyUp, |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::NoOp
|
||||
}),
|
||||
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers],
|
||||
]
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
use crate::shared::styled_select_child::*;
|
||||
use crate::shared::ToNode;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
@ -33,16 +34,6 @@ impl std::fmt::Display for Variant {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SelectOption {
|
||||
fn into_option(self) -> Node<Msg>;
|
||||
|
||||
fn into_value(self) -> Node<Msg>;
|
||||
|
||||
fn match_text_filter(&self, text_filter: &str) -> bool;
|
||||
|
||||
fn to_value(&self) -> u32;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
|
||||
pub struct StyledSelectState {
|
||||
pub field_id: FieldId,
|
||||
@ -79,10 +70,7 @@ impl StyledSelectState {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StyledSelect<Child>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
pub struct StyledSelect {
|
||||
id: FieldId,
|
||||
variant: Variant,
|
||||
dropdown_width: Option<usize>,
|
||||
@ -90,26 +78,20 @@ where
|
||||
valid: bool,
|
||||
is_multi: bool,
|
||||
allow_clear: bool,
|
||||
options: Vec<Child>,
|
||||
selected: Vec<Child>,
|
||||
options: Vec<StyledSelectChildBuilder>,
|
||||
selected: Vec<StyledSelectChildBuilder>,
|
||||
text_filter: String,
|
||||
opened: bool,
|
||||
}
|
||||
|
||||
impl<Child> ToNode for StyledSelect<Child>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
impl ToNode for StyledSelect {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Child> StyledSelect<Child>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
pub fn build(id: FieldId) -> StyledSelectBuilder<Child> {
|
||||
impl StyledSelect {
|
||||
pub fn build(id: FieldId) -> StyledSelectBuilder {
|
||||
StyledSelectBuilder {
|
||||
id,
|
||||
variant: None,
|
||||
@ -127,10 +109,7 @@ where
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledSelectBuilder<Child>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
pub struct StyledSelectBuilder {
|
||||
id: FieldId,
|
||||
variant: Option<Variant>,
|
||||
dropdown_width: Option<Option<usize>>,
|
||||
@ -138,17 +117,14 @@ where
|
||||
valid: Option<bool>,
|
||||
is_multi: Option<bool>,
|
||||
allow_clear: Option<bool>,
|
||||
options: Option<Vec<Child>>,
|
||||
selected: Option<Vec<Child>>,
|
||||
options: Option<Vec<StyledSelectChildBuilder>>,
|
||||
selected: Option<Vec<StyledSelectChildBuilder>>,
|
||||
text_filter: Option<String>,
|
||||
opened: Option<bool>,
|
||||
}
|
||||
|
||||
impl<Child> StyledSelectBuilder<Child>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
pub fn build(self) -> StyledSelect<Child> {
|
||||
impl StyledSelectBuilder {
|
||||
pub fn build(self) -> StyledSelect {
|
||||
StyledSelect {
|
||||
id: self.id,
|
||||
variant: self.variant.unwrap_or_default(),
|
||||
@ -195,12 +171,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn options(mut self, options: Vec<Child>) -> Self {
|
||||
pub fn options(mut self, options: Vec<StyledSelectChildBuilder>) -> Self {
|
||||
self.options = Some(options);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: Vec<Child>) -> Self {
|
||||
pub fn selected(mut self, selected: Vec<StyledSelectChildBuilder>) -> Self {
|
||||
self.selected = Some(selected);
|
||||
self
|
||||
}
|
||||
@ -210,16 +186,18 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty(mut self) -> Self {
|
||||
self.variant = Some(Variant::Empty);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn multi(mut self) -> Self {
|
||||
self.is_multi = Some(true);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg>
|
||||
where
|
||||
Child: SelectOption + PartialEq,
|
||||
{
|
||||
pub fn render(values: StyledSelect) -> Node<Msg> {
|
||||
let StyledSelect {
|
||||
id,
|
||||
variant,
|
||||
@ -247,6 +225,7 @@ where
|
||||
let dropdown_style = dropdown_width
|
||||
.map(|n| format!("width: {}px;", n))
|
||||
.unwrap_or_else(|| format!("width: 100%;"));
|
||||
|
||||
let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)];
|
||||
if !valid {
|
||||
select_class.push("invalid".to_string());
|
||||
@ -262,9 +241,11 @@ where
|
||||
|
||||
let children: Vec<Node<Msg>> = options
|
||||
.into_iter()
|
||||
.filter(|o| !selected.contains(&o) && o.match_text_filter(text_filter.as_str()))
|
||||
.filter(|o| !selected.contains(&o) && o.match_text(text_filter.as_str()))
|
||||
.map(|child| {
|
||||
let value = child.to_value();
|
||||
let child = child.build(DisplayType::SelectOption);
|
||||
let value = child.value();
|
||||
let node = child.into_node();
|
||||
let field_id = id.clone();
|
||||
let on_change = mouse_ev(Ev::Click, move |_| {
|
||||
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(value))
|
||||
@ -273,7 +254,7 @@ where
|
||||
attrs![At::Class => "option"],
|
||||
on_change,
|
||||
visibility_handler.clone(),
|
||||
child.into_option()
|
||||
node
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
@ -320,12 +301,16 @@ where
|
||||
} else {
|
||||
selected
|
||||
.into_iter()
|
||||
.map(|m| render_value(m.into_value()))
|
||||
.map(|m| render_value(m.build(DisplayType::SelectValue).into_node()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
seed::div![
|
||||
attrs![At::Class => select_class.join(" ")],
|
||||
attrs![At::Class => select_class.join(" "), At::Style => dropdown_style.as_str()],
|
||||
keyboard_ev(Ev::KeyUp, |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::NoOp
|
||||
}),
|
||||
div![
|
||||
attrs![At::Class => format!("valueContainer {}", variant)],
|
||||
visibility_handler,
|
||||
@ -333,7 +318,8 @@ where
|
||||
chevron_down,
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "dropDown", At::Style => dropdown_style],
|
||||
class!["dropDown"],
|
||||
attrs![At::Style => dropdown_style.as_str()],
|
||||
text_input,
|
||||
clear_icon,
|
||||
option_list
|
||||
@ -346,13 +332,12 @@ fn render_value(mut content: Node<Msg>) -> Node<Msg> {
|
||||
content
|
||||
}
|
||||
|
||||
fn into_multi_value<Opt>(opt: Opt, field_id: FieldId) -> Node<Msg>
|
||||
where
|
||||
Opt: SelectOption,
|
||||
{
|
||||
fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node<Msg> {
|
||||
let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node();
|
||||
let value = opt.to_value();
|
||||
let mut opt = opt.into_value();
|
||||
let child = opt.build(DisplayType::SelectValue);
|
||||
let value = child.value();
|
||||
|
||||
let mut opt = child.into_node();
|
||||
opt.add_class("value");
|
||||
opt.add_child(close_icon);
|
||||
|
||||
|
240
jirs-client/src/shared/styled_select_child.rs
Normal file
240
jirs-client/src/shared/styled_select_child.rs
Normal file
@ -0,0 +1,240 @@
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::shared::styled_select::Variant;
|
||||
use crate::shared::ToNode;
|
||||
use crate::Msg;
|
||||
|
||||
pub trait ToStyledSelectChild {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder;
|
||||
}
|
||||
|
||||
pub enum DisplayType {
|
||||
SelectOption,
|
||||
SelectValue,
|
||||
}
|
||||
|
||||
pub struct StyledSelectChild {
|
||||
name: Option<String>,
|
||||
icon: Option<Node<Msg>>,
|
||||
text: Option<String>,
|
||||
display_type: DisplayType,
|
||||
value: u32,
|
||||
class_list: Vec<String>,
|
||||
variant: Variant,
|
||||
}
|
||||
|
||||
impl StyledSelectChild {
|
||||
pub fn build() -> StyledSelectChildBuilder {
|
||||
StyledSelectChildBuilder {
|
||||
icon: None,
|
||||
text: None,
|
||||
name: None,
|
||||
value: 0,
|
||||
class_list: vec![],
|
||||
variant: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn value(&self) -> u32 {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl ToNode for StyledSelectChild {
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledSelectChildBuilder {
|
||||
icon: Option<Node<Msg>>,
|
||||
text: Option<String>,
|
||||
name: Option<String>,
|
||||
value: u32,
|
||||
class_list: Vec<String>,
|
||||
variant: Variant,
|
||||
}
|
||||
|
||||
impl PartialEq for StyledSelectChildBuilder {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.value == other.value
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledSelectChildBuilder {
|
||||
pub fn icon(mut self, icon: Node<Msg>) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text<S>(mut self, text: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.text = Some(text.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name<S>(mut self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(mut self, value: u32) -> Self {
|
||||
self.value = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn match_text(&self, text: &str) -> bool {
|
||||
self.text
|
||||
.as_ref()
|
||||
.map(|t| t.contains(text))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn add_class<S>(mut self, name: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.class_list.push(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self, display_type: DisplayType) -> StyledSelectChild {
|
||||
StyledSelectChild {
|
||||
name: self.name,
|
||||
icon: self.icon,
|
||||
text: self.text,
|
||||
display_type,
|
||||
value: self.value,
|
||||
class_list: self.class_list,
|
||||
variant: self.variant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(values: StyledSelectChild) -> Node<Msg> {
|
||||
let StyledSelectChild {
|
||||
name,
|
||||
icon,
|
||||
text,
|
||||
display_type,
|
||||
value: _,
|
||||
mut class_list,
|
||||
variant,
|
||||
} = values;
|
||||
|
||||
class_list.push(format!("{}", variant));
|
||||
|
||||
let label_class = match display_type {
|
||||
DisplayType::SelectOption => vec![
|
||||
"optionLabel".to_string(),
|
||||
variant.to_string(),
|
||||
name.as_ref().cloned().unwrap_or_default(),
|
||||
name.as_ref()
|
||||
.map(|s| format!("{}Label", s))
|
||||
.unwrap_or_default(),
|
||||
class_list.join(" "),
|
||||
],
|
||||
DisplayType::SelectValue => vec![
|
||||
"selectItemLabel".to_string(),
|
||||
variant.to_string(),
|
||||
name.as_ref().cloned().unwrap_or_default(),
|
||||
name.as_ref()
|
||||
.map(|s| format!("{}Label", s))
|
||||
.unwrap_or_default(),
|
||||
class_list.join(" "),
|
||||
],
|
||||
}
|
||||
.join(" ");
|
||||
|
||||
let wrapper_class = match display_type {
|
||||
DisplayType::SelectOption => vec![
|
||||
"optionItem".to_string(),
|
||||
name.as_ref().cloned().unwrap_or_default(),
|
||||
class_list.join(" "),
|
||||
],
|
||||
DisplayType::SelectValue => vec![
|
||||
"selectItem".to_string(),
|
||||
name.as_ref().cloned().unwrap_or_default(),
|
||||
class_list.join(" "),
|
||||
],
|
||||
}
|
||||
.join(" ");
|
||||
|
||||
let icon_node = match icon {
|
||||
Some(icon) => icon,
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
let label_node = match text {
|
||||
Some(text) => div![class![label_class.as_str()], text],
|
||||
_ => empty![],
|
||||
};
|
||||
|
||||
div![class![wrapper_class.as_str()], icon_node, label_node]
|
||||
}
|
||||
|
||||
impl ToStyledSelectChild for jirs_data::User {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder {
|
||||
let avatar = crate::shared::styled_avatar::StyledAvatar::build()
|
||||
.avatar_url(self.avatar_url.as_ref().cloned().unwrap_or_default())
|
||||
.size(20)
|
||||
.name(self.name.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
StyledSelectChild::build()
|
||||
.value(self.id as u32)
|
||||
.icon(avatar)
|
||||
.text(self.name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStyledSelectChild for jirs_data::IssuePriority {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder {
|
||||
let icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
|
||||
.add_class(self.to_string())
|
||||
.build()
|
||||
.into_node();
|
||||
let text = self.to_string();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.icon(icon)
|
||||
.value(self.clone().into())
|
||||
.text(text)
|
||||
.add_class(format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStyledSelectChild for jirs_data::IssueStatus {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder {
|
||||
let text = self.to_label();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.value(self.clone().into())
|
||||
.add_class(text.clone())
|
||||
.text(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToStyledSelectChild for jirs_data::IssueType {
|
||||
fn to_select_child(&self) -> StyledSelectChildBuilder {
|
||||
let name = self.to_label().to_owned();
|
||||
|
||||
let type_icon = crate::shared::styled_icon::StyledIcon::build(self.clone().into())
|
||||
.add_class(name.as_str())
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
StyledSelectChild::build()
|
||||
.add_class(name.as_str())
|
||||
.text(name)
|
||||
.icon(type_icon)
|
||||
.value(self.clone().into())
|
||||
}
|
||||
}
|
@ -140,6 +140,10 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
|
||||
Msg::InputChanged(id, value)
|
||||
});
|
||||
handlers.push(text_input_handler);
|
||||
handlers.push(keyboard_ev(Ev::KeyUp, |ev| {
|
||||
ev.stop_propagation();
|
||||
Msg::NoOp
|
||||
}));
|
||||
|
||||
class_list.push("textAreaInput".to_string());
|
||||
|
||||
|
@ -2,7 +2,6 @@ use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::Model;
|
||||
use crate::IssueId;
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
model.project_page.dragged_issue_id = Some(issue_id);
|
||||
|
@ -163,19 +163,6 @@ impl IssueStatus {
|
||||
IssueStatus::Done => "Done",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_payload(&self) -> &str {
|
||||
match self {
|
||||
IssueStatus::Backlog => "backlog",
|
||||
IssueStatus::Selected => "selected",
|
||||
IssueStatus::InProgress => "in_progress",
|
||||
IssueStatus::Done => "done",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_name(&self, name: &str) -> bool {
|
||||
self.to_payload() == name
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "backend", derive(FromSqlRow, AsExpression))]
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import api from '../../../../../shared/utils/api';
|
||||
import toast from '../../../../../shared/utils/toast';
|
||||
import api from '../../../../../shared/utils/api';
|
||||
import toast from '../../../../../shared/utils/toast';
|
||||
import { fetchCurrentUser } from "../../../../../actions/users";
|
||||
|
||||
import BodyForm from '../BodyForm';
|
||||
import ProTip from './ProTip';
|
||||
import BodyForm from '../BodyForm';
|
||||
import ProTip from './ProTip';
|
||||
import { Create, FakeTextarea, Right, UserAvatar } from './Styles';
|
||||
|
||||
class ProjectBoardIssueDetailsCommentsCreate extends React.Component {
|
||||
@ -26,8 +26,7 @@ class ProjectBoardIssueDetailsCommentsCreate extends React.Component {
|
||||
handleCommentCreate = async () => {
|
||||
try {
|
||||
this.setCreatingTrue();
|
||||
const response = await api.post(`/comments`, { body: this.state.body, issueId: this.props.issueId });
|
||||
console.log(response);
|
||||
await api.post(`/comments`, { body: this.state.body, issueId: this.props.issueId });
|
||||
await this.props.fetchIssue();
|
||||
this.setState({ isCreating: false, isFormOpen: false, body: '' });
|
||||
} catch (error) {
|
||||
|
Loading…
Reference in New Issue
Block a user