Multiple changes
This commit is contained in:
parent
57adfac3a4
commit
7803e0fc3d
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -861,7 +861,7 @@ dependencies = [
|
|||||||
"ansi_term",
|
"ansi_term",
|
||||||
"atty",
|
"atty",
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"strsim",
|
"strsim 0.8.0",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"vec_map",
|
"vec_map",
|
||||||
@ -1731,6 +1731,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"serde",
|
"serde",
|
||||||
|
"simsearch",
|
||||||
"syntect",
|
"syntect",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
@ -3394,6 +3395,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simsearch"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13b5ceaabc64e2d93a73aa29f8a7dd83dde9c02fdbee660c4551c7e659ce4185"
|
||||||
|
dependencies = [
|
||||||
|
"strsim 0.10.0",
|
||||||
|
"triple_accel",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@ -3487,6 +3498,12 @@ version = "0.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
@ -3803,6 +3820,12 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "triple_accel"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "trust-dns-proto"
|
name = "trust-dns-proto"
|
||||||
version = "0.18.0-alpha.2"
|
version = "0.18.0-alpha.2"
|
||||||
|
@ -17,6 +17,7 @@ serde = "*"
|
|||||||
bincode = "*"
|
bincode = "*"
|
||||||
toml = { version = "*" }
|
toml = { version = "*" }
|
||||||
|
|
||||||
|
simsearch = { version = "0.2" }
|
||||||
actix = { version = "0.10.0" }
|
actix = { version = "0.10.0" }
|
||||||
|
|
||||||
flate2 = { version = "*" }
|
flate2 = { version = "*" }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use {
|
use {
|
||||||
actix::{Actor, Handler, SyncContext},
|
actix::{Actor, Handler, SyncContext},
|
||||||
jirs_data::HighlightedCode,
|
jirs_data::HighlightedCode,
|
||||||
|
simsearch::SimSearch,
|
||||||
std::sync::Arc,
|
std::sync::Arc,
|
||||||
syntect::{
|
syntect::{
|
||||||
easy::HighlightLines,
|
easy::HighlightLines,
|
||||||
@ -14,6 +15,20 @@ mod load;
|
|||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref THEME_SET: Arc<ThemeSet> = Arc::new(load::integrated_themeset());
|
pub static ref THEME_SET: Arc<ThemeSet> = Arc::new(load::integrated_themeset());
|
||||||
pub static ref SYNTAX_SET: Arc<SyntaxSet> = Arc::new(load::integrated_syntaxset());
|
pub static ref SYNTAX_SET: Arc<SyntaxSet> = Arc::new(load::integrated_syntaxset());
|
||||||
|
pub static ref SIM_SEARCH: Arc<SimSearch<usize>> = Arc::new(create_search_engine());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_search_engine() -> SimSearch<usize> {
|
||||||
|
let mut engine = SimSearch::new();
|
||||||
|
for (idx, name) in SYNTAX_SET
|
||||||
|
.syntaxes()
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.as_str())
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
engine.insert(idx, name);
|
||||||
|
}
|
||||||
|
engine
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -23,23 +38,53 @@ pub enum HighlightError {
|
|||||||
ResultUnserializable,
|
ResultUnserializable,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hi<'l>(code: &'l str, lang: &'l str) -> Result<Vec<(Style, &'l str)>, HighlightError> {
|
#[derive(Debug)]
|
||||||
let set = SYNTAX_SET
|
pub struct HighlightActor {
|
||||||
|
theme_set: Arc<ThemeSet>,
|
||||||
|
syntax_set: Arc<SyntaxSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HighlightActor {
|
||||||
|
fn default() -> Self {
|
||||||
|
let theme_set = THEME_SET.clone();
|
||||||
|
let syntax_set = SYNTAX_SET.clone();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
theme_set,
|
||||||
|
syntax_set,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighlightActor {
|
||||||
|
fn hi<'l>(
|
||||||
|
&self,
|
||||||
|
code: &'l str,
|
||||||
|
lang: &'l str,
|
||||||
|
) -> Result<Vec<(Style, &'l str)>, HighlightError> {
|
||||||
|
let lang = SIM_SEARCH
|
||||||
|
.search(lang)
|
||||||
|
.first()
|
||||||
|
.and_then(|idx| self.syntax_set.syntaxes().get(*idx))
|
||||||
|
.map(|st| st.name.as_str())
|
||||||
|
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||||
|
|
||||||
|
let set = self
|
||||||
|
.syntax_set
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.find_syntax_by_name(lang)
|
.find_syntax_by_name(lang)
|
||||||
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||||
let theme: &syntect::highlighting::Theme = THEME_SET
|
let theme: &syntect::highlighting::Theme = self
|
||||||
|
.theme_set
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.themes
|
.themes
|
||||||
.get("GitHub")
|
.get("GitHub")
|
||||||
.ok_or_else(|| HighlightError::UnknownTheme)?;
|
.ok_or_else(|| HighlightError::UnknownTheme)?;
|
||||||
|
|
||||||
let mut hi = HighlightLines::new(set, theme);
|
let mut hi = HighlightLines::new(set, theme);
|
||||||
Ok(hi.highlight(code, SYNTAX_SET.as_ref()))
|
Ok(hi.highlight(code, self.syntax_set.as_ref()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct HighlightActor {}
|
|
||||||
|
|
||||||
impl Actor for HighlightActor {
|
impl Actor for HighlightActor {
|
||||||
type Context = SyncContext<Self>;
|
type Context = SyncContext<Self>;
|
||||||
@ -56,7 +101,7 @@ impl Handler<HighlightCode> for HighlightActor {
|
|||||||
type Result = Result<HighlightedCode, HighlightError>;
|
type Result = Result<HighlightedCode, HighlightError>;
|
||||||
|
|
||||||
fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
let res: Vec<(Style, &str)> = hi(&msg.code, &msg.lang)?;
|
let res: Vec<(Style, &str)> = self.hi(&msg.code, &msg.lang)?;
|
||||||
|
|
||||||
Ok(HighlightedCode {
|
Ok(HighlightedCode {
|
||||||
parts: res
|
parts: res
|
||||||
@ -117,3 +162,20 @@ impl Handler<TextHighlightCode> for HighlightActor {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(actix::Message)]
|
||||||
|
#[rtype(result = "Result<Vec<String>, HighlightError>")]
|
||||||
|
pub struct LoadSyntaxSet;
|
||||||
|
|
||||||
|
impl Handler<LoadSyntaxSet> for HighlightActor {
|
||||||
|
type Result = Result<Vec<String>, HighlightError>;
|
||||||
|
|
||||||
|
fn handle(&mut self, _msg: LoadSyntaxSet, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
Ok(self
|
||||||
|
.syntax_set
|
||||||
|
.syntaxes()
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use futures::executor::block_on;
|
use {
|
||||||
|
crate::{WebSocketActor, WsHandler, WsResult},
|
||||||
use jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg};
|
futures::executor::block_on,
|
||||||
|
jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg},
|
||||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
};
|
||||||
|
|
||||||
pub struct LoadIssueComments {
|
pub struct LoadIssueComments {
|
||||||
pub issue_id: IssueId,
|
pub issue_id: IssueId,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use futures::executor::block_on;
|
use {
|
||||||
|
crate::{WebSocketActor, WsHandler, WsResult},
|
||||||
use jirs_data::{EpicId, NameString, UserProject, WsMsg};
|
futures::executor::block_on,
|
||||||
|
jirs_data::{EpicId, NameString, UserProject, WsMsg},
|
||||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
};
|
||||||
|
|
||||||
pub struct LoadEpics;
|
pub struct LoadEpics;
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use futures::executor::block_on;
|
use {
|
||||||
|
crate::{WebSocketActor, WsHandler, WsResult},
|
||||||
use jirs_data::WsMsg;
|
futures::executor::block_on,
|
||||||
use jirs_data::{Code, Lang};
|
jirs_data::{Code, Lang, WsMsg},
|
||||||
|
};
|
||||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
|
||||||
|
|
||||||
pub struct HighlightCode(pub Lang, pub Code);
|
pub struct HighlightCode(pub Lang, pub Code);
|
||||||
|
|
||||||
|
@ -50,17 +50,13 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
|||||||
msg.title = Some(s);
|
msg.title = Some(s);
|
||||||
}
|
}
|
||||||
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
(IssueFieldId::Description, PayloadVariant::String(s)) => {
|
||||||
// let mut opts = comrak::ComrakOptions::default();
|
|
||||||
// opts.render.github_pre_lang = true;
|
|
||||||
// let html = comrak::markdown_to_html(s.as_str(), &opts);
|
|
||||||
|
|
||||||
let html: String = {
|
let html: String = {
|
||||||
use pulldown_cmark::*;
|
use pulldown_cmark::*;
|
||||||
let parser = pulldown_cmark::Parser::new(s.as_str());
|
let parser = pulldown_cmark::Parser::new(s.as_str());
|
||||||
enum ParseState {
|
enum ParseState {
|
||||||
Code(highlight_actor::TextHighlightCode),
|
Code(highlight_actor::TextHighlightCode),
|
||||||
Other,
|
Other,
|
||||||
};
|
}
|
||||||
let mut state = ParseState::Other;
|
let mut state = ParseState::Other;
|
||||||
|
|
||||||
let parser = parser.flat_map(|event| match event {
|
let parser = parser.flat_map(|event| match event {
|
||||||
|
@ -50,12 +50,21 @@
|
|||||||
transition: transform 0.1s;
|
transition: transform 0.1s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
background: var(--backgroundMedium);
|
||||||
|
border-color: var(--backgroundLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder.isActive {
|
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder.isActive {
|
||||||
box-shadow: 0 0 0 4px var(--primary);
|
box-shadow: 0 0 0 4px var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder > .letter {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder:hover {
|
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
@ -134,6 +143,19 @@
|
|||||||
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink {
|
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .dragCover {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .dragCover:-moz-drag-over {
|
||||||
|
border: var(--borderInputFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .issue {
|
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .issue {
|
||||||
|
@ -384,3 +384,40 @@ i.styledIcon.double-left:before {
|
|||||||
i.styledIcon.double-right:before {
|
i.styledIcon.double-right:before {
|
||||||
content: "\ea7c";
|
content: "\ea7c";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
i.styledIcon.task {
|
||||||
|
color: var(--task);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.bug {
|
||||||
|
color: var(--bug);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.story {
|
||||||
|
color: var(--story);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.epic {
|
||||||
|
color: var(--epic);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.highest {
|
||||||
|
color: var(--highest);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.high {
|
||||||
|
color: var(--high);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.medium {
|
||||||
|
color: var(--medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.low {
|
||||||
|
color: var(--low);
|
||||||
|
}
|
||||||
|
|
||||||
|
i.styledIcon.lowest {
|
||||||
|
color: var(--lowest);
|
||||||
|
}
|
||||||
|
@ -99,6 +99,7 @@ pub enum Msg {
|
|||||||
|
|
||||||
// inputs
|
// inputs
|
||||||
StrInputChanged(FieldId, String),
|
StrInputChanged(FieldId, String),
|
||||||
|
|
||||||
U32InputChanged(FieldId, u32),
|
U32InputChanged(FieldId, u32),
|
||||||
FileInputChanged(FieldId, Vec<File>),
|
FileInputChanged(FieldId, Vec<File>),
|
||||||
// Rte(FieldId, RteMsg),
|
// Rte(FieldId, RteMsg),
|
||||||
|
@ -8,7 +8,7 @@ use {
|
|||||||
ToNode,
|
ToNode,
|
||||||
},
|
},
|
||||||
ws::send_ws_msg,
|
ws::send_ws_msg,
|
||||||
FieldChange, FieldId, Msg, WebSocketChanged,
|
FieldChange, FieldId, Msg, OperationKind, ResourceKind,
|
||||||
},
|
},
|
||||||
jirs_data::{TimeTracking, WsMsg},
|
jirs_data::{TimeTracking, WsMsg},
|
||||||
seed::{prelude::*, *},
|
seed::{prelude::*, *},
|
||||||
@ -37,24 +37,19 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>
|
|||||||
model.modals.push(modal_type.as_ref().clone());
|
model.modals.push(modal_type.as_ref().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::ProjectIssuesLoaded(_issues))) => {
|
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => {
|
||||||
match model.page {
|
match model.page {
|
||||||
Page::EditIssue(issue_id) if model.modals.is_empty() => {
|
Page::EditIssue(issue_id) if model.modals.is_empty() => {
|
||||||
push_edit_modal(issue_id, model, orders)
|
push_edit_modal(issue_id, model, orders)
|
||||||
}
|
}
|
||||||
|
Page::AddIssue if model.modals.is_empty() => push_add_modal(model, orders),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::ChangePage(Page::EditIssue(issue_id)) => {
|
Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders),
|
||||||
push_edit_modal(*issue_id, model, orders);
|
|
||||||
}
|
|
||||||
|
|
||||||
Msg::ChangePage(Page::AddIssue) => {
|
Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders),
|
||||||
let mut modal = crate::modals::issues_create::Model::default();
|
|
||||||
modal.project_id = model.project.as_ref().map(|p| p.id);
|
|
||||||
model.modals.push(ModalType::AddIssue(Box::new(modal)));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
|
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
|
||||||
@ -123,6 +118,14 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
section![id!["modals"], modals]
|
section![id!["modals"], modals]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_add_modal(model: &mut Model, _orders: &mut impl Orders<Msg>) {
|
||||||
|
use crate::modals::issues_create::Model;
|
||||||
|
model.modals.push(ModalType::AddIssue(Box::new(Model {
|
||||||
|
project_id: model.project.as_ref().map(|p| p.id),
|
||||||
|
..Model::default()
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
let time_tracking_type = model
|
let time_tracking_type = model
|
||||||
.project
|
.project
|
||||||
|
@ -3,7 +3,7 @@ use {
|
|||||||
model::{IssueModal, ModalType},
|
model::{IssueModal, ModalType},
|
||||||
shared::styled_select::StyledSelectChanged,
|
shared::styled_select::StyledSelectChanged,
|
||||||
ws::send_ws_msg,
|
ws::send_ws_msg,
|
||||||
FieldId, Msg, WebSocketChanged,
|
FieldId, Msg, OperationKind, ResourceKind,
|
||||||
},
|
},
|
||||||
jirs_data::{IssueFieldId, UserId, WsMsg},
|
jirs_data::{IssueFieldId, UserId, WsMsg},
|
||||||
seed::prelude::*,
|
seed::prelude::*,
|
||||||
@ -48,7 +48,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
|||||||
time_remaining: modal.time_remaining,
|
time_remaining: modal.time_remaining,
|
||||||
project_id: modal.project_id.unwrap_or(project_id),
|
project_id: modal.project_id.unwrap_or(project_id),
|
||||||
user_ids: modal.user_ids.clone(),
|
user_ids: modal.user_ids.clone(),
|
||||||
reporter_id: modal.reporter_id.unwrap_or_else(|| user_id),
|
reporter_id: modal.reporter_id.unwrap_or(user_id),
|
||||||
epic_id: modal.epic_id,
|
epic_id: modal.epic_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,12 +64,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => {
|
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleCreated, _) => {
|
||||||
model.issues.push(issue.clone());
|
|
||||||
orders.skip().send_msg(Msg::ModalDropped);
|
|
||||||
}
|
|
||||||
|
|
||||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::EpicCreated(_))) => {
|
|
||||||
orders.skip().send_msg(Msg::ModalDropped);
|
orders.skip().send_msg(Msg::ModalDropped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ pub struct Model {
|
|||||||
pub time_remaining: StyledInputState,
|
pub time_remaining: StyledInputState,
|
||||||
pub time_remaining_select: StyledSelectState,
|
pub time_remaining_select: StyledSelectState,
|
||||||
|
|
||||||
|
pub title_state: StyledInputState,
|
||||||
pub description_state: StyledEditorState,
|
pub description_state: StyledEditorState,
|
||||||
|
|
||||||
// comments
|
// comments
|
||||||
@ -107,6 +108,11 @@ impl Model {
|
|||||||
.map(|n| vec![n as u32])
|
.map(|n| vec![n as u32])
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
),
|
),
|
||||||
|
title_state: StyledInputState::new(
|
||||||
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||||
|
issue.title.clone(),
|
||||||
|
)
|
||||||
|
.with_min(Some(3)),
|
||||||
description_state: StyledEditorState::new(
|
description_state: StyledEditorState::new(
|
||||||
Mode::View,
|
Mode::View,
|
||||||
issue.description_text.as_deref().unwrap_or(""),
|
issue.description_text.as_deref().unwrap_or(""),
|
||||||
@ -159,5 +165,7 @@ impl IssueModal for Model {
|
|||||||
self.time_remaining.update(msg);
|
self.time_remaining.update(msg);
|
||||||
self.time_remaining_select.update(msg, orders);
|
self.time_remaining_select.update(msg, orders);
|
||||||
self.epic_name_state.update(msg, orders);
|
self.epic_name_state.update(msg, orders);
|
||||||
|
|
||||||
|
self.title_state.update(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
issue.description_text.clone().unwrap_or_default();
|
issue.description_text.clone().unwrap_or_default();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||||
StyledSelectChanged::Changed(Some(value)),
|
StyledSelectChanged::Changed(Some(value)),
|
||||||
@ -40,6 +42,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// issue status id
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||||
StyledSelectChanged::Changed(Some(value)),
|
StyledSelectChanged::Changed(Some(value)),
|
||||||
@ -55,6 +59,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reporter id
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||||
StyledSelectChanged::Changed(Some(value)),
|
StyledSelectChanged::Changed(Some(value)),
|
||||||
@ -70,6 +76,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assignees
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||||
StyledSelectChanged::Changed(Some(value)),
|
StyledSelectChanged::Changed(Some(value)),
|
||||||
@ -89,8 +97,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||||
StyledSelectChanged::RemoveMulti(value),
|
StyledSelectChanged::RemoveMulti(value),
|
||||||
) => {
|
) => {
|
||||||
let mut old = vec![];
|
let old = std::mem::replace(&mut modal.payload.user_ids, vec![]);
|
||||||
std::mem::swap(&mut old, &mut modal.payload.user_ids);
|
|
||||||
let dropped = *value as i32;
|
let dropped = *value as i32;
|
||||||
for id in old.into_iter() {
|
for id in old.into_iter() {
|
||||||
if id != dropped {
|
if id != dropped {
|
||||||
@ -107,6 +114,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// priority
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||||
StyledSelectChanged::Changed(Some(value)),
|
StyledSelectChanged::Changed(Some(value)),
|
||||||
@ -122,6 +131,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title
|
||||||
Msg::StrInputChanged(
|
Msg::StrInputChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||||
value,
|
value,
|
||||||
@ -136,7 +147,10 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
model.ws.as_ref(),
|
model.ws.as_ref(),
|
||||||
orders,
|
orders,
|
||||||
);
|
);
|
||||||
|
orders.skip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Description
|
||||||
Msg::StrInputChanged(
|
Msg::StrInputChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||||
value,
|
value,
|
||||||
|
@ -151,8 +151,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
|||||||
.add_input_class("issueSummary")
|
.add_input_class("issueSummary")
|
||||||
.add_wrapper_class("issueSummary")
|
.add_wrapper_class("issueSummary")
|
||||||
.add_wrapper_class("textarea")
|
.add_wrapper_class("textarea")
|
||||||
.value(payload.title.as_str())
|
.state(&modal.title_state)
|
||||||
.valid(payload.title.len() >= 3)
|
|
||||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||||
IssueFieldId::Title,
|
IssueFieldId::Title,
|
||||||
)))
|
)))
|
@ -62,7 +62,7 @@ impl Page {
|
|||||||
match self {
|
match self {
|
||||||
Page::Project => "/board".to_string(),
|
Page::Project => "/board".to_string(),
|
||||||
Page::EditIssue(id) => format!("/issues/{id}", id = id),
|
Page::EditIssue(id) => format!("/issues/{id}", id = id),
|
||||||
Page::AddIssue => "/add-issues".to_string(),
|
Page::AddIssue => "/add-issue".to_string(),
|
||||||
Page::ProjectSettings => "/project-settings".to_string(),
|
Page::ProjectSettings => "/project-settings".to_string(),
|
||||||
Page::SignIn => "/login".to_string(),
|
Page::SignIn => "/login".to_string(),
|
||||||
Page::SignUp => "/register".to_string(),
|
Page::SignUp => "/register".to_string(),
|
||||||
@ -163,6 +163,8 @@ pub struct Model {
|
|||||||
pub user_projects: Vec<UserProject>,
|
pub user_projects: Vec<UserProject>,
|
||||||
pub projects: Vec<Project>,
|
pub projects: Vec<Project>,
|
||||||
pub epics: Vec<Epic>,
|
pub epics: Vec<Epic>,
|
||||||
|
|
||||||
|
pub show_extras: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
@ -197,6 +199,7 @@ impl Model {
|
|||||||
projects: vec![],
|
projects: vec![],
|
||||||
epics: vec![],
|
epics: vec![],
|
||||||
issues_by_id: Default::default(),
|
issues_by_id: Default::default(),
|
||||||
|
show_extras: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
.add_field(submit_field)
|
.add_field(submit_field)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
inner_layout(model, "profile", vec![content])
|
inner_layout(model, "profile", &[content])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
|
||||||
|
@ -38,31 +38,35 @@ impl ProjectPage {
|
|||||||
|
|
||||||
let statuses = statuses.iter().map(|s| (s.id, s.name.as_str()));
|
let statuses = statuses.iter().map(|s| (s.id, s.name.as_str()));
|
||||||
|
|
||||||
let mut issues: Vec<&Issue> = {
|
let mut issues: Vec<&Issue> = issues.iter().collect();
|
||||||
|
if self.recently_updated_filter {
|
||||||
let mut m = HashMap::new();
|
let mut m = HashMap::new();
|
||||||
let mut sorted = vec![];
|
let mut sorted = vec![];
|
||||||
for issue in issues.iter() {
|
for issue in issues.into_iter() {
|
||||||
sorted.push((issue.id, issue.updated_at));
|
sorted.push((issue.id, issue.updated_at));
|
||||||
m.insert(issue.id, issue);
|
m.insert(issue.id, issue);
|
||||||
}
|
}
|
||||||
sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||||
sorted
|
issues = sorted
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.take(10)
|
||||||
.flat_map(|(id, _)| m.remove(&id))
|
.flat_map(|(id, _)| m.remove(&id))
|
||||||
.collect()
|
.collect();
|
||||||
};
|
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||||
if self.recently_updated_filter {
|
|
||||||
issues = issues[0..10].to_vec()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for epic in epics {
|
for epic in epics {
|
||||||
let mut per_epic_map = EpicIssuePerStatus::default();
|
let mut per_epic_map = EpicIssuePerStatus {
|
||||||
per_epic_map.epic_name = epic.map(|(_, name)| name).unwrap_or_default().to_string();
|
epic_name: epic.map(|(_, name)| name).unwrap_or_default().to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
for (current_status_id, issue_status_name) in statuses.to_owned() {
|
for (current_status_id, issue_status_name) in statuses.to_owned() {
|
||||||
let mut per_status_map = StatusIssueIds::default();
|
let mut per_status_map = StatusIssueIds {
|
||||||
per_status_map.status_id = current_status_id;
|
status_id: current_status_id,
|
||||||
per_status_map.status_name = issue_status_name.to_string();
|
status_name: issue_status_name.to_string(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
for issue in issues.iter() {
|
for issue in issues.iter() {
|
||||||
if issue.epic_id == epic.map(|(id, _)| id)
|
if issue.epic_id == epic.map(|(id, _)| id)
|
||||||
&& issue_filter_status(issue, current_status_id)
|
&& issue_filter_status(issue, current_status_id)
|
||||||
|
@ -79,28 +79,58 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
|||||||
}
|
}
|
||||||
Msg::StrInputChanged(FieldId::TextFilterBoard, text) => {
|
Msg::StrInputChanged(FieldId::TextFilterBoard, text) => {
|
||||||
project_page.text_filter = text;
|
project_page.text_filter = text;
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Msg::ProjectAvatarFilterChanged(user_id, active) => {
|
Msg::ProjectAvatarFilterChanged(user_id, active) => {
|
||||||
if active {
|
if active {
|
||||||
project_page.active_avatar_filters = project_page
|
project_page.active_avatar_filters =
|
||||||
.active_avatar_filters
|
std::mem::replace(&mut project_page.active_avatar_filters, vec![])
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter_map(|id| if *id != user_id { Some(*id) } else { None })
|
.filter(|id| *id != user_id)
|
||||||
.collect();
|
.collect();
|
||||||
} else {
|
} else {
|
||||||
project_page.active_avatar_filters.push(user_id);
|
project_page.active_avatar_filters.push(user_id);
|
||||||
}
|
}
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Msg::ProjectToggleOnlyMy => {
|
Msg::ProjectToggleOnlyMy => {
|
||||||
project_page.only_my_filter = !project_page.only_my_filter;
|
project_page.only_my_filter = !project_page.only_my_filter;
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Msg::ProjectToggleRecentlyUpdated => {
|
Msg::ProjectToggleRecentlyUpdated => {
|
||||||
project_page.recently_updated_filter = !project_page.recently_updated_filter;
|
project_page.recently_updated_filter = !project_page.recently_updated_filter;
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Msg::ProjectClearFilters => {
|
Msg::ProjectClearFilters => {
|
||||||
project_page.active_avatar_filters = vec![];
|
project_page.active_avatar_filters = vec![];
|
||||||
project_page.recently_updated_filter = false;
|
project_page.recently_updated_filter = false;
|
||||||
project_page.only_my_filter = false;
|
project_page.only_my_filter = false;
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => {
|
Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => {
|
||||||
crate::ws::issue::drag_started(issue_id, model)
|
crate::ws::issue::drag_started(issue_id, model)
|
||||||
|
@ -1,36 +1,31 @@
|
|||||||
use {
|
use {
|
||||||
crate::{
|
crate::{
|
||||||
model::{Model, Page, PageContent},
|
model::Model,
|
||||||
shared::{
|
shared::{inner_layout, styled_button::StyledButton, styled_icon::Icon, ToNode},
|
||||||
inner_layout,
|
Msg,
|
||||||
styled_avatar::StyledAvatar,
|
|
||||||
styled_button::StyledButton,
|
|
||||||
styled_icon::{Icon, StyledIcon},
|
|
||||||
styled_input::StyledInput,
|
|
||||||
ToNode,
|
|
||||||
},
|
},
|
||||||
BoardPageChange, FieldId, Msg, PageChanged,
|
|
||||||
},
|
|
||||||
jirs_data::*,
|
|
||||||
seed::{prelude::*, *},
|
seed::{prelude::*, *},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod board;
|
||||||
|
mod filters;
|
||||||
|
|
||||||
pub fn view(model: &Model) -> Node<Msg> {
|
pub fn view(model: &Model) -> Node<Msg> {
|
||||||
let project_section = vec![
|
let project_section = [
|
||||||
breadcrumbs(model),
|
breadcrumbs(model),
|
||||||
header(),
|
header(model),
|
||||||
project_board_filters(model),
|
filters::project_board_filters(model),
|
||||||
project_board_lists(model),
|
board::project_board_lists(model),
|
||||||
];
|
];
|
||||||
|
|
||||||
inner_layout(model, "projectPage", project_section)
|
inner_layout(model, "projectPage", &project_section)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||||
let project_name = model
|
let project_name = model
|
||||||
.project
|
.project
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| p.name.clone())
|
.map(|p| p.name.as_str())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
div![
|
div![
|
||||||
C!["breadcrumbsContainer"],
|
C!["breadcrumbsContainer"],
|
||||||
@ -42,10 +37,13 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header() -> Node<Msg> {
|
fn header(model: &Model) -> Node<Msg> {
|
||||||
|
if !model.show_extras {
|
||||||
|
return Node::Empty;
|
||||||
|
}
|
||||||
let button = StyledButton::build()
|
let button = StyledButton::build()
|
||||||
.secondary()
|
.secondary()
|
||||||
.text("Github Repo")
|
.text("Repository")
|
||||||
.icon(Icon::Github)
|
.icon(Icon::Github)
|
||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
@ -58,256 +56,3 @@ fn header() -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_board_filters(model: &Model) -> Node<Msg> {
|
|
||||||
let project_page = match &model.page_content {
|
|
||||||
PageContent::Project(page_content) => page_content,
|
|
||||||
_ => return empty![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let search_input = StyledInput::build()
|
|
||||||
.icon(Icon::Search)
|
|
||||||
.valid(true)
|
|
||||||
.value(project_page.text_filter.as_str())
|
|
||||||
.build(FieldId::TextFilterBoard)
|
|
||||||
.into_node();
|
|
||||||
|
|
||||||
let only_my = StyledButton::build()
|
|
||||||
.empty()
|
|
||||||
.active(project_page.only_my_filter)
|
|
||||||
.text("Only My Issues")
|
|
||||||
.add_class("filterChild")
|
|
||||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
|
|
||||||
.build()
|
|
||||||
.into_node();
|
|
||||||
|
|
||||||
let recently_updated = StyledButton::build()
|
|
||||||
.empty()
|
|
||||||
.text("Recently Updated")
|
|
||||||
.add_class("filterChild")
|
|
||||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated))
|
|
||||||
.build()
|
|
||||||
.into_node();
|
|
||||||
|
|
||||||
let clear_all = if project_page.only_my_filter
|
|
||||||
|| project_page.recently_updated_filter
|
|
||||||
|| !project_page.active_avatar_filters.is_empty()
|
|
||||||
{
|
|
||||||
seed::button![
|
|
||||||
id!["clearAllFilters"],
|
|
||||||
C!["filterChild"],
|
|
||||||
"Clear all",
|
|
||||||
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
|
||||||
]
|
|
||||||
} else {
|
|
||||||
empty![]
|
|
||||||
};
|
|
||||||
|
|
||||||
div![
|
|
||||||
id!["projectBoardFilters"],
|
|
||||||
search_input,
|
|
||||||
avatars_filters(model),
|
|
||||||
only_my,
|
|
||||||
recently_updated,
|
|
||||||
clear_all
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn avatars_filters(model: &Model) -> Node<Msg> {
|
|
||||||
let project_page = match &model.page_content {
|
|
||||||
PageContent::Project(project_page) => project_page,
|
|
||||||
_ => return empty![],
|
|
||||||
};
|
|
||||||
let active_avatar_filters = &project_page.active_avatar_filters;
|
|
||||||
let avatars: Vec<Node<Msg>> = model
|
|
||||||
.users
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(idx, user)| {
|
|
||||||
let mut class_list = vec!["avatarIsActiveBorder"];
|
|
||||||
let user_id = user.id;
|
|
||||||
let active = active_avatar_filters.contains(&user_id);
|
|
||||||
if active {
|
|
||||||
class_list.push("isActive");
|
|
||||||
}
|
|
||||||
let styled_avatar = StyledAvatar::build()
|
|
||||||
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
|
||||||
.on_click(mouse_ev(Ev::Click, move |_| {
|
|
||||||
Msg::ProjectAvatarFilterChanged(user_id, active)
|
|
||||||
}))
|
|
||||||
.name(user.name.as_str())
|
|
||||||
.user_index(idx)
|
|
||||||
.build()
|
|
||||||
.into_node();
|
|
||||||
div![attrs![At::Class => class_list.join(" ")], styled_avatar]
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
div![id!["avatars"], C!["filterChild"], avatars]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_board_lists(model: &Model) -> Node<Msg> {
|
|
||||||
let project_page = match &model.page_content {
|
|
||||||
PageContent::Project(project_page) => project_page,
|
|
||||||
_ => return empty![],
|
|
||||||
};
|
|
||||||
let rows = project_page.visible_issues.iter().map(|per_epic| {
|
|
||||||
let columns: Vec<Node<Msg>> = per_epic
|
|
||||||
.per_status_issues
|
|
||||||
.iter()
|
|
||||||
.map(|per_status| {
|
|
||||||
let issues: Vec<&Issue> = per_status
|
|
||||||
.issue_ids
|
|
||||||
.iter()
|
|
||||||
.filter_map(|id| model.issues_by_id.get(id))
|
|
||||||
.collect();
|
|
||||||
project_issue_list(
|
|
||||||
model,
|
|
||||||
per_status.status_id,
|
|
||||||
&per_status.status_name,
|
|
||||||
issues.as_slice(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
div![
|
|
||||||
C!["row"],
|
|
||||||
div![C!["epicName"], per_epic.epic_name.as_str()],
|
|
||||||
div![C!["projectBoardLists"], columns]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
div![C!["rows"], rows]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_issue_list(
|
|
||||||
model: &Model,
|
|
||||||
status_id: IssueStatusId,
|
|
||||||
status_name: &str,
|
|
||||||
issues: &[&Issue],
|
|
||||||
) -> Node<Msg> {
|
|
||||||
let issues: Vec<Node<Msg>> = issues
|
|
||||||
.iter()
|
|
||||||
.map(|issue| project_issue(model, issue))
|
|
||||||
.collect();
|
|
||||||
let drop_handler = {
|
|
||||||
let send_status = status_id;
|
|
||||||
drag_ev(Ev::Drop, move |ev| {
|
|
||||||
ev.prevent_default();
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::IssueDropZone(send_status),
|
|
||||||
)))
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let drag_over_handler = {
|
|
||||||
let send_status = status_id;
|
|
||||||
drag_ev(Ev::DragOver, move |ev| {
|
|
||||||
ev.prevent_default();
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::IssueDragOverStatus(send_status),
|
|
||||||
)))
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
div![
|
|
||||||
C!["list"],
|
|
||||||
div![C!["title"], status_name, div![C!["issuesCount"]]],
|
|
||||||
div![
|
|
||||||
C!["issues"],
|
|
||||||
attrs![At::DropZone => "link"],
|
|
||||||
drop_handler,
|
|
||||||
drag_over_handler,
|
|
||||||
issues
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
|
||||||
let avatars: Vec<Node<Msg>> = issue
|
|
||||||
.user_ids
|
|
||||||
.iter()
|
|
||||||
.filter_map(|id| model.users_by_id.get(id))
|
|
||||||
.map(|user| {
|
|
||||||
StyledAvatar::build()
|
|
||||||
.size(24)
|
|
||||||
.name(user.name.as_str())
|
|
||||||
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
|
||||||
.user_index(0)
|
|
||||||
.build()
|
|
||||||
.into_node()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into())
|
|
||||||
.with_color(issue.issue_type.to_str())
|
|
||||||
.build()
|
|
||||||
.into_node();
|
|
||||||
let priority_icon = {
|
|
||||||
let icon = match issue.priority {
|
|
||||||
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
|
||||||
_ => Icon::ArrowUp,
|
|
||||||
};
|
|
||||||
StyledIcon::build(icon)
|
|
||||||
.with_color(issue.priority.to_str())
|
|
||||||
.build()
|
|
||||||
.into_node()
|
|
||||||
};
|
|
||||||
|
|
||||||
let issue_id = issue.id;
|
|
||||||
let drag_started = drag_ev(Ev::DragStart, move |_| {
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::IssueDragStarted(issue_id),
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::IssueDragStopped(issue_id),
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
let drag_over_handler = drag_ev(Ev::DragOver, move |ev| {
|
|
||||||
ev.prevent_default();
|
|
||||||
ev.stop_propagation();
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::ExchangePosition(issue_id),
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
|
|
||||||
let drag_out = drag_ev(Ev::DragLeave, move |_| {
|
|
||||||
Some(Msg::PageChanged(PageChanged::Board(
|
|
||||||
BoardPageChange::DragLeave(issue_id),
|
|
||||||
)))
|
|
||||||
});
|
|
||||||
let on_click = mouse_ev("click", move |ev| {
|
|
||||||
ev.prevent_default();
|
|
||||||
ev.stop_propagation();
|
|
||||||
seed::Url::new()
|
|
||||||
.add_path_part("issues")
|
|
||||||
.add_path_part(format!("{}", issue_id))
|
|
||||||
.go_and_push();
|
|
||||||
Msg::ChangePage(Page::EditIssue(issue_id))
|
|
||||||
});
|
|
||||||
|
|
||||||
let href = format!("/issues/{id}", id = issue_id);
|
|
||||||
|
|
||||||
a![
|
|
||||||
drag_started,
|
|
||||||
on_click,
|
|
||||||
C!["issueLink"],
|
|
||||||
attrs![At::Href => href],
|
|
||||||
div![
|
|
||||||
C!["issue"],
|
|
||||||
attrs![At::Draggable => true],
|
|
||||||
drag_stopped,
|
|
||||||
drag_over_handler,
|
|
||||||
drag_out,
|
|
||||||
p![C!["title"], issue.title.as_str()],
|
|
||||||
div![
|
|
||||||
C!["bottom"],
|
|
||||||
div![
|
|
||||||
div![C!["issueTypeIcon"], issue_type_icon],
|
|
||||||
div![C!["issuePriorityIcon"], priority_icon]
|
|
||||||
],
|
|
||||||
div![C!["assignees"], avatars,],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
181
jirs-client/src/pages/project_page/view/board.rs
Normal file
181
jirs-client/src/pages/project_page/view/board.rs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
model::PageContent,
|
||||||
|
shared::{styled_avatar::*, styled_icon::*, ToNode},
|
||||||
|
BoardPageChange, Model, Msg, Page, PageChanged,
|
||||||
|
},
|
||||||
|
jirs_data::*,
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||||
|
let project_page = match &model.page_content {
|
||||||
|
PageContent::Project(project_page) => project_page,
|
||||||
|
_ => return empty![],
|
||||||
|
};
|
||||||
|
let rows = project_page.visible_issues.iter().map(|per_epic| {
|
||||||
|
let columns: Vec<Node<Msg>> = per_epic
|
||||||
|
.per_status_issues
|
||||||
|
.iter()
|
||||||
|
.map(|per_status| {
|
||||||
|
let issues: Vec<&Issue> = per_status
|
||||||
|
.issue_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| model.issues_by_id.get(id))
|
||||||
|
.collect();
|
||||||
|
project_issue_list(
|
||||||
|
model,
|
||||||
|
per_status.status_id,
|
||||||
|
&per_status.status_name,
|
||||||
|
issues.as_slice(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
div![
|
||||||
|
C!["row"],
|
||||||
|
div![C!["epicName"], per_epic.epic_name.as_str()],
|
||||||
|
div![C!["projectBoardLists"], columns]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
div![C!["rows"], rows]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_issue_list(
|
||||||
|
model: &Model,
|
||||||
|
status_id: IssueStatusId,
|
||||||
|
status_name: &str,
|
||||||
|
issues: &[&Issue],
|
||||||
|
) -> Node<Msg> {
|
||||||
|
let issues: Vec<Node<Msg>> = issues
|
||||||
|
.iter()
|
||||||
|
.map(|issue| project_issue(model, issue))
|
||||||
|
.collect();
|
||||||
|
let drop_handler = {
|
||||||
|
let send_status = status_id;
|
||||||
|
drag_ev(Ev::Drop, move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::IssueDropZone(send_status),
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let drag_over_handler = {
|
||||||
|
let send_status = status_id;
|
||||||
|
drag_ev(Ev::DragOver, move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::IssueDragOverStatus(send_status),
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
div![
|
||||||
|
C!["list"],
|
||||||
|
div![C!["title"], status_name, div![C!["issuesCount"]]],
|
||||||
|
div![
|
||||||
|
C!["issues"],
|
||||||
|
attrs![At::DropZone => "link"],
|
||||||
|
drop_handler,
|
||||||
|
drag_over_handler,
|
||||||
|
issues
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_issue(model: &Model, issue: &Issue) -> Node<Msg> {
|
||||||
|
let is_dragging = match &model.page_content {
|
||||||
|
PageContent::Project(project_page) => project_page.issue_drag.is_dragging(),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let avatars: Vec<Node<Msg>> = issue
|
||||||
|
.user_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| model.users_by_id.get(id))
|
||||||
|
.map(|user| {
|
||||||
|
StyledAvatar::build()
|
||||||
|
.size(24)
|
||||||
|
.name(user.name.as_str())
|
||||||
|
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
||||||
|
.user_index(0)
|
||||||
|
.build()
|
||||||
|
.into_node()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into())
|
||||||
|
.with_color(issue.issue_type.to_str())
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let priority_icon = {
|
||||||
|
let icon = match issue.priority {
|
||||||
|
IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown,
|
||||||
|
_ => Icon::ArrowUp,
|
||||||
|
};
|
||||||
|
StyledIcon::build(icon)
|
||||||
|
.add_class(issue.priority.to_str())
|
||||||
|
.with_color(issue.priority.to_str())
|
||||||
|
.build()
|
||||||
|
.into_node()
|
||||||
|
};
|
||||||
|
|
||||||
|
let issue_id = issue.id;
|
||||||
|
let drag_started = drag_ev(Ev::DragStart, move |_| {
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::IssueDragStarted(issue_id),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let drag_stopped = drag_ev(Ev::DragEnd, move |_| {
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::IssueDragStopped(issue_id),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let drag_over_handler = drag_ev(Ev::DragEnter, move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
ev.stop_propagation();
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::ExchangePosition(issue_id),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let drag_out = drag_ev(Ev::DragLeave, move |_| {
|
||||||
|
Some(Msg::PageChanged(PageChanged::Board(
|
||||||
|
BoardPageChange::DragLeave(issue_id),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
let on_click = mouse_ev("click", move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
ev.stop_propagation();
|
||||||
|
seed::Url::new()
|
||||||
|
.add_path_part("issues")
|
||||||
|
.add_path_part(format!("{}", issue_id))
|
||||||
|
.go_and_push();
|
||||||
|
Msg::ChangePage(Page::EditIssue(issue_id))
|
||||||
|
});
|
||||||
|
|
||||||
|
let href = format!("/issues/{id}", id = issue_id);
|
||||||
|
|
||||||
|
a![
|
||||||
|
drag_started,
|
||||||
|
on_click,
|
||||||
|
C!["issueLink"],
|
||||||
|
attrs![At::Href => href],
|
||||||
|
IF![is_dragging => div![C!["dragCover"], drag_over_handler]],
|
||||||
|
div![
|
||||||
|
C!["issue"],
|
||||||
|
attrs![At::Draggable => true],
|
||||||
|
drag_stopped,
|
||||||
|
drag_out,
|
||||||
|
p![C!["title"], issue.title.as_str()],
|
||||||
|
div![
|
||||||
|
C!["bottom"],
|
||||||
|
div![
|
||||||
|
div![C!["issueTypeIcon"], issue_type_icon],
|
||||||
|
div![C!["issuePriorityIcon"], priority_icon]
|
||||||
|
],
|
||||||
|
div![C!["assignees"], avatars,],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
95
jirs-client/src/pages/project_page/view/filters.rs
Normal file
95
jirs-client/src/pages/project_page/view/filters.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
use {
|
||||||
|
crate::{
|
||||||
|
model::PageContent,
|
||||||
|
shared::{styled_avatar::*, styled_button::*, styled_icon::*, styled_input::*, ToNode},
|
||||||
|
FieldId, Model, Msg,
|
||||||
|
},
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||||
|
let project_page = match &model.page_content {
|
||||||
|
PageContent::Project(page_content) => page_content,
|
||||||
|
_ => return empty![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_input = StyledInput::build()
|
||||||
|
.icon(Icon::Search)
|
||||||
|
.valid(true)
|
||||||
|
.value(project_page.text_filter.as_str())
|
||||||
|
.build(FieldId::TextFilterBoard)
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let only_my = StyledButton::build()
|
||||||
|
.empty()
|
||||||
|
.active(project_page.only_my_filter)
|
||||||
|
.text("Only My Issues")
|
||||||
|
.add_class("filterChild")
|
||||||
|
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let recently_updated = StyledButton::build()
|
||||||
|
.empty()
|
||||||
|
.text("Recently Updated")
|
||||||
|
.add_class("filterChild")
|
||||||
|
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated))
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let clear_all = if project_page.only_my_filter
|
||||||
|
|| project_page.recently_updated_filter
|
||||||
|
|| !project_page.active_avatar_filters.is_empty()
|
||||||
|
{
|
||||||
|
seed::button![
|
||||||
|
id!["clearAllFilters"],
|
||||||
|
C!["filterChild"],
|
||||||
|
"Clear all",
|
||||||
|
mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
empty![]
|
||||||
|
};
|
||||||
|
|
||||||
|
div![
|
||||||
|
id!["projectBoardFilters"],
|
||||||
|
search_input,
|
||||||
|
avatars_filters(model),
|
||||||
|
only_my,
|
||||||
|
recently_updated,
|
||||||
|
clear_all
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||||
|
let project_page = match &model.page_content {
|
||||||
|
PageContent::Project(project_page) => project_page,
|
||||||
|
_ => return empty![],
|
||||||
|
};
|
||||||
|
let active_avatar_filters = &project_page.active_avatar_filters;
|
||||||
|
let avatars: Vec<Node<Msg>> = model
|
||||||
|
.users
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, user)| {
|
||||||
|
let user_id = user.id;
|
||||||
|
let active = active_avatar_filters.contains(&user_id);
|
||||||
|
let styled_avatar = StyledAvatar::build()
|
||||||
|
.avatar_url(user.avatar_url.as_deref().unwrap_or_default())
|
||||||
|
.on_click(mouse_ev(Ev::Click, move |_| {
|
||||||
|
Msg::ProjectAvatarFilterChanged(user_id, active)
|
||||||
|
}))
|
||||||
|
.name(user.name.as_str())
|
||||||
|
.user_index(idx)
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
div![
|
||||||
|
if active { Some(C!["isActive"]) } else { None },
|
||||||
|
C!["avatarIsActiveBorder"],
|
||||||
|
styled_avatar
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
div![id!["avatars"], C!["filterChild"], avatars]
|
||||||
|
}
|
@ -105,9 +105,9 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
|||||||
.build()
|
.build()
|
||||||
.into_node();
|
.into_node();
|
||||||
|
|
||||||
let project_section = vec![div![C!["formContainer"], form]];
|
let project_section = [div![C!["formContainer"], form]];
|
||||||
|
|
||||||
inner_layout(model, "projectSettings", project_section)
|
inner_layout(model, "projectSettings", &project_section)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build project name input with styled field wrapper
|
/// Build project name input with styled field wrapper
|
||||||
|
@ -29,7 +29,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
|
|
||||||
let body = section![C!["top"], h1![C!["header"], "Reports"], graph, list];
|
let body = section![C!["top"], h1![C!["header"], "Reports"], graph, list];
|
||||||
|
|
||||||
inner_layout(model, "reports", vec![body])
|
inner_layout(model, "reports", &[body])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
|
fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
|
||||||
|
@ -167,9 +167,5 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
ul![C!["invitationsList"], invitations],
|
ul![C!["invitationsList"], invitations],
|
||||||
];
|
];
|
||||||
|
|
||||||
inner_layout(
|
inner_layout(model, "users", &[form, users_section, invitations_section])
|
||||||
model,
|
|
||||||
"users",
|
|
||||||
vec![form, users_section, invitations_section],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,11 @@ pub struct DragState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DragState {
|
impl DragState {
|
||||||
|
#[inline]
|
||||||
|
pub fn is_dragging(&self) -> bool {
|
||||||
|
self.dragged_id.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn mark_dirty(&mut self, id: i32) {
|
pub fn mark_dirty(&mut self, id: i32) {
|
||||||
self.dirty.insert(id);
|
self.dirty.insert(id);
|
||||||
|
@ -77,7 +77,7 @@ pub fn divider() -> Node<Msg> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
|
pub fn inner_layout(model: &Model, page_name: &str, children: &[Node<Msg>]) -> Node<Msg> {
|
||||||
let modal_node = crate::modal::view(model);
|
let modal_node = crate::modal::view(model);
|
||||||
article![
|
article![
|
||||||
modal_node,
|
modal_node,
|
||||||
|
@ -103,7 +103,10 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
|||||||
C!["bottom"],
|
C!["bottom"],
|
||||||
navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)),
|
navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)),
|
||||||
messages,
|
messages,
|
||||||
about_tooltip(model, navbar_left_item("About", Icon::Help, None, None)),
|
IF![model.show_extras => about_tooltip(
|
||||||
|
model,
|
||||||
|
navbar_left_item("About", Icon::Help, None, None)
|
||||||
|
)],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
@ -145,17 +145,13 @@ pub fn render(values: StyledAvatar) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let style = format!(
|
|
||||||
"{shared}; width: {size}px; height: {size}px; font-size: calc({size}px / 1.7);",
|
|
||||||
shared = shared_style,
|
|
||||||
size = size
|
|
||||||
);
|
|
||||||
div![
|
div![
|
||||||
C!["styledAvatar letter"],
|
C!["styledAvatar letter"],
|
||||||
class_list,
|
class_list,
|
||||||
attrs![
|
attrs![
|
||||||
At::Class => format!("avatarColor{}", index + 1),
|
At::Class => format!("avatarColor{}", index + 1),
|
||||||
At::Style => style
|
At::Style => shared_style,
|
||||||
|
At::Title => name
|
||||||
],
|
],
|
||||||
span![letter],
|
span![letter],
|
||||||
on_click,
|
on_click,
|
||||||
|
@ -32,6 +32,8 @@ pub struct StyledInputState {
|
|||||||
id: FieldId,
|
id: FieldId,
|
||||||
touched: bool,
|
touched: bool,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub min: Option<usize>,
|
||||||
|
pub max: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StyledInputState {
|
impl StyledInputState {
|
||||||
@ -44,9 +46,23 @@ impl StyledInputState {
|
|||||||
id,
|
id,
|
||||||
touched: false,
|
touched: false,
|
||||||
value: value.into(),
|
value: value.into(),
|
||||||
|
min: None,
|
||||||
|
max: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_min(mut self, min: Option<usize>) -> Self {
|
||||||
|
self.min = min;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn with_max(mut self, max: Option<usize>) -> Self {
|
||||||
|
self.max = max;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn to_i32(&self) -> Option<i32> {
|
pub fn to_i32(&self) -> Option<i32> {
|
||||||
self.value.parse::<i32>().ok()
|
self.value.parse::<i32>().ok()
|
||||||
@ -80,11 +96,11 @@ impl StyledInputState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StyledInput<'l> {
|
pub struct StyledInput<'l, 'm: 'l> {
|
||||||
id: FieldId,
|
id: FieldId,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
valid: bool,
|
valid: bool,
|
||||||
value: Option<String>,
|
value: Option<&'m str>,
|
||||||
input_type: Option<&'l str>,
|
input_type: Option<&'l str>,
|
||||||
input_class_list: Vec<&'l str>,
|
input_class_list: Vec<&'l str>,
|
||||||
wrapper_class_list: Vec<&'l str>,
|
wrapper_class_list: Vec<&'l str>,
|
||||||
@ -93,9 +109,9 @@ pub struct StyledInput<'l> {
|
|||||||
input_handlers: Vec<EventHandler<Msg>>,
|
input_handlers: Vec<EventHandler<Msg>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledInput<'l> {
|
impl<'l, 'm: 'l> StyledInput<'l, 'm> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn build() -> StyledInputBuilder<'l> {
|
pub fn build() -> StyledInputBuilder<'l, 'm> {
|
||||||
StyledInputBuilder {
|
StyledInputBuilder {
|
||||||
icon: None,
|
icon: None,
|
||||||
valid: None,
|
valid: None,
|
||||||
@ -111,10 +127,10 @@ impl<'l> StyledInput<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct StyledInputBuilder<'l> {
|
pub struct StyledInputBuilder<'l, 'm: 'l> {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
valid: Option<bool>,
|
valid: Option<bool>,
|
||||||
value: Option<String>,
|
value: Option<&'m str>,
|
||||||
input_type: Option<&'l str>,
|
input_type: Option<&'l str>,
|
||||||
input_class_list: Vec<&'l str>,
|
input_class_list: Vec<&'l str>,
|
||||||
wrapper_class_list: Vec<&'l str>,
|
wrapper_class_list: Vec<&'l str>,
|
||||||
@ -123,7 +139,7 @@ pub struct StyledInputBuilder<'l> {
|
|||||||
input_handlers: Vec<EventHandler<Msg>>,
|
input_handlers: Vec<EventHandler<Msg>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> StyledInputBuilder<'l> {
|
impl<'l, 'm: 'l> StyledInputBuilder<'l, 'm> {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn icon(mut self, icon: Icon) -> Self {
|
pub fn icon(mut self, icon: Icon) -> Self {
|
||||||
self.icon = Some(icon);
|
self.icon = Some(icon);
|
||||||
@ -137,18 +153,27 @@ impl<'l> StyledInputBuilder<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn value<S>(mut self, v: S) -> Self
|
pub fn value(mut self, v: &'m str) -> Self {
|
||||||
where
|
self.value = Some(v);
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
self.value = Some(v.into());
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn state(self, state: &StyledInputState) -> Self {
|
pub fn state(self, state: &'m StyledInputState) -> Self {
|
||||||
self.value(state.value.as_str())
|
self.value(&state.value.as_str()).valid(
|
||||||
.valid(!state.touched || !state.value.is_empty())
|
match (
|
||||||
|
state.touched,
|
||||||
|
state.value.as_str(),
|
||||||
|
state.min.as_ref(),
|
||||||
|
state.max.as_ref(),
|
||||||
|
) {
|
||||||
|
(false, ..) => true,
|
||||||
|
(_, s, None, None) => !s.is_empty(),
|
||||||
|
(_, s, Some(min), None) => s.len() >= *min,
|
||||||
|
(_, s, None, Some(max)) => s.len() <= *max,
|
||||||
|
(_, s, Some(min), Some(max)) => s.len() >= *min && s.len() <= *max,
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -182,7 +207,7 @@ impl<'l> StyledInputBuilder<'l> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn build(self, id: FieldId) -> StyledInput<'l> {
|
pub fn build(self, id: FieldId) -> StyledInput<'l, 'm> {
|
||||||
StyledInput {
|
StyledInput {
|
||||||
id,
|
id,
|
||||||
icon: self.icon,
|
icon: self.icon,
|
||||||
@ -198,7 +223,7 @@ impl<'l> StyledInputBuilder<'l> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'l> ToNode for StyledInput<'l> {
|
impl<'l, 'm: 'l> ToNode for StyledInput<'l, 'm> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn into_node(self) -> Node<Msg> {
|
fn into_node(self) -> Node<Msg> {
|
||||||
render(self)
|
render(self)
|
||||||
@ -212,70 +237,50 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
|||||||
valid,
|
valid,
|
||||||
value,
|
value,
|
||||||
input_type,
|
input_type,
|
||||||
mut input_class_list,
|
input_class_list,
|
||||||
mut wrapper_class_list,
|
wrapper_class_list,
|
||||||
variant,
|
variant,
|
||||||
auto_focus,
|
auto_focus,
|
||||||
input_handlers,
|
input_handlers,
|
||||||
} = values;
|
} = values;
|
||||||
|
|
||||||
wrapper_class_list.push(variant.to_str());
|
let icon_node = icon
|
||||||
if !valid {
|
|
||||||
wrapper_class_list.push("invalid");
|
|
||||||
}
|
|
||||||
|
|
||||||
input_class_list.push(variant.to_str());
|
|
||||||
if icon.is_some() {
|
|
||||||
input_class_list.push("withIcon");
|
|
||||||
}
|
|
||||||
|
|
||||||
let icon = icon
|
|
||||||
.map(|icon| StyledIcon::build(icon).build().into_node())
|
.map(|icon| StyledIcon::build(icon).build().into_node())
|
||||||
.unwrap_or(Node::Empty);
|
.unwrap_or(Node::Empty);
|
||||||
|
|
||||||
let on_input = {
|
let on_change = {
|
||||||
let field_id = id.clone();
|
let field_id = id.clone();
|
||||||
ev(Ev::Input, move |event| {
|
ev(Ev::Change, move |event| {
|
||||||
event.stop_propagation();
|
event.stop_propagation();
|
||||||
let target = event.target().unwrap();
|
let target = event.target().unwrap();
|
||||||
let input = seed::to_input(&target);
|
Msg::StrInputChanged(field_id, seed::to_input(&target).value())
|
||||||
let value = input.value();
|
|
||||||
Msg::StrInputChanged(field_id, value)
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
let on_keyup = ev(Ev::KeyUp, move |event| {
|
|
||||||
event.stop_propagation();
|
|
||||||
None as Option<Msg>
|
|
||||||
});
|
|
||||||
let on_click = ev(Ev::Click, move |event| {
|
|
||||||
event.stop_propagation();
|
|
||||||
None as Option<Msg>
|
|
||||||
});
|
|
||||||
|
|
||||||
div![
|
div![
|
||||||
C!["styledInput"],
|
C!["styledInput"],
|
||||||
|
C![variant.to_str()],
|
||||||
|
if !valid { Some(C!["invalid"]) } else { None },
|
||||||
attrs!(
|
attrs!(
|
||||||
At::Class => wrapper_class_list.join(" "),
|
"class" => format!("{} {}", id, wrapper_class_list.join(" ")),
|
||||||
At::Class => format!("{}", id),
|
|
||||||
),
|
),
|
||||||
icon,
|
icon_node,
|
||||||
on_click,
|
|
||||||
on_keyup,
|
|
||||||
seed::input![
|
seed::input![
|
||||||
C!["inputElement"],
|
C!["inputElement"],
|
||||||
|
icon.as_ref().map(|_| C!["withIcon"]),
|
||||||
|
C![variant.to_str()],
|
||||||
attrs![
|
attrs![
|
||||||
At::Id => format!("{}", id),
|
"id" => format!("{}", id),
|
||||||
At::Class => input_class_list.join(" "),
|
At::Class => input_class_list.join(" "),
|
||||||
At::Value => value.unwrap_or_default(),
|
"value" => value.unwrap_or_default(),
|
||||||
At::Type => input_type.unwrap_or("text"),
|
"type" => input_type.unwrap_or("text"),
|
||||||
|
|
||||||
],
|
],
|
||||||
if auto_focus {
|
if auto_focus {
|
||||||
vec![attrs![At::AutoFocus => true]]
|
vec![attrs![At::AutoFocus => true]]
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
},
|
},
|
||||||
on_input,
|
on_change,
|
||||||
input_handlers,
|
input_handlers,
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
@ -15,67 +15,88 @@ pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
|||||||
project_page.issue_drag.drag(issue_id);
|
project_page.issue_drag.drag(issue_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
pub fn exchange_position(below_id: IssueId, model: &mut Model) {
|
||||||
let project_page = match &mut model.page_content {
|
let project_page = match &mut model.page_content {
|
||||||
PageContent::Project(project_page) => project_page,
|
PageContent::Project(project_page) => project_page,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
if project_page.issue_drag.dragged_or_last(issue_bellow_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
_ => return error!("Nothing is dragged"),
|
_ => return error!("Nothing is dragged"),
|
||||||
};
|
};
|
||||||
|
if below_id == dragged_id {
|
||||||
let mut below = None;
|
|
||||||
let mut dragged = None;
|
|
||||||
let mut issues = vec![];
|
|
||||||
std::mem::swap(&mut issues, &mut model.issues);
|
|
||||||
|
|
||||||
for issue in issues.into_iter() {
|
|
||||||
match issue.id {
|
|
||||||
id if id == issue_bellow_id => below = Some(issue),
|
|
||||||
id if id == dragged_id => dragged = Some(issue),
|
|
||||||
_ => model.issues.push(issue),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut below = match below {
|
|
||||||
Some(below) => below,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
let mut dragged = match dragged {
|
|
||||||
Some(issue) => issue,
|
|
||||||
_ => {
|
|
||||||
model.issues.push(below);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
let below_idx = model
|
||||||
if dragged.issue_status_id != below.issue_status_id {
|
|
||||||
let mut issues = vec![];
|
|
||||||
std::mem::swap(&mut issues, &mut model.issues);
|
|
||||||
for mut c in issues.into_iter() {
|
|
||||||
if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position {
|
|
||||||
c.list_position += 1;
|
|
||||||
project_page.issue_drag.mark_dirty(c.id);
|
|
||||||
}
|
|
||||||
model.issues.push(c);
|
|
||||||
}
|
|
||||||
dragged.list_position = below.list_position + 1;
|
|
||||||
dragged.issue_status_id = below.issue_status_id;
|
|
||||||
}
|
|
||||||
std::mem::swap(&mut dragged.list_position, &mut below.list_position);
|
|
||||||
|
|
||||||
project_page.issue_drag.mark_dirty(dragged.id);
|
|
||||||
project_page.issue_drag.mark_dirty(below.id);
|
|
||||||
|
|
||||||
model.issues.push(below);
|
|
||||||
model.issues.push(dragged);
|
|
||||||
model
|
|
||||||
.issues
|
.issues
|
||||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
.iter()
|
||||||
project_page.issue_drag.last_id = Some(issue_bellow_id);
|
.position(|issue| issue.id == below_id)
|
||||||
|
.unwrap_or(model.issues.len());
|
||||||
|
let dragged = model
|
||||||
|
.issues
|
||||||
|
.iter()
|
||||||
|
.position(|issue| issue.id == dragged_id)
|
||||||
|
.map(|idx| model.issues.remove(idx))
|
||||||
|
.unwrap();
|
||||||
|
let epic_id = dragged.epic_id;
|
||||||
|
model.issues.insert(below_idx, dragged);
|
||||||
|
let changed: Vec<(IssueId, i32)> = model
|
||||||
|
.issues
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|issue| issue.epic_id == epic_id)
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, issue)| {
|
||||||
|
issue.list_position = idx as i32;
|
||||||
|
(issue.id, issue.list_position)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for (id, pos) in changed {
|
||||||
|
if let Some(iss) = model.issues_by_id.get_mut(&id) {
|
||||||
|
iss.list_position = pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let dragged_pos = match model.issues_by_id.get(&dragged_id) {
|
||||||
|
// Some(i) => i.list_position,
|
||||||
|
// _ => return,
|
||||||
|
// };
|
||||||
|
// let below_pos = match model.issues_by_id.get(&below_id) {
|
||||||
|
// Some(i) => i.list_position,
|
||||||
|
// _ => return,
|
||||||
|
// };
|
||||||
|
// use seed::*;
|
||||||
|
// log!(format!(
|
||||||
|
// "exchange dragged {} {} below {} {}",
|
||||||
|
// dragged_id, dragged_pos, below_id, below_pos
|
||||||
|
// ));
|
||||||
|
// for issue in model.issues_by_id.values_mut() {
|
||||||
|
// if issue.id == below_id {
|
||||||
|
// issue.list_position = dragged_pos;
|
||||||
|
// } else if issue.id == dragged_id {
|
||||||
|
// issue.list_position = below_pos;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for issue in model.issues.iter_mut() {
|
||||||
|
// if issue.id == below_id {
|
||||||
|
// issue.list_position = dragged_pos;
|
||||||
|
// } else if issue.id == dragged_id {
|
||||||
|
// issue.list_position = below_pos;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// model
|
||||||
|
// .issues
|
||||||
|
// .sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||||
|
if let PageContent::Project(project_page) = &mut model.page_content {
|
||||||
|
project_page.rebuild_visible(
|
||||||
|
&model.epics,
|
||||||
|
&model.issue_statuses,
|
||||||
|
&model.issues,
|
||||||
|
&model.user,
|
||||||
|
);
|
||||||
|
project_page.issue_drag.mark_dirty(dragged_id);
|
||||||
|
project_page.issue_drag.mark_dirty(below_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
pub fn sync(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
|
@ -3,15 +3,24 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
pub concurrency: usize,
|
pub concurrency: usize,
|
||||||
|
#[serde(default = "Configuration::default_theme")]
|
||||||
|
pub theme: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Configuration {
|
impl Default for Configuration {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { concurrency: 2 }
|
Self {
|
||||||
|
concurrency: 2,
|
||||||
|
theme: Self::default_theme(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Configuration {
|
impl Configuration {
|
||||||
crate::rw!("highlight.toml");
|
crate::rw!("highlight.toml");
|
||||||
|
|
||||||
|
fn default_theme() -> String {
|
||||||
|
"Github".to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
crate::read!(Configuration);
|
crate::read!(Configuration);
|
||||||
|
Loading…
Reference in New Issue
Block a user