feat(tasks): B2a part 1 — archive schema + task_lists scaffolding
Migration v16 adds tasks.archived (default 0) + tasks.archived_at + idx_tasks_archived. TaskRow extended with both fields; list_tasks and list_subtasks default-filter archived rows; complete_subtask_and_check_parent SELECT lists updated to round-trip the new columns. TaskListRow + ImportSummary structs land in preparation for the task_lists CRUD that part 2 wires up. All 61 existing tests still pass. (SQL splitter is naive about ';' inside -- comments, so the v16 comment block was rewritten to avoid a bare semicolon mid-comment that the splitter was treating as a statement boundary.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -328,8 +328,9 @@ pub async fn insert_task(
|
||||
pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||
source_transcript_id, parent_task_id, energy \
|
||||
FROM tasks WHERE parent_task_id IS NULL ORDER BY created_at DESC",
|
||||
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||
FROM tasks WHERE parent_task_id IS NULL AND archived = 0 \
|
||||
ORDER BY created_at DESC",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
@@ -341,7 +342,8 @@ pub async fn list_tasks(pool: &SqlitePool) -> Result<Vec<TaskRow>> {
|
||||
pub async fn get_task_by_id(pool: &SqlitePool, id: &str) -> Result<Option<TaskRow>> {
|
||||
let row = sqlx::query(
|
||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||
source_transcript_id, parent_task_id, energy FROM tasks WHERE id = ?",
|
||||
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||
FROM tasks WHERE id = ?",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
@@ -431,8 +433,8 @@ pub async fn insert_subtask(
|
||||
pub async fn list_subtasks(pool: &SqlitePool, parent_id: &str) -> Result<Vec<TaskRow>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, text, bucket, list_id, effort, notes, done, done_at, created_at, \
|
||||
source_transcript_id, parent_task_id, energy \
|
||||
FROM tasks WHERE parent_task_id = ? ORDER BY created_at ASC",
|
||||
source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||
FROM tasks WHERE parent_task_id = ? AND archived = 0 ORDER BY created_at ASC",
|
||||
)
|
||||
.bind(parent_id)
|
||||
.fetch_all(pool)
|
||||
@@ -499,7 +501,8 @@ pub async fn complete_subtask_and_check_parent(
|
||||
}
|
||||
|
||||
let select_sql = "SELECT id, text, bucket, list_id, effort, notes, done, done_at, \
|
||||
created_at, source_transcript_id, parent_task_id, energy FROM tasks WHERE id = ?";
|
||||
created_at, source_transcript_id, parent_task_id, energy, archived, archived_at \
|
||||
FROM tasks WHERE id = ?";
|
||||
|
||||
let updated_subtask = sqlx::query(select_sql)
|
||||
.bind(subtask_id)
|
||||
@@ -872,6 +875,34 @@ pub struct TaskRow {
|
||||
/// migration v11). Unset is the expected normal case — the match-my-
|
||||
/// energy sort treats unset as Medium-equivalent.
|
||||
pub energy: Option<String>,
|
||||
/// B2a — Inbox-only archive flag (migration v16). `true` means the
|
||||
/// row is archived and excluded from the default `list_tasks` /
|
||||
/// `list_subtasks` query. `archived_at` stamps the archive time so
|
||||
/// the Archived view can sort newest-first.
|
||||
pub archived: bool,
|
||||
pub archived_at: Option<String>,
|
||||
}
|
||||
|
||||
/// B2a — task_lists row. Mirrors the `task_lists` table that has lived in
|
||||
/// migration v1 since the start; this struct + the surrounding CRUD wires
|
||||
/// the table that the frontend was previously stashing in localStorage.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TaskListRow {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub built_in: bool,
|
||||
pub profile_id: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Result of `import_task_lists` — how many rows landed and how many were
|
||||
/// skipped because their `id` already exists. The whole batch runs in one
|
||||
/// transaction so a duplicate inside the batch (rather than against the
|
||||
/// existing table) is a hard failure that rolls everything back.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImportSummary {
|
||||
pub imported: usize,
|
||||
pub skipped: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -948,6 +979,18 @@ fn task_row_from(r: sqlx::sqlite::SqliteRow) -> TaskRow {
|
||||
source_transcript_id: r.get("source_transcript_id"),
|
||||
parent_task_id: r.get("parent_task_id"),
|
||||
energy: r.get("energy"),
|
||||
archived: r.get::<i64, _>("archived") != 0,
|
||||
archived_at: r.get("archived_at"),
|
||||
}
|
||||
}
|
||||
|
||||
fn task_list_row_from(r: &sqlx::sqlite::SqliteRow) -> TaskListRow {
|
||||
TaskListRow {
|
||||
id: r.get("id"),
|
||||
name: r.get("name"),
|
||||
built_in: r.get::<i64, _>("built_in") != 0,
|
||||
profile_id: r.get("profile_id"),
|
||||
created_at: r.get("created_at"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -477,6 +477,24 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[
|
||||
ON transcripts(profile_id, created_at DESC);
|
||||
"#,
|
||||
),
|
||||
(
|
||||
16,
|
||||
"tasks: archived flag for Inbox-only archive (B2a)",
|
||||
r#"
|
||||
-- B2a: persistent archive surface. The default is 0 (visible) so all
|
||||
-- pre-existing rows behave as before. archived_at is a string ISO
|
||||
-- timestamp captured at archive time so the future "archived"
|
||||
-- view can sort newest-first, and is NULL while the row is live.
|
||||
--
|
||||
-- Per the v3 plan, archive is Inbox-only: the bulk auto-archive
|
||||
-- path (archive_inbox_older_than) only touches rows with
|
||||
-- bucket = inbox. Today/Soon/Later reflect explicit user
|
||||
-- choices and stay where the user put them.
|
||||
ALTER TABLE tasks ADD COLUMN archived INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE tasks ADD COLUMN archived_at TEXT;
|
||||
CREATE INDEX IF NOT EXISTS idx_tasks_archived ON tasks(archived);
|
||||
"#,
|
||||
),
|
||||
];
|
||||
|
||||
/// Split SQL into individual statements, respecting BEGIN...END trigger blocks.
|
||||
@@ -626,7 +644,7 @@ mod tests {
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 15);
|
||||
assert_eq!(count, 16);
|
||||
|
||||
sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')")
|
||||
.execute(&pool)
|
||||
@@ -645,7 +663,7 @@ mod tests {
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 15);
|
||||
assert_eq!(count, 16);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user