replace string settings with typed enums
- preserve local wall-time handling and import completion timestamps - validate invalid properties without overwriting items
This commit is contained in:
125
src/app.rs
125
src/app.rs
@@ -15,7 +15,10 @@ use crate::{
|
||||
MacroAction, MacroContext, MacroRuntime, MenuChoice, PromptKind, key_to_source, macro_name,
|
||||
parse_key_binding,
|
||||
},
|
||||
model::{Category, DocumentSettings, Item, ItemChanges, MacroDef, ViewDef},
|
||||
model::{
|
||||
Category, DateOrder, DocumentSettings, DonePolicy, Item, ItemChanges, MacroDef,
|
||||
TrashPolicy, ViewDef, ViewKind, WeekStart,
|
||||
},
|
||||
preferences::AppPreferences,
|
||||
};
|
||||
|
||||
@@ -397,7 +400,7 @@ impl App {
|
||||
let today = Local::now().date_naive();
|
||||
if today != self.current_day {
|
||||
self.current_day = today;
|
||||
if self.document_settings.trash_policy == "end-of-day" {
|
||||
if self.document_settings.trash_policy == TrashPolicy::EndOfDay {
|
||||
let count = self.db.empty_trash()?;
|
||||
if count > 0 {
|
||||
self.status = format!("Emptied {count} Trash item(s) at end of day");
|
||||
@@ -985,9 +988,9 @@ impl App {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let restore = self.current_view().kind == "trash";
|
||||
let restore = self.current_view().kind == ViewKind::Trash;
|
||||
if !restore
|
||||
&& self.document_settings.trash_policy == "immediate"
|
||||
&& self.document_settings.trash_policy == TrashPolicy::Immediate
|
||||
&& self.preferences.confirm_destructive
|
||||
{
|
||||
self.mode = Mode::Confirm {
|
||||
@@ -1000,7 +1003,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn apply_discard(&mut self, ids: Vec<i64>, restore: bool) -> Result<()> {
|
||||
let permanent = !restore && self.document_settings.trash_policy == "immediate";
|
||||
let permanent = !restore && self.document_settings.trash_policy == TrashPolicy::Immediate;
|
||||
self.db.discard(&ids, !restore)?;
|
||||
self.marked.clear();
|
||||
self.refresh()?;
|
||||
@@ -1174,13 +1177,33 @@ impl App {
|
||||
props.values[props.field].pop();
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let priority = props.values[0].parse().unwrap_or(3);
|
||||
let when_at = normalize_date(&self.db, &props.values[1])?;
|
||||
let alarm_at = normalize_date(&self.db, &props.values[2])?;
|
||||
let numeric_value = if props.values[3].trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
props.values[3].parse().ok()
|
||||
let parsed = (|| -> Result<_> {
|
||||
let priority = props.values[0]
|
||||
.parse::<i64>()
|
||||
.context("priority must be a number from 1 to 5")?;
|
||||
if !(1..=5).contains(&priority) {
|
||||
bail!("priority must be a number from 1 to 5");
|
||||
}
|
||||
let when_at = normalize_date(&self.db, &props.values[1])?;
|
||||
let alarm_at = normalize_date(&self.db, &props.values[2])?;
|
||||
let numeric_value = if props.values[3].trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
props.values[3]
|
||||
.parse::<f64>()
|
||||
.context("numeric value must be a number")?,
|
||||
)
|
||||
};
|
||||
Ok((priority, when_at, alarm_at, numeric_value))
|
||||
})();
|
||||
let (priority, when_at, alarm_at, numeric_value) = match parsed {
|
||||
Ok(values) => values,
|
||||
Err(error) => {
|
||||
self.status = format!("Cannot save properties: {error:#}");
|
||||
self.mode = Mode::Properties(props);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
self.db.update_item(
|
||||
props.item_id,
|
||||
@@ -1436,11 +1459,11 @@ impl App {
|
||||
values: vec![
|
||||
settings.description.clone(),
|
||||
yes_no(settings.backup_on_open),
|
||||
settings.trash_policy.clone(),
|
||||
settings.done_policy.clone(),
|
||||
settings.trash_policy.to_string(),
|
||||
settings.done_policy.to_string(),
|
||||
yes_no(settings.automatic_filing),
|
||||
settings.date_order.clone(),
|
||||
settings.week_start.clone(),
|
||||
settings.date_order.to_string(),
|
||||
settings.week_start.to_string(),
|
||||
settings.default_time.clone(),
|
||||
settings.morning_time.clone(),
|
||||
settings.afternoon_time.clone(),
|
||||
@@ -1471,7 +1494,7 @@ impl App {
|
||||
vec![
|
||||
c.name,
|
||||
parent,
|
||||
c.kind,
|
||||
c.kind.to_string(),
|
||||
c.match_text,
|
||||
yes_no(c.exclusive),
|
||||
condition,
|
||||
@@ -1502,10 +1525,10 @@ impl App {
|
||||
let sections = v.sections_spec();
|
||||
vec![
|
||||
v.name,
|
||||
v.kind,
|
||||
v.kind.to_string(),
|
||||
v.filter_value,
|
||||
v.filter_expr,
|
||||
v.sort_key,
|
||||
v.sort_key.to_string(),
|
||||
yes_no(v.show_done),
|
||||
columns,
|
||||
sections,
|
||||
@@ -1636,11 +1659,27 @@ impl App {
|
||||
let settings = DocumentSettings {
|
||||
description: form.values[0].trim().into(),
|
||||
backup_on_open: parse_yes_no(&form.values[1])?,
|
||||
trash_policy: form.values[2].trim().to_lowercase(),
|
||||
done_policy: form.values[3].trim().to_lowercase(),
|
||||
trash_policy: form.values[2]
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.parse::<TrashPolicy>()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
done_policy: form.values[3]
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.parse::<DonePolicy>()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
automatic_filing: parse_yes_no(&form.values[4])?,
|
||||
date_order: form.values[5].trim().to_lowercase(),
|
||||
week_start: form.values[6].trim().to_lowercase(),
|
||||
date_order: form.values[5]
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.parse::<DateOrder>()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
week_start: form.values[6]
|
||||
.trim()
|
||||
.to_lowercase()
|
||||
.parse::<WeekStart>()
|
||||
.map_err(anyhow::Error::msg)?,
|
||||
default_time: form.values[7].trim().into(),
|
||||
morning_time: form.values[8].trim().into(),
|
||||
afternoon_time: form.values[9].trim().into(),
|
||||
@@ -1707,7 +1746,7 @@ impl App {
|
||||
let Some(item_id) = self.selected_item().map(|i| i.id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(all) = self.views.iter().find(|v| v.kind == "list") else {
|
||||
let Some(all) = self.views.iter().find(|v| v.kind == ViewKind::List) else {
|
||||
return Ok(());
|
||||
};
|
||||
let choices = self
|
||||
@@ -1963,7 +2002,9 @@ fn normalize_date(db: &Database, value: &str) -> Result<Option<String>> {
|
||||
} else if chrono::DateTime::parse_from_rfc3339(s).is_ok() {
|
||||
Ok(Some(s.into()))
|
||||
} else {
|
||||
db.interpret_date(&format!("on {s}"))
|
||||
Ok(Some(db.interpret_date(&format!("on {s}"))?.with_context(
|
||||
|| format!("unrecognized date or time {s:?}"),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
fn inside(r: Rect, x: u16, y: u16) -> bool {
|
||||
@@ -2235,4 +2276,38 @@ mod tests {
|
||||
assert!(matches!(input.kind, InputKind::Note(_)));
|
||||
assert_eq!(input.value, "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_properties_remain_open_without_overwriting_the_item() {
|
||||
let directory = tempdir().unwrap();
|
||||
let path = directory.path().join("invalid-properties.agnd");
|
||||
let mut db = Database::open(&path).unwrap();
|
||||
let item_id = db.add_item("Keep my properties").unwrap();
|
||||
let mut app = App::new_with_preferences(
|
||||
db,
|
||||
path,
|
||||
AppPreferences::default(),
|
||||
directory.path().join("preferences.toml"),
|
||||
)
|
||||
.unwrap();
|
||||
app.mode = Mode::Properties(PropsState {
|
||||
item_id,
|
||||
values: [
|
||||
"not-a-priority".into(),
|
||||
"not-a-date".into(),
|
||||
String::new(),
|
||||
"not-a-number".into(),
|
||||
String::new(),
|
||||
],
|
||||
field: 0,
|
||||
});
|
||||
|
||||
app.handle_key(key(KeyCode::Enter)).unwrap();
|
||||
|
||||
assert!(matches!(app.mode, Mode::Properties(_)));
|
||||
assert!(app.status.starts_with("Cannot save properties:"));
|
||||
assert_eq!(app.items[0].priority, 3);
|
||||
assert!(app.items[0].when_at.is_none());
|
||||
assert!(app.items[0].numeric_value.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user