mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
Merge branch 'fix/v14311-settings' into release/v1.43.11
# Conflicts: # messages/de.json # messages/en.json # messages/es.json # messages/fr.json # messages/it.json # messages/ko.json # messages/nl.json # messages/pl.json
This commit is contained in:
commit
eabfeaef08
74 files changed
+2545
-436
No files matched your search
@@ -391,6 +391,10 @@ export const GET = apiHandler(async () => {
|
||||
// resolved `ai.capabilities.coach` and `modules.coach` below, which fold
|
||||
// in the record's own opt-out, the operator's switches and the grant.
|
||||
disableCoach: user.disableCoach ?? false,
|
||||
// The name the app addresses the account by (dashboard greeting,
|
||||
// reports). Set in the setup flow and edited on the profile card, which
|
||||
// seeds its field from here.
|
||||
displayName: user.displayName ?? null,
|
||||
// v1.7.0 — health-record export identity fields. All optional.
|
||||
fullName: user.fullName ?? null,
|
||||
insurerName: user.insurerName ?? null,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { notificationPreferenceSchema } from "@/lib/validations/notifications";
|
||||
import { EVENT_TYPES, CHANNEL_TYPE_LABELS } from "@/lib/notifications/types";
|
||||
import type { ChannelType } from "@/lib/notifications/types";
|
||||
import { decrypt, encrypt } from "@/lib/crypto";
|
||||
import { resolveEventDefaults } from "@/lib/notifications/event-defaults";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
const { user } = await requireAuth();
|
||||
@@ -92,6 +93,12 @@ export const GET = apiHandler(async () => {
|
||||
channels: channelData,
|
||||
preferences,
|
||||
eventTypes: EVENT_TYPES as unknown as string[],
|
||||
// What an event does on a channel with no stored row, resolved for this
|
||||
// account by the same function the dispatcher asks. A client renders a
|
||||
// switch without a row from this map, never from its own copy of the
|
||||
// defaults: the mood reminder's follows the reminder card, which a
|
||||
// constant table cannot know.
|
||||
eventDefaults: await resolveEventDefaults(user.id),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -191,6 +191,7 @@ function CoachPlansBody({ readOnly }: { readOnly: boolean }) {
|
||||
{(readOnly || group === "past") && (
|
||||
<ConfirmButton
|
||||
slot="coach-plan-delete"
|
||||
destructive
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="min-h-9"
|
||||
|
||||
@@ -126,4 +126,58 @@ describe("NotificationsPage", () => {
|
||||
const html = render();
|
||||
expect(html).toContain("won’t receive notifications");
|
||||
});
|
||||
|
||||
it("a switch without a stored row shows the server's default for this account", () => {
|
||||
// The mood reminder is delivered when the reminder card is on, with no
|
||||
// per-channel row. The constant table says OFF for it, so a matrix that
|
||||
// read the table showed the switch off while the reminder kept arriving.
|
||||
// The page renders what the server resolved, row-less events included.
|
||||
queryState.data = {
|
||||
channels: [
|
||||
{
|
||||
id: "c1",
|
||||
type: "TELEGRAM",
|
||||
label: "Telegram",
|
||||
enabled: true,
|
||||
globallyEnabled: true,
|
||||
},
|
||||
],
|
||||
preferences: [],
|
||||
eventTypes: ["MOOD_REMINDER", "PERSONAL_RECORD"],
|
||||
eventDefaults: { MOOD_REMINDER: true, PERSONAL_RECORD: false },
|
||||
};
|
||||
const html = render();
|
||||
const state = (label: string) =>
|
||||
new RegExp(
|
||||
`aria-label="${label} - Telegram"[^>]*data-state="(checked|unchecked)"|data-state="(checked|unchecked)"[^>]*aria-label="${label} - Telegram"`,
|
||||
).exec(html);
|
||||
const mood = state("Mood reminder");
|
||||
const record = state("Personal record");
|
||||
expect(mood, "mood switch rendered").not.toBeNull();
|
||||
expect(mood?.[1] ?? mood?.[2]).toBe("checked");
|
||||
expect(record?.[1] ?? record?.[2]).toBe("unchecked");
|
||||
});
|
||||
|
||||
it("a stored row still wins over the default", () => {
|
||||
queryState.data = {
|
||||
channels: [
|
||||
{
|
||||
id: "c1",
|
||||
type: "TELEGRAM",
|
||||
label: "Telegram",
|
||||
enabled: true,
|
||||
globallyEnabled: true,
|
||||
},
|
||||
],
|
||||
preferences: [
|
||||
{ channelId: "c1", eventType: "MOOD_REMINDER", enabled: false },
|
||||
],
|
||||
eventTypes: ["MOOD_REMINDER"],
|
||||
eventDefaults: { MOOD_REMINDER: true },
|
||||
};
|
||||
const html = render();
|
||||
expect(html).toMatch(
|
||||
/aria-label="Mood reminder - Telegram"[^>]*data-state="unchecked"|data-state="unchecked"[^>]*aria-label="Mood reminder - Telegram"/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,10 +12,6 @@ import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import Link from "next/link";
|
||||
import { Bell, Settings, AlertCircle } from "lucide-react";
|
||||
import {
|
||||
EVENT_DEFAULT_ENABLED,
|
||||
type EventType,
|
||||
} from "@/lib/notifications/types";
|
||||
import {
|
||||
eventDescriptionTranslationKey,
|
||||
eventTranslationKey,
|
||||
@@ -48,6 +44,11 @@ interface PreferencesData {
|
||||
channels: ChannelInfo[];
|
||||
preferences: Preference[];
|
||||
eventTypes: string[];
|
||||
/**
|
||||
* What each event does on a channel with no stored row, resolved by the
|
||||
* server for this account with the function the dispatcher uses.
|
||||
*/
|
||||
eventDefaults: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export default function NotificationsPage() {
|
||||
@@ -116,10 +117,10 @@ export default function NotificationsPage() {
|
||||
(p) => p.channelId === channelId && p.eventType === eventType,
|
||||
);
|
||||
if (pref) return pref.enabled;
|
||||
// v1.4.25 — per-event default. Most events stay opt-out
|
||||
// (no row = ON); PERSONAL_RECORD flips to opt-in (no row = OFF)
|
||||
// so a backfill doesn't fire hundreds of pushes on first sync.
|
||||
return EVENT_DEFAULT_ENABLED[eventType as EventType] ?? true;
|
||||
// No row: the server's resolved default for this account. Read, never
|
||||
// recomputed here: the mood reminder's follows the reminder card, and a
|
||||
// copy of the constant table showed it off while it was being delivered.
|
||||
return data?.eventDefaults?.[eventType] === true;
|
||||
}
|
||||
|
||||
// Cross-links to the other two notification surfaces (reminder types +
|
||||
|
||||
Reference in new issue
Block a user