mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
Give the account page's save-on-change preferences their own card
Language, units, glucose unit and the hour and date formats each save the moment they change, yet sat inside the profile form as if its Save covered them. They move to a Display card with no save button; the profile card keeps its fields, the timezone (which saves with the form) and its Save.
This commit is contained in:
1 parent
51374274aa
commit
2d79b80298
9 files changed
+152
-52
No files matched your search
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "Das hat nicht geklappt. Prüfe deine Eingabe und versuche es noch einmal."
|
||||
},
|
||||
"passwordTitle": "Passwort",
|
||||
"tokenNameLabel": "Token-Name"
|
||||
"tokenNameLabel": "Token-Name",
|
||||
"displayCard": {
|
||||
"title": "Darstellung",
|
||||
"description": "Sprache, Einheiten und Formate, gespeichert, sobald du sie änderst."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "That did not work. Check what you entered and try again."
|
||||
},
|
||||
"passwordTitle": "Password",
|
||||
"tokenNameLabel": "Token name"
|
||||
"tokenNameLabel": "Token name",
|
||||
"displayCard": {
|
||||
"title": "Display",
|
||||
"description": "Language, units and formats, saved as soon as you change them."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "No ha funcionado. Revisa lo que has introducido e inténtalo de nuevo."
|
||||
},
|
||||
"passwordTitle": "Contraseña",
|
||||
"tokenNameLabel": "Nombre del token"
|
||||
"tokenNameLabel": "Nombre del token",
|
||||
"displayCard": {
|
||||
"title": "Visualización",
|
||||
"description": "Idioma, unidades y formatos; se guardan en cuanto los cambias."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "Cela n'a pas fonctionné. Vérifiez votre saisie et réessayez."
|
||||
},
|
||||
"passwordTitle": "Mot de passe",
|
||||
"tokenNameLabel": "Nom du jeton"
|
||||
"tokenNameLabel": "Nom du jeton",
|
||||
"displayCard": {
|
||||
"title": "Affichage",
|
||||
"description": "Langue, unités et formats, enregistrés dès que vous les modifiez."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "Non ha funzionato. Controlla quanto inserito e riprova."
|
||||
},
|
||||
"passwordTitle": "Password",
|
||||
"tokenNameLabel": "Nome del token"
|
||||
"tokenNameLabel": "Nome del token",
|
||||
"displayCard": {
|
||||
"title": "Visualizzazione",
|
||||
"description": "Lingua, unità e formati, salvati appena li modifichi."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "확인하지 못했습니다. 입력한 내용을 확인하고 다시 시도하세요."
|
||||
},
|
||||
"passwordTitle": "비밀번호",
|
||||
"tokenNameLabel": "토큰 이름"
|
||||
"tokenNameLabel": "토큰 이름",
|
||||
"displayCard": {
|
||||
"title": "표시",
|
||||
"description": "언어, 단위, 형식은 바꾸는 즉시 저장돼요."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "관리자",
|
||||
|
||||
+5
-1
@@ -7173,7 +7173,11 @@
|
||||
"failed": "To się nie udało. Sprawdź wprowadzone dane i spróbuj ponownie."
|
||||
},
|
||||
"passwordTitle": "Hasło",
|
||||
"tokenNameLabel": "Nazwa tokena"
|
||||
"tokenNameLabel": "Nazwa tokena",
|
||||
"displayCard": {
|
||||
"title": "Wyświetlanie",
|
||||
"description": "Język, jednostki i formaty zapisywane od razu po zmianie."
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administracja",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Settings → Account: the controls that save on change (language, units,
|
||||
* glucose unit, hour and date format) sit in their own card with no save
|
||||
* button, apart from the profile form whose Save does not cover them.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { DisplayPreferencesCard } from "../index";
|
||||
|
||||
function render(): string {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: 0, enabled: false } },
|
||||
});
|
||||
return renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
<I18nProvider initialLocale="en">
|
||||
<DisplayPreferencesCard isAuthenticated={false} />
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Settings → Account display card", () => {
|
||||
it("holds the language select and carries no form or save button", () => {
|
||||
const html = render();
|
||||
expect(html).toContain('data-testid="settings-display-card"');
|
||||
expect(html).toContain('id="language-select"');
|
||||
expect(html).not.toContain("<form");
|
||||
expect(html).not.toContain('type="submit"');
|
||||
});
|
||||
|
||||
it("leaves the save-on-change controls out of the profile form", () => {
|
||||
const src = readFileSync(join(__dirname, "../index.tsx"), "utf8");
|
||||
const form = src.slice(src.indexOf("<form"), src.indexOf("</form>"));
|
||||
expect(form.length).toBeGreaterThan(0);
|
||||
for (const control of [
|
||||
"language-select",
|
||||
"UnitPreferenceSelect",
|
||||
"GlucoseUnitSelect",
|
||||
"TimeFormatSelect",
|
||||
"DateFormatSelect",
|
||||
]) {
|
||||
expect(form).not.toContain(control);
|
||||
}
|
||||
// The timezone saves with the form, so it stays inside it.
|
||||
expect(form).toContain("TimezonePicker");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2, Save, User } from "lucide-react";
|
||||
import { Languages, Loader2, Save, User } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SettingsCardActions } from "@/components/settings/_card-actions";
|
||||
@@ -62,7 +62,7 @@ import { dateOnlyKey } from "@/lib/tz/date-only";
|
||||
export { resolveInitialTimezone } from "./account-section-utils";
|
||||
|
||||
export function AccountSection() {
|
||||
const { t, locale, setLocale, pendingLocale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const { user, isLoading, isAuthenticated, refetch } = useAuth();
|
||||
// v1.16.4 — see `useMounted`: keeps the hydration render identical to
|
||||
// the SSR HTML when this boundary hydrates after `/api/auth/me`
|
||||
@@ -517,12 +517,10 @@ export function AccountSection() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date of birth + language share one paired grid row so the
|
||||
profile form keeps a single rhythm (every row two cells
|
||||
wide on sm+). Date of birth is the bottom of the
|
||||
biological-profile block; language is the only UI
|
||||
preference on this card. They sit together to close the
|
||||
"single-cell row" gap that broke the form's grid. */}
|
||||
{/* Date of birth + timezone share one paired grid row. The
|
||||
timezone stays on this card because it saves with the form
|
||||
(its own PUT runs beside the profile PUT); the display
|
||||
preferences that save on change live in their own card below. */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dob">{t("settings.dateOfBirth")}</Label>
|
||||
@@ -544,44 +542,7 @@ export function AccountSection() {
|
||||
</p>
|
||||
<FieldError id="dob-error" message={fieldErrors.dateOfBirth} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language-select">{t("settings.language")}</Label>
|
||||
{/* While a switch waits on its message bundle (dynamic
|
||||
import per locale), show the target value and lock the
|
||||
control — the context only flips locale + strings
|
||||
together once the bundle arrived. */}
|
||||
<NativeSelect
|
||||
id="language-select"
|
||||
value={pendingLocale ?? locale}
|
||||
disabled={pendingLocale !== null}
|
||||
aria-busy={pendingLocale !== null}
|
||||
onChange={(e) => setLocale(e.target.value as Locale)}
|
||||
>
|
||||
{locales.map((loc) => (
|
||||
<option key={loc} value={loc}>
|
||||
{localeLabels[loc as Locale]}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.languageDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timezone, unit system, glucose unit and the date/hour formats
|
||||
share one grid block — all personal display preferences (like
|
||||
language above). The glucose unit is its own dropdown rather
|
||||
than a branch of the unit system because metric countries are
|
||||
split on which unit they read glucose in. Every dropdown here
|
||||
PATCHes its own endpoint on change; the timezone saves through
|
||||
the form's submit handler. */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<TimezonePicker value={timezone} onChange={setTimezone} />
|
||||
<UnitPreferenceSelect isAuthenticated={isAuthenticated} />
|
||||
<GlucoseUnitSelect isAuthenticated={isAuthenticated} />
|
||||
<TimeFormatSelect isAuthenticated={isAuthenticated} />
|
||||
<DateFormatSelect isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
|
||||
{/* v1.7.0 — optional patient-identity fields surfaced on the
|
||||
@@ -702,6 +663,8 @@ export function AccountSection() {
|
||||
{recentProof.dialog}
|
||||
</SettingsCard>
|
||||
|
||||
<DisplayPreferencesCard isAuthenticated={isAuthenticated} />
|
||||
|
||||
{/* v1.18.0 (S5) — injection-site exclusions moved to the dedicated
|
||||
Medikamente settings section, where every medication-specific
|
||||
preference now lives. */}
|
||||
@@ -734,3 +697,59 @@ export function AccountSection() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Language, units and formats: every control here saves the moment it
|
||||
* changes (each PATCHes its own endpoint), so they sit in a card with no save
|
||||
* button. Inside the profile form they read as if the form's Save covered
|
||||
* them, and a person who changed the language and then cancelled the form
|
||||
* found it changed anyway.
|
||||
*/
|
||||
export function DisplayPreferencesCard({
|
||||
isAuthenticated,
|
||||
}: {
|
||||
isAuthenticated: boolean;
|
||||
}) {
|
||||
const { t, locale, setLocale, pendingLocale } = useTranslations();
|
||||
return (
|
||||
<SettingsCard data-testid="settings-display-card">
|
||||
<SettingsCardHeader
|
||||
icon={Languages}
|
||||
title={t("settings.displayCard.title")}
|
||||
description={t("settings.displayCard.description")}
|
||||
/>
|
||||
{/* The glucose unit is its own dropdown rather than a branch of the
|
||||
unit system because metric countries are split on which unit they
|
||||
read glucose in. */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="language-select">{t("settings.language")}</Label>
|
||||
{/* While a switch waits on its message bundle (dynamic import per
|
||||
locale), show the target value and lock the control — the
|
||||
context only flips locale + strings together once the bundle
|
||||
arrived. */}
|
||||
<NativeSelect
|
||||
id="language-select"
|
||||
value={pendingLocale ?? locale}
|
||||
disabled={pendingLocale !== null}
|
||||
aria-busy={pendingLocale !== null}
|
||||
onChange={(e) => setLocale(e.target.value as Locale)}
|
||||
>
|
||||
{locales.map((loc) => (
|
||||
<option key={loc} value={loc}>
|
||||
{localeLabels[loc as Locale]}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.languageDescription")}
|
||||
</p>
|
||||
</div>
|
||||
<UnitPreferenceSelect isAuthenticated={isAuthenticated} />
|
||||
<GlucoseUnitSelect isAuthenticated={isAuthenticated} />
|
||||
<TimeFormatSelect isAuthenticated={isAuthenticated} />
|
||||
<DateFormatSelect isAuthenticated={isAuthenticated} />
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user