notes: color leaves the model, the wire and all three surfaces
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 19s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 2m28s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m1s
Step 3 of M315, and the destructive half. Steps 1 and 2 stopped every read of this field: a card is one neutral surface per theme, and the only coloured thing on a board is a tag. What was left was a column written by a picker and read by nothing. Rule 22 — the old path comes out completely. No flag, no fallback, no "override if set". Server: the column, the `?color=` facet, the create/update/serialise paths, the sync assignment, the front-matter line, and Keep's colour map. Alembic 0029 drops it and sweeps `"color"` out of stored saved-filter params — a view that silently filtered on a field the app no longer has would return nothing and never say why. That sweep is Python, not `params::jsonb - 'color'`, because Postgres has no try-cast and one malformed blob would abort a migration that is running over somebody's saved views. `NOTE_COLORS` moves from `models/note.py` to `colors.py`. A palette defined on the model that lost one is an invitation to put the column back; labels still name a colour, so the vocabulary belongs where the normalizer already is. Core: the field, the facet, the `NoteCreateInput`, and every read and write in store/push/pull. Local schema v9 drops the column and does the same saved-filter sweep, guarded on `json_valid` so a corrupt blob loses a key rather than becoming NULL. The uniffi layer drops `NoteEdit::Color` and `NoteDraft.color` with it. Web: `ColorPicker.vue`, the per-card swatch popover and its stylesheet rule, the FilterBar colour row, the facet in the query round-trip, and the colour half of the editor's baseline-and-save. Android: the `ColorSheet`, the `Picker.COLOR` case, the toolbar's swatch dot, `EditorAction.SetColor`. ## The protocol: v4, and the floor deliberately stays at 3 Checked against `compat.rs` and the push handler rather than trusting the `#[serde(default)]` annotation, because the v2 precedent points the other way: v2 dropped `kind` and `title` and DID raise both floors, on the rule that dropping a field a client sends and expects back is breaking. `color` fails the second half of that test. A v3 client reading a v4 note gets `"default"` from its own serde default and draws the colour it derives locally — the board it drew yesterday. A v3 client pushing `color` has the key ignored, since `_assign_note_fields` reads its payload key by key and never validates the shape. Neither direction errors and neither shows anything wrong. `title` was the note's NAME; this is a field that no longer renders. So `SYNC_PROTOCOL_VERSION` and `CLIENT_PROTOCOL_VERSION` go to 4, and both floors stay at 3. `docs/sync.md` carries the reasoning and the per-version history, and its push example is brought back in line — it still listed `title`, `kind` and `items`, all gone before this. Import stays tolerant: a pre-M315 export or a Keep takeout carrying `color:` imports fine, the key simply read past. Old exports must still import. #3041 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
import ColorPicker from "./ColorPicker.vue";
|
||||
import Icon from "./Icon.vue";
|
||||
import LabelPicker from "./LabelPicker.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
@@ -9,7 +8,7 @@ import { fromLocalInput, toLocalInput } from "../notes/datetime";
|
||||
import { takeMorphOrigin } from "../composables/useEditorMorph";
|
||||
import { prefersReducedMotion } from "../composables/useReducedMotion";
|
||||
import type { Note, NoteLabel, NoteRevision } from "../stores/notes";
|
||||
import { labelChipClasses, type NoteColor } from "../notes/colors";
|
||||
import { labelChipClasses } from "../notes/colors";
|
||||
import {
|
||||
afterEnter,
|
||||
type EditorBlock,
|
||||
@@ -41,7 +40,6 @@ const body = computed(() => joinBlocks(blocks.value));
|
||||
function setBody(text: string): void {
|
||||
blocks.value = splitBlocks(text);
|
||||
}
|
||||
const color = ref<NoteColor>(props.note?.color ?? "default");
|
||||
const labelList = ref<NoteLabel[]>(props.note ? [...props.note.labels] : []);
|
||||
// Whether this editor is showing the checklist. A note HAS a checklist (M13 step 2)
|
||||
// rather than BEING one, so this is a view flag, not a property of the note: it turns
|
||||
@@ -80,10 +78,7 @@ const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const uploadError = ref("");
|
||||
|
||||
// Baseline for edit-mode change detection (save only when text actually changed).
|
||||
const baseline = ref<{ body: string; color: NoteColor }>({
|
||||
body: props.note?.body ?? "",
|
||||
color: (props.note?.color ?? "default") as NoteColor,
|
||||
});
|
||||
const baseline = ref<{ body: string }>({ body: props.note?.body ?? "" });
|
||||
|
||||
const isCreate = computed(() => noteId.value === null);
|
||||
const hasContent = computed(() => body.value.trim() !== "");
|
||||
@@ -96,7 +91,6 @@ const draftNote = computed<Note>(() => ({
|
||||
id: "",
|
||||
display_title: "",
|
||||
body: body.value,
|
||||
color: color.value,
|
||||
position: 0,
|
||||
pinned: false,
|
||||
archived: false,
|
||||
@@ -124,17 +118,16 @@ watch(
|
||||
(n) => {
|
||||
noteId.value = n?.id ?? null;
|
||||
setBody(n?.body ?? "");
|
||||
color.value = (n?.color ?? "default") as NoteColor;
|
||||
labelList.value = n ? [...n.labels] : [];
|
||||
baseline.value = { body: n?.body ?? "", color: (n?.color ?? "default") as NoteColor };
|
||||
baseline.value = { body: n?.body ?? "" };
|
||||
},
|
||||
);
|
||||
|
||||
// ---- persistence ----
|
||||
async function createFromFields(): Promise<void> {
|
||||
const created = await notes.create({ body: body.value, color: color.value });
|
||||
const created = await notes.create({ body: body.value });
|
||||
noteId.value = created.id;
|
||||
baseline.value = { body: created.body, color: created.color as NoteColor };
|
||||
baseline.value = { body: created.body };
|
||||
}
|
||||
|
||||
// Ensure a persisted note exists (for rich actions mid-compose). Returns its id, or
|
||||
@@ -161,12 +154,12 @@ async function flush(): Promise<void> {
|
||||
}
|
||||
const b = baseline.value;
|
||||
const nextBody = body.value;
|
||||
const changed = nextBody !== b.body || color.value !== b.color;
|
||||
const changed = nextBody !== b.body;
|
||||
if (!changed) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await notes.saveEdit(noteId.value as string, { body: nextBody, color: color.value });
|
||||
baseline.value = { body: nextBody, color: color.value };
|
||||
await notes.saveEdit(noteId.value as string, { body: nextBody });
|
||||
baseline.value = { body: nextBody };
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -175,9 +168,8 @@ async function flush(): Promise<void> {
|
||||
function resetCompose(): void {
|
||||
noteId.value = null;
|
||||
setBody("");
|
||||
color.value = "default";
|
||||
labelList.value = [];
|
||||
baseline.value = { body: "", color: "default" };
|
||||
baseline.value = { body: "" };
|
||||
uploadError.value = "";
|
||||
}
|
||||
|
||||
@@ -468,8 +460,7 @@ async function restoreRevisionAt(revId: string) {
|
||||
if (!id) return;
|
||||
const updated = await notes.restoreRevision(id, revId);
|
||||
setBody(updated.body);
|
||||
color.value = updated.color;
|
||||
baseline.value = { body: updated.body, color: updated.color };
|
||||
baseline.value = { body: updated.body };
|
||||
void loadRevisions(); // the pre-restore state became a new revision
|
||||
}
|
||||
function revLabel(iso: string | null): string {
|
||||
@@ -713,8 +704,10 @@ function revPreview(rev: NoteRevision): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 border-t border-neutral-100 px-3 py-2 dark:border-neutral-800">
|
||||
<ColorPicker v-model="color" />
|
||||
<!-- `justify-end`, not `justify-between`: the colour picker sat on the left of
|
||||
this row until M315 and the row was balanced around it. With one child left,
|
||||
`between` would push the actions to the far left of a full-width bar. -->
|
||||
<div class="flex items-center justify-end gap-2 border-t border-neutral-100 px-3 py-2 dark:border-neutral-800">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button
|
||||
v-if="richEnabled && !liveNote.trashed"
|
||||
|
||||
Reference in New Issue
Block a user