From 4784a1e5ddf6cff56e2b34914eda6c7499372c26 Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:38:14 +0200 Subject: [PATCH] fix(quality): clear the SonarQube findings on the 4.0.0 branch (#2062) The day-swipe move handler mixed classifying the gesture with following the finger, which put it at cognitive complexity 20. The classifying half is a pure question about a gesture, so it moves out as classifyLock: null keeps waiting for the next move, dead and h end the same way they did before. Also reads the reduce-motion flag through dataset, seeds the closest-year reduce with years[0] so it cannot throw if the empty guard above it ever loosens, and compares the in-flight preview entry against undefined instead of leaning on a promise being truthy. --- .../screens/trip/plan/useMPlanDaySwipe.ts | 42 ++++++++++++------- client/src/vacay/yearWindow.ts | 4 +- server/src/nest/collab/collab.service.ts | 2 +- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/client/src/mobile/screens/trip/plan/useMPlanDaySwipe.ts b/client/src/mobile/screens/trip/plan/useMPlanDaySwipe.ts index cc1cf4032..ceea2097c 100644 --- a/client/src/mobile/screens/trip/plan/useMPlanDaySwipe.ts +++ b/client/src/mobile/screens/trip/plan/useMPlanDaySwipe.ts @@ -79,11 +79,34 @@ function band(dx: number, free: number, factor: number, max: number): number { return Math.sign(dx) * Math.min(a <= free ? a : free + (a - free) * factor, max) } +/** + * Which way this finger is going, once it has moved far enough to tell. + * `null` means it has not, so the caller waits for the next move. + */ +function classifyLock( + g: Gesture, + dx: number, + dy: number, + now: number, + card: HTMLElement | null, + s: { dragging: boolean; editing: boolean }, +): Lock { + // The browser has already committed this gesture to scrolling, so obey it. + // scrollLeft counts too: overflow-y-auto on its own leaves overflow-x + // computing to auto, and a wide markdown table in a day note fills it. + if (card && (card.scrollTop !== g.scrollTop0 || card.scrollLeft !== g.scrollLeft0)) return 'dead' + if (Math.abs(dx) <= CLASSIFY_PX && Math.abs(dy) <= CLASSIFY_PX) return null + // Edit mode only: past the bridge's press window this finger belongs to a + // reorder drag. Go mode has no bridge, so a slow swipe still counts. + if (s.dragging || (s.editing && now - g.t0 > PRESS_WINDOW_MS)) return 'dead' + return Math.abs(dx) >= Math.abs(dy) * AXIS_BIAS ? 'h' : 'dead' +} + /** The media query plus the app's own appearance toggle, which writes the same * attribute PluginFrame reads. */ function reducedMotion(): boolean { if (typeof window === 'undefined') return false - if (document.documentElement.hasAttribute('data-reduce-motion')) return true + if (document.documentElement.dataset.reduceMotion !== undefined) return true return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false } @@ -297,21 +320,8 @@ export function useMPlanDaySwipe({ while (g.samples.length > 2 && now - g.samples[0].t > SAMPLE_MS) g.samples.shift() if (g.lock === null) { - const card = cardRef.current - // The browser has already committed this gesture to scrolling, so obey it. - // scrollLeft counts too: overflow-y-auto on its own leaves overflow-x - // computing to auto, and a wide markdown table in a day note fills it. - if (card && (card.scrollTop !== g.scrollTop0 || card.scrollLeft !== g.scrollLeft0)) { - g.lock = 'dead' - return - } - if (Math.abs(dx) <= CLASSIFY_PX && Math.abs(dy) <= CLASSIFY_PX) return - const s = latest.current - // Edit mode only: past the bridge's press window this finger belongs to a - // reorder drag. Go mode has no bridge, so a slow swipe still counts. - if (s.dragging || (s.editing && now - g.t0 > PRESS_WINDOW_MS)) { g.lock = 'dead'; return } - g.lock = Math.abs(dx) >= Math.abs(dy) * AXIS_BIAS ? 'h' : 'dead' - if (g.lock === 'dead') return + g.lock = classifyLock(g, dx, dy, now, cardRef.current, latest.current) + if (g.lock !== 'h') return } if (reducedMotion()) return // classify and commit, but never follow the finger diff --git a/client/src/vacay/yearWindow.ts b/client/src/vacay/yearWindow.ts index bad8e071d..4fca28b2e 100644 --- a/client/src/vacay/yearWindow.ts +++ b/client/src/vacay/yearWindow.ts @@ -110,9 +110,11 @@ export function currentPeriodYear(s: VacayYearSettings = DEFAULT_YEAR_SETTINGS, export function defaultPeriodYear(years: number[], s: VacayYearSettings = DEFAULT_YEAR_SETTINGS, today = new Date()): number { const current = currentPeriodYear(s, today) if (years.length === 0 || years.includes(current)) return current + // Seeded with years[0] rather than left to reduce's implicit first element: + // same result, and it cannot throw if the guard above ever loosens. return years.reduce((best, year) => { const distance = Math.abs(year - current) const bestDistance = Math.abs(best - current) return distance < bestDistance || (distance === bestDistance && year > best) ? year : best - }) + }, years[0]) } diff --git a/server/src/nest/collab/collab.service.ts b/server/src/nest/collab/collab.service.ts index 1812a4f07..ddfc64666 100644 --- a/server/src/nest/collab/collab.service.ts +++ b/server/src/nest/collab/collab.service.ts @@ -545,7 +545,7 @@ export class CollabService { // entry yet, since the first has not answered. Joining the running fetch keeps // that a single outbound request instead of twenty. const running = this.inFlight.get(url); - if (running) return { ...(await running), url }; + if (running !== undefined) return { ...(await running), url }; // Charged per outbound fetch rather than per request, which is what the // budget is actually protecting. Without a user there is no one to charge —