Support add column and fix delete column

This commit is contained in:
Adrian Wozniak 2020-05-20 10:18:37 +02:00
parent 9a9eff7c19
commit 778f56cf8d
6 changed files with 404 additions and 317 deletions

View File

@ -31,7 +31,7 @@ pub enum UsersPageChange {
#[derive(Clone, Debug, PartialEq)]
pub enum ProjectPageChange {
ResetForm,
SubmitForm,
SubmitProjectSettingsForm,
// dragging
ColumnDragStarted(IssueStatusId),
ColumnDragStopped(IssueStatusId),
@ -41,6 +41,7 @@ pub enum ProjectPageChange {
ColumnDropZone(IssueStatusId),
// edit issue status name
EditIssueStatusName(Option<IssueStatusId>),
SubmitIssueStatusForm,
}
#[derive(Clone, Debug, PartialEq)]

View File

@ -24,7 +24,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
model.ws.as_ref(),
);
}
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDelete(_))) => {
Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueStatusDeleted(_))) => {
orders.skip().send_msg(Msg::ModalDropped);
}
_ => (),

View File

@ -313,6 +313,12 @@ impl ProjectSettingsPage {
),
}
}
pub fn reset(&mut self) {
self.edit_column_id = None;
self.name.reset();
self.creating_issue_status = false;
}
}
#[derive(Debug, Default)]

View File

@ -24,218 +24,25 @@ use crate::{
model, FieldId, Msg, PageChanged, ProjectFieldId, ProjectPageChange, WebSocketChanged,
};
//######################################
// VIEW
//######################################
static TIME_TRACKING_FIBONACCI: &'static str = "Tracking employees time carries the risk of having them feel like they are being spied on. This is one of the most common fears that employees have when a time tracking system is implemented. No one likes to feel like theyre always being watched.";
static TIME_TRACKING_HOURLY: &'static str = "Employees may feel intimidated by demands to track their time. Or they could feel that theyre constantly being watched and evaluated. And for overly ambitious managers, employee time tracking may open the doors to excessive micromanaging.";
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::ProjectSettings {
return;
}
match msg {
Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model);
}
_ => (),
},
Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model);
if model.user.is_some() {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
}
_ => (),
}
if model.user.is_none() || model.project.is_none() {
return;
}
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return error!("bad content type"),
};
page.project_category_state.update(&msg, orders);
page.time_tracking.update(&msg);
page.name.update(&msg);
match msg {
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
page.payload.name = Some(text);
}
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Url), text) => {
page.payload.url = Some(text);
}
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Description), text) => {
page.payload.description = Some(text);
}
Msg::StyledSelectChanged(
FieldId::ProjectSettings(ProjectFieldId::Category),
StyledSelectChange::Changed(value),
) => {
let category = value.into();
page.payload.category = Some(category);
}
Msg::ModalChanged(TabChanged(
FieldId::ProjectSettings(ProjectFieldId::Description),
mode,
)) => {
page.description_mode = mode;
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm)) => {
send_ws_msg(
WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
description: page.payload.description.clone(),
category: page.payload.category.clone(),
time_tracking: Some(page.time_tracking.value.into()),
}),
model.ws.as_ref(),
);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
issue_status_id,
))) => {
page.column_drag.drag(issue_status_id);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(
_issue_status_id,
))) => page.column_drag.clear_last(),
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnExchangePosition(issue_bellow_id),
)) => exchange_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(
id,
))) => {
if page.edit_column_id.is_some() && id.is_none() {
let old_id = page.edit_column_id.as_ref().cloned();
let name = page.name.value.clone();
if let Some((id, pos)) = model
.issue_statuses
.iter()
.find(|is| Some(is.id) == old_id)
.map(|is| (is.id, is.position))
{
send_ws_msg(
WsMsg::IssueStatusUpdate(id, name.to_string(), pos),
model.ws.as_ref(),
);
}
}
page.name.value = model
.issue_statuses
.iter()
.find_map(|is| {
if Some(is.id) == id {
Some(is.name.clone())
} else {
None
}
})
.unwrap_or_default();
page.edit_column_id = id;
}
_ => (),
}
}
pub fn view(model: &model::Model) -> Node<Msg> {
let page = match &model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return empty![],
};
let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name))
.value(page.payload.name.as_ref().cloned().unwrap_or_default())
.height(39)
.max_height(39)
.disable_auto_resize()
.build()
.into_node();
let name_field = StyledField::build()
.label("Name")
.input(name)
.tip("")
.build()
.into_node();
let name_field = name_field(page);
let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url))
.height(39)
.max_height(39)
.disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default())
.build()
.into_node();
let url_field = StyledField::build()
.label("Url")
.input(url)
.tip("")
.build()
.into_node();
let url_field = url_field(page);
let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description))
.text(
page.payload
.description
.as_ref()
.cloned()
.unwrap_or_default(),
)
.update_on(Ev::Change)
.mode(page.description_mode.clone())
.build()
.into_node();
let description_field = StyledField::build()
.input(description)
.label("Description")
.tip("Describe the project in as much detail as you'd like.")
.build()
.into_node();
let description_field = description_field(page);
let category = StyledSelect::build(FieldId::ProjectSettings(ProjectFieldId::Category))
.opened(page.project_category_state.opened)
.text_filter(page.project_category_state.text_filter.as_str())
.valid(true)
.normal()
.options(
ProjectCategory::ordered()
.into_iter()
.map(|c| c.to_child())
.collect(),
)
.selected(vec![page
.payload
.category
.as_ref()
.cloned()
.unwrap_or_default()
.to_child()])
.build()
.into_node();
let category_field = StyledField::build()
.label("Project Category")
.input(category)
.build()
.into_node();
let category_field = category_field(page);
let time_tracking =
StyledCheckbox::build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking))
@ -259,6 +66,143 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build()
.into_node();
let columns_field = columns_section(model, page);
let save_button = StyledButton::build()
.add_class("actionButton")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitProjectSettingsForm,
))
}))
.text("Save changes")
.build()
.into_node();
let form = StyledForm::build()
.heading("Project Details")
.on_submit(ev(Ev::Submit, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitProjectSettingsForm,
))
}))
.add_field(name_field)
.add_field(url_field)
.add_field(description_field)
.add_field(category_field)
.add_field(time_tracking_field)
.add_field(save_button)
.add_field(columns_field)
.build()
.into_node();
let project_section = vec![div![class!["formContainer"], form]];
inner_layout(
model,
"projectSettings",
project_section,
crate::modal::view(model),
)
}
/// Build project name input with styled field wrapper
fn name_field(page: &Box<ProjectSettingsPage>) -> Node<Msg> {
let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name))
.value(page.payload.name.as_ref().cloned().unwrap_or_default())
.height(39)
.max_height(39)
.disable_auto_resize()
.build()
.into_node();
StyledField::build()
.label("Name")
.input(name)
.tip("")
.build()
.into_node()
}
/// Build project url input with styled field wrapper
fn url_field(page: &Box<ProjectSettingsPage>) -> Node<Msg> {
let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url))
.height(39)
.max_height(39)
.disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default())
.build()
.into_node();
StyledField::build()
.label("Url")
.input(url)
.tip("")
.build()
.into_node()
}
/// Build project description text area with styled field wrapper
fn description_field(page: &Box<ProjectSettingsPage>) -> Node<Msg> {
let description = StyledEditor::build(FieldId::ProjectSettings(ProjectFieldId::Description))
.text(
page.payload
.description
.as_ref()
.cloned()
.unwrap_or_default(),
)
.update_on(Ev::Change)
.mode(page.description_mode.clone())
.build()
.into_node();
StyledField::build()
.input(description)
.label("Description")
.tip("Describe the project in as much detail as you'd like.")
.build()
.into_node()
}
/// Build project category dropdown with styled field wrapper
fn category_field(page: &Box<ProjectSettingsPage>) -> Node<Msg> {
let category = StyledSelect::build(FieldId::ProjectSettings(ProjectFieldId::Category))
.opened(page.project_category_state.opened)
.text_filter(page.project_category_state.text_filter.as_str())
.valid(true)
.normal()
.options(
ProjectCategory::ordered()
.into_iter()
.map(|c| c.to_child())
.collect(),
)
.selected(vec![page
.payload
.category
.as_ref()
.cloned()
.unwrap_or_default()
.to_child()])
.build()
.into_node();
StyledField::build()
.label("Project Category")
.input(category)
.build()
.into_node()
}
fn build_page_content(model: &mut Model) {
let project = match &model.project {
Some(project) => project,
_ => return,
};
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
}
/// Build draggable columns preview with option to remove and add new columns
fn columns_section(model: &Model, page: &Box<ProjectSettingsPage>) -> Node<Msg> {
let width = 100f64 / (model.issue_statuses.len() + 1) as f64;
let column_style = format!("width: calc({width}% - 10px)", width = width);
let mut per_column_issue_count = HashMap::new();
@ -281,124 +225,13 @@ pub fn view(model: &model::Model) -> Node<Msg> {
add_column(page, column_style.as_str())
]
];
let columns_field = StyledField::build()
StyledField::build()
.add_class("columnsField")
.input(columns_section)
.label("Columns")
.tip("Double-click on name to change it.")
.build()
.into_node();
let save_button = StyledButton::build()
.add_class("actionButton")
.on_click(mouse_ev(Ev::Click, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm))
}))
.text("Save changes")
.build()
.into_node();
let form = StyledForm::build()
.heading("Project Details")
.on_submit(ev(Ev::Submit, |ev| {
ev.prevent_default();
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::SubmitForm))
}))
.add_field(name_field)
.add_field(url_field)
.add_field(description_field)
.add_field(category_field)
.add_field(time_tracking_field)
.add_field(save_button)
.add_field(columns_field)
.build()
.into_node();
let project_section = vec![div![class!["formContainer"], form]];
inner_layout(
model,
"projectSettings",
project_section,
crate::modal::view(model),
)
}
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return,
};
if page.column_drag.dragged_or_last(bellow_id) {
return;
}
let dragged_id = match page.column_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_statuses = vec![];
std::mem::swap(&mut issues_statuses, &mut model.issue_statuses);
for issue_status in issues_statuses.into_iter() {
match issue_status.id {
id if id == bellow_id => below = Some(issue_status),
id if id == dragged_id => dragged = Some(issue_status),
_ => model.issue_statuses.push(issue_status),
};
}
let mut below = match below {
Some(below) => below,
_ => return,
};
let mut dragged = match dragged {
Some(issue_status) => issue_status,
_ => {
model.issue_statuses.push(below);
return;
}
};
std::mem::swap(&mut dragged.position, &mut below.position);
page.column_drag.mark_dirty(dragged.id);
page.column_drag.mark_dirty(below.id);
model.issue_statuses.push(below);
model.issue_statuses.push(dragged);
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
page.column_drag.last_id = Some(bellow_id);
}
fn sync(model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return error!("bad content type"),
};
for id in page.column_drag.dirty.iter() {
let IssueStatus { name, position, .. } =
match model.issue_statuses.iter().find(|is| is.id == *id) {
Some(is) => is,
_ => continue,
};
send_ws_msg(
WsMsg::IssueStatusUpdate(*id, name.clone(), *position),
model.ws.as_ref(),
);
}
}
fn build_page_content(model: &mut Model) {
let project = match &model.project {
Some(project) => project,
_ => return,
};
model.page_content = PageContent::ProjectSettings(Box::new(ProjectSettingsPage::new(project)));
.into_node()
}
fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
@ -414,6 +247,12 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
ProjectPageChange::EditIssueStatusName(None),
))
});
let on_submit = ev(Ev::Submit, move |ev| {
ev.prevent_default();
Some(Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitIssueStatusForm,
)))
});
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.state(&page.name)
@ -423,7 +262,10 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
.build()
.into_node();
div![class!["columnPreview"], div![class!["columnName"], input]]
div![
class!["columnPreview"],
div![class!["columnName"], form![on_submit, input]]
]
} else {
let add_column = StyledIcon::build(Icon::Plus).build().into_node();
div![
@ -534,3 +376,225 @@ fn show_column_preview(
drag_out,
]
}
//#######################################
// Update
//#######################################
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::ProjectSettings {
return;
}
match msg {
Msg::WebSocketChange(ref change) => match change {
WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..)) => {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
WebSocketChanged::WsMsg(WsMsg::ProjectLoaded(..)) => {
build_page_content(model);
}
WebSocketChanged::WsMsg(WsMsg::IssueStatusCreated(_)) => {
match &mut model.page_content {
PageContent::ProjectSettings(page) if Some(0) == page.edit_column_id => {
page.reset();
}
_ => (),
};
}
_ => (),
},
Msg::ChangePage(Page::ProjectSettings) => {
build_page_content(model);
if model.user.is_some() {
send_ws_msg(WsMsg::ProjectRequest, model.ws.as_ref());
send_ws_msg(WsMsg::IssueStatusesRequest, model.ws.as_ref());
send_ws_msg(WsMsg::ProjectIssuesRequest, model.ws.as_ref());
}
}
_ => (),
}
if model.user.is_none() || model.project.is_none() {
return;
}
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return error!("bad content type"),
};
page.project_category_state.update(&msg, orders);
page.time_tracking.update(&msg);
page.name.update(&msg);
match msg {
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Name), text) => {
page.payload.name = Some(text);
}
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Url), text) => {
page.payload.url = Some(text);
}
Msg::StrInputChanged(FieldId::ProjectSettings(ProjectFieldId::Description), text) => {
page.payload.description = Some(text);
}
Msg::StyledSelectChanged(
FieldId::ProjectSettings(ProjectFieldId::Category),
StyledSelectChange::Changed(value),
) => {
let category = value.into();
page.payload.category = Some(category);
}
Msg::ModalChanged(TabChanged(
FieldId::ProjectSettings(ProjectFieldId::Description),
mode,
)) => {
page.description_mode = mode;
}
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitProjectSettingsForm,
)) => {
send_ws_msg(
WsMsg::ProjectUpdateRequest(UpdateProjectPayload {
id: page.payload.id,
name: page.payload.name.clone(),
url: page.payload.url.clone(),
description: page.payload.description.clone(),
category: page.payload.category.clone(),
time_tracking: Some(page.time_tracking.value.into()),
}),
model.ws.as_ref(),
);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStarted(
issue_status_id,
))) => {
page.column_drag.drag(issue_status_id);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragStopped(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDragLeave(
_issue_status_id,
))) => page.column_drag.clear_last(),
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::ColumnExchangePosition(issue_bellow_id),
)) => exchange_position(issue_bellow_id, model),
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::ColumnDropZone(
_issue_status_id,
))) => {
sync(model);
}
Msg::PageChanged(PageChanged::ProjectSettings(ProjectPageChange::EditIssueStatusName(
id,
))) => {
if page.edit_column_id.is_some() && id.is_none() {
let old_id = page.edit_column_id.as_ref().cloned();
let name = page.name.value.clone();
if let Some((id, pos)) = model
.issue_statuses
.iter()
.find(|is| Some(is.id) == old_id)
.map(|is| (is.id, is.position))
{
send_ws_msg(
WsMsg::IssueStatusUpdate(id, name.to_string(), pos),
model.ws.as_ref(),
);
}
}
page.name.value = model
.issue_statuses
.iter()
.find_map(|is| {
if Some(is.id) == id {
Some(is.name.clone())
} else {
None
}
})
.unwrap_or_default();
page.edit_column_id = id;
}
Msg::PageChanged(PageChanged::ProjectSettings(
ProjectPageChange::SubmitIssueStatusForm,
)) => {
let name = page.name.value.clone();
let position = model.issue_statuses.len();
let ws_msg = WsMsg::IssueStatusCreate(name, position as i32);
send_ws_msg(ws_msg, model.ws.as_ref());
}
_ => (),
}
}
fn exchange_position(bellow_id: IssueStatusId, model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return,
};
if page.column_drag.dragged_or_last(bellow_id) {
return;
}
let dragged_id = match page.column_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_statuses = vec![];
std::mem::swap(&mut issues_statuses, &mut model.issue_statuses);
for issue_status in issues_statuses.into_iter() {
match issue_status.id {
id if id == bellow_id => below = Some(issue_status),
id if id == dragged_id => dragged = Some(issue_status),
_ => model.issue_statuses.push(issue_status),
};
}
let mut below = match below {
Some(below) => below,
_ => return,
};
let mut dragged = match dragged {
Some(issue_status) => issue_status,
_ => {
model.issue_statuses.push(below);
return;
}
};
std::mem::swap(&mut dragged.position, &mut below.position);
page.column_drag.mark_dirty(dragged.id);
page.column_drag.mark_dirty(below.id);
model.issue_statuses.push(below);
model.issue_statuses.push(dragged);
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
page.column_drag.last_id = Some(bellow_id);
}
fn sync(model: &mut Model) {
let page = match &mut model.page_content {
PageContent::ProjectSettings(page) => page,
_ => return error!("bad content type"),
};
for id in page.column_drag.dirty.iter() {
let IssueStatus { name, position, .. } =
match model.issue_statuses.iter().find(|is| is.id == *id) {
Some(is) => is,
_ => continue,
};
send_ws_msg(
WsMsg::IssueStatusUpdate(*id, name.clone(), *position),
model.ws.as_ref(),
);
}
}

View File

@ -57,6 +57,10 @@ impl StyledInputState {
_ => (),
}
}
pub fn reset(&mut self) {
self.value.clear();
}
}
#[derive(Debug)]

View File

@ -86,6 +86,18 @@ pub fn update(msg: &WsMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
WsMsg::IssueStatusDeleted(dropped_id) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);
for is in old {
if is.id != *dropped_id {
model.issue_statuses.push(is);
}
}
model
.issue_statuses
.sort_by(|a, b| a.position.cmp(&b.position));
}
WsMsg::IssueDeleted(id) => {
let mut old = vec![];
std::mem::swap(&mut model.issue_statuses, &mut old);