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",
|
||||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"strsim 0.8.0",
|
||||
"textwrap",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
@ -1731,6 +1731,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde",
|
||||
"simsearch",
|
||||
"syntect",
|
||||
"toml",
|
||||
]
|
||||
@ -3394,6 +3395,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "slab"
|
||||
version = "0.4.2"
|
||||
@ -3487,6 +3498,12 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.0"
|
||||
@ -3803,6 +3820,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triple_accel"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c"
|
||||
|
||||
[[package]]
|
||||
name = "trust-dns-proto"
|
||||
version = "0.18.0-alpha.2"
|
||||
|
@ -17,6 +17,7 @@ serde = "*"
|
||||
bincode = "*"
|
||||
toml = { version = "*" }
|
||||
|
||||
simsearch = { version = "0.2" }
|
||||
actix = { version = "0.10.0" }
|
||||
|
||||
flate2 = { version = "*" }
|
||||
|
@ -1,6 +1,7 @@
|
||||
use {
|
||||
actix::{Actor, Handler, SyncContext},
|
||||
jirs_data::HighlightedCode,
|
||||
simsearch::SimSearch,
|
||||
std::sync::Arc,
|
||||
syntect::{
|
||||
easy::HighlightLines,
|
||||
@ -14,6 +15,20 @@ mod load;
|
||||
lazy_static::lazy_static! {
|
||||
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 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)]
|
||||
@ -23,23 +38,53 @@ pub enum HighlightError {
|
||||
ResultUnserializable,
|
||||
}
|
||||
|
||||
fn hi<'l>(code: &'l str, lang: &'l str) -> Result<Vec<(Style, &'l str)>, HighlightError> {
|
||||
let set = SYNTAX_SET
|
||||
.as_ref()
|
||||
.find_syntax_by_name(lang)
|
||||
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||
let theme: &syntect::highlighting::Theme = THEME_SET
|
||||
.as_ref()
|
||||
.themes
|
||||
.get("GitHub")
|
||||
.ok_or_else(|| HighlightError::UnknownTheme)?;
|
||||
|
||||
let mut hi = HighlightLines::new(set, theme);
|
||||
Ok(hi.highlight(code, SYNTAX_SET.as_ref()))
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightActor {
|
||||
theme_set: Arc<ThemeSet>,
|
||||
syntax_set: Arc<SyntaxSet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HighlightActor {}
|
||||
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()
|
||||
.find_syntax_by_name(lang)
|
||||
.ok_or_else(|| HighlightError::UnknownLanguage)?;
|
||||
let theme: &syntect::highlighting::Theme = self
|
||||
.theme_set
|
||||
.as_ref()
|
||||
.themes
|
||||
.get("GitHub")
|
||||
.ok_or_else(|| HighlightError::UnknownTheme)?;
|
||||
|
||||
let mut hi = HighlightLines::new(set, theme);
|
||||
Ok(hi.highlight(code, self.syntax_set.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for HighlightActor {
|
||||
type Context = SyncContext<Self>;
|
||||
@ -56,7 +101,7 @@ impl Handler<HighlightCode> for HighlightActor {
|
||||
type Result = Result<HighlightedCode, HighlightError>;
|
||||
|
||||
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 {
|
||||
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 jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg};
|
||||
|
||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
||||
use {
|
||||
crate::{WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadIssueComments {
|
||||
pub issue_id: IssueId,
|
||||
|
@ -1,8 +1,8 @@
|
||||
use futures::executor::block_on;
|
||||
|
||||
use jirs_data::{EpicId, NameString, UserProject, WsMsg};
|
||||
|
||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
||||
use {
|
||||
crate::{WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{EpicId, NameString, UserProject, WsMsg},
|
||||
};
|
||||
|
||||
pub struct LoadEpics;
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
use futures::executor::block_on;
|
||||
|
||||
use jirs_data::WsMsg;
|
||||
use jirs_data::{Code, Lang};
|
||||
|
||||
use crate::{WebSocketActor, WsHandler, WsResult};
|
||||
use {
|
||||
crate::{WebSocketActor, WsHandler, WsResult},
|
||||
futures::executor::block_on,
|
||||
jirs_data::{Code, Lang, WsMsg},
|
||||
};
|
||||
|
||||
pub struct HighlightCode(pub Lang, pub Code);
|
||||
|
||||
|
@ -50,17 +50,13 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
||||
msg.title = Some(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 = {
|
||||
use pulldown_cmark::*;
|
||||
let parser = pulldown_cmark::Parser::new(s.as_str());
|
||||
enum ParseState {
|
||||
Code(highlight_actor::TextHighlightCode),
|
||||
Other,
|
||||
};
|
||||
}
|
||||
let mut state = ParseState::Other;
|
||||
|
||||
let parser = parser.flat_map(|event| match event {
|
||||
|
@ -50,12 +50,21 @@
|
||||
transition: transform 0.1s;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--backgroundMedium);
|
||||
border-color: var(--backgroundLight)
|
||||
}
|
||||
|
||||
#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder.isActive {
|
||||
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 {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
@ -134,6 +143,19 @@
|
||||
#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink {
|
||||
display: block;
|
||||
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 {
|
||||
|
@ -384,3 +384,40 @@ i.styledIcon.double-left:before {
|
||||
i.styledIcon.double-right:before {
|
||||
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
|
||||
StrInputChanged(FieldId, String),
|
||||
|
||||
U32InputChanged(FieldId, u32),
|
||||
FileInputChanged(FieldId, Vec<File>),
|
||||
// Rte(FieldId, RteMsg),
|
||||
|
@ -8,7 +8,7 @@ use {
|
||||
ToNode,
|
||||
},
|
||||
ws::send_ws_msg,
|
||||
FieldChange, FieldId, Msg, WebSocketChanged,
|
||||
FieldChange, FieldId, Msg, OperationKind, ResourceKind,
|
||||
},
|
||||
jirs_data::{TimeTracking, WsMsg},
|
||||
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());
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::ProjectIssuesLoaded(_issues))) => {
|
||||
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => {
|
||||
match model.page {
|
||||
Page::EditIssue(issue_id) if model.modals.is_empty() => {
|
||||
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)) => {
|
||||
push_edit_modal(*issue_id, model, orders);
|
||||
}
|
||||
Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders),
|
||||
|
||||
Msg::ChangePage(Page::AddIssue) => {
|
||||
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)));
|
||||
}
|
||||
Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders),
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
Msg::GlobalKeyDown { key, .. } if key.eq("#") => {
|
||||
@ -123,6 +118,14 @@ pub fn view(model: &model::Model) -> Node<Msg> {
|
||||
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>) {
|
||||
let time_tracking_type = model
|
||||
.project
|
||||
|
@ -3,7 +3,7 @@ use {
|
||||
model::{IssueModal, ModalType},
|
||||
shared::styled_select::StyledSelectChanged,
|
||||
ws::send_ws_msg,
|
||||
FieldId, Msg, WebSocketChanged,
|
||||
FieldId, Msg, OperationKind, ResourceKind,
|
||||
},
|
||||
jirs_data::{IssueFieldId, UserId, WsMsg},
|
||||
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,
|
||||
project_id: modal.project_id.unwrap_or(project_id),
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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))) => {
|
||||
model.issues.push(issue.clone());
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::EpicCreated(_))) => {
|
||||
Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleCreated, _) => {
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ pub struct Model {
|
||||
pub time_remaining: StyledInputState,
|
||||
pub time_remaining_select: StyledSelectState,
|
||||
|
||||
pub title_state: StyledInputState,
|
||||
pub description_state: StyledEditorState,
|
||||
|
||||
// comments
|
||||
@ -107,6 +108,11 @@ impl Model {
|
||||
.map(|n| vec![n as u32])
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
title_state: StyledInputState::new(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||
issue.title.clone(),
|
||||
)
|
||||
.with_min(Some(3)),
|
||||
description_state: StyledEditorState::new(
|
||||
Mode::View,
|
||||
issue.description_text.as_deref().unwrap_or(""),
|
||||
@ -159,5 +165,7 @@ impl IssueModal for Model {
|
||||
self.time_remaining.update(msg);
|
||||
self.time_remaining_select.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();
|
||||
}
|
||||
}
|
||||
|
||||
// type
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
@ -40,6 +42,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders,
|
||||
);
|
||||
}
|
||||
|
||||
// issue status id
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
@ -55,6 +59,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders,
|
||||
);
|
||||
}
|
||||
|
||||
// reporter id
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
@ -70,6 +76,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders,
|
||||
);
|
||||
}
|
||||
|
||||
// assignees
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)),
|
||||
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)),
|
||||
StyledSelectChanged::RemoveMulti(value),
|
||||
) => {
|
||||
let mut old = vec![];
|
||||
std::mem::swap(&mut old, &mut modal.payload.user_ids);
|
||||
let old = std::mem::replace(&mut modal.payload.user_ids, vec![]);
|
||||
let dropped = *value as i32;
|
||||
for id in old.into_iter() {
|
||||
if id != dropped {
|
||||
@ -107,6 +114,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders,
|
||||
);
|
||||
}
|
||||
|
||||
// priority
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||
StyledSelectChanged::Changed(Some(value)),
|
||||
@ -122,6 +131,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
orders,
|
||||
);
|
||||
}
|
||||
|
||||
// Title
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)),
|
||||
value,
|
||||
@ -136,7 +147,10 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
model.ws.as_ref(),
|
||||
orders,
|
||||
);
|
||||
orders.skip();
|
||||
}
|
||||
|
||||
// Description
|
||||
Msg::StrInputChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)),
|
||||
value,
|
||||
|
@ -151,8 +151,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
.add_input_class("issueSummary")
|
||||
.add_wrapper_class("issueSummary")
|
||||
.add_wrapper_class("textarea")
|
||||
.value(payload.title.as_str())
|
||||
.valid(payload.title.len() >= 3)
|
||||
.state(&modal.title_state)
|
||||
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
|
||||
IssueFieldId::Title,
|
||||
)))
|
@ -62,7 +62,7 @@ impl Page {
|
||||
match self {
|
||||
Page::Project => "/board".to_string(),
|
||||
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::SignIn => "/login".to_string(),
|
||||
Page::SignUp => "/register".to_string(),
|
||||
@ -163,6 +163,8 @@ pub struct Model {
|
||||
pub user_projects: Vec<UserProject>,
|
||||
pub projects: Vec<Project>,
|
||||
pub epics: Vec<Epic>,
|
||||
|
||||
pub show_extras: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@ -197,6 +199,7 @@ impl Model {
|
||||
projects: vec![],
|
||||
epics: vec![],
|
||||
issues_by_id: Default::default(),
|
||||
show_extras: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
.add_field(submit_field)
|
||||
.build()
|
||||
.into_node();
|
||||
inner_layout(model, "profile", vec![content])
|
||||
inner_layout(model, "profile", &[content])
|
||||
}
|
||||
|
||||
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 mut issues: Vec<&Issue> = {
|
||||
let mut issues: Vec<&Issue> = issues.iter().collect();
|
||||
if self.recently_updated_filter {
|
||||
let mut m = HashMap::new();
|
||||
let mut sorted = vec![];
|
||||
for issue in issues.iter() {
|
||||
for issue in issues.into_iter() {
|
||||
sorted.push((issue.id, issue.updated_at));
|
||||
m.insert(issue.id, issue);
|
||||
}
|
||||
sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time));
|
||||
sorted
|
||||
issues = sorted
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.flat_map(|(id, _)| m.remove(&id))
|
||||
.collect()
|
||||
};
|
||||
if self.recently_updated_filter {
|
||||
issues = issues[0..10].to_vec()
|
||||
.collect();
|
||||
issues.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
}
|
||||
|
||||
for epic in epics {
|
||||
let mut per_epic_map = EpicIssuePerStatus::default();
|
||||
per_epic_map.epic_name = epic.map(|(_, name)| name).unwrap_or_default().to_string();
|
||||
let mut per_epic_map = EpicIssuePerStatus {
|
||||
epic_name: epic.map(|(_, name)| name).unwrap_or_default().to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (current_status_id, issue_status_name) in statuses.to_owned() {
|
||||
let mut per_status_map = StatusIssueIds::default();
|
||||
per_status_map.status_id = current_status_id;
|
||||
per_status_map.status_name = issue_status_name.to_string();
|
||||
let mut per_status_map = StatusIssueIds {
|
||||
status_id: current_status_id,
|
||||
status_name: issue_status_name.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
for issue in issues.iter() {
|
||||
if issue.epic_id == epic.map(|(id, _)| 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) => {
|
||||
project_page.text_filter = text;
|
||||
project_page.rebuild_visible(
|
||||
&model.epics,
|
||||
&model.issue_statuses,
|
||||
&model.issues,
|
||||
&model.user,
|
||||
);
|
||||
}
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active) => {
|
||||
if active {
|
||||
project_page.active_avatar_filters = project_page
|
||||
.active_avatar_filters
|
||||
.iter()
|
||||
.filter_map(|id| if *id != user_id { Some(*id) } else { None })
|
||||
.collect();
|
||||
project_page.active_avatar_filters =
|
||||
std::mem::replace(&mut project_page.active_avatar_filters, vec![])
|
||||
.into_iter()
|
||||
.filter(|id| *id != user_id)
|
||||
.collect();
|
||||
} else {
|
||||
project_page.active_avatar_filters.push(user_id);
|
||||
}
|
||||
project_page.rebuild_visible(
|
||||
&model.epics,
|
||||
&model.issue_statuses,
|
||||
&model.issues,
|
||||
&model.user,
|
||||
);
|
||||
}
|
||||
Msg::ProjectToggleOnlyMy => {
|
||||
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 => {
|
||||
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 => {
|
||||
project_page.active_avatar_filters = vec![];
|
||||
project_page.recently_updated_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))) => {
|
||||
crate::ws::issue::drag_started(issue_id, model)
|
||||
|
@ -1,36 +1,31 @@
|
||||
use {
|
||||
crate::{
|
||||
model::{Model, Page, PageContent},
|
||||
shared::{
|
||||
inner_layout,
|
||||
styled_avatar::StyledAvatar,
|
||||
styled_button::StyledButton,
|
||||
styled_icon::{Icon, StyledIcon},
|
||||
styled_input::StyledInput,
|
||||
ToNode,
|
||||
},
|
||||
BoardPageChange, FieldId, Msg, PageChanged,
|
||||
model::Model,
|
||||
shared::{inner_layout, styled_button::StyledButton, styled_icon::Icon, ToNode},
|
||||
Msg,
|
||||
},
|
||||
jirs_data::*,
|
||||
seed::{prelude::*, *},
|
||||
};
|
||||
|
||||
mod board;
|
||||
mod filters;
|
||||
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let project_section = vec![
|
||||
let project_section = [
|
||||
breadcrumbs(model),
|
||||
header(),
|
||||
project_board_filters(model),
|
||||
project_board_lists(model),
|
||||
header(model),
|
||||
filters::project_board_filters(model),
|
||||
board::project_board_lists(model),
|
||||
];
|
||||
|
||||
inner_layout(model, "projectPage", project_section)
|
||||
inner_layout(model, "projectPage", &project_section)
|
||||
}
|
||||
|
||||
fn breadcrumbs(model: &Model) -> Node<Msg> {
|
||||
let project_name = model
|
||||
.project
|
||||
.as_ref()
|
||||
.map(|p| p.name.clone())
|
||||
.map(|p| p.name.as_str())
|
||||
.unwrap_or_default();
|
||||
div![
|
||||
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()
|
||||
.secondary()
|
||||
.text("Github Repo")
|
||||
.text("Repository")
|
||||
.icon(Icon::Github)
|
||||
.build()
|
||||
.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()
|
||||
.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
|
||||
|
@ -29,7 +29,7 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
|
||||
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> {
|
||||
|
@ -167,9 +167,5 @@ pub fn view(model: &Model) -> Node<Msg> {
|
||||
ul![C!["invitationsList"], invitations],
|
||||
];
|
||||
|
||||
inner_layout(
|
||||
model,
|
||||
"users",
|
||||
vec![form, users_section, invitations_section],
|
||||
)
|
||||
inner_layout(model, "users", &[form, users_section, invitations_section])
|
||||
}
|
||||
|
@ -8,6 +8,11 @@ pub struct DragState {
|
||||
}
|
||||
|
||||
impl DragState {
|
||||
#[inline]
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
self.dragged_id.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mark_dirty(&mut self, id: i32) {
|
||||
self.dirty.insert(id);
|
||||
|
@ -77,7 +77,7 @@ pub fn divider() -> Node<Msg> {
|
||||
}
|
||||
|
||||
#[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);
|
||||
article![
|
||||
modal_node,
|
||||
|
@ -103,7 +103,10 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
|
||||
C!["bottom"],
|
||||
navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)),
|
||||
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![
|
||||
C!["styledAvatar letter"],
|
||||
class_list,
|
||||
attrs![
|
||||
At::Class => format!("avatarColor{}", index + 1),
|
||||
At::Style => style
|
||||
At::Style => shared_style,
|
||||
At::Title => name
|
||||
],
|
||||
span![letter],
|
||||
on_click,
|
||||
|
@ -32,6 +32,8 @@ pub struct StyledInputState {
|
||||
id: FieldId,
|
||||
touched: bool,
|
||||
pub value: String,
|
||||
pub min: Option<usize>,
|
||||
pub max: Option<usize>,
|
||||
}
|
||||
|
||||
impl StyledInputState {
|
||||
@ -44,9 +46,23 @@ impl StyledInputState {
|
||||
id,
|
||||
touched: false,
|
||||
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]
|
||||
pub fn to_i32(&self) -> Option<i32> {
|
||||
self.value.parse::<i32>().ok()
|
||||
@ -80,11 +96,11 @@ impl StyledInputState {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledInput<'l> {
|
||||
pub struct StyledInput<'l, 'm: 'l> {
|
||||
id: FieldId,
|
||||
icon: Option<Icon>,
|
||||
valid: bool,
|
||||
value: Option<String>,
|
||||
value: Option<&'m str>,
|
||||
input_type: Option<&'l str>,
|
||||
input_class_list: Vec<&'l str>,
|
||||
wrapper_class_list: Vec<&'l str>,
|
||||
@ -93,9 +109,9 @@ pub struct StyledInput<'l> {
|
||||
input_handlers: Vec<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledInput<'l> {
|
||||
impl<'l, 'm: 'l> StyledInput<'l, 'm> {
|
||||
#[inline]
|
||||
pub fn build() -> StyledInputBuilder<'l> {
|
||||
pub fn build() -> StyledInputBuilder<'l, 'm> {
|
||||
StyledInputBuilder {
|
||||
icon: None,
|
||||
valid: None,
|
||||
@ -111,10 +127,10 @@ impl<'l> StyledInput<'l> {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StyledInputBuilder<'l> {
|
||||
pub struct StyledInputBuilder<'l, 'm: 'l> {
|
||||
icon: Option<Icon>,
|
||||
valid: Option<bool>,
|
||||
value: Option<String>,
|
||||
value: Option<&'m str>,
|
||||
input_type: Option<&'l str>,
|
||||
input_class_list: Vec<&'l str>,
|
||||
wrapper_class_list: Vec<&'l str>,
|
||||
@ -123,7 +139,7 @@ pub struct StyledInputBuilder<'l> {
|
||||
input_handlers: Vec<EventHandler<Msg>>,
|
||||
}
|
||||
|
||||
impl<'l> StyledInputBuilder<'l> {
|
||||
impl<'l, 'm: 'l> StyledInputBuilder<'l, 'm> {
|
||||
#[inline]
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
@ -137,18 +153,27 @@ impl<'l> StyledInputBuilder<'l> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn value<S>(mut self, v: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.value = Some(v.into());
|
||||
pub fn value(mut self, v: &'m str) -> Self {
|
||||
self.value = Some(v);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn state(self, state: &StyledInputState) -> Self {
|
||||
self.value(state.value.as_str())
|
||||
.valid(!state.touched || !state.value.is_empty())
|
||||
pub fn state(self, state: &'m StyledInputState) -> Self {
|
||||
self.value(&state.value.as_str()).valid(
|
||||
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]
|
||||
@ -182,7 +207,7 @@ impl<'l> StyledInputBuilder<'l> {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn build(self, id: FieldId) -> StyledInput<'l> {
|
||||
pub fn build(self, id: FieldId) -> StyledInput<'l, 'm> {
|
||||
StyledInput {
|
||||
id,
|
||||
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]
|
||||
fn into_node(self) -> Node<Msg> {
|
||||
render(self)
|
||||
@ -212,70 +237,50 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
valid,
|
||||
value,
|
||||
input_type,
|
||||
mut input_class_list,
|
||||
mut wrapper_class_list,
|
||||
input_class_list,
|
||||
wrapper_class_list,
|
||||
variant,
|
||||
auto_focus,
|
||||
input_handlers,
|
||||
} = values;
|
||||
|
||||
wrapper_class_list.push(variant.to_str());
|
||||
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
|
||||
let icon_node = icon
|
||||
.map(|icon| StyledIcon::build(icon).build().into_node())
|
||||
.unwrap_or(Node::Empty);
|
||||
|
||||
let on_input = {
|
||||
let on_change = {
|
||||
let field_id = id.clone();
|
||||
ev(Ev::Input, move |event| {
|
||||
ev(Ev::Change, move |event| {
|
||||
event.stop_propagation();
|
||||
let target = event.target().unwrap();
|
||||
let input = seed::to_input(&target);
|
||||
let value = input.value();
|
||||
Msg::StrInputChanged(field_id, value)
|
||||
Msg::StrInputChanged(field_id, seed::to_input(&target).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![
|
||||
C!["styledInput"],
|
||||
C![variant.to_str()],
|
||||
if !valid { Some(C!["invalid"]) } else { None },
|
||||
attrs!(
|
||||
At::Class => wrapper_class_list.join(" "),
|
||||
At::Class => format!("{}", id),
|
||||
"class" => format!("{} {}", id, wrapper_class_list.join(" ")),
|
||||
),
|
||||
icon,
|
||||
on_click,
|
||||
on_keyup,
|
||||
icon_node,
|
||||
seed::input![
|
||||
C!["inputElement"],
|
||||
icon.as_ref().map(|_| C!["withIcon"]),
|
||||
C![variant.to_str()],
|
||||
attrs![
|
||||
At::Id => format!("{}", id),
|
||||
"id" => format!("{}", id),
|
||||
At::Class => input_class_list.join(" "),
|
||||
At::Value => value.unwrap_or_default(),
|
||||
At::Type => input_type.unwrap_or("text"),
|
||||
|
||||
"value" => value.unwrap_or_default(),
|
||||
"type" => input_type.unwrap_or("text"),
|
||||
],
|
||||
if auto_focus {
|
||||
vec![attrs![At::AutoFocus => true]]
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
on_input,
|
||||
on_change,
|
||||
input_handlers,
|
||||
],
|
||||
]
|
||||
|
@ -15,67 +15,88 @@ pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
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 {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => 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() {
|
||||
Some(id) => id,
|
||||
_ => return error!("Nothing is dragged"),
|
||||
};
|
||||
|
||||
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),
|
||||
};
|
||||
if below_id == dragged_id {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut below = match below {
|
||||
Some(below) => below,
|
||||
_ => return,
|
||||
};
|
||||
let mut dragged = match dragged {
|
||||
Some(issue) => issue,
|
||||
_ => {
|
||||
model.issues.push(below);
|
||||
return;
|
||||
}
|
||||
};
|
||||
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
|
||||
let below_idx = model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
project_page.issue_drag.last_id = Some(issue_bellow_id);
|
||||
.iter()
|
||||
.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>) {
|
||||
|
@ -3,15 +3,24 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Configuration {
|
||||
pub concurrency: usize,
|
||||
#[serde(default = "Configuration::default_theme")]
|
||||
pub theme: String,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
fn default() -> Self {
|
||||
Self { concurrency: 2 }
|
||||
Self {
|
||||
concurrency: 2,
|
||||
theme: Self::default_theme(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
crate::rw!("highlight.toml");
|
||||
|
||||
fn default_theme() -> String {
|
||||
"Github".to_string()
|
||||
}
|
||||
}
|
||||
crate::read!(Configuration);
|
||||
|
Loading…
Reference in New Issue
Block a user