Remove the title field — a note is named by its first line
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Failing after 7s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 7s
CI & Build / Python tests (push) Successful in 11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 31s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m45s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Failing after 7s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 7s
CI & Build / Python tests (push) Successful in 11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 31s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m45s
Operator (note 2897): "notes shouldn't have a title field." The concept of a NAME stays — search results, export filenames and the command palette all need one — but nothing is typed into it any more. `display_title` is now the first non-empty line of the body, falling back to the first checklist item. That fallback is what step 2 bought, and the reason this could not go first: a checklist had no body to be named from, so the title was its only name. Now every note has a body, and a note that is only a checklist is named by its first item. Gone everywhere: the column and note_revisions.title (0026), the field on the core's Note/NoteCreateInput/NoteRevision and its SQLite columns (user_version 7), `normalize_title`, the wire field, the FFI record and `NoteEdit::Title` / `ClearTitle`, the web editor's "Title (optional)" input and the card's <h3>, and the Android title field in both the compose sheet and the editor. **The search vector had to be rebuilt, not just left alone.** `notes.search_vector` is a STORED GENERATED column whose expression names `title` — Postgres refuses to drop a column another generated column depends on. It is dropped and recreated over `display_title` at weight A, which keeps the original intent: a note's NAME ranks above the rest of its body. **An imported title becomes the note's first body line.** Keep notes carry one, and so does any ThoughtSync export taken before this. Dropping it would silently lose text someone wrote; folding it in puts it exactly where a name now lives, so the note arrives named as it was. Skipped when the body already opens with that line, so re-importing an export this code produced doesn't stack duplicates. Two smaller things fell out. The Android editor loses its bold first field — one weight throughout, because the first line is the note's name but not a different KIND of text, which is most of step 4 arriving early. And `ClearTitle`'s justification comment moved to `ClearRemindAt`, which is now the surviving example of why NoteEdit is a list rather than a struct of options. Protocol note corrected to say what actually shipped: v2 is "no kind, no title", one bump for the pair. Verified with the local Rust gate this time, not by CI: fmt, clippy and 116 tests all green before pushing. It caught four things — orphaned serde attributes where fields were removed, a `wire::Preview.title` I deleted by mistake (a link preview still has one), nine retention fixtures inserting a dropped column, and four rustfmt diffs.
This commit is contained in:
@@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Serialize)]
|
||||
pub struct Note {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
/// title if set, else the note's first body line — always present, so body-only
|
||||
/// notes still have something to be called. Derived, never stored.
|
||||
/// The note's NAME: its first non-blank body line, else its first checklist item.
|
||||
/// Always present, so every note has something to be called. Derived at read time,
|
||||
/// never stored.
|
||||
pub display_title: String,
|
||||
pub body: String,
|
||||
pub color: String,
|
||||
@@ -71,7 +71,6 @@ pub struct LinkPreview {
|
||||
#[derive(Serialize)]
|
||||
pub struct NoteRevision {
|
||||
pub id: String,
|
||||
pub title: Option<String>,
|
||||
pub body: String,
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
@@ -128,8 +127,6 @@ fn default_color() -> String {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NoteCreateInput {
|
||||
#[serde(default)]
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default = "default_color")]
|
||||
|
||||
@@ -93,8 +93,8 @@ mod tests {
|
||||
let when = Utc::now() - age;
|
||||
let stamped = when.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES (?1, 'T', 'B', ?2, ?2, 1, ?2)",
|
||||
"INSERT INTO notes (id, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES (?1, 'B', ?2, ?2, 1, ?2)",
|
||||
rusqlite::params![id, stamped],
|
||||
)
|
||||
.expect("insert");
|
||||
@@ -149,8 +149,8 @@ mod tests {
|
||||
fn an_untrashed_note_is_never_swept() {
|
||||
let conn = db();
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, created_at, updated_at, trashed)
|
||||
VALUES ('live', 'T', 'B', '2020-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z', 0)",
|
||||
"INSERT INTO notes (id, body, created_at, updated_at, trashed)
|
||||
VALUES ('live', 'B', '2020-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z', 0)",
|
||||
[],
|
||||
)
|
||||
.expect("insert");
|
||||
@@ -163,8 +163,8 @@ mod tests {
|
||||
// "Age unknown" must never resolve to "delete it".
|
||||
let conn = db();
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES ('weird', 'T', 'B', '2020-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z', 1, 'not a date')",
|
||||
"INSERT INTO notes (id, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES ('weird', 'B', '2020-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z', 1, 'not a date')",
|
||||
[],
|
||||
)
|
||||
.expect("insert");
|
||||
@@ -179,8 +179,8 @@ mod tests {
|
||||
let conn = db();
|
||||
let stamped = (Utc::now() - Duration::days(40)).to_rfc3339();
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES ('server', 'T', 'B', ?1, ?1, 1, ?1)",
|
||||
"INSERT INTO notes (id, body, created_at, updated_at, trashed, trashed_at)
|
||||
VALUES ('server', 'B', ?1, ?1, 1, ?1)",
|
||||
rusqlite::params![stamped],
|
||||
)
|
||||
.expect("insert");
|
||||
|
||||
@@ -170,6 +170,16 @@ const SCHEMA_V6: &str = r#"
|
||||
ALTER TABLE notes DROP COLUMN kind;
|
||||
"#;
|
||||
|
||||
// v7 (M13 step 3): the title field is gone. A note is a body plus optional items, and
|
||||
// its NAME is the first non-empty line of that body, falling back to its first item —
|
||||
// derived at read time, never stored (see store::display_title).
|
||||
//
|
||||
// note_revisions loses its copy for the same reason: a revision snapshots a body.
|
||||
const SCHEMA_V7: &str = r#"
|
||||
ALTER TABLE notes DROP COLUMN title;
|
||||
ALTER TABLE note_revisions DROP COLUMN title;
|
||||
"#;
|
||||
|
||||
/// Bring the database up to the latest schema. Idempotent.
|
||||
pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch("PRAGMA foreign_keys = ON;")?;
|
||||
@@ -198,5 +208,9 @@ pub fn migrate(conn: &Connection) -> rusqlite::Result<()> {
|
||||
conn.execute_batch(SCHEMA_V6)?;
|
||||
conn.execute_batch("PRAGMA user_version = 6;")?;
|
||||
}
|
||||
if version < 7 {
|
||||
conn.execute_batch(SCHEMA_V7)?;
|
||||
conn.execute_batch("PRAGMA user_version = 7;")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+66
-74
@@ -24,30 +24,26 @@ fn new_id() -> String {
|
||||
Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
/// title if non-empty, else the first non-blank body line — always a string.
|
||||
fn display_title(title: Option<&str>, body: &str) -> String {
|
||||
if let Some(t) = title {
|
||||
let t = t.trim();
|
||||
if !t.is_empty() {
|
||||
return t.to_string();
|
||||
}
|
||||
/// The note's NAME: its first non-blank body line, else its first checklist item.
|
||||
///
|
||||
/// Mirrors `derive_display_title` in the server's notes/helpers.py — one rule written
|
||||
/// twice, and they have to agree or a synced note is called different things on either
|
||||
/// side of the wire.
|
||||
///
|
||||
/// Pure, and given the items rather than fetching them: every caller has already
|
||||
/// loaded them, so a query here would be a second trip for something already in hand.
|
||||
fn display_title(body: &str, items: &[ChecklistItem]) -> String {
|
||||
if let Some(line) = body.lines().map(str::trim).find(|l| !l.is_empty()) {
|
||||
return line.to_string();
|
||||
}
|
||||
body.lines()
|
||||
.map(str::trim)
|
||||
.find(|l| !l.is_empty())
|
||||
items
|
||||
.iter()
|
||||
.map(|i| i.text.trim())
|
||||
.find(|t| !t.is_empty())
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn normalize_title(raw: &str) -> Option<String> {
|
||||
let t = raw.trim();
|
||||
if t.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(t.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_like(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
.replace('%', "\\%")
|
||||
@@ -139,32 +135,29 @@ fn load_previews(conn: &Connection, note_id: &str) -> rusqlite::Result<Vec<LinkP
|
||||
|
||||
fn load_note(conn: &Connection, id: &str) -> rusqlite::Result<Note> {
|
||||
let mut note = conn.query_row(
|
||||
"SELECT id, title, body, color, position, pinned, archived, trashed, remind_at, recurrence, created_at, updated_at, trashed_at
|
||||
"SELECT id, body, color, position, pinned, archived, trashed, remind_at, recurrence, created_at, updated_at, trashed_at
|
||||
FROM notes WHERE id = ?1",
|
||||
[id],
|
||||
|r| {
|
||||
let title: Option<String> = r.get(1)?;
|
||||
let body: String = r.get(2)?;
|
||||
let dt = display_title(title.as_deref(), &body);
|
||||
let body: String = r.get(1)?;
|
||||
Ok(Note {
|
||||
id: r.get(0)?,
|
||||
title,
|
||||
display_title: dt,
|
||||
display_title: String::new(), // filled below — it may need a query
|
||||
body,
|
||||
color: r.get(3)?,
|
||||
position: r.get(4)?,
|
||||
pinned: r.get(5)?,
|
||||
archived: r.get(6)?,
|
||||
trashed: r.get(7)?,
|
||||
deleted_at: r.get(12)?,
|
||||
remind_at: r.get(8)?,
|
||||
recurrence: r.get(9)?,
|
||||
color: r.get(2)?,
|
||||
position: r.get(3)?,
|
||||
pinned: r.get(4)?,
|
||||
archived: r.get(5)?,
|
||||
trashed: r.get(6)?,
|
||||
deleted_at: r.get(11)?,
|
||||
remind_at: r.get(7)?,
|
||||
recurrence: r.get(8)?,
|
||||
labels: Vec::new(),
|
||||
items: Vec::new(),
|
||||
attachments: Vec::new(),
|
||||
previews: Vec::new(),
|
||||
created_at: r.get(10)?,
|
||||
updated_at: r.get(11)?,
|
||||
created_at: r.get(9)?,
|
||||
updated_at: r.get(10)?,
|
||||
})
|
||||
},
|
||||
)?;
|
||||
@@ -172,6 +165,8 @@ fn load_note(conn: &Connection, id: &str) -> rusqlite::Result<Note> {
|
||||
note.items = load_items(conn, id)?;
|
||||
note.attachments = load_attachments(conn, id)?;
|
||||
note.previews = load_previews(conn, id)?;
|
||||
// After the items, because a body-only-empty note is named by its first one.
|
||||
note.display_title = display_title(¬e.body, ¬e.items);
|
||||
Ok(note)
|
||||
}
|
||||
|
||||
@@ -267,7 +262,7 @@ pub fn list_notes(conn: &Connection, q: &ListQuery) -> rusqlite::Result<Vec<Note
|
||||
|
||||
if let Some(f) = &q.facets {
|
||||
if let Some(text) = f.q.as_deref().filter(|s| !s.is_empty()) {
|
||||
sql.push_str(" AND (title LIKE ? ESCAPE '\\' OR body LIKE ? ESCAPE '\\')");
|
||||
sql.push_str(" AND body LIKE ? ESCAPE '\\'");
|
||||
let pat = format!("%{}%", escape_like(text));
|
||||
binds.push(pat.clone());
|
||||
binds.push(pat);
|
||||
@@ -321,23 +316,31 @@ pub fn reminders(conn: &Connection) -> rusqlite::Result<Vec<Note>> {
|
||||
}
|
||||
|
||||
pub fn titles(conn: &Connection) -> rusqlite::Result<Vec<TitleEntry>> {
|
||||
let mut stmt = conn.prepare("SELECT id, title, body FROM notes WHERE trashed = 0")?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
let title: Option<String> = r.get(1)?;
|
||||
let body: String = r.get(2)?;
|
||||
Ok(TitleEntry {
|
||||
id: r.get(0)?,
|
||||
title: display_title(title.as_deref(), &body),
|
||||
// Names come from `load_note` rather than from a bare row, because a note whose
|
||||
// body is empty is named by its first checklist item — which a row here doesn't
|
||||
// have. The command palette reads this; correctness beats one query per note at
|
||||
// personal scale.
|
||||
let ids: Vec<String> = {
|
||||
let mut stmt = conn.prepare("SELECT id FROM notes WHERE trashed = 0")?;
|
||||
let rows = stmt.query_map([], |r| r.get(0))?;
|
||||
rows.collect::<rusqlite::Result<Vec<String>>>()?
|
||||
};
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
let note = load_note(conn, id)?;
|
||||
Ok(TitleEntry {
|
||||
id: note.id,
|
||||
title: note.display_title,
|
||||
})
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn search(conn: &Connection, q: &str) -> rusqlite::Result<Vec<Note>> {
|
||||
let pat = format!("%{}%", escape_like(q));
|
||||
let ids: Vec<String> = {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id FROM notes WHERE trashed = 0 AND (title LIKE ?1 ESCAPE '\\' OR body LIKE ?1 ESCAPE '\\') ORDER BY updated_at DESC",
|
||||
"SELECT id FROM notes WHERE trashed = 0 AND body LIKE ?1 ESCAPE '\\' ORDER BY updated_at DESC",
|
||||
)?;
|
||||
let rows = stmt.query_map([&pat], |r| r.get::<_, String>(0))?;
|
||||
rows.collect::<rusqlite::Result<Vec<String>>>()?
|
||||
@@ -350,16 +353,15 @@ pub fn search(conn: &Connection, q: &str) -> rusqlite::Result<Vec<Note>> {
|
||||
pub fn create_note(conn: &Connection, input: &NoteCreateInput) -> rusqlite::Result<Note> {
|
||||
let id = new_id();
|
||||
let ts = now();
|
||||
let title = normalize_title(&input.title);
|
||||
let position: i64 = conn.query_row(
|
||||
"SELECT COALESCE(MAX(position), 0) + 1 FROM notes",
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, color, position, created_at, updated_at, dirty)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?6, 1)",
|
||||
params![id, title, input.body, input.color, position, ts],
|
||||
"INSERT INTO notes (id, body, color, position, created_at, updated_at, dirty)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?5, 1)",
|
||||
params![id, input.body, input.color, position, ts],
|
||||
)?;
|
||||
if let Some(items) = &input.items {
|
||||
for (i, text) in items.iter().enumerate() {
|
||||
@@ -374,13 +376,11 @@ pub fn create_note(conn: &Connection, input: &NoteCreateInput) -> rusqlite::Resu
|
||||
}
|
||||
|
||||
fn snapshot_revision(conn: &Connection, id: &str) -> rusqlite::Result<()> {
|
||||
let (title, body): (Option<String>, String) =
|
||||
conn.query_row("SELECT title, body FROM notes WHERE id = ?1", [id], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?))
|
||||
})?;
|
||||
let body: String =
|
||||
conn.query_row("SELECT body FROM notes WHERE id = ?1", [id], |r| r.get(0))?;
|
||||
conn.execute(
|
||||
"INSERT INTO note_revisions (id, note_id, title, body, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
params![new_id(), id, title, body, now()],
|
||||
"INSERT INTO note_revisions (id, note_id, body, created_at) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![new_id(), id, body, now()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -391,20 +391,13 @@ pub fn update_note(conn: &Connection, id: &str, changes: &Value) -> rusqlite::Re
|
||||
.as_object()
|
||||
.ok_or_else(|| rusqlite::Error::InvalidParameterName("changes must be an object".into()))?;
|
||||
|
||||
// Snapshot the pre-edit title/body once if either is being changed (version history).
|
||||
if obj.contains_key("title") || obj.contains_key("body") {
|
||||
// Snapshot the pre-edit body before changing it (version history).
|
||||
if obj.contains_key("body") {
|
||||
snapshot_revision(conn, id)?;
|
||||
}
|
||||
|
||||
for (k, v) in obj {
|
||||
match k.as_str() {
|
||||
"title" => {
|
||||
let norm = v.as_str().and_then(normalize_title);
|
||||
conn.execute(
|
||||
"UPDATE notes SET title = ?1 WHERE id = ?2",
|
||||
params![norm, id],
|
||||
)?;
|
||||
}
|
||||
"body" => {
|
||||
let body = v.as_str().unwrap_or("");
|
||||
conn.execute(
|
||||
@@ -653,28 +646,27 @@ pub fn set_pref(conn: &Connection, key: &str, value: &str) -> rusqlite::Result<(
|
||||
|
||||
pub fn revisions(conn: &Connection, id: &str) -> rusqlite::Result<Vec<NoteRevision>> {
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT id, title, body, created_at FROM note_revisions WHERE note_id = ?1 ORDER BY created_at DESC")?;
|
||||
.prepare("SELECT id, body, created_at FROM note_revisions WHERE note_id = ?1 ORDER BY created_at DESC")?;
|
||||
let rows = stmt.query_map([id], |r| {
|
||||
Ok(NoteRevision {
|
||||
id: r.get(0)?,
|
||||
title: r.get(1)?,
|
||||
body: r.get(2)?,
|
||||
created_at: r.get(3)?,
|
||||
body: r.get(1)?,
|
||||
created_at: r.get(2)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect()
|
||||
}
|
||||
|
||||
pub fn restore_revision(conn: &Connection, id: &str, rev_id: &str) -> rusqlite::Result<Note> {
|
||||
let (title, body): (Option<String>, String) = conn.query_row(
|
||||
"SELECT title, body FROM note_revisions WHERE id = ?1 AND note_id = ?2",
|
||||
let body: String = conn.query_row(
|
||||
"SELECT body FROM note_revisions WHERE id = ?1 AND note_id = ?2",
|
||||
params![rev_id, id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
snapshot_revision(conn, id)?;
|
||||
conn.execute(
|
||||
"UPDATE notes SET title = ?1, body = ?2 WHERE id = ?3",
|
||||
params![title, body, id],
|
||||
"UPDATE notes SET body = ?1 WHERE id = ?2",
|
||||
params![body, id],
|
||||
)?;
|
||||
sync_tags(conn, id, &body)?;
|
||||
touch(conn, id)?;
|
||||
|
||||
@@ -240,12 +240,11 @@ fn upsert_note(conn: &Connection, note: &wire::Note) -> rusqlite::Result<()> {
|
||||
// `created_at` is deliberately absent from the UPDATE clause: a note's birth time
|
||||
// never changes, and the server's copy is the same value anyway.
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, color, position, pinned, archived,
|
||||
"INSERT INTO notes (id, body, color, position, pinned, archived,
|
||||
trashed, remind_at, recurrence, created_at, updated_at,
|
||||
sync_revision, trashed_at, dirty)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, 0)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 0)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
body = excluded.body,
|
||||
color = excluded.color,
|
||||
position = excluded.position,
|
||||
@@ -260,7 +259,6 @@ fn upsert_note(conn: &Connection, note: &wire::Note) -> rusqlite::Result<()> {
|
||||
dirty = 0",
|
||||
params![
|
||||
note.id,
|
||||
note.title,
|
||||
note.body,
|
||||
note.color,
|
||||
note.position,
|
||||
@@ -496,7 +494,6 @@ mod tests {
|
||||
fn note(id: &str, revision: i64) -> wire::Note {
|
||||
wire::Note {
|
||||
id: id.to_string(),
|
||||
title: Some("Title".into()),
|
||||
body: "Body".into(),
|
||||
color: "default".into(),
|
||||
position: 0,
|
||||
|
||||
+13
-20
@@ -62,8 +62,6 @@ pub struct Change {
|
||||
pub op: &'static str,
|
||||
pub edited_at: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub body: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -97,7 +95,6 @@ impl Change {
|
||||
id,
|
||||
op: "delete",
|
||||
edited_at,
|
||||
title: None,
|
||||
body: None,
|
||||
color: None,
|
||||
pinned: None,
|
||||
@@ -197,7 +194,6 @@ fn collect_labels(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rus
|
||||
name: Some(r.get(1)?),
|
||||
color: Some(r.get(2)?),
|
||||
edited_at: r.get(3)?,
|
||||
title: None,
|
||||
body: None,
|
||||
pinned: None,
|
||||
archived: None,
|
||||
@@ -233,7 +229,6 @@ fn collect_notes(conn: &Connection, out: &mut Vec<Change>, limit: usize) -> rusq
|
||||
/// The note's own columns. A named struct rather than a twelve-wide tuple so the
|
||||
/// field-to-column mapping stays readable at the call site.
|
||||
struct NoteRow {
|
||||
title: Option<String>,
|
||||
body: String,
|
||||
color: String,
|
||||
position: i64,
|
||||
@@ -248,23 +243,22 @@ struct NoteRow {
|
||||
|
||||
fn note_row(conn: &Connection, id: &str) -> rusqlite::Result<NoteRow> {
|
||||
conn.query_row(
|
||||
"SELECT title, body, color, position, pinned, archived, trashed,
|
||||
"SELECT body, color, position, pinned, archived, trashed,
|
||||
remind_at, recurrence, created_at, updated_at
|
||||
FROM notes WHERE id = ?1",
|
||||
params![id],
|
||||
|r| {
|
||||
Ok(NoteRow {
|
||||
title: r.get(0)?,
|
||||
body: r.get(1)?,
|
||||
color: r.get(2)?,
|
||||
position: r.get(3)?,
|
||||
pinned: r.get::<_, i64>(4)? != 0,
|
||||
archived: r.get::<_, i64>(5)? != 0,
|
||||
trashed: r.get::<_, i64>(6)? != 0,
|
||||
remind_at: r.get(7)?,
|
||||
recurrence: r.get(8)?,
|
||||
created_at: r.get(9)?,
|
||||
updated_at: r.get(10)?,
|
||||
body: r.get(0)?,
|
||||
color: r.get(1)?,
|
||||
position: r.get(2)?,
|
||||
pinned: r.get::<_, i64>(3)? != 0,
|
||||
archived: r.get::<_, i64>(4)? != 0,
|
||||
trashed: r.get::<_, i64>(5)? != 0,
|
||||
remind_at: r.get(6)?,
|
||||
recurrence: r.get(7)?,
|
||||
created_at: r.get(8)?,
|
||||
updated_at: r.get(9)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -303,7 +297,6 @@ fn note_change(conn: &Connection, id: &str) -> rusqlite::Result<Change> {
|
||||
// The local `updated_at` IS the client's edit time, which is what the
|
||||
// server's last-write-wins comparison runs against.
|
||||
edited_at: row.updated_at,
|
||||
title: row.title,
|
||||
body: Some(row.body),
|
||||
color: Some(row.color),
|
||||
pinned: Some(row.pinned),
|
||||
@@ -528,9 +521,9 @@ mod tests {
|
||||
|
||||
fn seed_note(conn: &Connection, id: &str, dirty: i64) {
|
||||
conn.execute(
|
||||
"INSERT INTO notes (id, title, body, color, position, pinned, archived,
|
||||
"INSERT INTO notes (id, body, color, position, pinned, archived,
|
||||
trashed, created_at, updated_at, sync_revision, dirty)
|
||||
VALUES (?1, 'T', 'B', 'default', 0, 0, 0, 0,
|
||||
VALUES (?1, 'B', 'default', 0, 0, 0, 0,
|
||||
'2026-07-26T00:00:00.000Z', '2026-07-26T00:00:00.000Z', 3, ?2)",
|
||||
params![id, dirty],
|
||||
)
|
||||
|
||||
@@ -24,8 +24,6 @@ pub struct ChangesPage {
|
||||
pub struct Note {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub title: Option<String>,
|
||||
#[serde(default)]
|
||||
pub body: String,
|
||||
#[serde(default = "default_color")]
|
||||
pub color: String,
|
||||
|
||||
Reference in New Issue
Block a user