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:
@@ -27,7 +27,6 @@ const notes = useNotesStore();
|
||||
const config = useConfigStore();
|
||||
|
||||
const noteId = ref<string | null>(props.note?.id ?? null);
|
||||
const title = ref(props.note?.title ?? "");
|
||||
const body = ref(props.note?.body ?? props.initialBody);
|
||||
const color = ref<NoteColor>(props.note?.color ?? "default");
|
||||
const labelList = ref<NoteLabel[]>(props.note ? [...props.note.labels] : []);
|
||||
@@ -42,14 +41,13 @@ const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const uploadError = ref("");
|
||||
|
||||
// Baseline for edit-mode change detection (save only when text actually changed).
|
||||
const baseline = ref<{ title: string | null; body: string; color: NoteColor }>({
|
||||
title: props.note?.title ?? null,
|
||||
const baseline = ref<{ body: string; color: NoteColor }>({
|
||||
body: props.note?.body ?? "",
|
||||
color: (props.note?.color ?? "default") as NoteColor,
|
||||
});
|
||||
|
||||
const isCreate = computed(() => noteId.value === null);
|
||||
const hasContent = computed(() => title.value.trim() !== "" || body.value.trim() !== "");
|
||||
const hasContent = computed(() => body.value.trim() !== "");
|
||||
// Rich features need a saved note; in compose they light up once there's content.
|
||||
const richEnabled = computed(() => !isCreate.value || hasContent.value);
|
||||
|
||||
@@ -57,7 +55,6 @@ const richEnabled = computed(() => !isCreate.value || hasContent.value);
|
||||
// template can read attachments/items/remind_at uniformly.
|
||||
const draftNote = computed<Note>(() => ({
|
||||
id: "",
|
||||
title: title.value.trim() || null,
|
||||
display_title: "",
|
||||
body: body.value,
|
||||
color: color.value,
|
||||
@@ -96,19 +93,18 @@ watch(
|
||||
() => props.note,
|
||||
(n) => {
|
||||
noteId.value = n?.id ?? null;
|
||||
title.value = n?.title ?? "";
|
||||
body.value = n?.body ?? "";
|
||||
color.value = (n?.color ?? "default") as NoteColor;
|
||||
labelList.value = n ? [...n.labels] : [];
|
||||
baseline.value = { title: n?.title ?? null, body: n?.body ?? "", color: (n?.color ?? "default") as NoteColor };
|
||||
baseline.value = { body: n?.body ?? "", color: (n?.color ?? "default") as NoteColor };
|
||||
},
|
||||
);
|
||||
|
||||
// ---- persistence ----
|
||||
async function createFromFields(): Promise<void> {
|
||||
const created = await notes.create({ title: title.value, body: body.value, color: color.value });
|
||||
const created = await notes.create({ body: body.value, color: color.value });
|
||||
noteId.value = created.id;
|
||||
baseline.value = { title: created.title, body: created.body, color: created.color as NoteColor };
|
||||
baseline.value = { body: created.body, color: created.color as NoteColor };
|
||||
}
|
||||
|
||||
// Ensure a persisted note exists (for rich actions mid-compose). Returns its id, or
|
||||
@@ -135,12 +131,12 @@ async function flush(): Promise<void> {
|
||||
}
|
||||
const b = baseline.value;
|
||||
const nextBody = body.value;
|
||||
const changed = (title.value.trim() || null) !== b.title || nextBody !== b.body || color.value !== b.color;
|
||||
const changed = nextBody !== b.body || color.value !== b.color;
|
||||
if (!changed) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await notes.saveEdit(noteId.value as string, { title: title.value, body: nextBody, color: color.value });
|
||||
baseline.value = { title: title.value.trim() || null, body: nextBody, color: color.value };
|
||||
await notes.saveEdit(noteId.value as string, { body: nextBody, color: color.value });
|
||||
baseline.value = { body: nextBody, color: color.value };
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -148,12 +144,11 @@ async function flush(): Promise<void> {
|
||||
|
||||
function resetCompose(): void {
|
||||
noteId.value = null;
|
||||
title.value = "";
|
||||
body.value = "";
|
||||
color.value = "default";
|
||||
labelList.value = [];
|
||||
checklistOpen.value = false;
|
||||
baseline.value = { title: null, body: "", color: "default" };
|
||||
baseline.value = { body: "", color: "default" };
|
||||
uploadError.value = "";
|
||||
}
|
||||
|
||||
@@ -249,12 +244,6 @@ function onBodyKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
function onTitleEnter(e: KeyboardEvent) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey && isCreate.value) void commitAndContinue();
|
||||
else bodyInput.value?.focus();
|
||||
}
|
||||
|
||||
// ---- reminder ----
|
||||
const reminderLocal = computed(() => toLocalInput(liveNote.value.remind_at));
|
||||
async function onReminderChange(e: Event) {
|
||||
@@ -413,10 +402,9 @@ async function restoreRevisionAt(revId: string) {
|
||||
const id = noteId.value;
|
||||
if (!id) return;
|
||||
const updated = await notes.restoreRevision(id, revId);
|
||||
title.value = updated.title ?? "";
|
||||
body.value = updated.body;
|
||||
color.value = updated.color;
|
||||
baseline.value = { title: updated.title, body: updated.body, color: updated.color };
|
||||
baseline.value = { body: updated.body, color: updated.color };
|
||||
void loadRevisions(); // the pre-restore state became a new revision
|
||||
}
|
||||
function revLabel(iso: string | null): string {
|
||||
@@ -424,9 +412,7 @@ function revLabel(iso: string | null): string {
|
||||
return new Date(iso).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
||||
}
|
||||
function revPreview(rev: NoteRevision): string {
|
||||
const t = (rev.title ?? "").trim();
|
||||
const b = rev.body.trim().replace(/\s+/g, " ");
|
||||
const s = t && b ? `${t} — ${b}` : t || b;
|
||||
const s = rev.body.trim().replace(/\s+/g, " ");
|
||||
if (!s) return "(empty)";
|
||||
return s.length > 80 ? `${s.slice(0, 80)}…` : s;
|
||||
}
|
||||
@@ -536,14 +522,6 @@ function revPreview(rev: NoteRevision): string {
|
||||
</div>
|
||||
<p v-if="unfurlError" class="text-xs text-red-600 dark:text-red-400">{{ unfurlError }}</p>
|
||||
|
||||
<input
|
||||
v-model="title"
|
||||
type="text"
|
||||
placeholder="Title (optional)"
|
||||
class="w-full bg-transparent text-base font-semibold outline-none placeholder:text-neutral-400"
|
||||
@keydown.enter="onTitleEnter"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
ref="bodyInput"
|
||||
v-model="body"
|
||||
|
||||
Reference in New Issue
Block a user