From 6c916cc257458ebf1d8a0ce2c8074c7edc74018e Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 9 Oct 2026 17:49:49 +0200 Subject: [PATCH] fix(collab): a note category shows the same colour on the phone as on desktop When the notes of one category carried two colours (a category recolour that failed halfway, or a note written over the API), the desktop panel painted the category in the colour of the last such note in the list while the phone notes tab used the first one, so the same category had different colours on the two devices. The phone's buildCategoryColorMap took the first colour on the stated assumption that every note in a category has the same colour, so first seen equals all seen; it was meant to match the desktop and only differed where that assumption breaks. The desktop rule (since v2.6.0) is the original. buildCategoryColorMap now keeps the last colour seen and the desktop panel builds its map from the same function, merged over its empty categories as before. Red before the fix: tests/unit/mobile/trip/collabModel.test.ts FE-MOB-CLBM-023 failed with "expected { Ideas: '#ef4444' } to deeply equal { Ideas: '#10b981' }". Changed assertion in tests/unit/mobile/trip/collabModel.test.ts FE-MOB-CLBM-011 (renamed from "keeps the first colour seen per category" to "keeps the last colour seen per category"): the expected map was { Ideas: '#ef4444' } and is now { Ideas: '#10b981' }. --- client/src/components/Collab/CollabNotes.tsx | 12 +++++------- client/src/components/Collab/collabModel.ts | 13 ++++++++----- client/tests/unit/mobile/trip/collabModel.test.ts | 13 +++++++++++-- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/client/src/components/Collab/CollabNotes.tsx b/client/src/components/Collab/CollabNotes.tsx index 95e540576..1eb336f2a 100644 --- a/client/src/components/Collab/CollabNotes.tsx +++ b/client/src/components/Collab/CollabNotes.tsx @@ -18,6 +18,7 @@ import type { User } from '../../types' import type { CollabNote } from './CollabNotes.types' import { announceNoteFilesChanged, useCollabNotesData } from './useCollabNotesData' import { FONT, NOTE_COLORS } from './CollabNotes.constants' +import { buildCategoryColorMap } from './collabModel' import { NoteFormModal } from './CollabNotesFormModal' import { CategorySettingsModal } from './CollabNotesCategorySettingsModal' import { NoteCard } from './CollabNotesCard' @@ -68,13 +69,10 @@ function useCollabNotes({ tripId, currentUser }: CollabNotesProps) { } // Category colors: from notes first, then from empty categories - const categoryColors = useMemo(() => { - const map = { ...emptyCategories } - for (const n of notes) { - if (n.category && n.color) map[n.category] = n.color - } - return map - }, [notes, emptyCategories]) + const categoryColors = useMemo( + () => ({ ...emptyCategories, ...buildCategoryColorMap(notes) }), + [notes, emptyCategories] + ) const getCategoryColor = (cat) => { if (!cat) return NOTE_COLORS[0].value diff --git a/client/src/components/Collab/collabModel.ts b/client/src/components/Collab/collabModel.ts index 59c2b6b3e..f58d2c2c4 100644 --- a/client/src/components/Collab/collabModel.ts +++ b/client/src/components/Collab/collabModel.ts @@ -150,13 +150,16 @@ export interface CollabNoteData { /** Category swatch palette, the same 6 hex values the desktop picker uses. */ export const NOTE_COLORS = ['#6366f1', '#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6']; -/** One color per category, taken from whatever any note in that category - * already has stored (every note in a category is written with the same - * color at create time, so first-seen === all-seen in practice). */ -export function buildCategoryColorMap(notes: CollabNoteData[]): Record { +/** One color per category, taken from the notes in it, for the desktop panel and the + * phone tab alike. Every note in a category is normally written with the same color, + * but a recolour that failed halfway or a note written over the API can leave two; + * then the last one in list order wins. */ +export function buildCategoryColorMap( + notes: readonly { category?: string | null; color?: string | null }[] +): Record { const map: Record = {}; for (const n of notes) { - if (n.category && n.color && !map[n.category]) map[n.category] = n.color; + if (n.category && n.color) map[n.category] = n.color; } return map; } diff --git a/client/tests/unit/mobile/trip/collabModel.test.ts b/client/tests/unit/mobile/trip/collabModel.test.ts index e676256df..4ea2f72c6 100644 --- a/client/tests/unit/mobile/trip/collabModel.test.ts +++ b/client/tests/unit/mobile/trip/collabModel.test.ts @@ -179,14 +179,23 @@ describe('collabModel — chat', () => { }); describe('collabModel — notes', () => { - it('FE-MOB-CLBM-011: buildCategoryColorMap keeps the first colour seen per category', () => { + it('FE-MOB-CLBM-011: buildCategoryColorMap keeps the last colour seen per category', () => { const notes = [ note({ id: 1, category: 'Ideas', color: '#ef4444' }), note({ id: 2, category: 'Ideas', color: '#10b981' }), note({ id: 3, category: 'Food', color: null }), note({ id: 4, category: null, color: '#3b82f6' }), ]; - expect(buildCategoryColorMap(notes)).toEqual({ Ideas: '#ef4444' }); + expect(buildCategoryColorMap(notes)).toEqual({ Ideas: '#10b981' }); + }); + + it('FE-MOB-CLBM-023: a category whose notes disagree takes the colour the desktop panel shows', () => { + // A category recolour that failed halfway leaves its notes on two colours. + const notes = [ + note({ id: 1, category: 'Ideas', color: '#ef4444' }), + note({ id: 2, category: 'Ideas', color: '#10b981' }), + ]; + expect(buildCategoryColorMap(notes)).toEqual({ Ideas: '#10b981' }); }); it('FE-MOB-CLBM-012: getCategoryColor round-robins fresh categories onto the palette', () => {