all: remove saved views, the Has-reminder filter and the Created range
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 1m29s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m34s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m49s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 9m29s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 1m29s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m34s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m49s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 9m29s
Step 18 of the audit follow-through (#5180), on the operator's decisions. Inkwell is for capture and recall (note 2897), and these three duplicated a surface that does the job already: - Saved views. They lived only on the web; the desktop kept its own set that never synced, and Android had none. Tags in the drawer already give one-click recall. Gone from the server (routes, model, migration 0038 drops the table), the core (store functions, schema v13 drops its table), the desktop commands, the web adapters, the drawer's Views list and the "Save view" link. - The "Has reminder" facet. The Reminders page lists them, sorted by due. - The FilterBar's "Created" range. Timeline is the date lens and keeps the created_after/created_before query it builds from local days, which also retires the UTC/local-day disagreement between the two (B3). An old link that still carries the removed keys opens the plain board; a web test pins that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -122,15 +122,6 @@ pub struct DueReminder {
|
||||
pub due_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SavedFilter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
/// Mirrors NoteFacets — stored as a JSON blob, round-tripped opaquely.
|
||||
pub params: serde_json::Value,
|
||||
pub position: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PublicConfig {
|
||||
pub site_name: String,
|
||||
@@ -179,8 +170,6 @@ pub struct Facets {
|
||||
#[serde(default)]
|
||||
pub label: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub has_reminder: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub has_attachment: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub created_after: Option<String>,
|
||||
|
||||
+18
-37
@@ -325,6 +325,12 @@ const SCHEMA_V12: &str = r#"
|
||||
ALTER TABLE notes ADD COLUMN state_at TEXT;
|
||||
"#;
|
||||
|
||||
// v13 (#5180): saved views are gone. They never synced, so this device's set was its
|
||||
// own and nothing else holds a copy.
|
||||
const SCHEMA_V13: &str = r#"
|
||||
DROP TABLE saved_filters;
|
||||
"#;
|
||||
|
||||
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
|
||||
let version: i64 = conn.query_row("PRAGMA user_version", [], |r| r.get(0))?;
|
||||
@@ -376,6 +382,10 @@ pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch(SCHEMA_V12)?;
|
||||
conn.execute_batch("PRAGMA user_version = 12;")?;
|
||||
}
|
||||
if version < 13 {
|
||||
conn.execute_batch(SCHEMA_V13)?;
|
||||
conn.execute_batch("PRAGMA user_version = 13;")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -490,7 +500,7 @@ mod tests {
|
||||
let version: i64 = conn
|
||||
.query_row("PRAGMA user_version", [], |r| r.get(0))
|
||||
.expect("version");
|
||||
assert_eq!(version, 12);
|
||||
assert_eq!(version, 13);
|
||||
}
|
||||
|
||||
/// Every attachment that predates v10 came down the feed, so it is already on the
|
||||
@@ -544,46 +554,17 @@ mod tests {
|
||||
assert!(label_columns.iter().any(|c| c == "color"));
|
||||
}
|
||||
|
||||
/// A stored view that filtered on colour loses that criterion and keeps the rest.
|
||||
/// The alternative — leaving the key — is a lens that silently narrows on a field
|
||||
/// the app no longer has and never says why it returned nothing.
|
||||
#[test]
|
||||
fn v9_sweeps_colour_out_of_saved_filters() {
|
||||
fn v13_drops_saved_views() {
|
||||
let conn = Connection::open_in_memory().expect("open");
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;").expect("fk");
|
||||
for batch in [
|
||||
SCHEMA_V1, SCHEMA_V2, SCHEMA_V3, SCHEMA_V4, SCHEMA_V5, SCHEMA_V6, SCHEMA_V7,
|
||||
] {
|
||||
conn.execute_batch(batch).expect("schema");
|
||||
}
|
||||
conn.execute_batch("PRAGMA user_version = 8;").expect("v8");
|
||||
for (id, params) in [
|
||||
("a", r#"{"color":"teal","q":"milk"}"#),
|
||||
("b", r#"{"q":"eggs"}"#),
|
||||
// Not JSON at all. It must come out UNCHANGED rather than NULL — a blob
|
||||
// this migration cannot read is not a blob it gets to destroy.
|
||||
("c", "not json"),
|
||||
] {
|
||||
conn.execute(
|
||||
"INSERT INTO saved_filters (id, name, params, created_at)
|
||||
VALUES (?1, ?1, ?2, '2026-08-28T00:00:00.000Z')",
|
||||
params![id, params],
|
||||
)
|
||||
.expect("seed");
|
||||
}
|
||||
|
||||
migrate(&conn).expect("migrate");
|
||||
|
||||
let read = |id: &str| -> String {
|
||||
conn.query_row(
|
||||
"SELECT params FROM saved_filters WHERE id = ?1",
|
||||
[id],
|
||||
let tables: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE name = 'saved_filters'",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.expect("read")
|
||||
};
|
||||
assert_eq!(read("a"), r#"{"q":"milk"}"#);
|
||||
assert_eq!(read("b"), r#"{"q":"eggs"}"#);
|
||||
assert_eq!(read("c"), "not json");
|
||||
.expect("count");
|
||||
assert_eq!(tables, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-85
@@ -414,9 +414,6 @@ pub fn list_notes(conn: &Connection, q: &ListQuery) -> rusqlite::Result<Vec<Note
|
||||
sql.push_str(" AND body LIKE ? ESCAPE '\\'");
|
||||
binds.push(format!("%{}%", escape_like(text)));
|
||||
}
|
||||
if f.has_reminder == Some(true) {
|
||||
sql.push_str(" AND remind_at IS NOT NULL");
|
||||
}
|
||||
if f.has_attachment == Some(true) {
|
||||
sql.push_str(" AND EXISTS (SELECT 1 FROM attachments a WHERE a.note_id = notes.id)");
|
||||
}
|
||||
@@ -1152,75 +1149,6 @@ pub fn merge_labels(
|
||||
load_label(conn, target_id)
|
||||
}
|
||||
|
||||
// ---- saved filters ----------------------------------------------------------
|
||||
|
||||
const SAVED_FILTER_SELECT: &str = "SELECT id, name, params, position FROM saved_filters";
|
||||
|
||||
/// A stored filter. Params that no longer parse read as no filter at all rather
|
||||
/// than failing the whole list.
|
||||
fn saved_filter_row(r: &rusqlite::Row<'_>) -> rusqlite::Result<SavedFilter> {
|
||||
let params_str: String = r.get(2)?;
|
||||
Ok(SavedFilter {
|
||||
id: r.get(0)?,
|
||||
name: r.get(1)?,
|
||||
params: serde_json::from_str(¶ms_str).unwrap_or_else(|_| json!({})),
|
||||
position: r.get(3)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list_saved_filters(conn: &Connection) -> rusqlite::Result<Vec<SavedFilter>> {
|
||||
let mut stmt = conn.prepare(&format!(
|
||||
"{SAVED_FILTER_SELECT} ORDER BY position ASC, name COLLATE NOCASE"
|
||||
))?;
|
||||
let rows = stmt.query_map([], saved_filter_row)?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn create_saved_filter(
|
||||
conn: &Connection,
|
||||
name: &str,
|
||||
params: &Value,
|
||||
) -> rusqlite::Result<SavedFilter> {
|
||||
let id = new_id();
|
||||
let position: i64 = conn.query_row(
|
||||
"SELECT COALESCE(MAX(position), 0) + 1 FROM saved_filters",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
let params_str = serde_json::to_string(params).unwrap_or_else(|_| "{}".to_string());
|
||||
conn.execute(
|
||||
"INSERT INTO saved_filters (id, name, params, position, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![id, name, params_str, position, now()],
|
||||
)?;
|
||||
Ok(SavedFilter {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
params: params.clone(),
|
||||
position,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_saved_filter(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
conn.execute("DELETE FROM saved_filters WHERE id = ?1", [id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_saved_filter(
|
||||
conn: &Connection,
|
||||
id: &str,
|
||||
name: &str,
|
||||
) -> rusqlite::Result<SavedFilter> {
|
||||
conn.execute(
|
||||
"UPDATE saved_filters SET name = ?1 WHERE id = ?2",
|
||||
params![name, id],
|
||||
)?;
|
||||
conn.query_row(
|
||||
&format!("{SAVED_FILTER_SELECT} WHERE id = ?1"),
|
||||
[id],
|
||||
saved_filter_row,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! The store had no tests of its own until two bugs shipped in it unnoticed:
|
||||
@@ -1557,28 +1485,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reminder_and_attachment_facets() {
|
||||
fn attachment_facet() {
|
||||
let conn = db();
|
||||
let reminded = note(&conn, "reminded");
|
||||
let attached = note(&conn, "attached");
|
||||
note(&conn, "plain");
|
||||
update_note(
|
||||
&conn,
|
||||
&reminded.id,
|
||||
&json!({"remind_at": "2026-12-01T09:00:00.000Z"}),
|
||||
)
|
||||
.expect("remind");
|
||||
conn.execute(
|
||||
"INSERT INTO attachments (id, note_id, url) VALUES ('a', ?1, '/x')",
|
||||
[&attached.id],
|
||||
)
|
||||
.expect("attach");
|
||||
|
||||
let r = list(
|
||||
&conn,
|
||||
json!({"view": "notes", "facets": {"has_reminder": true}}),
|
||||
);
|
||||
assert_eq!(ids(&r), vec![reminded.id.as_str()]);
|
||||
let a = list(
|
||||
&conn,
|
||||
json!({"view": "notes", "facets": {"has_attachment": true}}),
|
||||
|
||||
Reference in New Issue
Block a user