Add date time picker
This commit is contained in:
parent
493ab96198
commit
5c98e51a10
@ -29,7 +29,7 @@ tasks:
|
|||||||
export NODE_ENV=production
|
export NODE_ENV=production
|
||||||
./scripts/prod.sh
|
./scripts/prod.sh
|
||||||
export TAR_NAME=$(date -u +"%Y%m%d%H%M%s")
|
export TAR_NAME=$(date -u +"%Y%m%d%H%M%s")
|
||||||
tar -czvf ~/${TAR_NAME}.tar.gz ./build
|
tar -cJvf ~/${TAR_NAME}.tar.xz ./build
|
||||||
cp ~/${TAR_NAME}.tar.gz ~/latest.tar.gz
|
cp ~/${TAR_NAME}.tar.xz ~/latest.tar.xz
|
||||||
artifacts:
|
artifacts:
|
||||||
- latest.tar.gz
|
- latest.tar.gz
|
||||||
|
@ -39,7 +39,8 @@ tasks:
|
|||||||
cd jirs
|
cd jirs
|
||||||
cargo build --all --release
|
cargo build --all --release
|
||||||
strip -s ./target/release/jirs_server
|
strip -s ./target/release/jirs_server
|
||||||
|
tar -cJvf ~/server.tar.xz ./target/release/jirs_server
|
||||||
- deploy: |
|
- deploy: |
|
||||||
cp ~/jirs/target/release/jirs_server ~/
|
cp ~/server.tar.xz ~/latest.tar.xz
|
||||||
artifacts:
|
artifacts:
|
||||||
- jirs_server
|
- jirs_server
|
||||||
|
@ -50,7 +50,7 @@ https://git.sr.ht/~tsumanu/jirs
|
|||||||
* [ ] Epic `starts` and `ends` date
|
* [ ] Epic `starts` and `ends` date
|
||||||
* [X] Grouping by Epic
|
* [X] Grouping by Epic
|
||||||
* [X] Basic Rich Text Editor
|
* [X] Basic Rich Text Editor
|
||||||
* [ ] Insert Code in Rich Text Editor
|
* [X] Insert Code in Rich Text Editor
|
||||||
* [X] Code syntax
|
* [X] Code syntax
|
||||||
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE
|
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE
|
||||||
* [ ] Issues and filters view
|
* [ ] Issues and filters view
|
||||||
|
64
jirs-client/js/css/styledDateTimeInput.css
Normal file
64
jirs-client/js/css/styledDateTimeInput.css
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
.styledDateTimeInput {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TOOLTIP */
|
||||||
|
|
||||||
|
.dateTimeTooltip > .actions {
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar {
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week {
|
||||||
|
display: flex;
|
||||||
|
border-left: 1px solid var(--textDarkest);
|
||||||
|
border-right: 1px solid var(--textDarkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week:first-child {
|
||||||
|
border-top: 1px solid var(--textDarkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week:last-child {
|
||||||
|
border-bottom: 1px solid var(--textDarkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week.weekHeader {
|
||||||
|
border-bottom: 1px solid var(--textDarkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day {
|
||||||
|
width: calc(100% / 7);
|
||||||
|
text-align: center;
|
||||||
|
height: 3rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day.inCurrentMonth:hover,
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day.outCurrentMonth:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--asideIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day.inCurrentMonth {
|
||||||
|
color: var(--textDarkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day.outCurrentMonth {
|
||||||
|
color: var(--textLight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*.styledDateTimeInput > .calendar > .week > .day > .weekday {*/
|
||||||
|
/* font-family: var(--font-bold);*/
|
||||||
|
/* line-height: 1.5rem;*/
|
||||||
|
/* font-size: 1rem;*/
|
||||||
|
/* cursor: pointer;*/
|
||||||
|
/*}*/
|
||||||
|
|
||||||
|
.dateTimeTooltip > .calendar > .week > .day {
|
||||||
|
font-family: var(--font-medium);
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
@import "./css/styledPage.css";
|
@import "./css/styledPage.css";
|
||||||
@import "./css/styledLink.css";
|
@import "./css/styledLink.css";
|
||||||
@import "./css/styledRte.css";
|
@import "./css/styledRte.css";
|
||||||
|
@import "./css/styledDateTimeInput.css";
|
||||||
@import "./css/app.css";
|
@import "./css/app.css";
|
||||||
@import "./css/issue.css";
|
@import "./css/issue.css";
|
||||||
@import "./css/project.css";
|
@import "./css/project.css";
|
||||||
|
@ -74,7 +74,15 @@ impl std::fmt::Display for FieldId {
|
|||||||
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
|
EditIssueModalSection::Issue(IssueFieldId::ListPosition) => {
|
||||||
f.write_str("editIssue-listPosition")
|
f.write_str("editIssue-listPosition")
|
||||||
}
|
}
|
||||||
EditIssueModalSection::Issue(IssueFieldId::Epic) => f.write_str("editIssue-epic"),
|
EditIssueModalSection::Issue(IssueFieldId::EpicName) => {
|
||||||
|
f.write_str("editIssue-epicName")
|
||||||
|
}
|
||||||
|
EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt) => {
|
||||||
|
f.write_str("editIssue-epicStartsAt")
|
||||||
|
}
|
||||||
|
EditIssueModalSection::Issue(IssueFieldId::EpicEndsAt) => {
|
||||||
|
f.write_str("editIssue-epicEndsAt")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
FieldId::AddIssueModal(sub) => match sub {
|
FieldId::AddIssueModal(sub) => match sub {
|
||||||
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
|
IssueFieldId::Type => f.write_str("issueTypeAddIssueModal"),
|
||||||
@ -88,7 +96,9 @@ impl std::fmt::Display for FieldId {
|
|||||||
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
|
IssueFieldId::TimeSpent => f.write_str("addIssueModal-timeSpend"),
|
||||||
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
|
IssueFieldId::TimeRemaining => f.write_str("addIssueModal-timeRemaining"),
|
||||||
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
|
IssueFieldId::ListPosition => f.write_str("addIssueModal-listPosition"),
|
||||||
IssueFieldId::Epic => f.write_str("addIssueModal-epic"),
|
IssueFieldId::EpicName => f.write_str("addIssueModal-epicName"),
|
||||||
|
IssueFieldId::EpicStartsAt => f.write_str("addIssueModal-epicStartsAt"),
|
||||||
|
IssueFieldId::EpicEndsAt => f.write_str("addIssueModal-epicEndsAt"),
|
||||||
},
|
},
|
||||||
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
|
FieldId::TextFilterBoard => f.write_str("textFilterBoard"),
|
||||||
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
|
FieldId::CopyButtonLabel => f.write_str("copyButtonLabel"),
|
||||||
|
@ -8,6 +8,7 @@ pub use fields::*;
|
|||||||
use jirs_data::*;
|
use jirs_data::*;
|
||||||
|
|
||||||
use crate::model::{ModalType, Model, Page};
|
use crate::model::{ModalType, Model, Page};
|
||||||
|
use crate::shared::styled_date_time_input::DateTimeMsg;
|
||||||
use crate::shared::styled_rte::RteMsg;
|
use crate::shared::styled_rte::RteMsg;
|
||||||
use crate::shared::styled_select::StyledSelectChange;
|
use crate::shared::styled_select::StyledSelectChange;
|
||||||
use crate::shared::styled_tooltip::{Variant as StyledTooltip, Variant};
|
use crate::shared::styled_tooltip::{Variant as StyledTooltip, Variant};
|
||||||
@ -50,6 +51,7 @@ pub enum Msg {
|
|||||||
ProjectChanged(Option<Project>),
|
ProjectChanged(Option<Project>),
|
||||||
|
|
||||||
StyledSelectChanged(FieldId, StyledSelectChange),
|
StyledSelectChanged(FieldId, StyledSelectChange),
|
||||||
|
StyledDateTimeInputChanged(FieldId, DateTimeMsg),
|
||||||
InternalFailure(String),
|
InternalFailure(String),
|
||||||
ToggleTooltip(StyledTooltip),
|
ToggleTooltip(StyledTooltip),
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ use seed::{prelude::*, *};
|
|||||||
|
|
||||||
use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
|
use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg};
|
||||||
|
|
||||||
|
use crate::shared::styled_date_time_input::StyledDateTimeInput;
|
||||||
use crate::{
|
use crate::{
|
||||||
modal::issues::epic_field,
|
modal::issues::epic_field,
|
||||||
model::{AddIssueModal, IssueModal, ModalType, Model},
|
model::{AddIssueModal, IssueModal, ModalType, Model},
|
||||||
@ -226,7 +227,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
.values
|
.values
|
||||||
.get(0)
|
.get(0)
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(|n| Type::from(n))
|
.map(Type::from)
|
||||||
.unwrap_or_else(|| Type::Task);
|
.unwrap_or_else(|| Type::Task);
|
||||||
|
|
||||||
let issue_type_field = issue_type_field(modal);
|
let issue_type_field = issue_type_field(modal);
|
||||||
@ -238,7 +239,18 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
let form = match issue_type {
|
let form = match issue_type {
|
||||||
Type::Epic => {
|
Type::Epic => {
|
||||||
let name_field = name_field(modal);
|
let name_field = name_field(modal);
|
||||||
form.add_field(name_field)
|
|
||||||
|
let starts = StyledDateTimeInput::build()
|
||||||
|
.state(&modal.epic_starts_at_state)
|
||||||
|
.build(FieldId::AddIssueModal(IssueFieldId::EpicStartsAt))
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let end = StyledDateTimeInput::build()
|
||||||
|
.state(&modal.epic_ends_at_state)
|
||||||
|
.build(FieldId::AddIssueModal(IssueFieldId::EpicEndsAt))
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
form.add_field(name_field).add_field(starts).add_field(end)
|
||||||
}
|
}
|
||||||
Type::Task | Type::Story | Type::Bug => {
|
Type::Task | Type::Story | Type::Bug => {
|
||||||
let short_summary_field = short_summary_field(modal);
|
let short_summary_field = short_summary_field(modal);
|
||||||
@ -246,7 +258,8 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
let reporter_field = reporter_field(model, modal);
|
let reporter_field = reporter_field(model, modal);
|
||||||
let assignees_field = assignees_field(model, modal);
|
let assignees_field = assignees_field(model, modal);
|
||||||
let issue_priority_field = issue_priority_field(modal);
|
let issue_priority_field = issue_priority_field(modal);
|
||||||
let epic_field = epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::Epic));
|
let epic_field =
|
||||||
|
epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::EpicName));
|
||||||
|
|
||||||
form.add_field(short_summary_field)
|
form.add_field(short_summary_field)
|
||||||
.add_field(description_field)
|
.add_field(description_field)
|
||||||
@ -258,7 +271,6 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let submit = {
|
let submit = {
|
||||||
let issue_type = issue_type.clone();
|
|
||||||
StyledButton::build()
|
StyledButton::build()
|
||||||
.primary()
|
.primary()
|
||||||
.text(issue_type.submit_label())
|
.text(issue_type.submit_label())
|
||||||
|
@ -262,13 +262,13 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Msg::StyledSelectChanged(
|
Msg::StyledSelectChanged(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||||
StyledSelectChange::Changed(v),
|
StyledSelectChange::Changed(v),
|
||||||
) => {
|
) => {
|
||||||
send_ws_msg(
|
send_ws_msg(
|
||||||
WsMsg::IssueUpdate(
|
WsMsg::IssueUpdate(
|
||||||
modal.id,
|
modal.id,
|
||||||
IssueFieldId::Epic,
|
IssueFieldId::EpicName,
|
||||||
PayloadVariant::OptionI32(v.map(|n| n as EpicId).clone()),
|
PayloadVariant::OptionI32(v.map(|n| n as EpicId).clone()),
|
||||||
),
|
),
|
||||||
model.ws.as_ref(),
|
model.ws.as_ref(),
|
||||||
@ -803,7 +803,7 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
|||||||
let epic_field = epic_field(
|
let epic_field = epic_field(
|
||||||
model,
|
model,
|
||||||
modal,
|
modal,
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|| empty![]);
|
.unwrap_or_else(|| empty![]);
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ use jirs_data::*;
|
|||||||
use crate::modal::time_tracking::value_for_time_tracking;
|
use crate::modal::time_tracking::value_for_time_tracking;
|
||||||
use crate::shared::drag::DragState;
|
use crate::shared::drag::DragState;
|
||||||
use crate::shared::styled_checkbox::StyledCheckboxState;
|
use crate::shared::styled_checkbox::StyledCheckboxState;
|
||||||
|
use crate::shared::styled_date_time_input::StyledDateTimeInputState;
|
||||||
use crate::shared::styled_editor::Mode;
|
use crate::shared::styled_editor::Mode;
|
||||||
use crate::shared::styled_image_input::StyledImageInputState;
|
use crate::shared::styled_image_input::StyledImageInputState;
|
||||||
use crate::shared::styled_input::StyledInputState;
|
use crate::shared::styled_input::StyledInputState;
|
||||||
@ -69,7 +70,9 @@ pub struct EditIssueModal {
|
|||||||
pub reporter_state: StyledSelectState,
|
pub reporter_state: StyledSelectState,
|
||||||
pub assignees_state: StyledSelectState,
|
pub assignees_state: StyledSelectState,
|
||||||
pub priority_state: StyledSelectState,
|
pub priority_state: StyledSelectState,
|
||||||
pub epic_state: StyledSelectState,
|
pub epic_name_state: StyledSelectState,
|
||||||
|
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||||
|
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||||
|
|
||||||
pub estimate: StyledInputState,
|
pub estimate: StyledInputState,
|
||||||
pub estimate_select: StyledSelectState,
|
pub estimate_select: StyledSelectState,
|
||||||
@ -86,11 +89,11 @@ pub struct EditIssueModal {
|
|||||||
|
|
||||||
impl IssueModal for EditIssueModal {
|
impl IssueModal for EditIssueModal {
|
||||||
fn epic_id_value(&self) -> Option<u32> {
|
fn epic_id_value(&self) -> Option<u32> {
|
||||||
self.epic_state.values.get(0).cloned()
|
self.epic_name_state.values.get(0).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn epic_state(&self) -> &StyledSelectState {
|
fn epic_state(&self) -> &StyledSelectState {
|
||||||
&self.epic_state
|
&self.epic_name_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||||
@ -105,7 +108,7 @@ impl IssueModal for EditIssueModal {
|
|||||||
self.time_spent_select.update(msg, orders);
|
self.time_spent_select.update(msg, orders);
|
||||||
self.time_remaining.update(msg);
|
self.time_remaining.update(msg);
|
||||||
self.time_remaining_select.update(msg, orders);
|
self.time_remaining_select.update(msg, orders);
|
||||||
self.epic_state.update(msg, orders);
|
self.epic_name_state.update(msg, orders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,14 +152,6 @@ impl EditIssueModal {
|
|||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)),
|
||||||
vec![issue.priority.into()],
|
vec![issue.priority.into()],
|
||||||
),
|
),
|
||||||
epic_state: StyledSelectState::new(
|
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)),
|
|
||||||
issue
|
|
||||||
.epic_id
|
|
||||||
.as_ref()
|
|
||||||
.map(|id| vec![*id as u32])
|
|
||||||
.unwrap_or_default(),
|
|
||||||
),
|
|
||||||
estimate: StyledInputState::new(
|
estimate: StyledInputState::new(
|
||||||
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)),
|
||||||
value_for_time_tracking(&issue.estimate, &time_tracking_type),
|
value_for_time_tracking(&issue.estimate, &time_tracking_type),
|
||||||
@ -190,6 +185,23 @@ impl EditIssueModal {
|
|||||||
body: String::new(),
|
body: String::new(),
|
||||||
creating: false,
|
creating: false,
|
||||||
},
|
},
|
||||||
|
// epic
|
||||||
|
epic_name_state: StyledSelectState::new(
|
||||||
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicName)),
|
||||||
|
issue
|
||||||
|
.epic_id
|
||||||
|
.as_ref()
|
||||||
|
.map(|id| vec![*id as u32])
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||||
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||||
|
FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::EpicStartsAt)),
|
||||||
|
None,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,16 +226,19 @@ pub struct AddIssueModal {
|
|||||||
pub reporter_state: StyledSelectState,
|
pub reporter_state: StyledSelectState,
|
||||||
pub assignees_state: StyledSelectState,
|
pub assignees_state: StyledSelectState,
|
||||||
pub priority_state: StyledSelectState,
|
pub priority_state: StyledSelectState,
|
||||||
pub epic_state: StyledSelectState,
|
// epic
|
||||||
|
pub epic_name_state: StyledSelectState,
|
||||||
|
pub epic_starts_at_state: StyledDateTimeInputState,
|
||||||
|
pub epic_ends_at_state: StyledDateTimeInputState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IssueModal for AddIssueModal {
|
impl IssueModal for AddIssueModal {
|
||||||
fn epic_id_value(&self) -> Option<u32> {
|
fn epic_id_value(&self) -> Option<u32> {
|
||||||
self.epic_state.values.get(0).cloned()
|
self.epic_name_state.values.get(0).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn epic_state(&self) -> &StyledSelectState {
|
fn epic_state(&self) -> &StyledSelectState {
|
||||||
&self.epic_state
|
&self.epic_name_state
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||||
@ -232,7 +247,9 @@ impl IssueModal for AddIssueModal {
|
|||||||
self.reporter_state.update(msg, orders);
|
self.reporter_state.update(msg, orders);
|
||||||
self.type_state.update(msg, orders);
|
self.type_state.update(msg, orders);
|
||||||
self.priority_state.update(msg, orders);
|
self.priority_state.update(msg, orders);
|
||||||
self.epic_state.update(msg, orders);
|
self.epic_name_state.update(msg, orders);
|
||||||
|
self.epic_starts_at_state.update(msg, orders);
|
||||||
|
self.epic_ends_at_state.update(msg, orders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,7 +281,19 @@ impl Default for AddIssueModal {
|
|||||||
FieldId::AddIssueModal(IssueFieldId::Priority),
|
FieldId::AddIssueModal(IssueFieldId::Priority),
|
||||||
vec![],
|
vec![],
|
||||||
),
|
),
|
||||||
epic_state: StyledSelectState::new(FieldId::AddIssueModal(IssueFieldId::Epic), vec![]),
|
// epic
|
||||||
|
epic_name_state: StyledSelectState::new(
|
||||||
|
FieldId::AddIssueModal(IssueFieldId::EpicName),
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
epic_starts_at_state: StyledDateTimeInputState::new(
|
||||||
|
FieldId::AddIssueModal(IssueFieldId::EpicStartsAt),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
epic_ends_at_state: StyledDateTimeInputState::new(
|
||||||
|
FieldId::AddIssueModal(IssueFieldId::EpicEndsAt),
|
||||||
|
None,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ pub mod styled_avatar;
|
|||||||
pub mod styled_button;
|
pub mod styled_button;
|
||||||
pub mod styled_checkbox;
|
pub mod styled_checkbox;
|
||||||
pub mod styled_confirm_modal;
|
pub mod styled_confirm_modal;
|
||||||
|
pub mod styled_date_time_input;
|
||||||
pub mod styled_editor;
|
pub mod styled_editor;
|
||||||
pub mod styled_field;
|
pub mod styled_field;
|
||||||
pub mod styled_form;
|
pub mod styled_form;
|
||||||
|
232
jirs-client/src/shared/styled_date_time_input.rs
Normal file
232
jirs-client/src/shared/styled_date_time_input.rs
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
use chrono::Duration;
|
||||||
|
|
||||||
|
use {
|
||||||
|
chrono::prelude::*,
|
||||||
|
seed::{prelude::*, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::shared::styled_button::StyledButton;
|
||||||
|
use crate::shared::styled_icon::Icon;
|
||||||
|
use crate::shared::styled_tooltip::StyledTooltip;
|
||||||
|
use crate::{shared::ToNode, FieldId, Msg};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DateTimeMsg {
|
||||||
|
MonthChanged(Option<NaiveDateTime>),
|
||||||
|
DayChanged(Option<NaiveDateTime>),
|
||||||
|
PopupVisibilityChanged(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialOrd, PartialEq)]
|
||||||
|
pub struct StyledDateTimeInputState {
|
||||||
|
field_id: FieldId,
|
||||||
|
timestamp: Option<chrono::NaiveDateTime>,
|
||||||
|
popup_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledDateTimeInputState {
|
||||||
|
pub fn new(field_id: FieldId, timestamp: Option<NaiveDateTime>) -> Self {
|
||||||
|
Self {
|
||||||
|
field_id,
|
||||||
|
timestamp,
|
||||||
|
popup_visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) {
|
||||||
|
match msg {
|
||||||
|
Msg::StyledDateTimeInputChanged(field_id, DateTimeMsg::MonthChanged(new_date))
|
||||||
|
if field_id == &self.field_id =>
|
||||||
|
{
|
||||||
|
self.timestamp = *new_date;
|
||||||
|
}
|
||||||
|
Msg::StyledDateTimeInputChanged(field_id, DateTimeMsg::DayChanged(new_date))
|
||||||
|
if field_id == &self.field_id =>
|
||||||
|
{
|
||||||
|
self.timestamp = *new_date;
|
||||||
|
}
|
||||||
|
Msg::StyledDateTimeInputChanged(field_id, DateTimeMsg::PopupVisibilityChanged(b))
|
||||||
|
if field_id == &self.field_id =>
|
||||||
|
{
|
||||||
|
self.popup_visible = *b;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StyledDateTimeInput {
|
||||||
|
field_id: FieldId,
|
||||||
|
timestamp: Option<chrono::NaiveDateTime>,
|
||||||
|
popup_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledDateTimeInput {
|
||||||
|
pub fn build() -> StyledDateTimeInputBuilder {
|
||||||
|
StyledDateTimeInputBuilder {
|
||||||
|
timestamp: None,
|
||||||
|
popup_visible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToNode for StyledDateTimeInput {
|
||||||
|
fn into_node(self) -> Node<Msg> {
|
||||||
|
render(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StyledDateTimeInputBuilder {
|
||||||
|
timestamp: Option<chrono::NaiveDateTime>,
|
||||||
|
popup_visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledDateTimeInputBuilder {
|
||||||
|
pub fn state(mut self, state: &StyledDateTimeInputState) -> Self {
|
||||||
|
self.timestamp = state.timestamp;
|
||||||
|
self.popup_visible = state.popup_visible;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self, field_id: FieldId) -> StyledDateTimeInput {
|
||||||
|
StyledDateTimeInput {
|
||||||
|
field_id,
|
||||||
|
timestamp: self.timestamp,
|
||||||
|
popup_visible: self.popup_visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(values: StyledDateTimeInput) -> Node<Msg> {
|
||||||
|
let current = values
|
||||||
|
.timestamp
|
||||||
|
.unwrap_or_else(|| chrono::Utc::now().naive_utc());
|
||||||
|
let start = current.with_day0(0).unwrap().date();
|
||||||
|
let end = (start + Duration::days(32)).with_day0(0).unwrap();
|
||||||
|
|
||||||
|
let calendar_start = match start.weekday() {
|
||||||
|
Weekday::Mon => start,
|
||||||
|
Weekday::Tue => start - Duration::days(1),
|
||||||
|
Weekday::Wed => start - Duration::days(2),
|
||||||
|
Weekday::Thu => start - Duration::days(3),
|
||||||
|
Weekday::Fri => start - Duration::days(4),
|
||||||
|
Weekday::Sat => start - Duration::days(5),
|
||||||
|
Weekday::Sun => start - Duration::days(6),
|
||||||
|
};
|
||||||
|
let calendar_end = match end.weekday() {
|
||||||
|
Weekday::Mon => end + Duration::days(6),
|
||||||
|
Weekday::Tue => end + Duration::days(5),
|
||||||
|
Weekday::Wed => end + Duration::days(4),
|
||||||
|
Weekday::Thu => end + Duration::days(3),
|
||||||
|
Weekday::Fri => end + Duration::days(2),
|
||||||
|
Weekday::Sat => end + Duration::days(1),
|
||||||
|
Weekday::Sun => end,
|
||||||
|
};
|
||||||
|
let current_month_range = start..=end;
|
||||||
|
let mut current = calendar_start;
|
||||||
|
let mut weeks = vec![];
|
||||||
|
let range = calendar_start..=calendar_end;
|
||||||
|
let mut current_week = vec![];
|
||||||
|
loop {
|
||||||
|
if !range.contains(¤t) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.weekday() == Weekday::Mon && !current_week.is_empty() {
|
||||||
|
weeks.push(div![C!["week"], current_week]);
|
||||||
|
current_week = vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
current_week.push(day_cell(¤t, ¤t_month_range));
|
||||||
|
|
||||||
|
current += Duration::days(1);
|
||||||
|
}
|
||||||
|
if !current_week.is_empty() {
|
||||||
|
weeks.push(div![C!["week"], current_week]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let close_tooltip = {
|
||||||
|
let field_id = values.field_id.clone();
|
||||||
|
StyledButton::build()
|
||||||
|
.empty()
|
||||||
|
.icon(Icon::Close)
|
||||||
|
.on_click(mouse_ev(Ev::Click, move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
Some(Msg::StyledDateTimeInputChanged(
|
||||||
|
field_id,
|
||||||
|
DateTimeMsg::PopupVisibilityChanged(false),
|
||||||
|
))
|
||||||
|
}))
|
||||||
|
.build()
|
||||||
|
.into_node()
|
||||||
|
};
|
||||||
|
|
||||||
|
let tooltip = StyledTooltip::build()
|
||||||
|
.visible(values.popup_visible)
|
||||||
|
.add_class("dateTimeTooltip")
|
||||||
|
.add_child(h2![span!["Add table"], close_tooltip])
|
||||||
|
.add_child(div![
|
||||||
|
C!["actions"],
|
||||||
|
button![C!["prev"], "Prev"],
|
||||||
|
button![C!["next"], "Next"],
|
||||||
|
])
|
||||||
|
.add_child(div![
|
||||||
|
C!["calendar"],
|
||||||
|
div![
|
||||||
|
C!["weekHeader week"],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Mon).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Tue).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Wed).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Thu).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Fri).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Sat).as_str()],
|
||||||
|
div![C!["day"], format!("{}", Weekday::Sun).as_str()],
|
||||||
|
],
|
||||||
|
weeks
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.into_node();
|
||||||
|
|
||||||
|
let input = {
|
||||||
|
let field_id = values.field_id.clone();
|
||||||
|
let on_focus = ev(Ev::Click, move |ev| {
|
||||||
|
ev.prevent_default();
|
||||||
|
ev.stop_propagation();
|
||||||
|
Msg::StyledDateTimeInputChanged(field_id, DateTimeMsg::PopupVisibilityChanged(true))
|
||||||
|
});
|
||||||
|
let text = values
|
||||||
|
.timestamp
|
||||||
|
.map(|d| format!("{}", d))
|
||||||
|
.unwrap_or_default();
|
||||||
|
StyledButton::build()
|
||||||
|
.add_class("")
|
||||||
|
.on_click(on_focus)
|
||||||
|
.text(text)
|
||||||
|
.active(true)
|
||||||
|
.empty()
|
||||||
|
.build()
|
||||||
|
.into_node()
|
||||||
|
};
|
||||||
|
|
||||||
|
div![
|
||||||
|
C!["styledDateTimeInput"],
|
||||||
|
attrs![At::Class => format!("{}", values.field_id).as_str()],
|
||||||
|
input,
|
||||||
|
tooltip,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn day_cell(current: &NaiveDate, current_month_range: &RangeInclusive<NaiveDate>) -> Node<Msg> {
|
||||||
|
div![
|
||||||
|
C!["day"],
|
||||||
|
attrs![At::Class => format!("{}", current.weekday())],
|
||||||
|
if current_month_range.contains(¤t) {
|
||||||
|
C!["inCurrentMonth"]
|
||||||
|
} else {
|
||||||
|
C!["outCurrentMonth"]
|
||||||
|
},
|
||||||
|
format!("{}", current.day()).as_str(),
|
||||||
|
]
|
||||||
|
}
|
@ -55,5 +55,14 @@ pub enum IssueFieldId {
|
|||||||
TimeSpent,
|
TimeSpent,
|
||||||
TimeRemaining,
|
TimeRemaining,
|
||||||
IssueStatusId,
|
IssueStatusId,
|
||||||
Epic,
|
EpicName,
|
||||||
|
EpicStartsAt,
|
||||||
|
EpicEndsAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq, Hash)]
|
||||||
|
pub enum EpicFieldId {
|
||||||
|
Name,
|
||||||
|
StartsAt,
|
||||||
|
EndsAt,
|
||||||
}
|
}
|
||||||
|
@ -588,4 +588,6 @@ pub struct Epic {
|
|||||||
pub project_id: ProjectId,
|
pub project_id: ProjectId,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
|
pub starts_at: Option<NaiveDateTime>,
|
||||||
|
pub ends_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE epics
|
||||||
|
DROP COLUMN starts_at;
|
||||||
|
ALTER TABLE epics
|
||||||
|
DROP COLUMN ends_at;
|
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE epics
|
||||||
|
ADD COLUMN starts_at TIMESTAMP;
|
||||||
|
ALTER TABLE epics
|
||||||
|
ADD COLUMN ends_at TIMESTAMP;
|
@ -91,6 +91,18 @@ table! {
|
|||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
updated_at -> Timestamp,
|
updated_at -> Timestamp,
|
||||||
|
/// The `starts_at` column of the `epics` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Nullable<Timestamp>`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
starts_at -> Nullable<Timestamp>,
|
||||||
|
/// The `ends_at` column of the `epics` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Nullable<Timestamp>`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
ends_at -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ impl WsHandler<UpdateIssueHandler> for WebSocketActor {
|
|||||||
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
|
(IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => {
|
||||||
msg.time_remaining = o;
|
msg.time_remaining = o;
|
||||||
}
|
}
|
||||||
(IssueFieldId::Epic, PayloadVariant::OptionI32(o)) => {
|
(IssueFieldId::EpicName, PayloadVariant::OptionI32(o)) => {
|
||||||
msg.epic_id = Some(o);
|
msg.epic_id = Some(o);
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
|
Loading…
Reference in New Issue
Block a user