use crate::commands::CmdResult;
use crate::error::Result;
use crate::index::{DisplayPad, PadSelector};
use crate::model::{Scope, TodoStatus};
use crate::store::{Bucket, DataStore};
use uuid::Uuid;
use super::helpers::{indexed_pads, resolve_selectors};
pub fn run<S: DataStore>(
store: &mut S,
scope: Scope,
selectors: &[PadSelector],
) -> Result<CmdResult> {
let resolved = resolve_selectors(store, scope, selectors, true)?;
let mut result = CmdResult::default();
let mut deleted_uuids: Vec<Uuid> = Vec::new();
let mut processed_ids = std::collections::HashSet::new();
for (_display_index, uuid) in resolved {
if !processed_ids.insert(uuid) {
continue; }
let pad = store.get_pad(&uuid, scope, Bucket::Active)?;
let parent_id = pad.metadata.parent_id;
let descendants = super::helpers::get_descendant_ids(store, scope, &[uuid])?;
let mut ids_to_move = vec![uuid];
ids_to_move.extend(descendants.iter().filter(|id| !processed_ids.contains(id)));
store.move_pads(&ids_to_move, scope, Bucket::Active, Bucket::Deleted)?;
for id in &descendants {
processed_ids.insert(*id);
}
crate::todos::propagate_status_change(store, scope, parent_id)?;
deleted_uuids.push(uuid);
}
let indexed = indexed_pads(store, scope)?;
for uuid in deleted_uuids {
if let Some(dp) = super::helpers::find_pad_by_uuid(&indexed, uuid, |_| true) {
result.affected_pads.push(DisplayPad {
pad: dp.pad.clone(),
index: dp.index.clone(),
matches: None,
children: Vec::new(),
});
}
}
Ok(result)
}
pub fn run_completed<S: DataStore>(store: &mut S, scope: Scope) -> Result<CmdResult> {
let active_pads = store.list_pads(scope, Bucket::Active)?;
let done_ids: Vec<Uuid> = active_pads
.iter()
.filter(|p| p.metadata.status == TodoStatus::Done)
.map(|p| p.metadata.id)
.collect();
if done_ids.is_empty() {
return Ok(CmdResult::default());
}
let selectors: Vec<PadSelector> = done_ids.iter().map(|id| PadSelector::Uuid(*id)).collect();
run(store, scope, &selectors)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::{create, get};
use crate::index::DisplayIndex;
use crate::model::Scope;
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
#[test]
fn marks_pad_as_deleted() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Title".into(), "".into(), None).unwrap();
run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 1);
assert!(matches!(
deleted.listed_pads[0].index,
DisplayIndex::Deleted(1)
));
}
#[test]
fn delete_protected_pad_fails() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Protected".into(),
"".into(),
None,
)
.unwrap();
let pad_id = get::run(&store, Scope::Project, get::PadFilter::default(), &[])
.unwrap()
.listed_pads[0]
.pad
.metadata
.id;
let mut pad = store
.get_pad(&pad_id, Scope::Project, Bucket::Active)
.unwrap();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let result = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
);
assert!(result.is_err());
match result {
Err(crate::error::PadzError::Api(msg)) => {
assert!(msg.contains("Pinned pads are delete protected"));
}
_ => panic!("Expected Api error"),
}
}
#[test]
fn delete_parent_with_pinned_child_succeeds() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
crate::commands::pinning::pin(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
)
.unwrap();
let result = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
);
assert!(result.is_ok());
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 1);
assert_eq!(deleted.listed_pads[0].pad.metadata.title, "Parent");
assert_eq!(deleted.listed_pads[0].children.len(), 1);
}
#[test]
fn delete_nested_pad_via_path() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let result = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
);
assert!(result.is_ok());
let active = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(active.listed_pads.len(), 1);
assert_eq!(active.listed_pads[0].pad.metadata.title, "Parent");
assert_eq!(active.listed_pads[0].children.len(), 0); }
#[test]
fn delete_completed_deletes_done_pads() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Planned".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Done One".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Done Two".into(),
"".into(),
None,
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(2)])],
)
.unwrap();
let result = run_completed(&mut store, Scope::Project).unwrap();
assert_eq!(result.affected_pads.len(), 2);
let active = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(active.listed_pads.len(), 1);
assert_eq!(active.listed_pads[0].pad.metadata.title, "Planned");
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 2);
}
#[test]
fn delete_completed_with_no_done_pads_returns_empty() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Planned".into(),
"".into(),
None,
)
.unwrap();
let result = run_completed(&mut store, Scope::Project).unwrap();
assert!(result.affected_pads.is_empty());
let active = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(active.listed_pads.len(), 1);
}
#[test]
fn delete_completed_skips_in_progress_pads() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"In Progress".into(),
"".into(),
None,
)
.unwrap();
create::run(&mut store, Scope::Project, "Done".into(), "".into(), None).unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut ip_pad = pads
.iter()
.find(|p| p.metadata.title == "In Progress")
.unwrap()
.clone();
ip_pad.metadata.status = TodoStatus::InProgress;
store
.save_pad(&ip_pad, Scope::Project, Bucket::Active)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
let result = run_completed(&mut store, Scope::Project).unwrap();
assert_eq!(result.affected_pads.len(), 1);
let active = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(active.listed_pads.len(), 1);
assert_eq!(active.listed_pads[0].pad.metadata.title, "In Progress");
}
#[test]
fn delete_completed_with_done_parent_and_done_child() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
)
.unwrap();
crate::commands::status::complete(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
)
.unwrap();
let active_pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let child_pad = active_pads
.iter()
.find(|p| p.metadata.title == "Child")
.unwrap();
let parent_pad = active_pads
.iter()
.find(|p| p.metadata.title == "Parent")
.unwrap();
let selectors = vec![
PadSelector::Uuid(child_pad.metadata.id),
PadSelector::Uuid(parent_pad.metadata.id),
];
let result = run(&mut store, Scope::Project, &selectors).unwrap();
assert!(!result.affected_pads.is_empty());
let active = get::run(&store, Scope::Project, get::PadFilter::default(), &[]).unwrap();
assert_eq!(active.listed_pads.len(), 0);
let deleted = get::run(
&store,
Scope::Project,
get::PadFilter {
status: get::PadStatusFilter::Deleted,
..Default::default()
},
&[],
)
.unwrap();
assert_eq!(deleted.listed_pads.len(), 1); assert_eq!(deleted.listed_pads[0].children.len(), 1);
}
}