mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Merge pull request #313 from djmitche/issue89
Add support for annotations
This commit is contained in:
commit
acd4aefc17
8 changed files with 173 additions and 11 deletions
|
@ -48,6 +48,9 @@ pub struct Modification {
|
|||
|
||||
/// Remove tags
|
||||
pub remove_tags: HashSet<Tag>,
|
||||
|
||||
/// Add annotation
|
||||
pub annotate: Option<String>,
|
||||
}
|
||||
|
||||
/// A single argument that is part of a modification, used internally to this module
|
||||
|
@ -128,12 +131,12 @@ impl Modification {
|
|||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||
u.modifications.push(usage::Modification {
|
||||
syntax: "DESCRIPTION",
|
||||
summary: "Set description",
|
||||
summary: "Set description/annotation",
|
||||
description: "
|
||||
Set the task description. Multiple arguments are combined into a single
|
||||
space-separated description. To avoid surprises from shell quoting, prefer
|
||||
to use a single quoted argument, for example `ta 19 modify \"return library
|
||||
books\"`",
|
||||
Set the task description (or the task annotation for `ta annotate`). Multiple
|
||||
arguments are combined into a single space-separated description. To avoid
|
||||
surprises from shell quoting, prefer to use a single quoted argument, for example
|
||||
`ta 19 modify \"return library books\"`",
|
||||
});
|
||||
u.modifications.push(usage::Modification {
|
||||
syntax: "+TAG",
|
||||
|
|
|
@ -207,6 +207,13 @@ impl Modify {
|
|||
"start" => modification.active = Some(true),
|
||||
"stop" => modification.active = Some(false),
|
||||
"done" => modification.status = Some(Status::Completed),
|
||||
"annotate" => {
|
||||
// what would be parsed as a description is, here, used as the annotation
|
||||
if let DescriptionMod::Set(s) = modification.description {
|
||||
modification.description = DescriptionMod::None;
|
||||
modification.annotate = Some(s);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -225,6 +232,7 @@ impl Modify {
|
|||
arg_matching(literal("start")),
|
||||
arg_matching(literal("stop")),
|
||||
arg_matching(literal("done")),
|
||||
arg_matching(literal("annotate")),
|
||||
)),
|
||||
Modification::parse,
|
||||
)),
|
||||
|
@ -278,6 +286,13 @@ impl Modify {
|
|||
Mark all tasks matching the required filter as completed, additionally applying any given
|
||||
modifications.",
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "annotate",
|
||||
syntax: "<filter> annotate [modification]",
|
||||
summary: "Mark tasks as completed",
|
||||
description: "
|
||||
Add an annotation to all tasks matching the required filter.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -652,6 +667,23 @@ mod test {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_annotate() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
annotate: Some("sent invoice".into()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "annotate", "sent", "invoice"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report() {
|
||||
let subcommand = Subcommand::Report {
|
||||
|
|
|
@ -39,6 +39,11 @@ pub(crate) fn execute<W: WriteColor>(
|
|||
if let Some(wait) = task.get_wait() {
|
||||
t.add_row(row![b->"Wait", wait]);
|
||||
}
|
||||
let mut annotations: Vec<_> = task.get_annotations().collect();
|
||||
annotations.sort();
|
||||
for ann in annotations {
|
||||
t.add_row(row![b->"Annotation", format!("{}: {}", ann.entry, ann.description)]);
|
||||
}
|
||||
}
|
||||
t.print(w)?;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use taskchampion::TaskMut;
|
||||
use chrono::Utc;
|
||||
use taskchampion::{Annotation, TaskMut};
|
||||
|
||||
/// Apply the given modification
|
||||
pub(super) fn apply_modification(
|
||||
|
@ -41,5 +42,12 @@ pub(super) fn apply_modification(
|
|||
task.set_wait(wait)?;
|
||||
}
|
||||
|
||||
if let Some(ref ann) = modification.annotate {
|
||||
task.add_annotation(Annotation {
|
||||
entry: Utc::now(),
|
||||
description: ann.into(),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ The following keys, and key formats, are defined:
|
|||
* `start` - the most recent time at which this task was started (a task with no `start` key is not active)
|
||||
* `tag_<tag>` - indicates this task has tag `<tag>` (value is an empty string)
|
||||
* `wait` - indicates the time before which this task should be hidden, as it is not actionable
|
||||
* `annotation_<timestamp>` - value is an annotation created at the given time
|
||||
|
||||
The following are not yet implemented:
|
||||
|
||||
* `dep.<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
|
||||
* `annotation.<timestamp>` - value is an annotation created at the given time
|
||||
* `dep_<uuid>` - indicates this task depends on `<uuid>` (value is an empty string)
|
||||
|
|
|
@ -58,7 +58,7 @@ pub use errors::Error;
|
|||
pub use replica::Replica;
|
||||
pub use server::{Server, ServerConfig};
|
||||
pub use storage::StorageConfig;
|
||||
pub use task::{Priority, Status, Tag, Task, TaskMut};
|
||||
pub use task::{Annotation, Priority, Status, Tag, Task, TaskMut};
|
||||
pub use workingset::WorkingSet;
|
||||
|
||||
/// Re-exported type from the `uuid` crate, for ease of compatibility for consumers of this crate.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::Timestamp;
|
||||
|
||||
/// An annotation for a task
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Annotation {
|
||||
/// Time the annotation was made
|
||||
pub entry: Timestamp,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::tag::{SyntheticTag, TagInner};
|
||||
use super::{Status, Tag};
|
||||
use super::{Annotation, Status, Tag, Timestamp};
|
||||
use crate::replica::Replica;
|
||||
use crate::storage::TaskMap;
|
||||
use chrono::prelude::*;
|
||||
|
@ -145,6 +145,22 @@ impl Task {
|
|||
)
|
||||
}
|
||||
|
||||
/// Iterate over the task's annotations, in arbitrary order.
|
||||
pub fn get_annotations(&self) -> impl Iterator<Item = Annotation> + '_ {
|
||||
self.taskmap.iter().filter_map(|(k, v)| {
|
||||
if let Some(ts) = k.strip_prefix("annotation_") {
|
||||
if let Ok(ts) = ts.parse::<i64>() {
|
||||
return Some(Annotation {
|
||||
entry: Utc.timestamp(ts, 0),
|
||||
description: v.to_owned(),
|
||||
});
|
||||
}
|
||||
// note that invalid "annotation_*" are ignored
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
|
||||
self.get_timestamp("modified")
|
||||
}
|
||||
|
@ -225,6 +241,20 @@ impl<'r> TaskMut<'r> {
|
|||
self.set_string(format!("tag_{}", tag), None)
|
||||
}
|
||||
|
||||
/// Add a new annotation. Note that annotations with the same entry time
|
||||
/// will overwrite one another.
|
||||
pub fn add_annotation(&mut self, ann: Annotation) -> anyhow::Result<()> {
|
||||
self.set_string(
|
||||
format!("annotation_{}", ann.entry.timestamp()),
|
||||
Some(ann.description),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove an annotation, based on its entry time.
|
||||
pub fn remove_annotation(&mut self, entry: Timestamp) -> anyhow::Result<()> {
|
||||
self.set_string(format!("annotation_{}", entry.timestamp()), None)
|
||||
}
|
||||
|
||||
// -- utility functions
|
||||
|
||||
fn lastmod(&mut self) -> anyhow::Result<()> {
|
||||
|
@ -442,6 +472,90 @@ mod test {
|
|||
assert_eq!(tags, vec![utag("ok"), stag(SyntheticTag::Pending)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_annotations() {
|
||||
let task = Task::new(
|
||||
Uuid::new_v4(),
|
||||
vec![
|
||||
(
|
||||
String::from("annotation_1635301873"),
|
||||
String::from("left message"),
|
||||
),
|
||||
(
|
||||
String::from("annotation_1635301883"),
|
||||
String::from("left another message"),
|
||||
),
|
||||
(String::from("annotation_"), String::from("invalid")),
|
||||
(String::from("annotation_abcde"), String::from("invalid")),
|
||||
]
|
||||
.drain(..)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let mut anns: Vec<_> = task.get_annotations().collect();
|
||||
anns.sort();
|
||||
assert_eq!(
|
||||
anns,
|
||||
vec![
|
||||
Annotation {
|
||||
entry: Utc.timestamp(1635301873, 0),
|
||||
description: "left message".into()
|
||||
},
|
||||
Annotation {
|
||||
entry: Utc.timestamp(1635301883, 0),
|
||||
description: "left another message".into()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_annotation() {
|
||||
with_mut_task(|mut task| {
|
||||
task.add_annotation(Annotation {
|
||||
entry: Utc.timestamp(1635301900, 0),
|
||||
description: "right message".into(),
|
||||
})
|
||||
.unwrap();
|
||||
let k = "annotation_1635301900";
|
||||
assert_eq!(task.taskmap[k], "right message".to_owned());
|
||||
task.reload().unwrap();
|
||||
assert_eq!(task.taskmap[k], "right message".to_owned());
|
||||
// adding with same time overwrites..
|
||||
task.add_annotation(Annotation {
|
||||
entry: Utc.timestamp(1635301900, 0),
|
||||
description: "right message 2".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(task.taskmap[k], "right message 2".to_owned());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_annotation() {
|
||||
with_mut_task(|mut task| {
|
||||
task.set_string("annotation_1635301873", Some("left message".into()))
|
||||
.unwrap();
|
||||
task.set_string("annotation_1635301883", Some("left another message".into()))
|
||||
.unwrap();
|
||||
|
||||
task.remove_annotation(Utc.timestamp(1635301873, 0))
|
||||
.unwrap();
|
||||
|
||||
task.reload().unwrap();
|
||||
|
||||
let mut anns: Vec<_> = task.get_annotations().collect();
|
||||
anns.sort();
|
||||
assert_eq!(
|
||||
anns,
|
||||
vec![Annotation {
|
||||
entry: Utc.timestamp(1635301883, 0),
|
||||
description: "left another message".into()
|
||||
}]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start() {
|
||||
with_mut_task(|mut task| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue