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:
+28
-21
@@ -51,9 +51,10 @@ def test_all_note_routes_registered(app):
|
||||
|
||||
def test_is_empty_note():
|
||||
assert is_empty_note(None, None)
|
||||
assert is_empty_note("", " ")
|
||||
assert not is_empty_note("title", "")
|
||||
assert not is_empty_note("", "body")
|
||||
assert is_empty_note(" ", [])
|
||||
assert not is_empty_note("body")
|
||||
# A note that is only a checklist is not empty — it just has nothing in its body.
|
||||
assert not is_empty_note("", ["milk"])
|
||||
|
||||
|
||||
def test_normalize_color():
|
||||
@@ -69,9 +70,9 @@ def test_palette_has_core_colors():
|
||||
|
||||
|
||||
def test_serialize_shape():
|
||||
n = Note(title="t", body="b", color="blue", pinned=True, archived=False)
|
||||
n = Note(body="b", color="blue", pinned=True, archived=False)
|
||||
s = n.serialize()
|
||||
assert s["title"] == "t"
|
||||
assert "title" not in s # there is no title field any more (M13 step 3)
|
||||
assert s["body"] == "b"
|
||||
assert s["color"] == "blue"
|
||||
assert s["pinned"] is True
|
||||
@@ -122,30 +123,34 @@ async def test_reorder_requires_auth(app):
|
||||
# notice a route coming back, and the removal is one commit rather than a fossil.
|
||||
|
||||
|
||||
def test_derive_display_title_explicit_wins():
|
||||
assert derive_display_title("My Title", "some body line") == "My Title"
|
||||
assert derive_display_title(" Padded ", "body") == "Padded"
|
||||
|
||||
|
||||
def test_derive_display_title_from_first_body_line():
|
||||
assert derive_display_title(None, "first line\nsecond line") == "first line"
|
||||
assert derive_display_title("", " spaced first \nnext") == "spaced first"
|
||||
def test_derive_display_title_is_the_first_body_line():
|
||||
assert derive_display_title("first line\nsecond line") == "first line"
|
||||
assert derive_display_title(" spaced first \nnext") == "spaced first"
|
||||
# leading blank/whitespace lines are skipped to the first line with content
|
||||
assert derive_display_title(None, "\n \nreal line\nmore") == "real line"
|
||||
# a whitespace-only title falls through to the body
|
||||
assert derive_display_title(" ", "body wins") == "body wins"
|
||||
assert derive_display_title("\n \nreal line\nmore") == "real line"
|
||||
|
||||
|
||||
def test_derive_display_title_falls_back_to_the_first_item():
|
||||
# What step 2 bought: a note that is only a checklist still has a name. Without
|
||||
# this it would have none at all, which is why the title could not go first.
|
||||
assert derive_display_title("", "milk") == "milk"
|
||||
assert derive_display_title(" \n ", " eggs ") == "eggs"
|
||||
# The body still wins when it has anything to say.
|
||||
assert derive_display_title("shopping", "milk") == "shopping"
|
||||
|
||||
|
||||
def test_derive_display_title_empty():
|
||||
assert derive_display_title(None, None) == ""
|
||||
assert derive_display_title("", "") == ""
|
||||
assert derive_display_title(" ", " \n ") == ""
|
||||
assert derive_display_title(None) == ""
|
||||
assert derive_display_title("") == ""
|
||||
assert derive_display_title(" \n ", None) == ""
|
||||
assert derive_display_title(" \n ", " ") == ""
|
||||
|
||||
|
||||
def test_derive_display_title_caps_length():
|
||||
long = "x" * 300
|
||||
assert derive_display_title(None, long) == "x" * 200
|
||||
assert derive_display_title(long, "body") == "x" * 200
|
||||
assert derive_display_title(long) == "x" * 200
|
||||
# the item fallback is capped on the same rule
|
||||
assert derive_display_title("", long) == "x" * 200
|
||||
|
||||
|
||||
def test_parse_tags():
|
||||
@@ -371,6 +376,8 @@ def test_native_spec_roundtrip_fields():
|
||||
"attachments": [{"file": "attachments/ab/img.png", "mime": "image/png"}],
|
||||
}
|
||||
spec = _native_spec(n)
|
||||
# The spec still CARRIES a title — an export taken before M13 has one, and
|
||||
# _create_imported_note folds it into the body rather than dropping it.
|
||||
assert spec["title"] == "T"
|
||||
assert spec["body"] == "b"
|
||||
assert spec["color"] == "blue"
|
||||
|
||||
Reference in New Issue
Block a user