Save estimated time. Change path on drop add issue. Better support for multi-page
This commit is contained in:
parent
ff5a43efc6
commit
cb6f41fe0a
@ -177,6 +177,9 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::ChangePage(page) => {
|
||||
model.page = page.clone();
|
||||
}
|
||||
Msg::ToggleAboutTooltip => {
|
||||
model.about_tooltip_visible = !model.about_tooltip_visible;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
crate::ws::update(&msg, model, orders);
|
||||
|
@ -109,6 +109,19 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody), text) => {
|
||||
modal.comment_form.body = text.clone();
|
||||
}
|
||||
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate), value) => {
|
||||
match value.parse::<i32>() {
|
||||
Ok(n) if !value.is_empty() => {
|
||||
modal.payload.estimate = Some(n);
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
_ if value.is_empty() => {
|
||||
modal.payload.estimate = None;
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Msg::SaveComment => {
|
||||
let msg = match modal.comment_form.id {
|
||||
Some(id) => WsMsg::UpdateComment(UpdateCommentPayload {
|
||||
@ -564,6 +577,14 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
|
||||
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
|
||||
.valid(true)
|
||||
.value(
|
||||
payload
|
||||
.estimate
|
||||
.as_ref()
|
||||
.map(|n| n.to_string())
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.build()
|
||||
.into_node();
|
||||
let estimate_field = StyledField::build()
|
||||
|
@ -16,7 +16,7 @@ mod issue_details;
|
||||
pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
|
||||
match msg {
|
||||
Msg::ModalDropped => match model.modals.pop() {
|
||||
Some(ModalType::EditIssue(..)) => {
|
||||
Some(ModalType::EditIssue(..)) | Some(ModalType::AddIssue(..)) => {
|
||||
seed::push_route(vec!["board"]);
|
||||
orders.send_msg(Msg::ChangePage(Page::Project));
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ impl EditIssueModal {
|
||||
priority_state: StyledSelectState::new(FieldId::EditIssueModal(
|
||||
EditIssueModalFieldId::Priority,
|
||||
)),
|
||||
description_editor_mode: Mode::Editor,
|
||||
description_editor_mode: Mode::View,
|
||||
comment_form: CommentForm {
|
||||
id: None,
|
||||
body: String::new(),
|
||||
@ -178,9 +178,8 @@ pub struct UpdateProjectForm {
|
||||
pub fields: UpdateProjectPayload,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProjectPage {
|
||||
pub about_tooltip_visible: bool,
|
||||
pub text_filter: String,
|
||||
pub active_avatar_filters: Vec<UserId>,
|
||||
pub only_my_filter: bool,
|
||||
@ -190,10 +189,23 @@ pub struct ProjectPage {
|
||||
pub dirty_issues: Vec<IssueId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectSettingsPage {
|
||||
pub payload: UpdateProjectPayload,
|
||||
pub project_type_state: StyledSelectState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PageContent {
|
||||
Project(ProjectPage),
|
||||
ProjectSettings(ProjectSettingsPage),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
pub host_url: String,
|
||||
pub access_token: Option<Uuid>,
|
||||
pub about_tooltip_visible: bool,
|
||||
|
||||
// mapped
|
||||
pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>,
|
||||
@ -208,7 +220,7 @@ pub struct Model {
|
||||
|
||||
// pages
|
||||
pub page: Page,
|
||||
pub project_page: ProjectPage,
|
||||
pub page_content: PageContent,
|
||||
|
||||
pub project: Option<Project>,
|
||||
pub user: Option<User>,
|
||||
@ -231,19 +243,11 @@ impl Default for Model {
|
||||
comments_by_project_id: Default::default(),
|
||||
page: Page::Project,
|
||||
host_url,
|
||||
project_page: ProjectPage {
|
||||
about_tooltip_visible: false,
|
||||
text_filter: "".to_string(),
|
||||
active_avatar_filters: vec![],
|
||||
only_my_filter: false,
|
||||
recently_updated_filter: false,
|
||||
dragged_issue_id: None,
|
||||
last_drag_exchange_id: None,
|
||||
dirty_issues: vec![],
|
||||
},
|
||||
page_content: PageContent::Project(ProjectPage::default()),
|
||||
modals: vec![],
|
||||
project: None,
|
||||
comments: vec![],
|
||||
about_tooltip_visible: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ use seed::{prelude::*, *};
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::{ModalType, Model, Page};
|
||||
use crate::model::{ModalType, Model, Page, PageContent};
|
||||
use crate::shared::styled_avatar::StyledAvatar;
|
||||
use crate::shared::styled_button::StyledButton;
|
||||
use crate::shared::styled_icon::{Icon, StyledIcon};
|
||||
@ -14,6 +14,11 @@ use crate::shared::{drag_ev, inner_layout, ToNode};
|
||||
use crate::{EditIssueModalFieldId, FieldId, Msg};
|
||||
|
||||
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
match msg {
|
||||
Msg::ChangePage(Page::Project)
|
||||
| Msg::ChangePage(Page::AddIssue)
|
||||
@ -44,9 +49,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
}
|
||||
orders.skip().send_msg(Msg::ModalDropped);
|
||||
}
|
||||
Msg::ToggleAboutTooltip => {
|
||||
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
|
||||
}
|
||||
Msg::StyledSelectChanged(
|
||||
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
|
||||
StyledSelectChange::Text(text),
|
||||
@ -64,12 +66,11 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
}
|
||||
}
|
||||
Msg::InputChanged(FieldId::TextFilterBoard, text) => {
|
||||
model.project_page.text_filter = text;
|
||||
project_page.text_filter = text;
|
||||
}
|
||||
Msg::ProjectAvatarFilterChanged(user_id, active) => match active {
|
||||
true => {
|
||||
model.project_page.active_avatar_filters = model
|
||||
.project_page
|
||||
project_page.active_avatar_filters = project_page
|
||||
.active_avatar_filters
|
||||
.iter()
|
||||
.filter(|id| **id != user_id)
|
||||
@ -77,25 +78,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
|
||||
.collect();
|
||||
}
|
||||
false => {
|
||||
model.project_page.active_avatar_filters.push(user_id);
|
||||
project_page.active_avatar_filters.push(user_id);
|
||||
}
|
||||
},
|
||||
Msg::ProjectToggleOnlyMy => {
|
||||
model.project_page.only_my_filter = !model.project_page.only_my_filter;
|
||||
project_page.only_my_filter = !project_page.only_my_filter;
|
||||
}
|
||||
Msg::ProjectToggleRecentlyUpdated => {
|
||||
model.project_page.recently_updated_filter =
|
||||
!model.project_page.recently_updated_filter;
|
||||
project_page.recently_updated_filter = !project_page.recently_updated_filter;
|
||||
}
|
||||
Msg::ProjectClearFilters => {
|
||||
let pp = &mut model.project_page;
|
||||
pp.active_avatar_filters = vec![];
|
||||
pp.recently_updated_filter = false;
|
||||
pp.only_my_filter = false;
|
||||
project_page.active_avatar_filters = vec![];
|
||||
project_page.recently_updated_filter = false;
|
||||
project_page.only_my_filter = false;
|
||||
}
|
||||
Msg::IssueDragStarted(issue_id) => crate::ws::issue::drag_started(issue_id, model),
|
||||
Msg::IssueDragStopped(_) => {
|
||||
model.project_page.dragged_issue_id = None;
|
||||
project_page.dragged_issue_id = None;
|
||||
}
|
||||
Msg::ExchangePosition(issue_bellow_id) => {
|
||||
crate::ws::issue::exchange_position(issue_bellow_id, model)
|
||||
@ -165,11 +164,14 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
.build()
|
||||
.into_node();
|
||||
|
||||
let project_page = &model.project_page;
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(page_content) => page_content,
|
||||
_ => return empty![],
|
||||
};
|
||||
|
||||
let only_my = StyledButton::build()
|
||||
.empty()
|
||||
.active(model.project_page.only_my_filter)
|
||||
.active(project_page.only_my_filter)
|
||||
.text("Only My Issues")
|
||||
.on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy))
|
||||
.build()
|
||||
@ -205,7 +207,11 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
|
||||
}
|
||||
|
||||
fn avatars_filters(model: &Model) -> Node<Msg> {
|
||||
let active_avatar_filters = &model.project_page.active_avatar_filters;
|
||||
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()
|
||||
@ -242,7 +248,11 @@ fn project_board_lists(model: &Model) -> Node<Msg> {
|
||||
}
|
||||
|
||||
fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg> {
|
||||
let ids = if model.project_page.recently_updated_filter {
|
||||
let project_page = match &model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return empty![],
|
||||
};
|
||||
let ids = if project_page.recently_updated_filter {
|
||||
let mut v: Vec<(IssueId, NaiveDateTime)> = model
|
||||
.issues
|
||||
.iter()
|
||||
@ -261,8 +271,8 @@ fn project_issue_list(model: &Model, status: jirs_data::IssueStatus) -> Node<Msg
|
||||
.iter()
|
||||
.filter(|issue| {
|
||||
issue_filter_status(issue, &status)
|
||||
&& issue_filter_with_text(issue, model.project_page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, model.project_page.only_my_filter, &model.user)
|
||||
&& issue_filter_with_text(issue, project_page.text_filter.as_str())
|
||||
&& issue_filter_with_only_my(issue, project_page.only_my_filter, &model.user)
|
||||
&& issue_filter_with_only_recent(issue, &ids)
|
||||
})
|
||||
.map(|issue| project_issue(model, issue))
|
||||
|
@ -4,7 +4,6 @@ use crate::shared::styled_editor::StyledEditor;
|
||||
use crate::shared::styled_field::StyledField;
|
||||
use crate::shared::styled_form::StyledForm;
|
||||
use crate::shared::styled_input::StyledInput;
|
||||
use crate::shared::styled_select::StyledSelect;
|
||||
use crate::shared::{inner_layout, ToNode};
|
||||
use crate::{model, FieldId, Msg, ProjectSettingsFieldId};
|
||||
|
||||
|
@ -61,14 +61,15 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
|
||||
.into_node();
|
||||
|
||||
styled_tooltip::StyledTooltip {
|
||||
visible: model.project_page.about_tooltip_visible,
|
||||
visible: model.about_tooltip_visible,
|
||||
class_name: "aboutTooltipPopup".to_string(),
|
||||
children: div![
|
||||
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
|
||||
attrs![At::Class => "feedbackDropdown"],
|
||||
div![
|
||||
attrs![At::Class => "feedbackImageCont"],
|
||||
img![attrs![At::Src => "/feedback.png", At::Class => "feedbackImage"]]
|
||||
img![attrs![At::Src => "/feedback.png"]],
|
||||
class!["feedbackImage"],
|
||||
],
|
||||
div![
|
||||
attrs![At::Class => "feedbackParagraph"],
|
||||
|
@ -9,6 +9,7 @@ pub struct StyledInput {
|
||||
id: FieldId,
|
||||
icon: Option<Icon>,
|
||||
valid: bool,
|
||||
value: Option<String>,
|
||||
}
|
||||
|
||||
impl StyledInput {
|
||||
@ -17,6 +18,7 @@ impl StyledInput {
|
||||
id,
|
||||
icon: None,
|
||||
valid: None,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -26,6 +28,7 @@ pub struct StyledInputBuilder {
|
||||
id: FieldId,
|
||||
icon: Option<Icon>,
|
||||
valid: Option<bool>,
|
||||
value: Option<String>,
|
||||
}
|
||||
|
||||
impl StyledInputBuilder {
|
||||
@ -39,11 +42,20 @@ impl StyledInputBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value<S>(mut self, v: S) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.value = Some(v.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> StyledInput {
|
||||
StyledInput {
|
||||
id: self.id,
|
||||
icon: self.icon,
|
||||
valid: self.valid.unwrap_or_default(),
|
||||
value: self.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,7 +67,12 @@ impl ToNode for StyledInput {
|
||||
}
|
||||
|
||||
pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
let StyledInput { id, icon, valid } = values;
|
||||
let StyledInput {
|
||||
id,
|
||||
icon,
|
||||
valid,
|
||||
value,
|
||||
} = values;
|
||||
|
||||
let mut wrapper_class_list = vec!["styledInput".to_string(), format!("{}", id)];
|
||||
if !valid {
|
||||
@ -74,7 +91,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
|
||||
let mut handlers = vec![];
|
||||
|
||||
handlers.push(input_ev(Ev::KeyUp, move |value| {
|
||||
handlers.push(input_ev(Ev::Change, move |value| {
|
||||
Msg::InputChanged(id, value)
|
||||
}));
|
||||
|
||||
@ -85,6 +102,12 @@ pub fn render(values: StyledInput) -> Node<Msg> {
|
||||
ev.stop_propagation();
|
||||
Msg::NoOp
|
||||
}),
|
||||
seed::input![attrs![At::Class => input_class_list.join(" ")], handlers],
|
||||
seed::input![
|
||||
attrs![
|
||||
At::Class => input_class_list.join(" "),
|
||||
At::Value => value.unwrap_or_default(),
|
||||
],
|
||||
handlers
|
||||
],
|
||||
]
|
||||
}
|
||||
|
@ -1,21 +1,29 @@
|
||||
use jirs_data::*;
|
||||
|
||||
use crate::api::send_ws_msg;
|
||||
use crate::model::Model;
|
||||
use crate::model::{Model, PageContent, ProjectPage};
|
||||
|
||||
pub fn drag_started(issue_id: IssueId, model: &mut Model) {
|
||||
model.project_page.dragged_issue_id = Some(issue_id);
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
project_page.dragged_issue_id = Some(issue_id);
|
||||
|
||||
mark_dirty(issue_id, model);
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
|
||||
pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
if model.project_page.dragged_issue_id == Some(issue_bellow_id)
|
||||
|| model.project_page.last_drag_exchange_id == Some(issue_bellow_id)
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
if project_page.dragged_issue_id == Some(issue_bellow_id)
|
||||
|| project_page.last_drag_exchange_id == Some(issue_bellow_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let dragged_id = match model.project_page.dragged_issue_id {
|
||||
let dragged_id = match project_page.dragged_issue_id {
|
||||
Some(id) => id,
|
||||
_ => return,
|
||||
};
|
||||
@ -50,7 +58,7 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
for mut c in issues.into_iter() {
|
||||
if c.status == below.status && c.list_position > below.list_position {
|
||||
c.list_position += 1;
|
||||
mark_dirty(c.id, model);
|
||||
mark_dirty(c.id, project_page);
|
||||
}
|
||||
model.issues.push(c);
|
||||
}
|
||||
@ -59,20 +67,25 @@ pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) {
|
||||
}
|
||||
std::mem::swap(&mut dragged.list_position, &mut below.list_position);
|
||||
|
||||
mark_dirty(dragged.id, model);
|
||||
mark_dirty(below.id, model);
|
||||
mark_dirty(dragged.id, project_page);
|
||||
mark_dirty(below.id, project_page);
|
||||
|
||||
model.issues.push(below);
|
||||
model.issues.push(dragged);
|
||||
model
|
||||
.issues
|
||||
.sort_by(|a, b| a.list_position.cmp(&b.list_position));
|
||||
model.project_page.last_drag_exchange_id = Some(issue_bellow_id);
|
||||
project_page.last_drag_exchange_id = Some(issue_bellow_id);
|
||||
}
|
||||
|
||||
pub fn dropped(_status: IssueStatus, model: &mut Model) {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for issue in model.issues.iter() {
|
||||
if !model.project_page.dirty_issues.contains(&issue.id) {
|
||||
if !project_page.dirty_issues.contains(&issue.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -91,14 +104,19 @@ pub fn dropped(_status: IssueStatus, model: &mut Model) {
|
||||
reporter_id: issue.reporter_id,
|
||||
user_ids: issue.user_ids.clone(),
|
||||
};
|
||||
model.project_page.dragged_issue_id = None;
|
||||
project_page.dragged_issue_id = None;
|
||||
send_ws_msg(WsMsg::IssueUpdateRequest(issue.id, payload));
|
||||
model.project_page.last_drag_exchange_id = None;
|
||||
project_page.last_drag_exchange_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_status(status: IssueStatus, model: &mut Model) {
|
||||
let issue_id = match model.project_page.dragged_issue_id.as_ref().cloned() {
|
||||
let project_page = match &mut model.page_content {
|
||||
PageContent::Project(project_page) => project_page,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let issue_id = match project_page.dragged_issue_id.as_ref().cloned() {
|
||||
Some(issue_id) => issue_id,
|
||||
_ => return,
|
||||
};
|
||||
@ -136,12 +154,12 @@ pub fn change_status(status: IssueStatus, model: &mut Model) {
|
||||
issue.list_position = pos + 1;
|
||||
model.issues.push(issue);
|
||||
|
||||
mark_dirty(issue_id, model);
|
||||
mark_dirty(issue_id, project_page);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mark_dirty(id: IssueId, model: &mut Model) {
|
||||
if !model.project_page.dirty_issues.contains(&id) {
|
||||
model.project_page.dirty_issues.push(id);
|
||||
fn mark_dirty(id: IssueId, project_page: &mut ProjectPage) {
|
||||
if !project_page.dirty_issues.contains(&id) {
|
||||
project_page.dirty_issues.push(id);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user