mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(measurements): offer no edit or delete on a computed score
Recovery, strain and other computed rows in the measurement list carried a pencil and a bin. The pencil opened an edit sheet labelled "Value ():" whose save the server then refused, and the bin deleted a score the engine owns. Both are gone for COMPUTED rows. Readings from a device or a provider keep both controls: the server takes a note or time edit on them, and a wrong scale reading has to stay removable. The server is unchanged.
This commit is contained in:
1 parent
a28c6b55ab
commit
af673a63da
2 files changed
+90
-3
No files matched your search
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* A computed score offers neither edit nor delete in the measurement list;
|
||||
* a reading of the person's own, or one a device or provider sent, keeps
|
||||
* both.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
vi.mock("@/hooks/use-record-capabilities", () => ({
|
||||
useRecordCapabilities: () => ({
|
||||
canManageDomain: () => true,
|
||||
inSharedRecord: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { Measurement } from "../list-model";
|
||||
import {
|
||||
MeasurementRowActions,
|
||||
type MeasurementRowContext,
|
||||
} from "../row-actions";
|
||||
|
||||
const rows: MeasurementRowContext = {
|
||||
canManage: true,
|
||||
selectedIds: new Set<string>(),
|
||||
expandedDayKeys: new Set<string>(),
|
||||
toggleExpand: () => {},
|
||||
navigateForType: () => {},
|
||||
startEdit: () => {},
|
||||
onToggleRow: () => {},
|
||||
onDelete: () => {},
|
||||
// The row's value formatting; the actions never read it.
|
||||
display: {} as MeasurementRowContext["display"],
|
||||
};
|
||||
|
||||
function render(source: string): string {
|
||||
const m: Measurement = {
|
||||
id: "m1",
|
||||
type: "RECOVERY_SCORE",
|
||||
value: 85,
|
||||
unit: "score",
|
||||
source,
|
||||
measuredAt: "2026-11-10T12:00:00.000Z",
|
||||
notes: null,
|
||||
};
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<MeasurementRowActions
|
||||
m={m}
|
||||
rows={rows}
|
||||
isGrouped={false}
|
||||
isExpanded={false}
|
||||
drilldownId="d1"
|
||||
layout="desktop"
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("MeasurementRowActions", () => {
|
||||
it("offers no edit and no delete for a computed score", () => {
|
||||
const html = render("COMPUTED");
|
||||
expect(html).not.toContain('aria-label="Edit"');
|
||||
expect(html).not.toContain('aria-label="Delete"');
|
||||
});
|
||||
|
||||
it.each(["MANUAL", "WITHINGS", "APPLE_HEALTH"])(
|
||||
"keeps edit and delete for a %s reading",
|
||||
(source) => {
|
||||
const html = render(source);
|
||||
expect(html).toContain('aria-label="Edit"');
|
||||
expect(html).toContain('aria-label="Delete"');
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -43,10 +43,22 @@ export function rowState(m: Measurement, rows: MeasurementRowContext) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A score the server computes (recovery, strain, stress) offers neither
|
||||
* edit nor delete: its value is the engine's, the server refuses a changed
|
||||
* one on save, and the edit sheet has nothing of the person's to correct.
|
||||
* Readings from a device or a provider keep both: the server takes a note or
|
||||
* time edit on them, and a wrong scale reading has to be removable.
|
||||
*/
|
||||
export function isServerComputed(m: Pick<Measurement, "source">): boolean {
|
||||
return m.source === "COMPUTED";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* buttons are 40 px, the phone list's the large icon size.
|
||||
* grouped row; edit (owner only) and delete for a reading, neither for a
|
||||
* computed score. The table's buttons are 40 px, the phone list's the large
|
||||
* icon size.
|
||||
*/
|
||||
export function MeasurementRowActions({
|
||||
m,
|
||||
@@ -85,7 +97,7 @@ export function MeasurementRowActions({
|
||||
className={`h-4 w-4 transition-transform ${isExpanded ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</Button>
|
||||
) : (
|
||||
) : isServerComputed(m) ? null : (
|
||||
<>
|
||||
{canManage && (
|
||||
<Button
|
||||
|
||||
Reference in new issue
Block a user