From ac77e52e770155281e4d025600ba037905c721ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 23 Oct 2026 13:03:39 +0200 Subject: [PATCH] refactor(measurements): hand both row layouts one row context The table and the card list each took nine props per row and passed them on to the shared row actions. One MeasurementRowContext carries them, and rowState derives a row's grouping, selection and drill-down state in one place for both layouts. --- .../list/measurement-card-list.tsx | 75 ++++--------------- .../measurements/list/measurement-table.tsx | 75 +++++-------------- .../measurements/list/row-actions.tsx | 39 +++++++--- .../measurements/measurement-list.tsx | 18 +++-- 4 files changed, 72 insertions(+), 135 deletions(-) diff --git a/src/components/measurements/list/measurement-card-list.tsx b/src/components/measurements/list/measurement-card-list.tsx index 78da09634..ce5f90d09 100644 --- a/src/components/measurements/list/measurement-card-list.tsx +++ b/src/components/measurements/list/measurement-card-list.tsx @@ -16,60 +16,31 @@ import { } from "../measurement-list-meta"; import { DayDrillDown, SleepNightCaption } from "./day-drill-down"; -import { MeasurementRowActions, typeLabel } from "./row-actions"; +import { + MeasurementRowActions, + rowState, + typeLabel, + type MeasurementRowContext, +} from "./row-actions"; import { formatMeasurementSource, - isGroupedRow, sourceBadgeClass, truncateComment, type Measurement, } from "./list-model"; -import type { MeasurementListData } from "./use-measurement-list-data"; -import type { MeasurementListFilters } from "./use-measurement-list-filters"; -import type { MeasurementRowDisplay } from "./use-measurement-row-display"; /** The phone list: one card per reading, day or night. */ export function MeasurementCardList({ measurements, - canManage, - filters, - list, - display, - expandedDayKeys, - toggleExpand, - navigateForType, - startEdit, + rows, }: { measurements: Measurement[]; - canManage: boolean; - filters: MeasurementListFilters; - list: MeasurementListData; - display: MeasurementRowDisplay; - expandedDayKeys: ReadonlySet; - toggleExpand: (dayKey: string) => void; - navigateForType: (type: string) => void; - startEdit: (m: Measurement) => void; + rows: MeasurementRowContext; }) { - const { selectedIds } = filters; - const { onToggleRow, deleteMutation } = list; return (
{measurements.map((m) => ( - deleteMutation.mutate(id)} - display={display} - toggleExpand={toggleExpand} - navigateForType={navigateForType} - startEdit={startEdit} - /> + ))}
); @@ -78,32 +49,17 @@ export function MeasurementCardList({ /** One card, and under a grouped card its open drill-down. */ function MeasurementCard({ m, - canManage, - isSelected, - isExpanded, - onToggleRow, - onDelete, - display, - toggleExpand, - navigateForType, - startEdit, + rows, }: { m: Measurement; - canManage: boolean; - isSelected: boolean; - isExpanded: boolean; - onToggleRow: (id: string) => void; - onDelete: (id: string) => void; - display: MeasurementRowDisplay; - toggleExpand: (dayKey: string) => void; - navigateForType: (type: string) => void; - startEdit: (m: Measurement) => void; + rows: MeasurementRowContext; }) { + const { canManage, onToggleRow, navigateForType, display } = rows; + const { isGrouped, isSelected, isExpanded } = rowState(m, rows); const { t } = useTranslations(); const fmt = useFormatters(); const { rowDisplay, rowIdentityAttrs } = display; const Icon = TYPE_ICONS[m.type]; - const isGrouped = isGroupedRow(m); const isSleep = m.type === "SLEEP_DURATION"; const drilldownId = `drilldown-mobile-${m.dayKey ?? m.id}`; const label = typeLabel(m.type, t); @@ -229,14 +185,11 @@ function MeasurementCard({
diff --git a/src/components/measurements/list/measurement-table.tsx b/src/components/measurements/list/measurement-table.tsx index 1ba980ccd..b90e33fc2 100644 --- a/src/components/measurements/list/measurement-table.tsx +++ b/src/components/measurements/list/measurement-table.tsx @@ -21,44 +21,37 @@ import { measurementUnitLabel } from "@/lib/measurements/unit-label"; import { MEASUREMENT_TYPE_COLORS as TYPE_COLORS } from "../measurement-list-meta"; import { DayDrillDown, SleepNightCaption } from "./day-drill-down"; -import { MeasurementRowActions, typeLabel } from "./row-actions"; +import { + MeasurementRowActions, + rowState, + typeLabel, + type MeasurementRowContext, +} from "./row-actions"; import { formatMeasurementSource, - isGroupedRow, sourceBadgeClass, truncateComment, type Measurement, } from "./list-model"; import type { MeasurementListData } from "./use-measurement-list-data"; import type { MeasurementListFilters } from "./use-measurement-list-filters"; -import type { MeasurementRowDisplay } from "./use-measurement-row-display"; /** The desktop table: one row per reading, day or night, sortable columns. */ export function MeasurementTable({ measurements, - canManage, + rows, filters, list, - display, - expandedDayKeys, - toggleExpand, - navigateForType, - startEdit, }: { measurements: Measurement[]; - canManage: boolean; + rows: MeasurementRowContext; filters: MeasurementListFilters; list: MeasurementListData; - display: MeasurementRowDisplay; - expandedDayKeys: ReadonlySet; - toggleExpand: (dayKey: string) => void; - navigateForType: (type: string) => void; - startEdit: (m: Measurement) => void; }) { const { t } = useTranslations(); - const { sortBy, sortDir, toggleSort, selectedIds } = filters; - const { selectAll, pageIds, onToggleSelectAll, onToggleRow, deleteMutation } = - list; + const { canManage } = rows; + const { sortBy, sortDir, toggleSort } = filters; + const { selectAll, pageIds, onToggleSelectAll } = list; return (
{measurements.map((m) => ( - deleteMutation.mutate(id)} - display={display} - toggleExpand={toggleExpand} - navigateForType={navigateForType} - startEdit={startEdit} - /> + ))} @@ -149,31 +126,16 @@ export function MeasurementTable({ /** One table row, and under a grouped row its open drill-down. */ function MeasurementTableRow({ m, - canManage, - isSelected, - isExpanded, - onToggleRow, - onDelete, - display, - toggleExpand, - navigateForType, - startEdit, + rows, }: { m: Measurement; - canManage: boolean; - isSelected: boolean; - isExpanded: boolean; - onToggleRow: (id: string) => void; - onDelete: (id: string) => void; - display: MeasurementRowDisplay; - toggleExpand: (dayKey: string) => void; - navigateForType: (type: string) => void; - startEdit: (m: Measurement) => void; + rows: MeasurementRowContext; }) { + const { canManage, onToggleRow, navigateForType, display } = rows; + const { isGrouped, isSelected, isExpanded } = rowState(m, rows); const { t } = useTranslations(); const fmt = useFormatters(); const { rowDisplay, rowIdentityAttrs } = display; - const isGrouped = isGroupedRow(m); // A sleep night is grouped (its chevron opens the stage segments) but // reads as a duration with a nap caption, not as a daily total. const isSleep = m.type === "SLEEP_DURATION"; @@ -289,14 +251,11 @@ function MeasurementTableRow({
diff --git a/src/components/measurements/list/row-actions.tsx b/src/components/measurements/list/row-actions.tsx index 143d65562..92a0c0244 100644 --- a/src/components/measurements/list/row-actions.tsx +++ b/src/components/measurements/list/row-actions.tsx @@ -8,7 +8,8 @@ import { useTranslations } from "@/lib/i18n/context"; import { MEASUREMENT_TYPE_LABEL_KEYS as TYPE_LABEL_KEYS } from "../measurement-list-meta"; -import type { Measurement } from "./list-model"; +import { isGroupedRow, type Measurement } from "./list-model"; +import type { MeasurementRowDisplay } from "./use-measurement-row-display"; type Translate = ReturnType["t"]; @@ -17,6 +18,31 @@ export function typeLabel(type: string, t: Translate): string { return TYPE_LABEL_KEYS[type] ? t(TYPE_LABEL_KEYS[type]) : type; } +/** What every row of either layout needs from the list around it. */ +export interface MeasurementRowContext { + canManage: boolean; + selectedIds: ReadonlySet; + expandedDayKeys: ReadonlySet; + toggleExpand: (dayKey: string) => void; + navigateForType: (type: string) => void; + startEdit: (m: Measurement) => void; + onToggleRow: (id: string) => void; + onDelete: (id: string) => void; + display: MeasurementRowDisplay; +} + +/** A row's grouping, selection and drill-down state. */ +export function rowState(m: Measurement, rows: MeasurementRowContext) { + const isGrouped = isGroupedRow(m); + return { + isGrouped, + isSelected: rows.selectedIds.has(m.id), + isExpanded: isGrouped + ? rows.expandedDayKeys.has(m.dayKey as string) + : false, + }; +} + /** * A row's controls: the chevron into a day's or night's samples for a * grouped row; edit (owner only) and delete for a reading. The table's @@ -24,25 +50,20 @@ export function typeLabel(type: string, t: Translate): string { */ export function MeasurementRowActions({ m, + rows, isGrouped, isExpanded, drilldownId, - canManage, layout, - toggleExpand, - startEdit, - onDelete, }: { m: Measurement; + rows: MeasurementRowContext; isGrouped: boolean; isExpanded: boolean; drilldownId: string; - canManage: boolean; layout: "desktop" | "mobile"; - toggleExpand: (dayKey: string) => void; - startEdit: (m: Measurement) => void; - onDelete: (id: string) => void; }) { + const { canManage, toggleExpand, startEdit, onDelete } = rows; const { t } = useTranslations(); const desktop = layout === "desktop"; const size = desktop ? "icon" : "icon-lg"; diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx index ac65e359e..9004bbc9e 100644 --- a/src/components/measurements/measurement-list.tsx +++ b/src/components/measurements/measurement-list.tsx @@ -24,6 +24,7 @@ import { MeasurementEditSheet } from "./list/measurement-edit-sheet"; import { MeasurementFilterRail } from "./list/measurement-filter-rail"; import { MeasurementTable } from "./list/measurement-table"; import type { Measurement } from "./list/list-model"; +import type { MeasurementRowContext } from "./list/row-actions"; import { useMeasurementEdit } from "./list/use-measurement-edit"; import { useMeasurementListData } from "./list/use-measurement-list-data"; import { useMeasurementListFilters } from "./list/use-measurement-list-filters"; @@ -149,15 +150,16 @@ export function MeasurementList({ ); } - const rowProps = { + const rows: MeasurementRowContext = { canManage, - filters, - list, - display, + selectedIds: filters.selectedIds, expandedDayKeys, toggleExpand, navigateForType, - startEdit: (m: Measurement) => edit.startEdit(m), + startEdit: edit.startEdit, + onToggleRow: list.onToggleRow, + onDelete: (id) => list.deleteMutation.mutate(id), + display, }; return ( @@ -202,13 +204,15 @@ export function MeasurementList({ {!isMobile && ( )} {isMobile && ( )}