mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
fix(mood): stop offering to clear a value that cannot be cleared
An entry always carries a five-point label, so it always implies a pleasantness value: the create path derived one from a null and the capture sheet still showed a clear control for it, which meant a button the server undid on the way to the database. One rule on both routes now — a null asks for a value that cannot exist and gets the derivation, on create and on edit alike — and the control is gone for that field. The other four are genuinely optional and keep theirs.
This commit is contained in:
1 parent
aa56246ba2
commit
0a39d50b05
4 files changed
+75
-5
No files matched your search
@@ -139,7 +139,15 @@ export const PUT = apiHandler(
|
||||
// v1.37 — level-A values are per-field on this path: omitted keeps the
|
||||
// stored value, an explicit number replaces it, an explicit null clears
|
||||
// it. Built one at a time from the parsed body, never spread.
|
||||
if (data.a1 !== undefined) updateData.moodA1 = data.a1;
|
||||
//
|
||||
// Pleasantness is the exception, and it is the same exception the create
|
||||
// path makes: an entry always carries a five-point label, so it always
|
||||
// implies a pleasantness value, and a null falls back to the derivation
|
||||
// rather than emptying the column. One rule on both routes — the capture
|
||||
// surfaces therefore offer no clear control for it.
|
||||
if (data.a1 !== undefined) {
|
||||
updateData.moodA1 = data.a1 ?? deriveA1(data.mood ?? existing.mood);
|
||||
}
|
||||
if (data.a2 !== undefined) updateData.stressA2 = data.a2;
|
||||
if (data.a3 !== undefined) updateData.energyA3 = data.a3;
|
||||
if (data.a4 !== undefined) updateData.connectionA4 = data.a4;
|
||||
|
||||
@@ -145,6 +145,19 @@ describe("MoodForm — level A replaces the factor ratings (v1.37)", () => {
|
||||
// Nothing here defaults a value: the state starts empty and stays empty.
|
||||
expect(formSrc).toContain("useState<LevelAState>(EMPTY_LEVEL_A)");
|
||||
});
|
||||
|
||||
it("offers no clear control for pleasantness, matching the write path", () => {
|
||||
// An entry always carries a label, so it always implies a pleasantness
|
||||
// value and both write routes derive one from a null. A clear button here
|
||||
// would be a control the server undoes on the way to the database.
|
||||
const fieldsSrc = readFileSync(
|
||||
resolve(__dirname, "../mood-level-a-fields.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
expect(fieldsSrc).toMatch(
|
||||
/onClear=\{\s*dimension\.key === "a1"\s*\?\s*undefined/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MoodList — rated factor editing", () => {
|
||||
|
||||
@@ -169,10 +169,18 @@ export function MoodLevelASection({
|
||||
})
|
||||
}
|
||||
clearLabel={t("mood.dimensionClear")}
|
||||
onClear={() => {
|
||||
if (dimension.key === "a1") onA1Touched?.();
|
||||
onChange({ ...value, [dimension.key]: null });
|
||||
}}
|
||||
// Pleasantness has no clear control, and that is the same rule
|
||||
// the server keeps: an entry always carries a five-point label,
|
||||
// so it always implies a pleasantness value, and there is no
|
||||
// state in which the field is honestly unanswered. Offering a
|
||||
// control that the write path would immediately undo would be a
|
||||
// button that quietly does nothing. The other four are genuinely
|
||||
// optional and keep theirs.
|
||||
onClear={
|
||||
dimension.key === "a1"
|
||||
? undefined
|
||||
: () => onChange({ ...value, [dimension.key]: null })
|
||||
}
|
||||
onValueChange={(next) => {
|
||||
if (dimension.key === "a1") onA1Touched?.();
|
||||
onChange({ ...value, [dimension.key]: next });
|
||||
|
||||
@@ -429,4 +429,45 @@ describe("mood writers agree on the derived level-A value", () => {
|
||||
// An explicit null takes back an answer.
|
||||
expect(row.stressA2).toBeNull();
|
||||
});
|
||||
|
||||
it("will not empty pleasantness, on either route", async () => {
|
||||
await createUserSession();
|
||||
const { POST: postSingle } = await import("@/app/api/mood-entries/route");
|
||||
const { PUT } = await import("@/app/api/mood-entries/[id]/route");
|
||||
|
||||
// An entry always carries a five-point label, so it always implies a
|
||||
// pleasantness value. A null asks for one that cannot exist, and both
|
||||
// routes answer it the same way: derive from the label. The capture
|
||||
// surfaces therefore offer no clear control for this one field, which is
|
||||
// the UI half of the same rule.
|
||||
const created = await postSingle(
|
||||
jsonRequest("/api/mood-entries", {
|
||||
mood: "SUPER_GUT",
|
||||
moodLoggedAt: "2026-05-16T14:00:00.000Z",
|
||||
a1: null,
|
||||
}),
|
||||
);
|
||||
expect(created.status).toBe(201);
|
||||
const createdJson = (await created.json()) as {
|
||||
data: { id: string; a1: number };
|
||||
};
|
||||
expect(createdJson.data.a1).toBe(9);
|
||||
const id = createdJson.data.id;
|
||||
|
||||
const cleared = await PUT(
|
||||
new NextRequest(`http://localhost/api/mood-entries/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ a1: null }),
|
||||
}),
|
||||
{ params: Promise.resolve({ id }) },
|
||||
);
|
||||
expect(cleared.status).toBe(200);
|
||||
|
||||
const row = await getPrismaClient().moodEntry.findUniqueOrThrow({
|
||||
where: { id },
|
||||
});
|
||||
// Derived from the entry's own stored label, not emptied.
|
||||
expect(row.moodA1).toBe(9);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user