aboutsummaryrefslogtreecommitdiff
path: root/src/gui/form
diff options
context:
space:
mode:
authorJoris2022-03-12 13:27:29 +0100
committerJoris2022-03-12 13:36:09 +0100
commitd584df359640176ec4bc06f59d1e8d42ab17a413 (patch)
tree6cfaf676fc2ecf4e61067aa376fb2bed0d984d79 /src/gui/form
parentaad7b9601dfa05255d5c24f4a6377d9a25646d45 (diff)
downloadcalendar-d584df359640176ec4bc06f59d1e8d42ab17a413.tar.gz
calendar-d584df359640176ec4bc06f59d1e8d42ab17a413.tar.bz2
calendar-d584df359640176ec4bc06f59d1e8d42ab17a413.zip
Update and delete recurring events
Diffstat (limited to 'src/gui/form')
-rw-r--r--src/gui/form/mod.rs238
-rw-r--r--src/gui/form/repetition.rs47
2 files changed, 227 insertions, 58 deletions
diff --git a/src/gui/form/mod.rs b/src/gui/form/mod.rs
index 57ccac7..68e6539 100644
--- a/src/gui/form/mod.rs
+++ b/src/gui/form/mod.rs
@@ -2,8 +2,13 @@ mod repetition;
use gtk4 as gtk;
+use anyhow::Result;
+use chrono::{NaiveDate, NaiveTime};
use gtk::glib;
use gtk::prelude::*;
+use rusqlite::Connection;
+use thiserror::Error;
+use uuid::Uuid;
use crate::{
db,
@@ -11,12 +16,70 @@ use crate::{
model::{event, event::Event},
};
-pub async fn show(app: &App, event: Event, is_new: bool) {
+pub async fn repetition_dialog(app: &App, date: NaiveDate, event: Event) {
let dialog = gtk::Dialog::builder()
.transient_for(&*app.window)
.modal(true)
- .title(if is_new { "Ajouter" } else { "Modifier" })
- .css_classes(vec!["g-Form".to_string()])
+ .title("Modifier")
+ .css_classes(vec!["g-Dialog".to_string()])
+ .build();
+
+ let content_area = dialog.content_area();
+
+ let lines = gtk::Box::builder()
+ .orientation(gtk::Orientation::Vertical)
+ .build();
+ content_area.append(&lines);
+
+ let button = gtk::Button::builder()
+ .label("Cette occurence")
+ .margin_bottom(10)
+ .build();
+ lines.append(&button);
+ let tx = app.tx.clone();
+ button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| {
+ update::send(tx.clone(), Msg::ShowUpdateRepetitionForm { date, event: event.clone() });
+ dialog.close()
+ }));
+
+ let button = gtk::Button::builder()
+ .label("Toutes les occurences")
+ .build();
+ lines.append(&button);
+ let tx = app.tx.clone();
+ button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| {
+ update::send(tx.clone(), Msg::ShowUpdateForm { event: event.clone() });
+ dialog.close()
+ }));
+
+ dialog.run_future().await;
+}
+
+#[derive(Clone)]
+pub enum Target {
+ New { date: NaiveDate },
+ Update { event: Event },
+ UpdateRepetition { event: Event, date: NaiveDate },
+}
+
+pub async fn show(app: &App, target: Target) {
+ let event = match target {
+ Target::New { .. } => None,
+ Target::Update { ref event } => Some(event.clone()),
+ Target::UpdateRepetition { ref event, .. } => Some(event.clone()),
+ };
+
+ let title = if event.is_some() {
+ "Modifier"
+ } else {
+ "Ajouter"
+ };
+
+ let dialog = gtk::Dialog::builder()
+ .transient_for(&*app.window)
+ .modal(true)
+ .title(title)
+ .css_classes(vec!["g-Dialog".to_string()])
.build();
let content_area = dialog.content_area();
@@ -29,7 +92,6 @@ pub async fn show(app: &App, event: Event, is_new: bool) {
let columns = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
- columns.add_css_class("g-Form__Columns");
lines.append(&columns);
// First column
@@ -37,79 +99,145 @@ pub async fn show(app: &App, event: Event, is_new: bool) {
let column1 = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
- column1.add_css_class("g-Form__Inputs");
columns.append(&column1);
- let name = entry(&event.name);
+ let name = event.as_ref().map(|e| entry(&e.name)).unwrap_or_default();
column1.append(&label("Événement"));
column1.append(&name);
- let date = entry(&event.date.format(event::DATE_FORMAT).to_string());
+ let date = match target {
+ Target::New { date } => date,
+ Target::Update { ref event } => event.date,
+ Target::UpdateRepetition { date, .. } => date,
+ };
+ let date = entry(&date.format(event::DATE_FORMAT).to_string());
column1.append(&label("Jour"));
column1.append(&date);
- let start = entry(
- &event
- .start
- .map(event::pprint_time)
- .unwrap_or_else(|| "".to_string()),
- );
+ let start = event
+ .as_ref()
+ .map(|e| time_entry(e.start))
+ .unwrap_or_else(|| entry(""));
column1.append(&label("Début"));
column1.append(&start);
- let end = entry(
- &event
- .end
- .map(event::pprint_time)
- .unwrap_or_else(|| "".to_string()),
- );
+ let end = event
+ .as_ref()
+ .map(|e| time_entry(e.end))
+ .unwrap_or_else(|| entry(""));
column1.append(&label("Fin"));
column1.append(&end);
// Second column
- let repetition_model = repetition::view(&event);
+ let repetition = match target {
+ Target::Update { ref event } => event.repetition.as_ref(),
+ _ => None,
+ };
+ let repetition_model = repetition::view(repetition);
columns.append(&repetition_model.view);
// Buttons
+ let button_title = match target {
+ Target::New { .. } => "Créer",
+ Target::Update { .. } => "Modifier",
+ Target::UpdateRepetition { .. } => "Modifier l’occurence",
+ };
+
let button = gtk::Button::builder()
- .label(if is_new { "Créer" } else { "Modifier" })
+ .label(button_title)
.margin_bottom(10)
.build();
lines.append(&button);
let conn = app.conn.clone();
let tx = app.tx.clone();
- button.connect_clicked(glib::clone!(@weak dialog, @strong event => move |_| {
+ button.connect_clicked(glib::clone!(@weak dialog, @strong target, @strong event => move |_| {
match repetition::validate(&repetition_model) {
Ok(repetition) => {
- match event::validate(event.id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) {
+ let id = match &target {
+ Target::Update {event} => event.id,
+ _ => Uuid::new_v4(),
+ };
+ match event::validate(id, date.buffer().text(), name.buffer().text(), start.buffer().text(), end.buffer().text(), repetition) {
Some(new) => {
- match if is_new { db::insert(&conn, &new) } else { db::update(&conn, &new) } {
- Ok(_) => {
- let msg = if is_new { Msg::AddEvent { new } } else { Msg::UpdateEvent { old: event.clone(), new } };
- update::send(tx.clone(), msg);
- dialog.close()
- },
- Err(err) => eprintln!("Error when upserting event: {err}")
+ match &target {
+ Target::New {..} => {
+ match db::insert(&conn, &new) {
+ Ok(_) => {
+ update::send(tx.clone(), Msg::AddEvent { new });
+ dialog.close()
+ },
+ Err(err) => eprintln!("Error when inserting event: {}", err)
+ }
+ }
+ Target::Update {event} => {
+ match db::update(&conn, &new) {
+ Ok(_) => {
+ update::send(tx.clone(), Msg::UpdateEvent { old: event.clone(), new });
+ dialog.close()
+ },
+ Err(err) => eprintln!("Error when updating event: {}", err)
+ }
+ }
+ Target::UpdateRepetition { event, date } => {
+ // TODO: improve intermediate error state
+ match delete_repetition_occurence(&conn, event, *date) {
+ Ok(occurence) => {
+ match db::insert(&conn, &new) {
+ Ok(_) => {
+ update::send(tx.clone(), Msg::UpdateEventOccurence {
+ event: event.clone(),
+ occurence,
+ date: *date,
+ new
+ })
+ }
+ Err(err) => eprintln!("Error when updating repetition: {}", err)
+ };
+ dialog.close()
+ },
+ Err(err) => eprintln!("Error when updating repetition: {}", err)
+ }
+ }
}
}
- None => eprintln!("Event is not valid: {event:?}")
+ None => eprintln!("Event is not valid.")
}
},
- Err(message) => eprintln!("{message}")
+ Err(message) => eprintln!("{}", message)
}
}));
- if !is_new {
- let button = gtk::Button::builder().label("Supprimer").build();
+ if let Some(event) = event {
+ let label = match target {
+ Target::Update { .. } => "Supprimer",
+ _ => "Supprimer l’occurence",
+ };
+ let button = gtk::Button::builder().label(label).build();
lines.append(&button);
let conn = app.conn.clone();
let tx = app.tx.clone();
button.connect_clicked(glib::clone!(@weak dialog => move |_| {
- if db::delete(&conn, &event.id).is_ok() {
- update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() });
- dialog.close()
+ match target {
+ Target::UpdateRepetition { date, .. } => {
+ match delete_repetition_occurence(&conn, &event, date) {
+ Ok(occurence) => {
+ update::send(tx.clone(), Msg::DeleteOccurence { event: event.clone(), date, occurence });
+ dialog.close()
+ }
+ Err(err) => {
+ eprintln!("{:?}", err);
+ }
+ }
+ }
+ _ => {
+ let operation = db::delete(&conn, &event.id);
+ if operation.is_ok() {
+ update::send(tx.clone(), Msg::DeleteEvent { event: event.clone() });
+ dialog.close()
+ }
+ }
}
}));
}
@@ -117,6 +245,42 @@ pub async fn show(app: &App, event: Event, is_new: bool) {
dialog.run_future().await;
}
+#[derive(Error, Debug)]
+enum DeleteError {
+ #[error("Repetition not found")]
+ RepetitionNotFound,
+ #[error("Occurence not found")]
+ OccurenceNotFound,
+}
+
+fn delete_repetition_occurence(
+ conn: &Connection,
+ event: &Event,
+ occurence_date: NaiveDate,
+) -> Result<usize> {
+ if let Some(ref repetition) = event.repetition {
+ if let Some(occurence) = repetition.occurence_index(event.date, occurence_date) {
+ let mut event = event.clone();
+ let mut repetition = repetition.clone();
+ repetition.removed_occurences.insert(occurence);
+ event.repetition = Some(repetition);
+ db::update(conn, &event).map(|_| occurence)
+ } else {
+ Err(anyhow::Error::new(DeleteError::OccurenceNotFound))
+ }
+ } else {
+ Err(anyhow::Error::new(DeleteError::RepetitionNotFound))
+ }
+}
+
+fn time_entry(time: Option<NaiveTime>) -> gtk::Entry {
+ entry(
+ &time
+ .map(event::pprint_time)
+ .unwrap_or_else(|| "".to_string()),
+ )
+}
+
fn entry(text: &str) -> gtk::Entry {
gtk::Entry::builder().text(text).margin_bottom(10).build()
}
diff --git a/src/gui/form/repetition.rs b/src/gui/form/repetition.rs
index accb091..1d36765 100644
--- a/src/gui/form/repetition.rs
+++ b/src/gui/form/repetition.rs
@@ -2,13 +2,11 @@ use gtk4 as gtk;
use chrono::{Weekday, Weekday::*};
use gtk::prelude::*;
+use std::collections::HashSet;
-use crate::{
- model::event::Event,
- model::{
- repetition,
- repetition::{DayOfMonth, Repetition},
- },
+use crate::model::{
+ repetition,
+ repetition::{DayOfMonth, Frequency, Repetition},
};
static WEEKDAYS_STR: [&str; 7] = [
@@ -29,7 +27,7 @@ pub struct Model {
yearly_radio: gtk::CheckButton,
}
-pub fn view(event: &Event) -> Model {
+pub fn view(repetition: Option<&Repetition>) -> Model {
let view = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
@@ -39,12 +37,14 @@ pub fn view(event: &Event) -> Model {
let no_radio = gtk::CheckButton::builder()
.label("Non")
- .active(event.repetition.is_none())
+ .active(repetition.is_none())
.build();
view.append(&no_radio);
- let default = match event.repetition {
- Some(Repetition::Daily { period }) => period.to_string(),
+ let frequency = repetition.as_ref().map(|r| r.frequency.clone());
+
+ let default = match frequency {
+ Some(Frequency::Daily { period }) => period.to_string(),
_ => "".to_string(),
};
let day_interval_entry = gtk::Entry::builder().text(&default).build();
@@ -56,8 +56,8 @@ pub fn view(event: &Event) -> Model {
);
view.append(&day_interval_box);
- let default = match event.repetition {
- Some(Repetition::Monthly {
+ let default = match frequency {
+ Some(Frequency::Monthly {
day: DayOfMonth::Day { day },
}) => day.to_string(),
_ => "".to_string(),
@@ -67,8 +67,8 @@ pub fn view(event: &Event) -> Model {
radio_input(&no_radio, !default.is_empty(), &monthly_entry, "Mensuel");
view.append(&monthly_box);
- let (active, default) = match event.repetition {
- Some(Repetition::Monthly {
+ let (active, default) = match frequency {
+ Some(Frequency::Monthly {
day: DayOfMonth::Weekday { weekday },
}) => (true, weekday),
_ => (false, Mon),
@@ -83,7 +83,7 @@ pub fn view(event: &Event) -> Model {
let yearly_radio = gtk::CheckButton::builder()
.group(&no_radio)
.label("Annuel")
- .active(event.repetition == Some(Repetition::Yearly))
+ .active(frequency == Some(Frequency::Yearly))
.build();
view.append(&yearly_radio);
@@ -127,24 +127,29 @@ fn label(text: &str) -> gtk::Label {
}
pub fn validate(model: &Model) -> Result<Option<Repetition>, String> {
- if model.no_radio.is_active() {
+ let frequency = if model.no_radio.is_active() {
Ok(None)
} else if model.day_interval_radio.is_active() {
let period = repetition::validate_period(&model.day_interval_entry.buffer().text())?;
- Ok(Some(Repetition::Daily { period }))
+ Ok(Some(Frequency::Daily { period }))
} else if model.monthly_radio.is_active() {
let day = repetition::validate_day(&model.monthly_entry.buffer().text())?;
- Ok(Some(Repetition::Monthly {
+ Ok(Some(Frequency::Monthly {
day: DayOfMonth::Day { day },
}))
} else if model.first_day_radio.is_active() {
let weekday = WEEKDAYS[model.first_day_dropdown.selected() as usize];
- Ok(Some(Repetition::Monthly {
+ Ok(Some(Frequency::Monthly {
day: DayOfMonth::Weekday { weekday },
}))
} else if model.yearly_radio.is_active() {
- Ok(Some(Repetition::Yearly))
+ Ok(Some(Frequency::Yearly))
} else {
Err("Aucune option n’a été sélectionnée".to_string())
- }
+ }?;
+
+ Ok(frequency.map(|frequency| Repetition {
+ frequency,
+ removed_occurences: HashSet::new(),
+ }))
}