mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
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.
This commit is contained in:
1 parent
d7f0d88d59
commit
ac77e52e77
4 files changed
+72
-135
No files matched your search
@@ -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<string>;
|
||||
toggleExpand: (dayKey: string) => void;
|
||||
navigateForType: (type: string) => void;
|
||||
startEdit: (m: Measurement) => void;
|
||||
rows: MeasurementRowContext;
|
||||
}) {
|
||||
const { selectedIds } = filters;
|
||||
const { onToggleRow, deleteMutation } = list;
|
||||
return (
|
||||
<div data-slot="measurement-rows" className="space-y-2 md:hidden">
|
||||
{measurements.map((m) => (
|
||||
<MeasurementCard
|
||||
key={m.id}
|
||||
m={m}
|
||||
canManage={canManage}
|
||||
isSelected={selectedIds.has(m.id)}
|
||||
isExpanded={
|
||||
isGroupedRow(m) ? expandedDayKeys.has(m.dayKey as string) : false
|
||||
}
|
||||
onToggleRow={onToggleRow}
|
||||
onDelete={(id) => deleteMutation.mutate(id)}
|
||||
display={display}
|
||||
toggleExpand={toggleExpand}
|
||||
navigateForType={navigateForType}
|
||||
startEdit={startEdit}
|
||||
/>
|
||||
<MeasurementCard key={m.id} m={m} rows={rows} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -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({
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<MeasurementRowActions
|
||||
m={m}
|
||||
rows={rows}
|
||||
isGrouped={isGrouped}
|
||||
isExpanded={isExpanded}
|
||||
drilldownId={drilldownId}
|
||||
canManage={canManage}
|
||||
layout="mobile"
|
||||
toggleExpand={toggleExpand}
|
||||
startEdit={startEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<string>;
|
||||
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 (
|
||||
<div
|
||||
// `measurement-rows` marks the READ rows, not the list frame:
|
||||
@@ -122,23 +115,7 @@ export function MeasurementTable({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{measurements.map((m) => (
|
||||
<MeasurementTableRow
|
||||
key={m.id}
|
||||
m={m}
|
||||
canManage={canManage}
|
||||
isSelected={selectedIds.has(m.id)}
|
||||
isExpanded={
|
||||
isGroupedRow(m)
|
||||
? expandedDayKeys.has(m.dayKey as string)
|
||||
: false
|
||||
}
|
||||
onToggleRow={onToggleRow}
|
||||
onDelete={(id) => deleteMutation.mutate(id)}
|
||||
display={display}
|
||||
toggleExpand={toggleExpand}
|
||||
navigateForType={navigateForType}
|
||||
startEdit={startEdit}
|
||||
/>
|
||||
<MeasurementTableRow key={m.id} m={m} rows={rows} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -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({
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<MeasurementRowActions
|
||||
m={m}
|
||||
rows={rows}
|
||||
isGrouped={isGrouped}
|
||||
isExpanded={isExpanded}
|
||||
drilldownId={drilldownId}
|
||||
canManage={canManage}
|
||||
layout="desktop"
|
||||
toggleExpand={toggleExpand}
|
||||
startEdit={startEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -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<typeof useTranslations>["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<string>;
|
||||
expandedDayKeys: ReadonlySet<string>;
|
||||
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";
|
||||
|
||||
@@ -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 && (
|
||||
<MeasurementTable
|
||||
measurements={data.measurements}
|
||||
{...rowProps}
|
||||
rows={rows}
|
||||
filters={filters}
|
||||
list={list}
|
||||
/>
|
||||
)}
|
||||
{isMobile && (
|
||||
<MeasurementCardList
|
||||
measurements={data.measurements}
|
||||
{...rowProps}
|
||||
rows={rows}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in new issue
Block a user