generated from eblume/project-template
feat: Inbox view — outstanding tasks with no project (§8.2)
A sixth built-in filter view (listed below On Deck) showing every outstanding task filed under no project — the capture inbox to triage. New ListFilter.unfiled predicate kept purely in matches(); the inbox ViewSpec is un-gated (no attention/do-date filter) so nothing hides from triage. Surfaces automatically in the heph-tui sidebar and `heph view`. Tests cover the predicate and the view spec; navigation tests updated for the 6th view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0c45bbb5f9
commit
01ae561a74
4 changed files with 58 additions and 9 deletions
|
|
@ -3,10 +3,11 @@
|
|||
//! A view is a **predicate expressed as data** (mirroring §7's "order as
|
||||
//! data"): the engine [`Store::list`](crate::store::Store::list) takes a
|
||||
//! [`ListFilter`] and returns the matching outstanding tasks as
|
||||
//! [`RankedTask`] rows. The five built-in [`ViewSpec`]s (Top of Mind / Tasks /
|
||||
//! [`RankedTask`] rows. The built-in [`ViewSpec`]s (Top of Mind / Tasks /
|
||||
//! Work Tasks / Chores / On Deck) are derived from the owner's Todoist filter
|
||||
//! queries (see `docs/explanation/design.md` §6.2.1) and realized in heph terms
|
||||
//! (attention: p1→red, p2→orange, p4→white, p3→blue).
|
||||
//! (attention: p1→red, p2→orange, p4→white, p3→blue), plus an **Inbox** of
|
||||
//! project-less tasks to triage.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -36,6 +37,9 @@ pub struct ListFilter {
|
|||
pub exclude_projects: Vec<String>,
|
||||
/// Apply the §7 do-date candidacy gate: `do_date` is `None` or `<= now`.
|
||||
pub actionable: bool,
|
||||
/// Keep only tasks with **no project** — the capture "Inbox" to triage and
|
||||
/// file. (Pairs naturally with an empty `scope`; a scoped Inbox is empty.)
|
||||
pub unfiled: bool,
|
||||
}
|
||||
|
||||
impl ListFilter {
|
||||
|
|
@ -70,6 +74,9 @@ impl ListFilter {
|
|||
if self.actionable && task.do_date.is_some_and(|d| d > now) {
|
||||
return false;
|
||||
}
|
||||
if self.unfiled && task.project_id.is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -94,12 +101,14 @@ pub struct ViewSpec {
|
|||
pub exclude_names: &'static [&'static str],
|
||||
/// Whether the §7 do-date candidacy gate applies.
|
||||
pub actionable: bool,
|
||||
/// Whether to keep only project-less tasks (see [`ListFilter::unfiled`]).
|
||||
pub unfiled: bool,
|
||||
}
|
||||
|
||||
/// The five built-in views (tech-spec §8.2), each realized from the verbatim
|
||||
/// Todoist query in design §6.2.1.
|
||||
/// The built-in views (tech-spec §8.2): five realized from the verbatim
|
||||
/// Todoist query in design §6.2.1, plus an Inbox of project-less tasks.
|
||||
// Sidebar / `heph view` order (owner's preference): Top of Mind, Tasks,
|
||||
// Work Tasks, Chores, On Deck.
|
||||
// Work Tasks, Chores, On Deck, Inbox.
|
||||
pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
||||
// (p1|p2) & (no date|today|overdue)
|
||||
ViewSpec {
|
||||
|
|
@ -110,6 +119,7 @@ pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
|||
scope_names: &[],
|
||||
exclude_names: &[],
|
||||
actionable: true,
|
||||
unfiled: false,
|
||||
},
|
||||
// !p3 & (…) & !(#Daily Routine|#Work Routine|#Chores|#Camano Chores|#Work|…) & !subtask
|
||||
ViewSpec {
|
||||
|
|
@ -126,6 +136,7 @@ pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
|||
"Daily Routine",
|
||||
],
|
||||
actionable: true,
|
||||
unfiled: false,
|
||||
},
|
||||
// #Work & !p3 & (…) & !subtask
|
||||
ViewSpec {
|
||||
|
|
@ -136,6 +147,7 @@ pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
|||
scope_names: &["Work"],
|
||||
exclude_names: &[],
|
||||
actionable: true,
|
||||
unfiled: false,
|
||||
},
|
||||
// (today|overdue|no date) & (#Chores|#Camano Chores)
|
||||
ViewSpec {
|
||||
|
|
@ -146,6 +158,7 @@ pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
|||
scope_names: &["Chores", "Camano Chores"],
|
||||
exclude_names: &[],
|
||||
actionable: true,
|
||||
unfiled: false,
|
||||
},
|
||||
// p3 & (no date|overdue|today)
|
||||
ViewSpec {
|
||||
|
|
@ -156,6 +169,19 @@ pub const BUILTIN_VIEWS: &[ViewSpec] = &[
|
|||
scope_names: &[],
|
||||
exclude_names: &[],
|
||||
actionable: true,
|
||||
unfiled: false,
|
||||
},
|
||||
// Tasks filed under no project — the capture inbox to triage and file. Shows
|
||||
// everything unfiled (no attention/do-date gate) so nothing hides from triage.
|
||||
ViewSpec {
|
||||
name: "inbox",
|
||||
title: "Inbox",
|
||||
attention_in: &[],
|
||||
attention_not: &[],
|
||||
scope_names: &[],
|
||||
exclude_names: &[],
|
||||
actionable: false,
|
||||
unfiled: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -241,6 +267,26 @@ mod tests {
|
|||
assert!(!f.matches(&none, NOW));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfiled_keeps_only_projectless_tasks() {
|
||||
let f = ListFilter {
|
||||
unfiled: true,
|
||||
..Default::default()
|
||||
};
|
||||
let none = task("none");
|
||||
let mut filed = task("filed");
|
||||
filed.project_id = Some("work".into());
|
||||
assert!(f.matches(&none, NOW));
|
||||
assert!(!f.matches(&filed, NOW));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inbox_view_is_unfiled_and_ungated() {
|
||||
let spec = builtin("inbox").expect("inbox view exists");
|
||||
assert!(spec.unfiled);
|
||||
assert!(!spec.actionable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exclude_drops_listed_projects_but_keeps_projectless() {
|
||||
let f = ListFilter {
|
||||
|
|
@ -276,6 +322,7 @@ mod tests {
|
|||
assert_eq!(builtin("tom").unwrap().title, "Top of Mind");
|
||||
assert_eq!(builtin("ondeck").unwrap().attention_in, &[Attention::Blue]);
|
||||
assert!(builtin("nope").is_none());
|
||||
assert_eq!(BUILTIN_VIEWS.len(), 5);
|
||||
assert_eq!(builtin("inbox").unwrap().title, "Inbox");
|
||||
assert_eq!(BUILTIN_VIEWS.len(), 6);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ pub(super) fn view(
|
|||
scope,
|
||||
exclude_projects,
|
||||
actionable: spec.actionable,
|
||||
unfiled: spec.unfiled,
|
||||
};
|
||||
list(conn, owner, now, &filter)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,8 +179,8 @@ fn moving_the_sidebar_switches_the_task_list() {
|
|||
#[test]
|
||||
fn sidebar_skips_headers_into_the_projects_section() {
|
||||
let mut app = App::new(fixture()).unwrap();
|
||||
// 5 built-in views: step down 5 times crosses the "Projects" header to p1.
|
||||
for _ in 0..5 {
|
||||
// 6 built-in views: step down 6 times crosses the "Projects" header to p1.
|
||||
for _ in 0..6 {
|
||||
app.move_sidebar(1);
|
||||
}
|
||||
assert_eq!(app.task_pane_title(), "Camano");
|
||||
|
|
@ -231,7 +231,7 @@ fn quick_add_files_under_the_current_project_when_no_tag_given() {
|
|||
let mut app = App::new(fake).unwrap();
|
||||
|
||||
// Select the project so the new task is filed there.
|
||||
for _ in 0..5 {
|
||||
for _ in 0..6 {
|
||||
app.move_sidebar(1);
|
||||
}
|
||||
assert_eq!(app.task_pane_title(), "Camano");
|
||||
|
|
|
|||
1
docs/changelog.d/v1-inbox-view.feature.md
Normal file
1
docs/changelog.d/v1-inbox-view.feature.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- **Inbox view** (§8.2): a sixth built-in filter view, listed below On Deck, showing every outstanding task with **no project** — the capture inbox to triage and file. A new `ListFilter.unfiled` predicate (kept purely in `matches()`) drives it; the `inbox` `ViewSpec` is deliberately un-gated (no attention or do-date filter) so nothing hides from triage. Appears automatically in the `heph-tui` sidebar and `heph view`; `heph view inbox` from the CLI.
|
||||
Loading…
Add table
Add a link
Reference in a new issue